diff options
author | Daniel Sandler <dsandler@android.com> | 2013-06-05 22:57:57 -0400 |
---|---|---|
committer | Daniel Sandler <dsandler@android.com> | 2013-06-05 23:30:20 -0400 |
commit | 325dc23624160689e59fbac708cf6f222b20d025 (patch) | |
tree | 3c6a13a52a6e5688c7e4404890e5e8f88d544856 /src/com/android/launcher3 | |
parent | b582cd201fccece65d36b8915cf84fef3546cffa (diff) | |
download | android_packages_apps_Trebuchet-325dc23624160689e59fbac708cf6f222b20d025.tar.gz android_packages_apps_Trebuchet-325dc23624160689e59fbac708cf6f222b20d025.tar.bz2 android_packages_apps_Trebuchet-325dc23624160689e59fbac708cf6f222b20d025.zip |
Launcher2 is now Launcher3.
Changes include
- moving from com.android.launcher{,2} to
com.android.launcher3
- removing wallpapers
- new temporary icon
Change-Id: I1eabd06059e94a8f3bdf6b620777bd1d2b7c212b
Diffstat (limited to 'src/com/android/launcher3')
78 files changed, 34358 insertions, 0 deletions
diff --git a/src/com/android/launcher3/AccessibleTabView.java b/src/com/android/launcher3/AccessibleTabView.java new file mode 100644 index 000000000..90a78656e --- /dev/null +++ b/src/com/android/launcher3/AccessibleTabView.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.TextView; + +/** + * We use a custom tab view to process our own focus traversals. + */ +public class AccessibleTabView extends TextView { + public AccessibleTabView(Context context) { + super(context); + } + + public AccessibleTabView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AccessibleTabView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return FocusHelper.handleTabKeyEvent(this, keyCode, event) + || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return FocusHelper.handleTabKeyEvent(this, keyCode, event) + || super.onKeyUp(keyCode, event); + } +} diff --git a/src/com/android/launcher3/AddAdapter.java b/src/com/android/launcher3/AddAdapter.java new file mode 100644 index 000000000..ad15e75c6 --- /dev/null +++ b/src/com/android/launcher3/AddAdapter.java @@ -0,0 +1,103 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import java.util.ArrayList; + +import com.android.launcher3.R; + +/** + * Adapter showing the types of items that can be added to a {@link Workspace}. + */ +public class AddAdapter extends BaseAdapter { + + private final LayoutInflater mInflater; + + private final ArrayList<ListItem> mItems = new ArrayList<ListItem>(); + + public static final int ITEM_SHORTCUT = 0; + public static final int ITEM_APPWIDGET = 1; + public static final int ITEM_APPLICATION = 2; + public static final int ITEM_WALLPAPER = 3; + + /** + * Specific item in our list. + */ + public class ListItem { + public final CharSequence text; + public final Drawable image; + public final int actionTag; + + public ListItem(Resources res, int textResourceId, int imageResourceId, int actionTag) { + text = res.getString(textResourceId); + if (imageResourceId != -1) { + image = res.getDrawable(imageResourceId); + } else { + image = null; + } + this.actionTag = actionTag; + } + } + + public AddAdapter(Launcher launcher) { + super(); + + mInflater = (LayoutInflater) launcher.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + // Create default actions + Resources res = launcher.getResources(); + + mItems.add(new ListItem(res, R.string.group_wallpapers, + R.mipmap.ic_launcher_wallpaper, ITEM_WALLPAPER)); + } + + public View getView(int position, View convertView, ViewGroup parent) { + ListItem item = (ListItem) getItem(position); + + if (convertView == null) { + convertView = mInflater.inflate(R.layout.add_list_item, parent, false); + } + + TextView textView = (TextView) convertView; + textView.setTag(item); + textView.setText(item.text); + textView.setCompoundDrawablesWithIntrinsicBounds(item.image, null, null, null); + + return convertView; + } + + public int getCount() { + return mItems.size(); + } + + public Object getItem(int position) { + return mItems.get(position); + } + + public long getItemId(int position) { + return position; + } +} diff --git a/src/com/android/launcher3/Alarm.java b/src/com/android/launcher3/Alarm.java new file mode 100644 index 000000000..91f9bd091 --- /dev/null +++ b/src/com/android/launcher3/Alarm.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2010 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.os.Handler; + +public class Alarm implements Runnable{ + // if we reach this time and the alarm hasn't been cancelled, call the listener + private long mAlarmTriggerTime; + + // if we've scheduled a call to run() (ie called mHandler.postDelayed), this variable is true. + // We use this to avoid having multiple pending callbacks + private boolean mWaitingForCallback; + + private Handler mHandler; + private OnAlarmListener mAlarmListener; + private boolean mAlarmPending = false; + + public Alarm() { + mHandler = new Handler(); + } + + public void setOnAlarmListener(OnAlarmListener alarmListener) { + mAlarmListener = alarmListener; + } + + // Sets the alarm to go off in a certain number of milliseconds. If the alarm is already set, + // it's overwritten and only the new alarm setting is used + public void setAlarm(long millisecondsInFuture) { + long currentTime = System.currentTimeMillis(); + mAlarmPending = true; + mAlarmTriggerTime = currentTime + millisecondsInFuture; + if (!mWaitingForCallback) { + mHandler.postDelayed(this, mAlarmTriggerTime - currentTime); + mWaitingForCallback = true; + } + } + + public void cancelAlarm() { + mAlarmTriggerTime = 0; + mAlarmPending = false; + } + + // this is called when our timer runs out + public void run() { + mWaitingForCallback = false; + if (mAlarmTriggerTime != 0) { + long currentTime = System.currentTimeMillis(); + if (mAlarmTriggerTime > currentTime) { + // We still need to wait some time to trigger spring loaded mode-- + // post a new callback + mHandler.postDelayed(this, Math.max(0, mAlarmTriggerTime - currentTime)); + mWaitingForCallback = true; + } else { + mAlarmPending = false; + if (mAlarmListener != null) { + mAlarmListener.onAlarm(this); + } + } + } + } + + public boolean alarmPending() { + return mAlarmPending; + } +} + +interface OnAlarmListener { + public void onAlarm(Alarm alarm); +} diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java new file mode 100644 index 000000000..e74dc2133 --- /dev/null +++ b/src/com/android/launcher3/AllAppsList.java @@ -0,0 +1,221 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + + +/** + * Stores the list of all applications for the all apps view. + */ +class AllAppsList { + public static final int DEFAULT_APPLICATIONS_NUMBER = 42; + + /** The list off all apps. */ + public ArrayList<ApplicationInfo> data = + new ArrayList<ApplicationInfo>(DEFAULT_APPLICATIONS_NUMBER); + /** The list of apps that have been added since the last notify() call. */ + public ArrayList<ApplicationInfo> added = + new ArrayList<ApplicationInfo>(DEFAULT_APPLICATIONS_NUMBER); + /** The list of apps that have been removed since the last notify() call. */ + public ArrayList<ApplicationInfo> removed = new ArrayList<ApplicationInfo>(); + /** The list of apps that have been modified since the last notify() call. */ + public ArrayList<ApplicationInfo> modified = new ArrayList<ApplicationInfo>(); + + private IconCache mIconCache; + + /** + * Boring constructor. + */ + public AllAppsList(IconCache iconCache) { + mIconCache = iconCache; + } + + /** + * Add the supplied ApplicationInfo objects to the list, and enqueue it into the + * list to broadcast when notify() is called. + * + * If the app is already in the list, doesn't add it. + */ + public void add(ApplicationInfo info) { + if (findActivity(data, info.componentName)) { + return; + } + data.add(info); + added.add(info); + } + + public void clear() { + data.clear(); + // TODO: do we clear these too? + added.clear(); + removed.clear(); + modified.clear(); + } + + public int size() { + return data.size(); + } + + public ApplicationInfo get(int index) { + return data.get(index); + } + + /** + * Add the icons for the supplied apk called packageName. + */ + public void addPackage(Context context, String packageName) { + final List<ResolveInfo> matches = findActivitiesForPackage(context, packageName); + + if (matches.size() > 0) { + for (ResolveInfo info : matches) { + add(new ApplicationInfo(context.getPackageManager(), info, mIconCache, null)); + } + } + } + + /** + * Remove the apps for the given apk identified by packageName. + */ + public void removePackage(String packageName) { + final List<ApplicationInfo> data = this.data; + for (int i = data.size() - 1; i >= 0; i--) { + ApplicationInfo info = data.get(i); + final ComponentName component = info.intent.getComponent(); + if (packageName.equals(component.getPackageName())) { + removed.add(info); + data.remove(i); + } + } + // This is more aggressive than it needs to be. + mIconCache.flush(); + } + + /** + * Add and remove icons for this package which has been updated. + */ + public void updatePackage(Context context, String packageName) { + final List<ResolveInfo> matches = findActivitiesForPackage(context, packageName); + if (matches.size() > 0) { + // Find disabled/removed activities and remove them from data and add them + // to the removed list. + for (int i = data.size() - 1; i >= 0; i--) { + final ApplicationInfo applicationInfo = data.get(i); + final ComponentName component = applicationInfo.intent.getComponent(); + if (packageName.equals(component.getPackageName())) { + if (!findActivity(matches, component)) { + removed.add(applicationInfo); + mIconCache.remove(component); + data.remove(i); + } + } + } + + // Find enabled activities and add them to the adapter + // Also updates existing activities with new labels/icons + int count = matches.size(); + for (int i = 0; i < count; i++) { + final ResolveInfo info = matches.get(i); + ApplicationInfo applicationInfo = findApplicationInfoLocked( + info.activityInfo.applicationInfo.packageName, + info.activityInfo.name); + if (applicationInfo == null) { + add(new ApplicationInfo(context.getPackageManager(), info, mIconCache, null)); + } else { + mIconCache.remove(applicationInfo.componentName); + mIconCache.getTitleAndIcon(applicationInfo, info, null); + modified.add(applicationInfo); + } + } + } else { + // Remove all data for this package. + for (int i = data.size() - 1; i >= 0; i--) { + final ApplicationInfo applicationInfo = data.get(i); + final ComponentName component = applicationInfo.intent.getComponent(); + if (packageName.equals(component.getPackageName())) { + removed.add(applicationInfo); + mIconCache.remove(component); + data.remove(i); + } + } + } + } + + /** + * Query the package manager for MAIN/LAUNCHER activities in the supplied package. + */ + private static List<ResolveInfo> findActivitiesForPackage(Context context, String packageName) { + final PackageManager packageManager = context.getPackageManager(); + + final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + mainIntent.setPackage(packageName); + + final List<ResolveInfo> apps = packageManager.queryIntentActivities(mainIntent, 0); + return apps != null ? apps : new ArrayList<ResolveInfo>(); + } + + /** + * Returns whether <em>apps</em> contains <em>component</em>. + */ + private static boolean findActivity(List<ResolveInfo> apps, ComponentName component) { + final String className = component.getClassName(); + for (ResolveInfo info : apps) { + final ActivityInfo activityInfo = info.activityInfo; + if (activityInfo.name.equals(className)) { + return true; + } + } + return false; + } + + /** + * Returns whether <em>apps</em> contains <em>component</em>. + */ + private static boolean findActivity(ArrayList<ApplicationInfo> apps, ComponentName component) { + final int N = apps.size(); + for (int i=0; i<N; i++) { + final ApplicationInfo info = apps.get(i); + if (info.componentName.equals(component)) { + return true; + } + } + return false; + } + + /** + * Find an ApplicationInfo object for the given packageName and className. + */ + private ApplicationInfo findApplicationInfoLocked(String packageName, String className) { + for (ApplicationInfo info: data) { + final ComponentName component = info.intent.getComponent(); + if (packageName.equals(component.getPackageName()) + && className.equals(component.getClassName())) { + return info; + } + } + return null; + } +} diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java new file mode 100644 index 000000000..8e968f83c --- /dev/null +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -0,0 +1,471 @@ +package com.android.launcher3; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.graphics.Rect; +import android.view.Gravity; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.launcher3.R; + +public class AppWidgetResizeFrame extends FrameLayout { + private LauncherAppWidgetHostView mWidgetView; + private CellLayout mCellLayout; + private DragLayer mDragLayer; + private Workspace mWorkspace; + private ImageView mLeftHandle; + private ImageView mRightHandle; + private ImageView mTopHandle; + private ImageView mBottomHandle; + + private boolean mLeftBorderActive; + private boolean mRightBorderActive; + private boolean mTopBorderActive; + private boolean mBottomBorderActive; + + private int mWidgetPaddingLeft; + private int mWidgetPaddingRight; + private int mWidgetPaddingTop; + private int mWidgetPaddingBottom; + + private int mBaselineWidth; + private int mBaselineHeight; + private int mBaselineX; + private int mBaselineY; + private int mResizeMode; + + private int mRunningHInc; + private int mRunningVInc; + private int mMinHSpan; + private int mMinVSpan; + private int mDeltaX; + private int mDeltaY; + private int mDeltaXAddOn; + private int mDeltaYAddOn; + + private int mBackgroundPadding; + private int mTouchTargetWidth; + + private int mTopTouchRegionAdjustment = 0; + private int mBottomTouchRegionAdjustment = 0; + + int[] mDirectionVector = new int[2]; + int[] mLastDirectionVector = new int[2]; + + final int SNAP_DURATION = 150; + final int BACKGROUND_PADDING = 24; + final float DIMMED_HANDLE_ALPHA = 0f; + final float RESIZE_THRESHOLD = 0.66f; + + private static Rect mTmpRect = new Rect(); + + public static final int LEFT = 0; + public static final int TOP = 1; + public static final int RIGHT = 2; + public static final int BOTTOM = 3; + + private Launcher mLauncher; + + public AppWidgetResizeFrame(Context context, + LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer) { + + super(context); + mLauncher = (Launcher) context; + mCellLayout = cellLayout; + mWidgetView = widgetView; + mResizeMode = widgetView.getAppWidgetInfo().resizeMode; + mDragLayer = dragLayer; + mWorkspace = (Workspace) dragLayer.findViewById(R.id.workspace); + + final AppWidgetProviderInfo info = widgetView.getAppWidgetInfo(); + int[] result = Launcher.getMinSpanForWidget(mLauncher, info); + mMinHSpan = result[0]; + mMinVSpan = result[1]; + + setBackgroundResource(R.drawable.widget_resize_frame_holo); + setPadding(0, 0, 0, 0); + + LayoutParams lp; + mLeftHandle = new ImageView(context); + mLeftHandle.setImageResource(R.drawable.widget_resize_handle_left); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + Gravity.START | Gravity.CENTER_VERTICAL); + addView(mLeftHandle, lp); + + mRightHandle = new ImageView(context); + mRightHandle.setImageResource(R.drawable.widget_resize_handle_right); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + Gravity.END | Gravity.CENTER_VERTICAL); + addView(mRightHandle, lp); + + mTopHandle = new ImageView(context); + mTopHandle.setImageResource(R.drawable.widget_resize_handle_top); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + Gravity.CENTER_HORIZONTAL | Gravity.TOP); + addView(mTopHandle, lp); + + mBottomHandle = new ImageView(context); + mBottomHandle.setImageResource(R.drawable.widget_resize_handle_bottom); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); + addView(mBottomHandle, lp); + + Rect p = AppWidgetHostView.getDefaultPaddingForWidget(context, + widgetView.getAppWidgetInfo().provider, null); + mWidgetPaddingLeft = p.left; + mWidgetPaddingTop = p.top; + mWidgetPaddingRight = p.right; + mWidgetPaddingBottom = p.bottom; + + if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) { + mTopHandle.setVisibility(GONE); + mBottomHandle.setVisibility(GONE); + } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) { + mLeftHandle.setVisibility(GONE); + mRightHandle.setVisibility(GONE); + } + + final float density = mLauncher.getResources().getDisplayMetrics().density; + mBackgroundPadding = (int) Math.ceil(density * BACKGROUND_PADDING); + mTouchTargetWidth = 2 * mBackgroundPadding; + + // When we create the resize frame, we first mark all cells as unoccupied. The appropriate + // cells (same if not resized, or different) will be marked as occupied when the resize + // frame is dismissed. + mCellLayout.markCellsAsUnoccupiedForView(mWidgetView); + } + + public boolean beginResizeIfPointInRegion(int x, int y) { + boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0; + boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0; + + mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive; + mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive; + mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive; + mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment) + && verticalActive; + + boolean anyBordersActive = mLeftBorderActive || mRightBorderActive + || mTopBorderActive || mBottomBorderActive; + + mBaselineWidth = getMeasuredWidth(); + mBaselineHeight = getMeasuredHeight(); + mBaselineX = getLeft(); + mBaselineY = getTop(); + + if (anyBordersActive) { + mLeftHandle.setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); + mRightHandle.setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA); + mTopHandle.setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); + mBottomHandle.setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); + } + return anyBordersActive; + } + + /** + * Here we bound the deltas such that the frame cannot be stretched beyond the extents + * of the CellLayout, and such that the frame's borders can't cross. + */ + public void updateDeltas(int deltaX, int deltaY) { + if (mLeftBorderActive) { + mDeltaX = Math.max(-mBaselineX, deltaX); + mDeltaX = Math.min(mBaselineWidth - 2 * mTouchTargetWidth, mDeltaX); + } else if (mRightBorderActive) { + mDeltaX = Math.min(mDragLayer.getWidth() - (mBaselineX + mBaselineWidth), deltaX); + mDeltaX = Math.max(-mBaselineWidth + 2 * mTouchTargetWidth, mDeltaX); + } + + if (mTopBorderActive) { + mDeltaY = Math.max(-mBaselineY, deltaY); + mDeltaY = Math.min(mBaselineHeight - 2 * mTouchTargetWidth, mDeltaY); + } else if (mBottomBorderActive) { + mDeltaY = Math.min(mDragLayer.getHeight() - (mBaselineY + mBaselineHeight), deltaY); + mDeltaY = Math.max(-mBaselineHeight + 2 * mTouchTargetWidth, mDeltaY); + } + } + + public void visualizeResizeForDelta(int deltaX, int deltaY) { + visualizeResizeForDelta(deltaX, deltaY, false); + } + + /** + * Based on the deltas, we resize the frame, and, if needed, we resize the widget. + */ + private void visualizeResizeForDelta(int deltaX, int deltaY, boolean onDismiss) { + updateDeltas(deltaX, deltaY); + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + + if (mLeftBorderActive) { + lp.x = mBaselineX + mDeltaX; + lp.width = mBaselineWidth - mDeltaX; + } else if (mRightBorderActive) { + lp.width = mBaselineWidth + mDeltaX; + } + + if (mTopBorderActive) { + lp.y = mBaselineY + mDeltaY; + lp.height = mBaselineHeight - mDeltaY; + } else if (mBottomBorderActive) { + lp.height = mBaselineHeight + mDeltaY; + } + + resizeWidgetIfNeeded(onDismiss); + requestLayout(); + } + + /** + * Based on the current deltas, we determine if and how to resize the widget. + */ + private void resizeWidgetIfNeeded(boolean onDismiss) { + int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap(); + int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap(); + + int deltaX = mDeltaX + mDeltaXAddOn; + int deltaY = mDeltaY + mDeltaYAddOn; + + float hSpanIncF = 1.0f * deltaX / xThreshold - mRunningHInc; + float vSpanIncF = 1.0f * deltaY / yThreshold - mRunningVInc; + + int hSpanInc = 0; + int vSpanInc = 0; + int cellXInc = 0; + int cellYInc = 0; + + int countX = mCellLayout.getCountX(); + int countY = mCellLayout.getCountY(); + + if (Math.abs(hSpanIncF) > RESIZE_THRESHOLD) { + hSpanInc = Math.round(hSpanIncF); + } + if (Math.abs(vSpanIncF) > RESIZE_THRESHOLD) { + vSpanInc = Math.round(vSpanIncF); + } + + if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return; + + + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams(); + + int spanX = lp.cellHSpan; + int spanY = lp.cellVSpan; + int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX; + int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY; + + int hSpanDelta = 0; + int vSpanDelta = 0; + + // For each border, we bound the resizing based on the minimum width, and the maximum + // expandability. + if (mLeftBorderActive) { + cellXInc = Math.max(-cellX, hSpanInc); + cellXInc = Math.min(lp.cellHSpan - mMinHSpan, cellXInc); + hSpanInc *= -1; + hSpanInc = Math.min(cellX, hSpanInc); + hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc); + hSpanDelta = -hSpanInc; + + } else if (mRightBorderActive) { + hSpanInc = Math.min(countX - (cellX + spanX), hSpanInc); + hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc); + hSpanDelta = hSpanInc; + } + + if (mTopBorderActive) { + cellYInc = Math.max(-cellY, vSpanInc); + cellYInc = Math.min(lp.cellVSpan - mMinVSpan, cellYInc); + vSpanInc *= -1; + vSpanInc = Math.min(cellY, vSpanInc); + vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc); + vSpanDelta = -vSpanInc; + } else if (mBottomBorderActive) { + vSpanInc = Math.min(countY - (cellY + spanY), vSpanInc); + vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc); + vSpanDelta = vSpanInc; + } + + mDirectionVector[0] = 0; + mDirectionVector[1] = 0; + // Update the widget's dimensions and position according to the deltas computed above + if (mLeftBorderActive || mRightBorderActive) { + spanX += hSpanInc; + cellX += cellXInc; + if (hSpanDelta != 0) { + mDirectionVector[0] = mLeftBorderActive ? -1 : 1; + } + } + + if (mTopBorderActive || mBottomBorderActive) { + spanY += vSpanInc; + cellY += cellYInc; + if (vSpanDelta != 0) { + mDirectionVector[1] = mTopBorderActive ? -1 : 1; + } + } + + if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return; + + // We always want the final commit to match the feedback, so we make sure to use the + // last used direction vector when committing the resize / reorder. + if (onDismiss) { + mDirectionVector[0] = mLastDirectionVector[0]; + mDirectionVector[1] = mLastDirectionVector[1]; + } else { + mLastDirectionVector[0] = mDirectionVector[0]; + mLastDirectionVector[1] = mDirectionVector[1]; + } + + if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView, + mDirectionVector, onDismiss)) { + lp.tmpCellX = cellX; + lp.tmpCellY = cellY; + lp.cellHSpan = spanX; + lp.cellVSpan = spanY; + mRunningVInc += vSpanDelta; + mRunningHInc += hSpanDelta; + if (!onDismiss) { + updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY); + } + } + mWidgetView.requestLayout(); + } + + static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, + int spanX, int spanY) { + + getWidgetSizeRanges(launcher, spanX, spanY, mTmpRect); + widgetView.updateAppWidgetSize(null, mTmpRect.left, mTmpRect.top, + mTmpRect.right, mTmpRect.bottom); + } + + static Rect getWidgetSizeRanges(Launcher launcher, int spanX, int spanY, Rect rect) { + if (rect == null) { + rect = new Rect(); + } + Rect landMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.LANDSCAPE); + Rect portMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.PORTRAIT); + final float density = launcher.getResources().getDisplayMetrics().density; + + // Compute landscape size + int cellWidth = landMetrics.left; + int cellHeight = landMetrics.top; + int widthGap = landMetrics.right; + int heightGap = landMetrics.bottom; + int landWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density); + int landHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density); + + // Compute portrait size + cellWidth = portMetrics.left; + cellHeight = portMetrics.top; + widthGap = portMetrics.right; + heightGap = portMetrics.bottom; + int portWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density); + int portHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density); + rect.set(portWidth, landHeight, landWidth, portHeight); + return rect; + } + + /** + * This is the final step of the resize. Here we save the new widget size and position + * to LauncherModel and animate the resize frame. + */ + public void commitResize() { + resizeWidgetIfNeeded(true); + requestLayout(); + } + + public void onTouchUp() { + int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap(); + int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap(); + + mDeltaXAddOn = mRunningHInc * xThreshold; + mDeltaYAddOn = mRunningVInc * yThreshold; + mDeltaX = 0; + mDeltaY = 0; + + post(new Runnable() { + @Override + public void run() { + snapToWidget(true); + } + }); + } + + public void snapToWidget(boolean animate) { + final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + int xOffset = mCellLayout.getLeft() + mCellLayout.getPaddingLeft() + + mDragLayer.getPaddingLeft() - mWorkspace.getScrollX(); + int yOffset = mCellLayout.getTop() + mCellLayout.getPaddingTop() + + mDragLayer.getPaddingTop() - mWorkspace.getScrollY(); + + int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding - mWidgetPaddingLeft - + mWidgetPaddingRight; + int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding - mWidgetPaddingTop - + mWidgetPaddingBottom; + + int newX = mWidgetView.getLeft() - mBackgroundPadding + xOffset + mWidgetPaddingLeft; + int newY = mWidgetView.getTop() - mBackgroundPadding + yOffset + mWidgetPaddingTop; + + // We need to make sure the frame's touchable regions lie fully within the bounds of the + // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions + // down accordingly to provide a proper touch target. + if (newY < 0) { + // In this case we shift the touch region down to start at the top of the DragLayer + mTopTouchRegionAdjustment = -newY; + } else { + mTopTouchRegionAdjustment = 0; + } + if (newY + newHeight > mDragLayer.getHeight()) { + // In this case we shift the touch region up to end at the bottom of the DragLayer + mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight()); + } else { + mBottomTouchRegionAdjustment = 0; + } + + if (!animate) { + lp.width = newWidth; + lp.height = newHeight; + lp.x = newX; + lp.y = newY; + mLeftHandle.setAlpha(1.0f); + mRightHandle.setAlpha(1.0f); + mTopHandle.setAlpha(1.0f); + mBottomHandle.setAlpha(1.0f); + requestLayout(); + } else { + PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth); + PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height, + newHeight); + PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX); + PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY); + ObjectAnimator oa = + LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y); + ObjectAnimator leftOa = LauncherAnimUtils.ofFloat(mLeftHandle, "alpha", 1.0f); + ObjectAnimator rightOa = LauncherAnimUtils.ofFloat(mRightHandle, "alpha", 1.0f); + ObjectAnimator topOa = LauncherAnimUtils.ofFloat(mTopHandle, "alpha", 1.0f); + ObjectAnimator bottomOa = LauncherAnimUtils.ofFloat(mBottomHandle, "alpha", 1.0f); + oa.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + requestLayout(); + } + }); + AnimatorSet set = LauncherAnimUtils.createAnimatorSet(); + if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) { + set.playTogether(oa, topOa, bottomOa); + } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) { + set.playTogether(oa, leftOa, rightOa); + } else { + set.playTogether(oa, leftOa, rightOa, topOa, bottomOa); + } + + set.setDuration(SNAP_DURATION); + set.start(); + } + } +} diff --git a/src/com/android/launcher3/ApplicationInfo.java b/src/com/android/launcher3/ApplicationInfo.java new file mode 100644 index 000000000..4659e7e85 --- /dev/null +++ b/src/com/android/launcher3/ApplicationInfo.java @@ -0,0 +1,133 @@ +/* + * 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.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Represents an app in AllAppsView. + */ +class ApplicationInfo extends ItemInfo { + private static final String TAG = "Launcher2.ApplicationInfo"; + + /** + * The intent used to start the application. + */ + Intent intent; + + /** + * A bitmap version of the application icon. + */ + Bitmap iconBitmap; + + /** + * The time at which the app was first installed. + */ + long firstInstallTime; + + ComponentName componentName; + + static final int DOWNLOADED_FLAG = 1; + static final int UPDATED_SYSTEM_APP_FLAG = 2; + + int flags = 0; + + ApplicationInfo() { + itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; + } + + /** + * Must not hold the Context. + */ + public ApplicationInfo(PackageManager pm, ResolveInfo info, IconCache iconCache, + HashMap<Object, CharSequence> labelCache) { + final String packageName = info.activityInfo.applicationInfo.packageName; + + this.componentName = new ComponentName(packageName, info.activityInfo.name); + this.container = ItemInfo.NO_ID; + this.setActivity(componentName, + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + + try { + int appFlags = pm.getApplicationInfo(packageName, 0).flags; + if ((appFlags & android.content.pm.ApplicationInfo.FLAG_SYSTEM) == 0) { + flags |= DOWNLOADED_FLAG; + + if ((appFlags & android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { + flags |= UPDATED_SYSTEM_APP_FLAG; + } + } + firstInstallTime = pm.getPackageInfo(packageName, 0).firstInstallTime; + } catch (NameNotFoundException e) { + Log.d(TAG, "PackageManager.getApplicationInfo failed for " + packageName); + } + + iconCache.getTitleAndIcon(this, info, labelCache); + } + + public ApplicationInfo(ApplicationInfo info) { + super(info); + componentName = info.componentName; + title = info.title.toString(); + intent = new Intent(info.intent); + flags = info.flags; + firstInstallTime = info.firstInstallTime; + } + + /** + * Creates the application intent based on a component name and various launch flags. + * Sets {@link #itemType} to {@link LauncherSettings.BaseLauncherColumns#ITEM_TYPE_APPLICATION}. + * + * @param className the class name of the component representing the intent + * @param launchFlags the launch flags + */ + final void setActivity(ComponentName className, int launchFlags) { + intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(className); + intent.setFlags(launchFlags); + itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION; + } + + @Override + public String toString() { + return "ApplicationInfo(title=" + title.toString() + ")"; + } + + public static void dumpApplicationInfoList(String tag, String label, + ArrayList<ApplicationInfo> list) { + Log.d(tag, label + " size=" + list.size()); + for (ApplicationInfo info: list) { + Log.d(tag, " title=\"" + info.title + "\" iconBitmap=" + + info.iconBitmap + " firstInstallTime=" + + info.firstInstallTime); + } + } + + public ShortcutInfo makeShortcut() { + return new ShortcutInfo(this); + } +} diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java new file mode 100644 index 000000000..43a5259a6 --- /dev/null +++ b/src/com/android/launcher3/AppsCustomizePagedView.java @@ -0,0 +1,1710 @@ +/* + * Copyright (C) 2011 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.AnimatorSet; +import android.animation.ValueAnimator; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Process; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.GridLayout; +import android.widget.ImageView; +import android.widget.Toast; + +import com.android.launcher3.R; +import com.android.launcher3.DropTarget.DragObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * A simple callback interface which also provides the results of the task. + */ +interface AsyncTaskCallback { + void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data); +} + +/** + * The data needed to perform either of the custom AsyncTasks. + */ +class AsyncTaskPageData { + enum Type { + LoadWidgetPreviewData + } + + AsyncTaskPageData(int p, ArrayList<Object> l, int cw, int ch, AsyncTaskCallback bgR, + AsyncTaskCallback postR, WidgetPreviewLoader w) { + page = p; + items = l; + generatedImages = new ArrayList<Bitmap>(); + maxImageWidth = cw; + maxImageHeight = ch; + doInBackgroundCallback = bgR; + postExecuteCallback = postR; + widgetPreviewLoader = w; + } + void cleanup(boolean cancelled) { + // Clean up any references to source/generated bitmaps + if (generatedImages != null) { + if (cancelled) { + for (int i = 0; i < generatedImages.size(); i++) { + widgetPreviewLoader.recycleBitmap(items.get(i), generatedImages.get(i)); + } + } + generatedImages.clear(); + } + } + int page; + ArrayList<Object> items; + ArrayList<Bitmap> sourceImages; + ArrayList<Bitmap> generatedImages; + int maxImageWidth; + int maxImageHeight; + AsyncTaskCallback doInBackgroundCallback; + AsyncTaskCallback postExecuteCallback; + WidgetPreviewLoader widgetPreviewLoader; +} + +/** + * A generic template for an async task used in AppsCustomize. + */ +class AppsCustomizeAsyncTask extends AsyncTask<AsyncTaskPageData, Void, AsyncTaskPageData> { + AppsCustomizeAsyncTask(int p, AsyncTaskPageData.Type ty) { + page = p; + threadPriority = Process.THREAD_PRIORITY_DEFAULT; + dataType = ty; + } + @Override + protected AsyncTaskPageData doInBackground(AsyncTaskPageData... params) { + if (params.length != 1) return null; + // Load each of the widget previews in the background + params[0].doInBackgroundCallback.run(this, params[0]); + return params[0]; + } + @Override + protected void onPostExecute(AsyncTaskPageData result) { + // All the widget previews are loaded, so we can just callback to inflate the page + result.postExecuteCallback.run(this, result); + } + + void setThreadPriority(int p) { + threadPriority = p; + } + void syncThreadPriority() { + Process.setThreadPriority(threadPriority); + } + + // The page that this async task is associated with + AsyncTaskPageData.Type dataType; + int page; + int threadPriority; +} + +/** + * The Apps/Customize page that displays all the applications, widgets, and shortcuts. + */ +public class AppsCustomizePagedView extends PagedViewWithDraggableItems implements + View.OnClickListener, View.OnKeyListener, DragSource, + PagedViewIcon.PressedCallback, PagedViewWidget.ShortPressListener, + LauncherTransitionable { + static final String TAG = "AppsCustomizePagedView"; + + /** + * The different content types that this paged view can show. + */ + public enum ContentType { + Applications, + Widgets + } + + // Refs + private Launcher mLauncher; + private DragController mDragController; + private final LayoutInflater mLayoutInflater; + private final PackageManager mPackageManager; + + // Save and Restore + private int mSaveInstanceStateItemIndex = -1; + private PagedViewIcon mPressedIcon; + + // Content + private ArrayList<ApplicationInfo> mApps; + private ArrayList<Object> mWidgets; + + // Cling + private boolean mHasShownAllAppsCling; + private int mClingFocusedX; + private int mClingFocusedY; + + // Caching + private Canvas mCanvas; + private IconCache mIconCache; + + // Dimens + private int mContentWidth; + private int mMaxAppCellCountX, mMaxAppCellCountY; + private int mWidgetCountX, mWidgetCountY; + private int mWidgetWidthGap, mWidgetHeightGap; + private PagedViewCellLayout mWidgetSpacingLayout; + private int mNumAppsPages; + private int mNumWidgetPages; + + // Relating to the scroll and overscroll effects + Workspace.ZInterpolator mZInterpolator = new Workspace.ZInterpolator(0.5f); + private static float CAMERA_DISTANCE = 6500; + private static float TRANSITION_SCALE_FACTOR = 0.74f; + private static float TRANSITION_PIVOT = 0.65f; + private static float TRANSITION_MAX_ROTATION = 22; + private static final boolean PERFORM_OVERSCROLL_ROTATION = true; + private AccelerateInterpolator mAlphaInterpolator = new AccelerateInterpolator(0.9f); + private DecelerateInterpolator mLeftScreenAlphaInterpolator = new DecelerateInterpolator(4); + + // Previews & outlines + ArrayList<AppsCustomizeAsyncTask> mRunningTasks; + private static final int sPageSleepDelay = 200; + + private Runnable mInflateWidgetRunnable = null; + private Runnable mBindWidgetRunnable = null; + static final int WIDGET_NO_CLEANUP_REQUIRED = -1; + static final int WIDGET_PRELOAD_PENDING = 0; + static final int WIDGET_BOUND = 1; + static final int WIDGET_INFLATED = 2; + int mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; + int mWidgetLoadingId = -1; + PendingAddWidgetInfo mCreateWidgetInfo = null; + private boolean mDraggingWidget = false; + + private Toast mWidgetInstructionToast; + + // Deferral of loading widget previews during launcher transitions + private boolean mInTransition; + private ArrayList<AsyncTaskPageData> mDeferredSyncWidgetPageItems = + new ArrayList<AsyncTaskPageData>(); + private ArrayList<Runnable> mDeferredPrepareLoadWidgetPreviewsTasks = + new ArrayList<Runnable>(); + + private Rect mTmpRect = new Rect(); + + // Used for drawing shortcut previews + BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); + PaintCache mCachedShortcutPreviewPaint = new PaintCache(); + CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); + + // Used for drawing widget previews + CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); + RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); + RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); + PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); + + WidgetPreviewLoader mWidgetPreviewLoader; + + private boolean mInBulkBind; + private boolean mNeedToUpdatePageCountsAndInvalidateData; + + public AppsCustomizePagedView(Context context, AttributeSet attrs) { + super(context, attrs); + mLayoutInflater = LayoutInflater.from(context); + mPackageManager = context.getPackageManager(); + mApps = new ArrayList<ApplicationInfo>(); + mWidgets = new ArrayList<Object>(); + mIconCache = ((LauncherApplication) context.getApplicationContext()).getIconCache(); + mCanvas = new Canvas(); + mRunningTasks = new ArrayList<AppsCustomizeAsyncTask>(); + + // Save the default widget preview background + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0); + mMaxAppCellCountX = a.getInt(R.styleable.AppsCustomizePagedView_maxAppCellCountX, -1); + mMaxAppCellCountY = a.getInt(R.styleable.AppsCustomizePagedView_maxAppCellCountY, -1); + mWidgetWidthGap = + a.getDimensionPixelSize(R.styleable.AppsCustomizePagedView_widgetCellWidthGap, 0); + mWidgetHeightGap = + a.getDimensionPixelSize(R.styleable.AppsCustomizePagedView_widgetCellHeightGap, 0); + mWidgetCountX = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountX, 2); + mWidgetCountY = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountY, 2); + mClingFocusedX = a.getInt(R.styleable.AppsCustomizePagedView_clingFocusedX, 0); + mClingFocusedY = a.getInt(R.styleable.AppsCustomizePagedView_clingFocusedY, 0); + a.recycle(); + mWidgetSpacingLayout = new PagedViewCellLayout(getContext()); + + // The padding on the non-matched dimension for the default widget preview icons + // (top + bottom) + mFadeInAdjacentScreens = false; + + // Unless otherwise specified this view is important for accessibility. + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + @Override + protected void init() { + super.init(); + mCenterPagesVertically = false; + + Context context = getContext(); + Resources r = context.getResources(); + setDragSlopeThreshold(r.getInteger(R.integer.config_appsCustomizeDragSlopeThreshold)/100f); + } + + /** Returns the item index of the center item on this page so that we can restore to this + * item index when we rotate. */ + private int getMiddleComponentIndexOnCurrentPage() { + int i = -1; + if (getPageCount() > 0) { + int currentPage = getCurrentPage(); + if (currentPage < mNumAppsPages) { + PagedViewCellLayout layout = (PagedViewCellLayout) getPageAt(currentPage); + PagedViewCellLayoutChildren childrenLayout = layout.getChildrenLayout(); + int numItemsPerPage = mCellCountX * mCellCountY; + int childCount = childrenLayout.getChildCount(); + if (childCount > 0) { + i = (currentPage * numItemsPerPage) + (childCount / 2); + } + } else { + int numApps = mApps.size(); + PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(currentPage); + int numItemsPerPage = mWidgetCountX * mWidgetCountY; + int childCount = layout.getChildCount(); + if (childCount > 0) { + i = numApps + + ((currentPage - mNumAppsPages) * numItemsPerPage) + (childCount / 2); + } + } + } + return i; + } + + /** Get the index of the item to restore to if we need to restore the current page. */ + int getSaveInstanceStateIndex() { + if (mSaveInstanceStateItemIndex == -1) { + mSaveInstanceStateItemIndex = getMiddleComponentIndexOnCurrentPage(); + } + return mSaveInstanceStateItemIndex; + } + + /** Returns the page in the current orientation which is expected to contain the specified + * item index. */ + int getPageForComponent(int index) { + if (index < 0) return 0; + + if (index < mApps.size()) { + int numItemsPerPage = mCellCountX * mCellCountY; + return (index / numItemsPerPage); + } else { + int numItemsPerPage = mWidgetCountX * mWidgetCountY; + return mNumAppsPages + ((index - mApps.size()) / numItemsPerPage); + } + } + + /** Restores the page for an item at the specified index */ + void restorePageForIndex(int index) { + if (index < 0) return; + mSaveInstanceStateItemIndex = index; + } + + private void updatePageCounts() { + mNumWidgetPages = (int) Math.ceil(mWidgets.size() / + (float) (mWidgetCountX * mWidgetCountY)); + mNumAppsPages = (int) Math.ceil((float) mApps.size() / (mCellCountX * mCellCountY)); + } + + protected void onDataReady(int width, int height) { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = new WidgetPreviewLoader(mLauncher); + } + + // Note that we transpose the counts in portrait so that we get a similar layout + boolean isLandscape = getResources().getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE; + int maxCellCountX = Integer.MAX_VALUE; + int maxCellCountY = Integer.MAX_VALUE; + if (LauncherApplication.isScreenLarge()) { + maxCellCountX = (isLandscape ? LauncherModel.getCellCountX() : + LauncherModel.getCellCountY()); + maxCellCountY = (isLandscape ? LauncherModel.getCellCountY() : + LauncherModel.getCellCountX()); + } + if (mMaxAppCellCountX > -1) { + maxCellCountX = Math.min(maxCellCountX, mMaxAppCellCountX); + } + // Temp hack for now: only use the max cell count Y for widget layout + int maxWidgetCellCountY = maxCellCountY; + if (mMaxAppCellCountY > -1) { + maxWidgetCellCountY = Math.min(maxWidgetCellCountY, mMaxAppCellCountY); + } + + // Now that the data is ready, we can calculate the content width, the number of cells to + // use for each page + mWidgetSpacingLayout.setGap(mPageLayoutWidthGap, mPageLayoutHeightGap); + mWidgetSpacingLayout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, + mPageLayoutPaddingRight, mPageLayoutPaddingBottom); + mWidgetSpacingLayout.calculateCellCount(width, height, maxCellCountX, maxCellCountY); + mCellCountX = mWidgetSpacingLayout.getCellCountX(); + mCellCountY = mWidgetSpacingLayout.getCellCountY(); + updatePageCounts(); + + // Force a measure to update recalculate the gaps + int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST); + mWidgetSpacingLayout.calculateCellCount(width, height, maxCellCountX, maxWidgetCellCountY); + mWidgetSpacingLayout.measure(widthSpec, heightSpec); + mContentWidth = mWidgetSpacingLayout.getContentWidth(); + + AppsCustomizeTabHost host = (AppsCustomizeTabHost) getTabHost(); + final boolean hostIsTransitioning = host.isTransitioning(); + + // Restore the page + int page = getPageForComponent(mSaveInstanceStateItemIndex); + invalidatePageData(Math.max(0, page), hostIsTransitioning); + + // Show All Apps cling if we are finished transitioning, otherwise, we will try again when + // the transition completes in AppsCustomizeTabHost (otherwise the wrong offsets will be + // returned while animating) + if (!hostIsTransitioning) { + post(new Runnable() { + @Override + public void run() { + showAllAppsCling(); + } + }); + } + } + + void showAllAppsCling() { + if (!mHasShownAllAppsCling && isDataReady()) { + mHasShownAllAppsCling = true; + // Calculate the position for the cling punch through + int[] offset = new int[2]; + int[] pos = mWidgetSpacingLayout.estimateCellPosition(mClingFocusedX, mClingFocusedY); + mLauncher.getDragLayer().getLocationInDragLayer(this, offset); + // PagedViews are centered horizontally but top aligned + // Note we have to shift the items up now that Launcher sits under the status bar + pos[0] += (getMeasuredWidth() - mWidgetSpacingLayout.getMeasuredWidth()) / 2 + + offset[0]; + pos[1] += offset[1] - mLauncher.getDragLayer().getPaddingTop(); + mLauncher.showFirstRunAllAppsCling(pos); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + if (!isDataReady()) { + if (!mApps.isEmpty() && !mWidgets.isEmpty()) { + setDataIsReady(); + setMeasuredDimension(width, height); + onDataReady(width, height); + } + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + public void onPackagesUpdated(ArrayList<Object> widgetsAndShortcuts) { + // Get the list of widgets and shortcuts + mWidgets.clear(); + for (Object o : widgetsAndShortcuts) { + if (o instanceof AppWidgetProviderInfo) { + AppWidgetProviderInfo widget = (AppWidgetProviderInfo) o; + widget.label = widget.label.trim(); + if (widget.minWidth > 0 && widget.minHeight > 0) { + // Ensure that all widgets we show can be added on a workspace of this size + int[] spanXY = Launcher.getSpanForWidget(mLauncher, widget); + int[] minSpanXY = Launcher.getMinSpanForWidget(mLauncher, widget); + int minSpanX = Math.min(spanXY[0], minSpanXY[0]); + int minSpanY = Math.min(spanXY[1], minSpanXY[1]); + if (minSpanX <= LauncherModel.getCellCountX() && + minSpanY <= LauncherModel.getCellCountY()) { + mWidgets.add(widget); + } else { + Log.e(TAG, "Widget " + widget.provider + " can not fit on this device (" + + widget.minWidth + ", " + widget.minHeight + ")"); + } + } else { + Log.e(TAG, "Widget " + widget.provider + " has invalid dimensions (" + + widget.minWidth + ", " + widget.minHeight + ")"); + } + } else { + // just add shortcuts + mWidgets.add(o); + } + } + updatePageCountsAndInvalidateData(); + } + + public void setBulkBind(boolean bulkBind) { + if (bulkBind) { + mInBulkBind = true; + } else { + mInBulkBind = false; + if (mNeedToUpdatePageCountsAndInvalidateData) { + updatePageCountsAndInvalidateData(); + } + } + } + + private void updatePageCountsAndInvalidateData() { + if (mInBulkBind) { + mNeedToUpdatePageCountsAndInvalidateData = true; + } else { + updatePageCounts(); + invalidateOnDataChange(); + mNeedToUpdatePageCountsAndInvalidateData = false; + } + } + + @Override + public void onClick(View v) { + // When we have exited all apps or are in transition, disregard clicks + if (!mLauncher.isAllAppsVisible() || + mLauncher.getWorkspace().isSwitchingState()) return; + + if (v instanceof PagedViewIcon) { + // Animate some feedback to the click + final ApplicationInfo appInfo = (ApplicationInfo) v.getTag(); + + // Lock the drawable state to pressed until we return to Launcher + if (mPressedIcon != null) { + mPressedIcon.lockDrawableState(); + } + + // NOTE: We want all transitions from launcher to act as if the wallpaper were enabled + // to be consistent. So re-enable the flag here, and we will re-disable it as necessary + // when Launcher resumes and we are still in AllApps. + mLauncher.updateWallpaperVisibility(true); + mLauncher.startActivitySafely(v, appInfo.intent, appInfo); + + } else if (v instanceof PagedViewWidget) { + // Let the user know that they have to long press to add a widget + if (mWidgetInstructionToast != null) { + mWidgetInstructionToast.cancel(); + } + mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add, + Toast.LENGTH_SHORT); + mWidgetInstructionToast.show(); + + // Create a little animation to show that the widget can move + float offsetY = getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY); + final ImageView p = (ImageView) v.findViewById(R.id.widget_preview); + AnimatorSet bounce = LauncherAnimUtils.createAnimatorSet(); + ValueAnimator tyuAnim = LauncherAnimUtils.ofFloat(p, "translationY", offsetY); + tyuAnim.setDuration(125); + ValueAnimator tydAnim = LauncherAnimUtils.ofFloat(p, "translationY", 0f); + tydAnim.setDuration(100); + bounce.play(tyuAnim).before(tydAnim); + bounce.setInterpolator(new AccelerateInterpolator()); + bounce.start(); + } + } + + public boolean onKey(View v, int keyCode, KeyEvent event) { + return FocusHelper.handleAppsCustomizeKeyEvent(v, keyCode, event); + } + + /* + * PagedViewWithDraggableItems implementation + */ + @Override + protected void determineDraggingStart(android.view.MotionEvent ev) { + // Disable dragging by pulling an app down for now. + } + + private void beginDraggingApplication(View v) { + mLauncher.getWorkspace().onDragStartedWithItem(v); + mLauncher.getWorkspace().beginDragShared(v, this); + } + + Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { + Bundle options = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + AppWidgetResizeFrame.getWidgetSizeRanges(mLauncher, info.spanX, info.spanY, mTmpRect); + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(mLauncher, + info.componentName, null); + + float density = getResources().getDisplayMetrics().density; + int xPaddingDips = (int) ((padding.left + padding.right) / density); + int yPaddingDips = (int) ((padding.top + padding.bottom) / density); + + options = new Bundle(); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, + mTmpRect.left - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, + mTmpRect.top - yPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, + mTmpRect.right - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, + mTmpRect.bottom - yPaddingDips); + } + return options; + } + + private void preloadWidget(final PendingAddWidgetInfo info) { + final AppWidgetProviderInfo pInfo = info.info; + final Bundle options = getDefaultOptionsForWidget(mLauncher, info); + + if (pInfo.configure != null) { + info.bindOptions = options; + return; + } + + mWidgetCleanupState = WIDGET_PRELOAD_PENDING; + mBindWidgetRunnable = new Runnable() { + @Override + public void run() { + mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId(); + // Options will be null for platforms with JB or lower, so this serves as an + // SDK level check. + if (options == null) { + if (AppWidgetManager.getInstance(mLauncher).bindAppWidgetIdIfAllowed( + mWidgetLoadingId, info.componentName)) { + mWidgetCleanupState = WIDGET_BOUND; + } + } else { + if (AppWidgetManager.getInstance(mLauncher).bindAppWidgetIdIfAllowed( + mWidgetLoadingId, info.componentName, options)) { + mWidgetCleanupState = WIDGET_BOUND; + } + } + } + }; + post(mBindWidgetRunnable); + + mInflateWidgetRunnable = new Runnable() { + @Override + public void run() { + if (mWidgetCleanupState != WIDGET_BOUND) { + return; + } + AppWidgetHostView hostView = mLauncher. + getAppWidgetHost().createView(getContext(), mWidgetLoadingId, pInfo); + info.boundWidget = hostView; + mWidgetCleanupState = WIDGET_INFLATED; + hostView.setVisibility(INVISIBLE); + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(info.spanX, + info.spanY, info, false); + + // We want the first widget layout to be the correct size. This will be important + // for width size reporting to the AppWidgetManager. + DragLayer.LayoutParams lp = new DragLayer.LayoutParams(unScaledSize[0], + unScaledSize[1]); + lp.x = lp.y = 0; + lp.customPosition = true; + hostView.setLayoutParams(lp); + mLauncher.getDragLayer().addView(hostView); + } + }; + post(mInflateWidgetRunnable); + } + + @Override + public void onShortPress(View v) { + // We are anticipating a long press, and we use this time to load bind and instantiate + // the widget. This will need to be cleaned up if it turns out no long press occurs. + if (mCreateWidgetInfo != null) { + // Just in case the cleanup process wasn't properly executed. This shouldn't happen. + cleanupWidgetPreloading(false); + } + mCreateWidgetInfo = new PendingAddWidgetInfo((PendingAddWidgetInfo) v.getTag()); + preloadWidget(mCreateWidgetInfo); + } + + private void cleanupWidgetPreloading(boolean widgetWasAdded) { + if (!widgetWasAdded) { + // If the widget was not added, we may need to do further cleanup. + PendingAddWidgetInfo info = mCreateWidgetInfo; + mCreateWidgetInfo = null; + + if (mWidgetCleanupState == WIDGET_PRELOAD_PENDING) { + // We never did any preloading, so just remove pending callbacks to do so + removeCallbacks(mBindWidgetRunnable); + removeCallbacks(mInflateWidgetRunnable); + } else if (mWidgetCleanupState == WIDGET_BOUND) { + // Delete the widget id which was allocated + if (mWidgetLoadingId != -1) { + mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); + } + + // We never got around to inflating the widget, so remove the callback to do so. + removeCallbacks(mInflateWidgetRunnable); + } else if (mWidgetCleanupState == WIDGET_INFLATED) { + // Delete the widget id which was allocated + if (mWidgetLoadingId != -1) { + mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); + } + + // The widget was inflated and added to the DragLayer -- remove it. + AppWidgetHostView widget = info.boundWidget; + mLauncher.getDragLayer().removeView(widget); + } + } + mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; + mWidgetLoadingId = -1; + mCreateWidgetInfo = null; + PagedViewWidget.resetShortPressTarget(); + } + + @Override + public void cleanUpShortPress(View v) { + if (!mDraggingWidget) { + cleanupWidgetPreloading(false); + } + } + + private boolean beginDraggingWidget(View v) { + mDraggingWidget = true; + // Get the widget preview as the drag representation + ImageView image = (ImageView) v.findViewById(R.id.widget_preview); + PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); + + // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and + // we abort the drag. + if (image.getDrawable() == null) { + mDraggingWidget = false; + return false; + } + + // Compose the drag image + Bitmap preview; + Bitmap outline; + float scale = 1f; + Point previewPadding = null; + + if (createItemInfo instanceof PendingAddWidgetInfo) { + // This can happen in some weird cases involving multi-touch. We can't start dragging + // the widget if this is null, so we break out. + if (mCreateWidgetInfo == null) { + return false; + } + + PendingAddWidgetInfo createWidgetInfo = mCreateWidgetInfo; + createItemInfo = createWidgetInfo; + int spanX = createItemInfo.spanX; + int spanY = createItemInfo.spanY; + int[] size = mLauncher.getWorkspace().estimateItemSize(spanX, spanY, + createWidgetInfo, true); + + FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable(); + float minScale = 1.25f; + int maxWidth, maxHeight; + maxWidth = Math.min((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]); + maxHeight = Math.min((int) (previewDrawable.getIntrinsicHeight() * minScale), size[1]); + + int[] previewSizeBeforeScale = new int[1]; + + preview = mWidgetPreviewLoader.generateWidgetPreview(createWidgetInfo.componentName, + createWidgetInfo.previewImage, createWidgetInfo.icon, spanX, spanY, + maxWidth, maxHeight, null, previewSizeBeforeScale); + + // Compare the size of the drag preview to the preview in the AppsCustomize tray + int previewWidthInAppsCustomize = Math.min(previewSizeBeforeScale[0], + mWidgetPreviewLoader.maxWidthForWidgetPreview(spanX)); + scale = previewWidthInAppsCustomize / (float) preview.getWidth(); + + // The bitmap in the AppsCustomize tray is always the the same size, so there + // might be extra pixels around the preview itself - this accounts for that + if (previewWidthInAppsCustomize < previewDrawable.getIntrinsicWidth()) { + int padding = + (previewDrawable.getIntrinsicWidth() - previewWidthInAppsCustomize) / 2; + previewPadding = new Point(padding, 0); + } + } else { + PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); + Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.shortcutActivityInfo); + preview = Bitmap.createBitmap(icon.getIntrinsicWidth(), + icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + + mCanvas.setBitmap(preview); + mCanvas.save(); + WidgetPreviewLoader.renderDrawableToBitmap(icon, preview, 0, 0, + icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + mCanvas.restore(); + mCanvas.setBitmap(null); + createItemInfo.spanX = createItemInfo.spanY = 1; + } + + // Don't clip alpha values for the drag outline if we're using the default widget preview + boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo && + (((PendingAddWidgetInfo) createItemInfo).previewImage == 0)); + + // Save the preview for the outline generation, then dim the preview + outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(), + false); + + // Start the drag + mLauncher.lockScreenOrientation(); + mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, clipAlpha); + mDragController.startDrag(image, preview, this, createItemInfo, + DragController.DRAG_ACTION_COPY, previewPadding, scale); + outline.recycle(); + preview.recycle(); + return true; + } + + @Override + protected boolean beginDragging(final View v) { + if (!super.beginDragging(v)) return false; + + if (v instanceof PagedViewIcon) { + beginDraggingApplication(v); + } else if (v instanceof PagedViewWidget) { + if (!beginDraggingWidget(v)) { + return false; + } + } + + // We delay entering spring-loaded mode slightly to make sure the UI + // thready is free of any work. + postDelayed(new Runnable() { + @Override + public void run() { + // We don't enter spring-loaded mode if the drag has been cancelled + if (mLauncher.getDragController().isDragging()) { + // Dismiss the cling + mLauncher.dismissAllAppsCling(null); + + // Reset the alpha on the dragged icon before we drag + resetDrawableState(); + + // Go into spring loaded mode (must happen before we startDrag()) + mLauncher.enterSpringLoadedDragMode(); + } + } + }, 150); + + return true; + } + + /** + * Clean up after dragging. + * + * @param target where the item was dragged to (can be null if the item was flung) + */ + private void endDragging(View target, boolean isFlingToDelete, boolean success) { + if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && + !(target instanceof DeleteDropTarget))) { + // Exit spring loaded mode if we have not successfully dropped or have not handled the + // drop in Workspace + mLauncher.exitSpringLoadedDragMode(); + } + mLauncher.unlockScreenOrientation(false); + } + + @Override + public View getContent() { + return null; + } + + @Override + public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { + mInTransition = true; + if (toWorkspace) { + cancelAllTasks(); + } + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + mInTransition = false; + for (AsyncTaskPageData d : mDeferredSyncWidgetPageItems) { + onSyncWidgetPageItems(d); + } + mDeferredSyncWidgetPageItems.clear(); + for (Runnable r : mDeferredPrepareLoadWidgetPreviewsTasks) { + r.run(); + } + mDeferredPrepareLoadWidgetPreviewsTasks.clear(); + mForceDrawAllChildrenNextFrame = !toWorkspace; + } + + @Override + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + // Return early and wait for onFlingToDeleteCompleted if this was the result of a fling + if (isFlingToDelete) return; + + endDragging(target, false, success); + + // Display an error message if the drag failed due to there not being enough space on the + // target layout we were dropping on. + if (!success) { + boolean showOutOfSpaceMessage = false; + if (target instanceof Workspace) { + int currentScreen = mLauncher.getCurrentWorkspaceScreen(); + Workspace workspace = (Workspace) target; + CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); + ItemInfo itemInfo = (ItemInfo) d.dragInfo; + if (layout != null) { + layout.calculateSpans(itemInfo); + showOutOfSpaceMessage = + !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); + } + } + if (showOutOfSpaceMessage) { + mLauncher.showOutOfSpaceMessage(false); + } + + d.deferDragViewCleanupPostAnimation = false; + } + cleanupWidgetPreloading(success); + mDraggingWidget = false; + } + + @Override + public void onFlingToDeleteCompleted() { + // We just dismiss the drag when we fling, so cleanup here + endDragging(null, true, true); + cleanupWidgetPreloading(false); + mDraggingWidget = false; + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + cancelAllTasks(); + } + + public void clearAllWidgetPages() { + cancelAllTasks(); + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View v = getPageAt(i); + if (v instanceof PagedViewGridLayout) { + ((PagedViewGridLayout) v).removeAllViewsOnPage(); + mDirtyPageContent.set(i, true); + } + } + } + + private void cancelAllTasks() { + // Clean up all the async tasks + Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); + while (iter.hasNext()) { + AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + task.cancel(false); + iter.remove(); + mDirtyPageContent.set(task.page, true); + + // We've already preallocated the views for the data to load into, so clear them as well + View v = getPageAt(task.page); + if (v instanceof PagedViewGridLayout) { + ((PagedViewGridLayout) v).removeAllViewsOnPage(); + } + } + mDeferredSyncWidgetPageItems.clear(); + mDeferredPrepareLoadWidgetPreviewsTasks.clear(); + } + + public void setContentType(ContentType type) { + if (type == ContentType.Widgets) { + invalidatePageData(mNumAppsPages, true); + } else if (type == ContentType.Applications) { + invalidatePageData(0, true); + } + } + + protected void snapToPage(int whichPage, int delta, int duration) { + super.snapToPage(whichPage, delta, duration); + updateCurrentTab(whichPage); + + // Update the thread priorities given the direction lookahead + Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); + while (iter.hasNext()) { + AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + int pageIndex = task.page; + if ((mNextPage > mCurrentPage && pageIndex >= mCurrentPage) || + (mNextPage < mCurrentPage && pageIndex <= mCurrentPage)) { + task.setThreadPriority(getThreadPriorityForPage(pageIndex)); + } else { + task.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); + } + } + } + + private void updateCurrentTab(int currentPage) { + AppsCustomizeTabHost tabHost = getTabHost(); + if (tabHost != null) { + String tag = tabHost.getCurrentTabTag(); + if (tag != null) { + if (currentPage >= mNumAppsPages && + !tag.equals(tabHost.getTabTagForContentType(ContentType.Widgets))) { + tabHost.setCurrentTabFromContent(ContentType.Widgets); + } else if (currentPage < mNumAppsPages && + !tag.equals(tabHost.getTabTagForContentType(ContentType.Applications))) { + tabHost.setCurrentTabFromContent(ContentType.Applications); + } + } + } + } + + /* + * Apps PagedView implementation + */ + private void setVisibilityOnChildren(ViewGroup layout, int visibility) { + int childCount = layout.getChildCount(); + for (int i = 0; i < childCount; ++i) { + layout.getChildAt(i).setVisibility(visibility); + } + } + private void setupPage(PagedViewCellLayout layout) { + layout.setCellCount(mCellCountX, mCellCountY); + layout.setGap(mPageLayoutWidthGap, mPageLayoutHeightGap); + layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, + mPageLayoutPaddingRight, mPageLayoutPaddingBottom); + + // Note: We force a measure here to get around the fact that when we do layout calculations + // immediately after syncing, we don't have a proper width. That said, we already know the + // expected page width, so we can actually optimize by hiding all the TextView-based + // children that are expensive to measure, and let that happen naturally later. + setVisibilityOnChildren(layout, View.GONE); + int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST); + layout.setMinimumWidth(getPageContentWidth()); + layout.measure(widthSpec, heightSpec); + setVisibilityOnChildren(layout, View.VISIBLE); + } + + public void syncAppsPageItems(int page, boolean immediate) { + // ensure that we have the right number of items on the pages + final boolean isRtl = isLayoutRtl(); + int numCells = mCellCountX * mCellCountY; + int startIndex = page * numCells; + int endIndex = Math.min(startIndex + numCells, mApps.size()); + PagedViewCellLayout layout = (PagedViewCellLayout) getPageAt(page); + + layout.removeAllViewsOnPage(); + ArrayList<Object> items = new ArrayList<Object>(); + ArrayList<Bitmap> images = new ArrayList<Bitmap>(); + for (int i = startIndex; i < endIndex; ++i) { + ApplicationInfo info = mApps.get(i); + PagedViewIcon icon = (PagedViewIcon) mLayoutInflater.inflate( + R.layout.apps_customize_application, layout, false); + icon.applyFromApplicationInfo(info, true, this); + icon.setOnClickListener(this); + icon.setOnLongClickListener(this); + icon.setOnTouchListener(this); + icon.setOnKeyListener(this); + + int index = i - startIndex; + int x = index % mCellCountX; + int y = index / mCellCountX; + if (isRtl) { + x = mCellCountX - x - 1; + } + layout.addViewToCellLayout(icon, -1, i, new PagedViewCellLayout.LayoutParams(x,y, 1,1)); + + items.add(info); + images.add(info.iconBitmap); + } + + enableHwLayersOnVisiblePages(); + } + + /** + * A helper to return the priority for loading of the specified widget page. + */ + private int getWidgetPageLoadPriority(int page) { + // If we are snapping to another page, use that index as the target page index + int toPage = mCurrentPage; + if (mNextPage > -1) { + toPage = mNextPage; + } + + // We use the distance from the target page as an initial guess of priority, but if there + // are no pages of higher priority than the page specified, then bump up the priority of + // the specified page. + Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); + int minPageDiff = Integer.MAX_VALUE; + while (iter.hasNext()) { + AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + minPageDiff = Math.abs(task.page - toPage); + } + + int rawPageDiff = Math.abs(page - toPage); + return rawPageDiff - Math.min(rawPageDiff, minPageDiff); + } + /** + * Return the appropriate thread priority for loading for a given page (we give the current + * page much higher priority) + */ + private int getThreadPriorityForPage(int page) { + // TODO-APPS_CUSTOMIZE: detect number of cores and set thread priorities accordingly below + int pageDiff = getWidgetPageLoadPriority(page); + if (pageDiff <= 0) { + return Process.THREAD_PRIORITY_LESS_FAVORABLE; + } else if (pageDiff <= 1) { + return Process.THREAD_PRIORITY_LOWEST; + } else { + return Process.THREAD_PRIORITY_LOWEST; + } + } + private int getSleepForPage(int page) { + int pageDiff = getWidgetPageLoadPriority(page); + return Math.max(0, pageDiff * sPageSleepDelay); + } + /** + * Creates and executes a new AsyncTask to load a page of widget previews. + */ + private void prepareLoadWidgetPreviewsTask(int page, ArrayList<Object> widgets, + int cellWidth, int cellHeight, int cellCountX) { + + // Prune all tasks that are no longer needed + Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); + while (iter.hasNext()) { + AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + int taskPage = task.page; + if (taskPage < getAssociatedLowerPageBound(mCurrentPage) || + taskPage > getAssociatedUpperPageBound(mCurrentPage)) { + task.cancel(false); + iter.remove(); + } else { + task.setThreadPriority(getThreadPriorityForPage(taskPage)); + } + } + + // We introduce a slight delay to order the loading of side pages so that we don't thrash + final int sleepMs = getSleepForPage(page); + AsyncTaskPageData pageData = new AsyncTaskPageData(page, widgets, cellWidth, cellHeight, + new AsyncTaskCallback() { + @Override + public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { + try { + try { + Thread.sleep(sleepMs); + } catch (Exception e) {} + loadWidgetPreviewsInBackground(task, data); + } finally { + if (task.isCancelled()) { + data.cleanup(true); + } + } + } + }, + new AsyncTaskCallback() { + @Override + public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { + mRunningTasks.remove(task); + if (task.isCancelled()) return; + // do cleanup inside onSyncWidgetPageItems + onSyncWidgetPageItems(data); + } + }, mWidgetPreviewLoader); + + // Ensure that the task is appropriately prioritized and runs in parallel + AppsCustomizeAsyncTask t = new AppsCustomizeAsyncTask(page, + AsyncTaskPageData.Type.LoadWidgetPreviewData); + t.setThreadPriority(getThreadPriorityForPage(page)); + t.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pageData); + mRunningTasks.add(t); + } + + /* + * Widgets PagedView implementation + */ + private void setupPage(PagedViewGridLayout layout) { + layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, + mPageLayoutPaddingRight, mPageLayoutPaddingBottom); + + // Note: We force a measure here to get around the fact that when we do layout calculations + // immediately after syncing, we don't have a proper width. + int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST); + layout.setMinimumWidth(getPageContentWidth()); + layout.measure(widthSpec, heightSpec); + } + + public void syncWidgetPageItems(final int page, final boolean immediate) { + int numItemsPerPage = mWidgetCountX * mWidgetCountY; + + // Calculate the dimensions of each cell we are giving to each widget + final ArrayList<Object> items = new ArrayList<Object>(); + int contentWidth = mWidgetSpacingLayout.getContentWidth(); + final int cellWidth = ((contentWidth - mPageLayoutPaddingLeft - mPageLayoutPaddingRight + - ((mWidgetCountX - 1) * mWidgetWidthGap)) / mWidgetCountX); + int contentHeight = mWidgetSpacingLayout.getContentHeight(); + final int cellHeight = ((contentHeight - mPageLayoutPaddingTop - mPageLayoutPaddingBottom + - ((mWidgetCountY - 1) * mWidgetHeightGap)) / mWidgetCountY); + + // Prepare the set of widgets to load previews for in the background + int offset = (page - mNumAppsPages) * numItemsPerPage; + for (int i = offset; i < Math.min(offset + numItemsPerPage, mWidgets.size()); ++i) { + items.add(mWidgets.get(i)); + } + + // Prepopulate the pages with the other widget info, and fill in the previews later + final PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); + layout.setColumnCount(layout.getCellCountX()); + for (int i = 0; i < items.size(); ++i) { + Object rawInfo = items.get(i); + PendingAddItemInfo createItemInfo = null; + PagedViewWidget widget = (PagedViewWidget) mLayoutInflater.inflate( + R.layout.apps_customize_widget, layout, false); + if (rawInfo instanceof AppWidgetProviderInfo) { + // Fill in the widget information + AppWidgetProviderInfo info = (AppWidgetProviderInfo) rawInfo; + createItemInfo = new PendingAddWidgetInfo(info, null, null); + + // Determine the widget spans and min resize spans. + int[] spanXY = Launcher.getSpanForWidget(mLauncher, info); + createItemInfo.spanX = spanXY[0]; + createItemInfo.spanY = spanXY[1]; + int[] minSpanXY = Launcher.getMinSpanForWidget(mLauncher, info); + createItemInfo.minSpanX = minSpanXY[0]; + createItemInfo.minSpanY = minSpanXY[1]; + + widget.applyFromAppWidgetProviderInfo(info, -1, spanXY, mWidgetPreviewLoader); + widget.setTag(createItemInfo); + widget.setShortPressListener(this); + } else if (rawInfo instanceof ResolveInfo) { + // Fill in the shortcuts information + ResolveInfo info = (ResolveInfo) rawInfo; + createItemInfo = new PendingAddShortcutInfo(info.activityInfo); + createItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + createItemInfo.componentName = new ComponentName(info.activityInfo.packageName, + info.activityInfo.name); + widget.applyFromResolveInfo(mPackageManager, info, mWidgetPreviewLoader); + widget.setTag(createItemInfo); + } + widget.setOnClickListener(this); + widget.setOnLongClickListener(this); + widget.setOnTouchListener(this); + widget.setOnKeyListener(this); + + // Layout each widget + int ix = i % mWidgetCountX; + int iy = i / mWidgetCountX; + GridLayout.LayoutParams lp = new GridLayout.LayoutParams( + GridLayout.spec(iy, GridLayout.START), + GridLayout.spec(ix, GridLayout.TOP)); + lp.width = cellWidth; + lp.height = cellHeight; + lp.setGravity(Gravity.TOP | Gravity.START); + if (ix > 0) lp.leftMargin = mWidgetWidthGap; + if (iy > 0) lp.topMargin = mWidgetHeightGap; + layout.addView(widget, lp); + } + + // wait until a call on onLayout to start loading, because + // PagedViewWidget.getPreviewSize() will return 0 if it hasn't been laid out + // TODO: can we do a measure/layout immediately? + layout.setOnLayoutListener(new Runnable() { + public void run() { + // Load the widget previews + int maxPreviewWidth = cellWidth; + int maxPreviewHeight = cellHeight; + if (layout.getChildCount() > 0) { + PagedViewWidget w = (PagedViewWidget) layout.getChildAt(0); + int[] maxSize = w.getPreviewSize(); + maxPreviewWidth = maxSize[0]; + maxPreviewHeight = maxSize[1]; + } + + mWidgetPreviewLoader.setPreviewSize( + maxPreviewWidth, maxPreviewHeight, mWidgetSpacingLayout); + if (immediate) { + AsyncTaskPageData data = new AsyncTaskPageData(page, items, + maxPreviewWidth, maxPreviewHeight, null, null, mWidgetPreviewLoader); + loadWidgetPreviewsInBackground(null, data); + onSyncWidgetPageItems(data); + } else { + if (mInTransition) { + mDeferredPrepareLoadWidgetPreviewsTasks.add(this); + } else { + prepareLoadWidgetPreviewsTask(page, items, + maxPreviewWidth, maxPreviewHeight, mWidgetCountX); + } + } + layout.setOnLayoutListener(null); + } + }); + } + private void loadWidgetPreviewsInBackground(AppsCustomizeAsyncTask task, + AsyncTaskPageData data) { + // loadWidgetPreviewsInBackground can be called without a task to load a set of widget + // previews synchronously + if (task != null) { + // Ensure that this task starts running at the correct priority + task.syncThreadPriority(); + } + + // Load each of the widget/shortcut previews + ArrayList<Object> items = data.items; + ArrayList<Bitmap> images = data.generatedImages; + int count = items.size(); + for (int i = 0; i < count; ++i) { + if (task != null) { + // Ensure we haven't been cancelled yet + if (task.isCancelled()) break; + // Before work on each item, ensure that this task is running at the correct + // priority + task.syncThreadPriority(); + } + + images.add(mWidgetPreviewLoader.getPreview(items.get(i))); + } + } + + private void onSyncWidgetPageItems(AsyncTaskPageData data) { + if (mInTransition) { + mDeferredSyncWidgetPageItems.add(data); + return; + } + try { + int page = data.page; + PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); + + ArrayList<Object> items = data.items; + int count = items.size(); + for (int i = 0; i < count; ++i) { + PagedViewWidget widget = (PagedViewWidget) layout.getChildAt(i); + if (widget != null) { + Bitmap preview = data.generatedImages.get(i); + widget.applyPreview(new FastBitmapDrawable(preview), i); + } + } + + enableHwLayersOnVisiblePages(); + + // Update all thread priorities + Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); + while (iter.hasNext()) { + AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + int pageIndex = task.page; + task.setThreadPriority(getThreadPriorityForPage(pageIndex)); + } + } finally { + data.cleanup(false); + } + } + + @Override + public void syncPages() { + removeAllViews(); + cancelAllTasks(); + + Context context = getContext(); + for (int j = 0; j < mNumWidgetPages; ++j) { + PagedViewGridLayout layout = new PagedViewGridLayout(context, mWidgetCountX, + mWidgetCountY); + setupPage(layout); + addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + } + + for (int i = 0; i < mNumAppsPages; ++i) { + PagedViewCellLayout layout = new PagedViewCellLayout(context); + setupPage(layout); + addView(layout); + } + } + + @Override + public void syncPageItems(int page, boolean immediate) { + if (page < mNumAppsPages) { + syncAppsPageItems(page, immediate); + } else { + syncWidgetPageItems(page, immediate); + } + } + + // We want our pages to be z-ordered such that the further a page is to the left, the higher + // it is in the z-order. This is important to insure touch events are handled correctly. + View getPageAt(int index) { + return getChildAt(indexToPage(index)); + } + + @Override + protected int indexToPage(int index) { + return getChildCount() - index - 1; + } + + // In apps customize, we have a scrolling effect which emulates pulling cards off of a stack. + @Override + protected void screenScrolled(int screenCenter) { + final boolean isRtl = isLayoutRtl(); + super.screenScrolled(screenCenter); + + for (int i = 0; i < getChildCount(); i++) { + View v = getPageAt(i); + if (v != null) { + float scrollProgress = getScrollProgress(screenCenter, v, i); + + float interpolatedProgress; + float translationX; + float maxScrollProgress = Math.max(0, scrollProgress); + float minScrollProgress = Math.min(0, scrollProgress); + + if (isRtl) { + translationX = maxScrollProgress * v.getMeasuredWidth(); + interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(maxScrollProgress)); + } else { + translationX = minScrollProgress * v.getMeasuredWidth(); + interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(minScrollProgress)); + } + float scale = (1 - interpolatedProgress) + + interpolatedProgress * TRANSITION_SCALE_FACTOR; + + float alpha; + if (isRtl && (scrollProgress > 0)) { + alpha = mAlphaInterpolator.getInterpolation(1 - Math.abs(maxScrollProgress)); + } else if (!isRtl && (scrollProgress < 0)) { + alpha = mAlphaInterpolator.getInterpolation(1 - Math.abs(scrollProgress)); + } else { + // On large screens we need to fade the page as it nears its leftmost position + alpha = mLeftScreenAlphaInterpolator.getInterpolation(1 - scrollProgress); + } + + v.setCameraDistance(mDensity * CAMERA_DISTANCE); + int pageWidth = v.getMeasuredWidth(); + int pageHeight = v.getMeasuredHeight(); + + if (PERFORM_OVERSCROLL_ROTATION) { + float xPivot = isRtl ? 1f - TRANSITION_PIVOT : TRANSITION_PIVOT; + boolean isOverscrollingFirstPage = isRtl ? scrollProgress > 0 : scrollProgress < 0; + boolean isOverscrollingLastPage = isRtl ? scrollProgress < 0 : scrollProgress > 0; + + if (i == 0 && isOverscrollingFirstPage) { + // Overscroll to the left + v.setPivotX(xPivot * pageWidth); + v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress); + scale = 1.0f; + alpha = 1.0f; + // On the first page, we don't want the page to have any lateral motion + translationX = 0; + } else if (i == getChildCount() - 1 && isOverscrollingLastPage) { + // Overscroll to the right + v.setPivotX((1 - xPivot) * pageWidth); + v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress); + scale = 1.0f; + alpha = 1.0f; + // On the last page, we don't want the page to have any lateral motion. + translationX = 0; + } else { + v.setPivotY(pageHeight / 2.0f); + v.setPivotX(pageWidth / 2.0f); + v.setRotationY(0f); + } + } + + v.setTranslationX(translationX); + v.setScaleX(scale); + v.setScaleY(scale); + v.setAlpha(alpha); + + // If the view has 0 alpha, we set it to be invisible so as to prevent + // it from accepting touches + if (alpha == 0) { + v.setVisibility(INVISIBLE); + } else if (v.getVisibility() != VISIBLE) { + v.setVisibility(VISIBLE); + } + } + } + + enableHwLayersOnVisiblePages(); + } + + private void enableHwLayersOnVisiblePages() { + final int screenCount = getChildCount(); + + getVisiblePages(mTempVisiblePagesRange); + int leftScreen = mTempVisiblePagesRange[0]; + int rightScreen = mTempVisiblePagesRange[1]; + int forceDrawScreen = -1; + if (leftScreen == rightScreen) { + // make sure we're caching at least two pages always + if (rightScreen < screenCount - 1) { + rightScreen++; + forceDrawScreen = rightScreen; + } else if (leftScreen > 0) { + leftScreen--; + forceDrawScreen = leftScreen; + } + } else { + forceDrawScreen = leftScreen + 1; + } + + for (int i = 0; i < screenCount; i++) { + final View layout = (View) getPageAt(i); + if (!(leftScreen <= i && i <= rightScreen && + (i == forceDrawScreen || shouldDrawChild(layout)))) { + layout.setLayerType(LAYER_TYPE_NONE, null); + } + } + + for (int i = 0; i < screenCount; i++) { + final View layout = (View) getPageAt(i); + + if (leftScreen <= i && i <= rightScreen && + (i == forceDrawScreen || shouldDrawChild(layout))) { + if (layout.getLayerType() != LAYER_TYPE_HARDWARE) { + layout.setLayerType(LAYER_TYPE_HARDWARE, null); + } + } + } + } + + protected void overScroll(float amount) { + acceleratedOverScroll(amount); + } + + /** + * Used by the parent to get the content width to set the tab bar to + * @return + */ + public int getPageContentWidth() { + return mContentWidth; + } + + @Override + protected void onPageEndMoving() { + super.onPageEndMoving(); + mForceDrawAllChildrenNextFrame = true; + // We reset the save index when we change pages so that it will be recalculated on next + // rotation + mSaveInstanceStateItemIndex = -1; + } + + /* + * AllAppsView implementation + */ + public void setup(Launcher launcher, DragController dragController) { + mLauncher = launcher; + mDragController = dragController; + } + + /** + * We should call thise method whenever the core data changes (mApps, mWidgets) so that we can + * appropriately determine when to invalidate the PagedView page data. In cases where the data + * has yet to be set, we can requestLayout() and wait for onDataReady() to be called in the + * next onMeasure() pass, which will trigger an invalidatePageData() itself. + */ + private void invalidateOnDataChange() { + if (!isDataReady()) { + // The next layout pass will trigger data-ready if both widgets and apps are set, so + // request a layout to trigger the page data when ready. + requestLayout(); + } else { + cancelAllTasks(); + invalidatePageData(); + } + } + + public void setApps(ArrayList<ApplicationInfo> list) { + mApps = list; + Collections.sort(mApps, LauncherModel.getAppNameComparator()); + updatePageCountsAndInvalidateData(); + } + private void addAppsWithoutInvalidate(ArrayList<ApplicationInfo> list) { + // We add it in place, in alphabetical order + int count = list.size(); + for (int i = 0; i < count; ++i) { + ApplicationInfo info = list.get(i); + int index = Collections.binarySearch(mApps, info, LauncherModel.getAppNameComparator()); + if (index < 0) { + mApps.add(-(index + 1), info); + } + } + } + public void addApps(ArrayList<ApplicationInfo> list) { + addAppsWithoutInvalidate(list); + updatePageCountsAndInvalidateData(); + } + private int findAppByComponent(List<ApplicationInfo> list, ApplicationInfo item) { + ComponentName removeComponent = item.intent.getComponent(); + int length = list.size(); + for (int i = 0; i < length; ++i) { + ApplicationInfo info = list.get(i); + if (info.intent.getComponent().equals(removeComponent)) { + return i; + } + } + return -1; + } + private void removeAppsWithoutInvalidate(ArrayList<ApplicationInfo> list) { + // loop through all the apps and remove apps that have the same component + int length = list.size(); + for (int i = 0; i < length; ++i) { + ApplicationInfo info = list.get(i); + int removeIndex = findAppByComponent(mApps, info); + if (removeIndex > -1) { + mApps.remove(removeIndex); + } + } + } + public void removeApps(ArrayList<ApplicationInfo> appInfos) { + removeAppsWithoutInvalidate(appInfos); + updatePageCountsAndInvalidateData(); + } + public void updateApps(ArrayList<ApplicationInfo> list) { + // We remove and re-add the updated applications list because it's properties may have + // changed (ie. the title), and this will ensure that the items will be in their proper + // place in the list. + removeAppsWithoutInvalidate(list); + addAppsWithoutInvalidate(list); + updatePageCountsAndInvalidateData(); + } + + public void reset() { + // If we have reset, then we should not continue to restore the previous state + mSaveInstanceStateItemIndex = -1; + + AppsCustomizeTabHost tabHost = getTabHost(); + String tag = tabHost.getCurrentTabTag(); + if (tag != null) { + if (!tag.equals(tabHost.getTabTagForContentType(ContentType.Applications))) { + tabHost.setCurrentTabFromContent(ContentType.Applications); + } + } + + if (mCurrentPage != 0) { + invalidatePageData(0); + } + } + + private AppsCustomizeTabHost getTabHost() { + return (AppsCustomizeTabHost) mLauncher.findViewById(R.id.apps_customize_pane); + } + + public void dumpState() { + // TODO: Dump information related to current list of Applications, Widgets, etc. + ApplicationInfo.dumpApplicationInfoList(TAG, "mApps", mApps); + dumpAppWidgetProviderInfoList(TAG, "mWidgets", mWidgets); + } + + private void dumpAppWidgetProviderInfoList(String tag, String label, + ArrayList<Object> list) { + Log.d(tag, label + " size=" + list.size()); + for (Object i: list) { + if (i instanceof AppWidgetProviderInfo) { + AppWidgetProviderInfo info = (AppWidgetProviderInfo) i; + Log.d(tag, " label=\"" + info.label + "\" previewImage=" + info.previewImage + + " resizeMode=" + info.resizeMode + " configure=" + info.configure + + " initialLayout=" + info.initialLayout + + " minWidth=" + info.minWidth + " minHeight=" + info.minHeight); + } else if (i instanceof ResolveInfo) { + ResolveInfo info = (ResolveInfo) i; + Log.d(tag, " label=\"" + info.loadLabel(mPackageManager) + "\" icon=" + + info.icon); + } + } + } + + public void surrender() { + // TODO: If we are in the middle of any process (ie. for holographic outlines, etc) we + // should stop this now. + + // Stop all background tasks + cancelAllTasks(); + } + + @Override + public void iconPressed(PagedViewIcon icon) { + // Reset the previously pressed icon and store a reference to the pressed icon so that + // we can reset it on return to Launcher (in Launcher.onResume()) + if (mPressedIcon != null) { + mPressedIcon.resetDrawableState(); + } + mPressedIcon = icon; + } + + public void resetDrawableState() { + if (mPressedIcon != null) { + mPressedIcon.resetDrawableState(); + mPressedIcon = null; + } + } + + /* + * We load an extra page on each side to prevent flashes from scrolling and loading of the + * widget previews in the background with the AsyncTasks. + */ + final static int sLookBehindPageCount = 2; + final static int sLookAheadPageCount = 2; + protected int getAssociatedLowerPageBound(int page) { + final int count = getChildCount(); + int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1); + int windowMinIndex = Math.max(Math.min(page - sLookBehindPageCount, count - windowSize), 0); + return windowMinIndex; + } + protected int getAssociatedUpperPageBound(int page) { + final int count = getChildCount(); + int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1); + int windowMaxIndex = Math.min(Math.max(page + sLookAheadPageCount, windowSize - 1), + count - 1); + return windowMaxIndex; + } + + @Override + protected String getCurrentPageDescription() { + int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + int stringId = R.string.default_scroll_format; + int count = 0; + + if (page < mNumAppsPages) { + stringId = R.string.apps_customize_apps_scroll_format; + count = mNumAppsPages; + } else { + page -= mNumAppsPages; + stringId = R.string.apps_customize_widgets_scroll_format; + count = mNumWidgetPages; + } + + return String.format(getContext().getString(stringId), page + 1, count); + } +} diff --git a/src/com/android/launcher3/AppsCustomizeTabHost.java b/src/com/android/launcher3/AppsCustomizeTabHost.java new file mode 100644 index 000000000..5d50fec03 --- /dev/null +++ b/src/com/android/launcher3/AppsCustomizeTabHost.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TabHost; +import android.widget.TabWidget; +import android.widget.TextView; + +import com.android.launcher3.R; + +import java.util.ArrayList; + +public class AppsCustomizeTabHost extends TabHost implements LauncherTransitionable, + TabHost.OnTabChangeListener { + static final String LOG_TAG = "AppsCustomizeTabHost"; + + private static final String APPS_TAB_TAG = "APPS"; + private static final String WIDGETS_TAB_TAG = "WIDGETS"; + + private final LayoutInflater mLayoutInflater; + private ViewGroup mTabs; + private ViewGroup mTabsContainer; + private AppsCustomizePagedView mAppsCustomizePane; + private FrameLayout mAnimationBuffer; + private LinearLayout mContent; + + private boolean mInTransition; + private boolean mTransitioningToWorkspace; + private boolean mResetAfterTransition; + private Runnable mRelayoutAndMakeVisible; + + public AppsCustomizeTabHost(Context context, AttributeSet attrs) { + super(context, attrs); + mLayoutInflater = LayoutInflater.from(context); + mRelayoutAndMakeVisible = new Runnable() { + public void run() { + mTabs.requestLayout(); + mTabsContainer.setAlpha(1f); + } + }; + } + + /** + * Convenience methods to select specific tabs. We want to set the content type immediately + * in these cases, but we note that we still call setCurrentTabByTag() so that the tab view + * reflects the new content (but doesn't do the animation and logic associated with changing + * tabs manually). + */ + void setContentTypeImmediate(AppsCustomizePagedView.ContentType type) { + setOnTabChangedListener(null); + onTabChangedStart(); + onTabChangedEnd(type); + setCurrentTabByTag(getTabTagForContentType(type)); + setOnTabChangedListener(this); + } + void selectAppsTab() { + setContentTypeImmediate(AppsCustomizePagedView.ContentType.Applications); + } + void selectWidgetsTab() { + setContentTypeImmediate(AppsCustomizePagedView.ContentType.Widgets); + } + + /** + * Setup the tab host and create all necessary tabs. + */ + @Override + protected void onFinishInflate() { + // Setup the tab host + setup(); + + final ViewGroup tabsContainer = (ViewGroup) findViewById(R.id.tabs_container); + final TabWidget tabs = getTabWidget(); + final AppsCustomizePagedView appsCustomizePane = (AppsCustomizePagedView) + findViewById(R.id.apps_customize_pane_content); + mTabs = tabs; + mTabsContainer = tabsContainer; + mAppsCustomizePane = appsCustomizePane; + mAnimationBuffer = (FrameLayout) findViewById(R.id.animation_buffer); + mContent = (LinearLayout) findViewById(R.id.apps_customize_content); + if (tabs == null || mAppsCustomizePane == null) throw new Resources.NotFoundException(); + + // Configure the tabs content factory to return the same paged view (that we change the + // content filter on) + TabContentFactory contentFactory = new TabContentFactory() { + public View createTabContent(String tag) { + return appsCustomizePane; + } + }; + + // Create the tabs + TextView tabView; + String label; + label = getContext().getString(R.string.all_apps_button_label); + tabView = (TextView) mLayoutInflater.inflate(R.layout.tab_widget_indicator, tabs, false); + tabView.setText(label); + tabView.setContentDescription(label); + addTab(newTabSpec(APPS_TAB_TAG).setIndicator(tabView).setContent(contentFactory)); + label = getContext().getString(R.string.widgets_tab_label); + tabView = (TextView) mLayoutInflater.inflate(R.layout.tab_widget_indicator, tabs, false); + tabView.setText(label); + tabView.setContentDescription(label); + addTab(newTabSpec(WIDGETS_TAB_TAG).setIndicator(tabView).setContent(contentFactory)); + setOnTabChangedListener(this); + + // Setup the key listener to jump between the last tab view and the market icon + AppsCustomizeTabKeyEventListener keyListener = new AppsCustomizeTabKeyEventListener(); + View lastTab = tabs.getChildTabViewAt(tabs.getTabCount() - 1); + lastTab.setOnKeyListener(keyListener); + View shopButton = findViewById(R.id.market_button); + shopButton.setOnKeyListener(keyListener); + + // Hide the tab bar until we measure + mTabsContainer.setAlpha(0f); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + boolean remeasureTabWidth = (mTabs.getLayoutParams().width <= 0); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Set the width of the tab list to the content width + if (remeasureTabWidth) { + int contentWidth = mAppsCustomizePane.getPageContentWidth(); + if (contentWidth > 0 && mTabs.getLayoutParams().width != contentWidth) { + // Set the width and show the tab bar + mTabs.getLayoutParams().width = contentWidth; + mRelayoutAndMakeVisible.run(); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + public boolean onInterceptTouchEvent(MotionEvent ev) { + // If we are mid transitioning to the workspace, then intercept touch events here so we + // can ignore them, otherwise we just let all apps handle the touch events. + if (mInTransition && mTransitioningToWorkspace) { + return true; + } + return super.onInterceptTouchEvent(ev); + }; + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Allow touch events to fall through to the workspace if we are transitioning there + if (mInTransition && mTransitioningToWorkspace) { + return super.onTouchEvent(event); + } + + // Intercept all touch events up to the bottom of the AppsCustomizePane so they do not fall + // through to the workspace and trigger showWorkspace() + if (event.getY() < mAppsCustomizePane.getBottom()) { + return true; + } + return super.onTouchEvent(event); + } + + private void onTabChangedStart() { + mAppsCustomizePane.hideScrollingIndicator(false); + } + + private void reloadCurrentPage() { + if (!LauncherApplication.isScreenLarge()) { + mAppsCustomizePane.flashScrollingIndicator(true); + } + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); + mAppsCustomizePane.requestFocus(); + } + + private void onTabChangedEnd(AppsCustomizePagedView.ContentType type) { + mAppsCustomizePane.setContentType(type); + } + + @Override + public void onTabChanged(String tabId) { + final AppsCustomizePagedView.ContentType type = getContentTypeForTabTag(tabId); + + // Animate the changing of the tab content by fading pages in and out + final Resources res = getResources(); + final int duration = res.getInteger(R.integer.config_tabTransitionDuration); + + // We post a runnable here because there is a delay while the first page is loading and + // the feedback from having changed the tab almost feels better than having it stick + post(new Runnable() { + @Override + public void run() { + if (mAppsCustomizePane.getMeasuredWidth() <= 0 || + mAppsCustomizePane.getMeasuredHeight() <= 0) { + reloadCurrentPage(); + return; + } + + // Take the visible pages and re-parent them temporarily to mAnimatorBuffer + // and then cross fade to the new pages + int[] visiblePageRange = new int[2]; + mAppsCustomizePane.getVisiblePages(visiblePageRange); + if (visiblePageRange[0] == -1 && visiblePageRange[1] == -1) { + // If we can't get the visible page ranges, then just skip the animation + reloadCurrentPage(); + return; + } + ArrayList<View> visiblePages = new ArrayList<View>(); + for (int i = visiblePageRange[0]; i <= visiblePageRange[1]; i++) { + visiblePages.add(mAppsCustomizePane.getPageAt(i)); + } + + // We want the pages to be rendered in exactly the same way as they were when + // their parent was mAppsCustomizePane -- so set the scroll on mAnimationBuffer + // to be exactly the same as mAppsCustomizePane, and below, set the left/top + // parameters to be correct for each of the pages + mAnimationBuffer.scrollTo(mAppsCustomizePane.getScrollX(), 0); + + // mAppsCustomizePane renders its children in reverse order, so + // add the pages to mAnimationBuffer in reverse order to match that behavior + for (int i = visiblePages.size() - 1; i >= 0; i--) { + View child = visiblePages.get(i); + if (child instanceof PagedViewCellLayout) { + ((PagedViewCellLayout) child).resetChildrenOnKeyListeners(); + } else if (child instanceof PagedViewGridLayout) { + ((PagedViewGridLayout) child).resetChildrenOnKeyListeners(); + } + PagedViewWidget.setDeletePreviewsWhenDetachedFromWindow(false); + mAppsCustomizePane.removeView(child); + PagedViewWidget.setDeletePreviewsWhenDetachedFromWindow(true); + mAnimationBuffer.setAlpha(1f); + mAnimationBuffer.setVisibility(View.VISIBLE); + LayoutParams p = new FrameLayout.LayoutParams(child.getMeasuredWidth(), + child.getMeasuredHeight()); + p.setMargins((int) child.getLeft(), (int) child.getTop(), 0, 0); + mAnimationBuffer.addView(child, p); + } + + // Toggle the new content + onTabChangedStart(); + onTabChangedEnd(type); + + // Animate the transition + ObjectAnimator outAnim = LauncherAnimUtils.ofFloat(mAnimationBuffer, "alpha", 0f); + outAnim.addListener(new AnimatorListenerAdapter() { + private void clearAnimationBuffer() { + mAnimationBuffer.setVisibility(View.GONE); + PagedViewWidget.setRecyclePreviewsWhenDetachedFromWindow(false); + mAnimationBuffer.removeAllViews(); + PagedViewWidget.setRecyclePreviewsWhenDetachedFromWindow(true); + } + @Override + public void onAnimationEnd(Animator animation) { + clearAnimationBuffer(); + } + @Override + public void onAnimationCancel(Animator animation) { + clearAnimationBuffer(); + } + }); + ObjectAnimator inAnim = LauncherAnimUtils.ofFloat(mAppsCustomizePane, "alpha", 1f); + inAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + reloadCurrentPage(); + } + }); + + final AnimatorSet animSet = LauncherAnimUtils.createAnimatorSet(); + animSet.playTogether(outAnim, inAnim); + animSet.setDuration(duration); + animSet.start(); + } + }); + } + + public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) { + setOnTabChangedListener(null); + setCurrentTabByTag(getTabTagForContentType(type)); + setOnTabChangedListener(this); + } + + /** + * Returns the content type for the specified tab tag. + */ + public AppsCustomizePagedView.ContentType getContentTypeForTabTag(String tag) { + if (tag.equals(APPS_TAB_TAG)) { + return AppsCustomizePagedView.ContentType.Applications; + } else if (tag.equals(WIDGETS_TAB_TAG)) { + return AppsCustomizePagedView.ContentType.Widgets; + } + return AppsCustomizePagedView.ContentType.Applications; + } + + /** + * Returns the tab tag for a given content type. + */ + public String getTabTagForContentType(AppsCustomizePagedView.ContentType type) { + if (type == AppsCustomizePagedView.ContentType.Applications) { + return APPS_TAB_TAG; + } else if (type == AppsCustomizePagedView.ContentType.Widgets) { + return WIDGETS_TAB_TAG; + } + return APPS_TAB_TAG; + } + + /** + * Disable focus on anything under this view in the hierarchy if we are not visible. + */ + @Override + public int getDescendantFocusability() { + if (getVisibility() != View.VISIBLE) { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } + return super.getDescendantFocusability(); + } + + void reset() { + if (mInTransition) { + // Defer to after the transition to reset + mResetAfterTransition = true; + } else { + // Reset immediately + mAppsCustomizePane.reset(); + } + } + + private void enableAndBuildHardwareLayer() { + // isHardwareAccelerated() checks if we're attached to a window and if that + // window is HW accelerated-- we were sometimes not attached to a window + // and buildLayer was throwing an IllegalStateException + if (isHardwareAccelerated()) { + // Turn on hardware layers for performance + setLayerType(LAYER_TYPE_HARDWARE, null); + + // force building the layer, so you don't get a blip early in an animation + // when the layer is created layer + buildLayer(); + } + } + + @Override + public View getContent() { + return mContent; + } + + /* LauncherTransitionable overrides */ + @Override + public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { + mAppsCustomizePane.onLauncherTransitionPrepare(l, animated, toWorkspace); + mInTransition = true; + mTransitioningToWorkspace = toWorkspace; + + if (toWorkspace) { + // Going from All Apps -> Workspace + setVisibilityOfSiblingsWithLowerZOrder(VISIBLE); + // Stop the scrolling indicator - we don't want All Apps to be invalidating itself + // during the transition, especially since it has a hardware layer set on it + mAppsCustomizePane.cancelScrollingIndicatorAnimations(); + } else { + // Going from Workspace -> All Apps + mContent.setVisibility(VISIBLE); + + // Make sure the current page is loaded (we start loading the side pages after the + // transition to prevent slowing down the animation) + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true); + + if (!LauncherApplication.isScreenLarge()) { + mAppsCustomizePane.showScrollingIndicator(true); + } + } + + if (mResetAfterTransition) { + mAppsCustomizePane.reset(); + mResetAfterTransition = false; + } + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + if (animated) { + enableAndBuildHardwareLayer(); + } + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + // Do nothing + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + mAppsCustomizePane.onLauncherTransitionEnd(l, animated, toWorkspace); + mInTransition = false; + if (animated) { + setLayerType(LAYER_TYPE_NONE, null); + } + + if (!toWorkspace) { + // Dismiss the workspace cling + l.dismissWorkspaceCling(null); + // Show the all apps cling (if not already shown) + mAppsCustomizePane.showAllAppsCling(); + // Make sure adjacent pages are loaded (we wait until after the transition to + // prevent slowing down the animation) + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); + + if (!LauncherApplication.isScreenLarge()) { + mAppsCustomizePane.hideScrollingIndicator(false); + } + + // Going from Workspace -> All Apps + // NOTE: We should do this at the end since we check visibility state in some of the + // cling initialization/dismiss code above. + setVisibilityOfSiblingsWithLowerZOrder(INVISIBLE); + } + } + + private void setVisibilityOfSiblingsWithLowerZOrder(int visibility) { + ViewGroup parent = (ViewGroup) getParent(); + if (parent == null) return; + + final int count = parent.getChildCount(); + if (!isChildrenDrawingOrderEnabled()) { + for (int i = 0; i < count; i++) { + final View child = parent.getChildAt(i); + if (child == this) { + break; + } else { + if (child.getVisibility() == GONE) { + continue; + } + child.setVisibility(visibility); + } + } + } else { + throw new RuntimeException("Failed; can't get z-order of views"); + } + } + + public void onWindowVisible() { + if (getVisibility() == VISIBLE) { + mContent.setVisibility(VISIBLE); + // We unload the widget previews when the UI is hidden, so need to reload pages + // Load the current page synchronously, and the neighboring pages asynchronously + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true); + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); + } + } + + public void onTrimMemory() { + mContent.setVisibility(GONE); + // Clear the widget pages of all their subviews - this will trigger the widget previews + // to delete their bitmaps + mAppsCustomizePane.clearAllWidgetPages(); + } + + boolean isTransitioning() { + return mInTransition; + } +} diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java new file mode 100644 index 000000000..7cac8a68c --- /dev/null +++ b/src/com/android/launcher3/BubbleTextView.java @@ -0,0 +1,342 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.TextView; + +/** + * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan + * because we want to make the bubble taller than the text and TextView's clip is + * too aggressive. + */ +public class BubbleTextView extends TextView { + static final float CORNER_RADIUS = 4.0f; + static final float SHADOW_LARGE_RADIUS = 4.0f; + static final float SHADOW_SMALL_RADIUS = 1.75f; + static final float SHADOW_Y_OFFSET = 2.0f; + static final int SHADOW_LARGE_COLOUR = 0xDD000000; + static final int SHADOW_SMALL_COLOUR = 0xCC000000; + static final float PADDING_H = 8.0f; + static final float PADDING_V = 3.0f; + + private int mPrevAlpha = -1; + + private final HolographicOutlineHelper mOutlineHelper = new HolographicOutlineHelper(); + private final Canvas mTempCanvas = new Canvas(); + private final Rect mTempRect = new Rect(); + private boolean mDidInvalidateForPressedState; + private Bitmap mPressedOrFocusedBackground; + private int mFocusedOutlineColor; + private int mFocusedGlowColor; + private int mPressedOutlineColor; + private int mPressedGlowColor; + + private boolean mBackgroundSizeChanged; + private Drawable mBackground; + + private boolean mStayPressed; + private CheckLongPressHelper mLongPressHelper; + + public BubbleTextView(Context context) { + super(context); + init(); + } + + public BubbleTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + mLongPressHelper = new CheckLongPressHelper(this); + mBackground = getBackground(); + + final Resources res = getContext().getResources(); + mFocusedOutlineColor = mFocusedGlowColor = mPressedOutlineColor = mPressedGlowColor = + res.getColor(android.R.color.holo_blue_light); + + setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); + } + + public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) { + Bitmap b = info.getIcon(iconCache); + + setCompoundDrawablesWithIntrinsicBounds(null, + new FastBitmapDrawable(b), + null, null); + setText(info.title); + setTag(info); + } + + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) { + mBackgroundSizeChanged = true; + } + return super.setFrame(left, top, right, bottom); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mBackground || super.verifyDrawable(who); + } + + @Override + public void setTag(Object tag) { + if (tag != null) { + LauncherModel.checkItemInfo((ItemInfo) tag); + } + super.setTag(tag); + } + + @Override + protected void drawableStateChanged() { + if (isPressed()) { + // In this case, we have already created the pressed outline on ACTION_DOWN, + // so we just need to do an invalidate to trigger draw + if (!mDidInvalidateForPressedState) { + setCellLayoutPressedOrFocusedIcon(); + } + } else { + // Otherwise, either clear the pressed/focused background, or create a background + // for the focused state + final boolean backgroundEmptyBefore = mPressedOrFocusedBackground == null; + if (!mStayPressed) { + mPressedOrFocusedBackground = null; + } + if (isFocused()) { + if (getLayout() == null) { + // In some cases, we get focus before we have been layed out. Set the + // background to null so that it will get created when the view is drawn. + mPressedOrFocusedBackground = null; + } else { + mPressedOrFocusedBackground = createGlowingOutline( + mTempCanvas, mFocusedGlowColor, mFocusedOutlineColor); + } + mStayPressed = false; + setCellLayoutPressedOrFocusedIcon(); + } + final boolean backgroundEmptyNow = mPressedOrFocusedBackground == null; + if (!backgroundEmptyBefore && backgroundEmptyNow) { + setCellLayoutPressedOrFocusedIcon(); + } + } + + Drawable d = mBackground; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + super.drawableStateChanged(); + } + + /** + * Draw this BubbleTextView into the given Canvas. + * + * @param destCanvas the canvas to draw on + * @param padding the horizontal and vertical padding to use when drawing + */ + private void drawWithPadding(Canvas destCanvas, int padding) { + final Rect clipRect = mTempRect; + getDrawingRect(clipRect); + + // adjust the clip rect so that we don't include the text label + clipRect.bottom = + getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V + getLayout().getLineTop(0); + + // Draw the View into the bitmap. + // The translate of scrollX and scrollY is necessary when drawing TextViews, because + // they set scrollX and scrollY to large values to achieve centered text + destCanvas.save(); + destCanvas.scale(getScaleX(), getScaleY(), + (getWidth() + padding) / 2, (getHeight() + padding) / 2); + destCanvas.translate(-getScrollX() + padding / 2, -getScrollY() + padding / 2); + destCanvas.clipRect(clipRect, Op.REPLACE); + draw(destCanvas); + destCanvas.restore(); + } + + /** + * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createGlowingOutline(Canvas canvas, int outlineColor, int glowColor) { + final int padding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS; + final Bitmap b = Bitmap.createBitmap( + getWidth() + padding, getHeight() + padding, Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + drawWithPadding(canvas, padding); + mOutlineHelper.applyExtraThickExpensiveOutlineWithBlur(b, canvas, glowColor, outlineColor); + canvas.setBitmap(null); + + return b; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Call the superclass onTouchEvent first, because sometimes it changes the state to + // isPressed() on an ACTION_UP + boolean result = super.onTouchEvent(event); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // So that the pressed outline is visible immediately when isPressed() is true, + // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time + // to create it) + if (mPressedOrFocusedBackground == null) { + mPressedOrFocusedBackground = createGlowingOutline( + mTempCanvas, mPressedGlowColor, mPressedOutlineColor); + } + // Invalidate so the pressed state is visible, or set a flag so we know that we + // have to call invalidate as soon as the state is "pressed" + if (isPressed()) { + mDidInvalidateForPressedState = true; + setCellLayoutPressedOrFocusedIcon(); + } else { + mDidInvalidateForPressedState = false; + } + + mLongPressHelper.postCheckForLongPress(); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + // If we've touched down and up on an item, and it's still not "pressed", then + // destroy the pressed outline + if (!isPressed()) { + mPressedOrFocusedBackground = null; + } + + mLongPressHelper.cancelLongPress(); + break; + } + return result; + } + + void setStayPressed(boolean stayPressed) { + mStayPressed = stayPressed; + if (!stayPressed) { + mPressedOrFocusedBackground = null; + } + setCellLayoutPressedOrFocusedIcon(); + } + + void setCellLayoutPressedOrFocusedIcon() { + if (getParent() instanceof ShortcutAndWidgetContainer) { + ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) getParent(); + if (parent != null) { + CellLayout layout = (CellLayout) parent.getParent(); + layout.setPressedOrFocusedIcon((mPressedOrFocusedBackground != null) ? this : null); + } + } + } + + void clearPressedOrFocusedBackground() { + mPressedOrFocusedBackground = null; + setCellLayoutPressedOrFocusedIcon(); + } + + Bitmap getPressedOrFocusedBackground() { + return mPressedOrFocusedBackground; + } + + int getPressedOrFocusedBackgroundPadding() { + return HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS / 2; + } + + @Override + public void draw(Canvas canvas) { + final Drawable background = mBackground; + if (background != null) { + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + + if (mBackgroundSizeChanged) { + background.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); + mBackgroundSizeChanged = false; + } + + if ((scrollX | scrollY) == 0) { + background.draw(canvas); + } else { + canvas.translate(scrollX, scrollY); + background.draw(canvas); + canvas.translate(-scrollX, -scrollY); + } + } + + // If text is transparent, don't draw any shadow + if (getCurrentTextColor() == getResources().getColor(android.R.color.transparent)) { + getPaint().clearShadowLayer(); + super.draw(canvas); + return; + } + + // We enhance the shadow by drawing the shadow twice + getPaint().setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); + super.draw(canvas); + canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(), + getScrollX() + getWidth(), + getScrollY() + getHeight(), Region.Op.INTERSECT); + getPaint().setShadowLayer(SHADOW_SMALL_RADIUS, 0.0f, 0.0f, SHADOW_SMALL_COLOUR); + super.draw(canvas); + canvas.restore(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mBackground != null) mBackground.setCallback(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mBackground != null) mBackground.setCallback(null); + } + + @Override + protected boolean onSetAlpha(int alpha) { + if (mPrevAlpha != alpha) { + mPrevAlpha = alpha; + super.onSetAlpha(alpha); + } + return true; + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + mLongPressHelper.cancelLongPress(); + } +} diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java new file mode 100644 index 000000000..a7486a8e3 --- /dev/null +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import com.android.launcher3.R; + + +/** + * Implements a DropTarget. + */ +public class ButtonDropTarget extends TextView implements DropTarget, DragController.DragListener { + + protected final int mTransitionDuration; + + protected Launcher mLauncher; + private int mBottomDragPadding; + protected TextView mText; + protected SearchDropTargetBar mSearchDropTargetBar; + + /** Whether this drop target is active for the current drag */ + protected boolean mActive; + + /** The paint applied to the drag view on hover */ + protected int mHoverColor = 0; + + public ButtonDropTarget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ButtonDropTarget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + Resources r = getResources(); + mTransitionDuration = r.getInteger(R.integer.config_dropTargetBgTransitionDuration); + mBottomDragPadding = r.getDimensionPixelSize(R.dimen.drop_target_drag_padding); + } + + void setLauncher(Launcher launcher) { + mLauncher = launcher; + } + + public boolean acceptDrop(DragObject d) { + return false; + } + + public void setSearchDropTargetBar(SearchDropTargetBar searchDropTargetBar) { + mSearchDropTargetBar = searchDropTargetBar; + } + + protected Drawable getCurrentDrawable() { + Drawable[] drawables = getCompoundDrawablesRelative(); + for (int i = 0; i < drawables.length; ++i) { + if (drawables[i] != null) { + return drawables[i]; + } + } + return null; + } + + public void onDrop(DragObject d) { + } + + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + + public void onDragEnter(DragObject d) { + d.dragView.setColor(mHoverColor); + } + + public void onDragOver(DragObject d) { + // Do nothing + } + + public void onDragExit(DragObject d) { + d.dragView.setColor(0); + } + + public void onDragStart(DragSource source, Object info, int dragAction) { + // Do nothing + } + + public boolean isDropEnabled() { + return mActive; + } + + public void onDragEnd() { + // Do nothing + } + + @Override + public void getHitRect(android.graphics.Rect outRect) { + super.getHitRect(outRect); + outRect.bottom += mBottomDragPadding; + } + + private boolean isRtl() { + return (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + } + + Rect getIconRect(int viewWidth, int viewHeight, int drawableWidth, int drawableHeight) { + DragLayer dragLayer = mLauncher.getDragLayer(); + + // Find the rect to animate to (the view is center aligned) + Rect to = new Rect(); + dragLayer.getViewRectRelativeToSelf(this, to); + + final int width = drawableWidth; + final int height = drawableHeight; + + final int left; + final int right; + + if (isRtl()) { + right = to.right - getPaddingRight(); + left = right - width; + } else { + left = to.left + getPaddingLeft(); + right = left + width; + } + + final int top = to.top + (getMeasuredHeight() - height) / 2; + final int bottom = top + height; + + to.set(left, top, right, bottom); + + // Center the destination rect about the trash icon + final int xOffset = (int) -(viewWidth - width) / 2; + final int yOffset = (int) -(viewHeight - height) / 2; + to.offset(xOffset, yOffset); + + return to; + } + + @Override + public DropTarget getDropTargetDelegate(DragObject d) { + return null; + } + + public void getLocationInDragLayer(int[] loc) { + mLauncher.getDragLayer().getLocationInDragLayer(this, loc); + } +} diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java new file mode 100644 index 000000000..842037c31 --- /dev/null +++ b/src/com/android/launcher3/CellLayout.java @@ -0,0 +1,3338 @@ +/* + * 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.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.NinePatchDrawable; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LayoutAnimationController; + +import com.android.launcher3.R; +import com.android.launcher3.FolderIcon.FolderRingAnimator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Stack; + +public class CellLayout extends ViewGroup { + static final String TAG = "CellLayout"; + + private Launcher mLauncher; + private int mCellWidth; + private int mCellHeight; + + private int mCountX; + private int mCountY; + + private int mOriginalWidthGap; + private int mOriginalHeightGap; + private int mWidthGap; + private int mHeightGap; + private int mMaxGap; + private boolean mScrollingTransformsDirty = false; + + private final Rect mRect = new Rect(); + private final CellInfo mCellInfo = new CellInfo(); + + // These are temporary variables to prevent having to allocate a new object just to + // return an (x, y) value from helper functions. Do NOT use them to maintain other state. + private final int[] mTmpXY = new int[2]; + private final int[] mTmpPoint = new int[2]; + int[] mTempLocation = new int[2]; + + boolean[][] mOccupied; + boolean[][] mTmpOccupied; + private boolean mLastDownOnOccupiedCell = false; + + private OnTouchListener mInterceptTouchListener; + + private ArrayList<FolderRingAnimator> mFolderOuterRings = new ArrayList<FolderRingAnimator>(); + private int[] mFolderLeaveBehindCell = {-1, -1}; + + private int mForegroundAlpha = 0; + private float mBackgroundAlpha; + private float mBackgroundAlphaMultiplier = 1.0f; + + private Drawable mNormalBackground; + private Drawable mActiveGlowBackground; + private Drawable mOverScrollForegroundDrawable; + private Drawable mOverScrollLeft; + private Drawable mOverScrollRight; + private Rect mBackgroundRect; + private Rect mForegroundRect; + private int mForegroundPadding; + + // If we're actively dragging something over this screen, mIsDragOverlapping is true + private boolean mIsDragOverlapping = false; + private final Point mDragCenter = new Point(); + + // These arrays are used to implement the drag visualization on x-large screens. + // They are used as circular arrays, indexed by mDragOutlineCurrent. + private Rect[] mDragOutlines = new Rect[4]; + private float[] mDragOutlineAlphas = new float[mDragOutlines.length]; + private InterruptibleInOutAnimator[] mDragOutlineAnims = + new InterruptibleInOutAnimator[mDragOutlines.length]; + + // Used as an index into the above 3 arrays; indicates which is the most current value. + private int mDragOutlineCurrent = 0; + private final Paint mDragOutlinePaint = new Paint(); + + private BubbleTextView mPressedOrFocusedIcon; + + private HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new + HashMap<CellLayout.LayoutParams, Animator>(); + private HashMap<View, ReorderHintAnimation> + mShakeAnimators = new HashMap<View, ReorderHintAnimation>(); + + private boolean mItemPlacementDirty = false; + + // When a drag operation is in progress, holds the nearest cell to the touch point + private final int[] mDragCell = new int[2]; + + private boolean mDragging = false; + + private TimeInterpolator mEaseOutInterpolator; + private ShortcutAndWidgetContainer mShortcutsAndWidgets; + + private boolean mIsHotseat = false; + private float mHotseatScale = 1f; + + public static final int MODE_DRAG_OVER = 0; + public static final int MODE_ON_DROP = 1; + public static final int MODE_ON_DROP_EXTERNAL = 2; + public static final int MODE_ACCEPT_DROP = 3; + private static final boolean DESTRUCTIVE_REORDER = false; + private static final boolean DEBUG_VISUALIZE_OCCUPIED = false; + + static final int LANDSCAPE = 0; + static final int PORTRAIT = 1; + + private static final float REORDER_HINT_MAGNITUDE = 0.12f; + private static final int REORDER_ANIMATION_DURATION = 150; + private float mReorderHintAnimationMagnitude; + + private ArrayList<View> mIntersectingViews = new ArrayList<View>(); + private Rect mOccupiedRect = new Rect(); + private int[] mDirectionVector = new int[2]; + int[] mPreviousReorderDirection = new int[2]; + private static final int INVALID_DIRECTION = -100; + private DropTarget.DragEnforcer mDragEnforcer; + + private final static PorterDuffXfermode sAddBlendMode = + new PorterDuffXfermode(PorterDuff.Mode.ADD); + private final static Paint sPaint = new Paint(); + + public CellLayout(Context context) { + this(context, null); + } + + public CellLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CellLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mDragEnforcer = new DropTarget.DragEnforcer(context); + + // A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show + // the user where a dragged item will land when dropped. + setWillNotDraw(false); + setClipToPadding(false); + mLauncher = (Launcher) context; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0); + + mCellWidth = a.getDimensionPixelSize(R.styleable.CellLayout_cellWidth, 10); + mCellHeight = a.getDimensionPixelSize(R.styleable.CellLayout_cellHeight, 10); + mWidthGap = mOriginalWidthGap = a.getDimensionPixelSize(R.styleable.CellLayout_widthGap, 0); + mHeightGap = mOriginalHeightGap = a.getDimensionPixelSize(R.styleable.CellLayout_heightGap, 0); + mMaxGap = a.getDimensionPixelSize(R.styleable.CellLayout_maxGap, 0); + mCountX = LauncherModel.getCellCountX(); + mCountY = LauncherModel.getCellCountY(); + mOccupied = new boolean[mCountX][mCountY]; + mTmpOccupied = new boolean[mCountX][mCountY]; + mPreviousReorderDirection[0] = INVALID_DIRECTION; + mPreviousReorderDirection[1] = INVALID_DIRECTION; + + a.recycle(); + + setAlwaysDrawnWithCacheEnabled(false); + + final Resources res = getResources(); + mHotseatScale = (res.getInteger(R.integer.hotseat_item_scale_percentage) / 100f); + + mNormalBackground = res.getDrawable(R.drawable.homescreen_blue_normal_holo); + mActiveGlowBackground = res.getDrawable(R.drawable.homescreen_blue_strong_holo); + + mOverScrollLeft = res.getDrawable(R.drawable.overscroll_glow_left); + mOverScrollRight = res.getDrawable(R.drawable.overscroll_glow_right); + mForegroundPadding = + res.getDimensionPixelSize(R.dimen.workspace_overscroll_drawable_padding); + + mReorderHintAnimationMagnitude = (REORDER_HINT_MAGNITUDE * + res.getDimensionPixelSize(R.dimen.app_icon_size)); + + mNormalBackground.setFilterBitmap(true); + mActiveGlowBackground.setFilterBitmap(true); + + // Initialize the data structures used for the drag visualization. + + mEaseOutInterpolator = new DecelerateInterpolator(2.5f); // Quint ease out + + + mDragCell[0] = mDragCell[1] = -1; + for (int i = 0; i < mDragOutlines.length; i++) { + mDragOutlines[i] = new Rect(-1, -1, -1, -1); + } + + // When dragging things around the home screens, we show a green outline of + // where the item will land. The outlines gradually fade out, leaving a trail + // behind the drag path. + // Set up all the animations that are used to implement this fading. + final int duration = res.getInteger(R.integer.config_dragOutlineFadeTime); + final float fromAlphaValue = 0; + final float toAlphaValue = (float)res.getInteger(R.integer.config_dragOutlineMaxAlpha); + + Arrays.fill(mDragOutlineAlphas, fromAlphaValue); + + for (int i = 0; i < mDragOutlineAnims.length; i++) { + final InterruptibleInOutAnimator anim = + new InterruptibleInOutAnimator(this, duration, fromAlphaValue, toAlphaValue); + anim.getAnimator().setInterpolator(mEaseOutInterpolator); + final int thisIndex = i; + anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + final Bitmap outline = (Bitmap)anim.getTag(); + + // If an animation is started and then stopped very quickly, we can still + // get spurious updates we've cleared the tag. Guard against this. + if (outline == null) { + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + Object val = animation.getAnimatedValue(); + Log.d(TAG, "anim " + thisIndex + " update: " + val + + ", isStopped " + anim.isStopped()); + } + // Try to prevent it from continuing to run + animation.cancel(); + } else { + mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue(); + CellLayout.this.invalidate(mDragOutlines[thisIndex]); + } + } + }); + // The animation holds a reference to the drag outline bitmap as long is it's + // running. This way the bitmap can be GCed when the animations are complete. + anim.getAnimator().addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if ((Float) ((ValueAnimator) animation).getAnimatedValue() == 0f) { + anim.setTag(null); + } + } + }); + mDragOutlineAnims[i] = anim; + } + + mBackgroundRect = new Rect(); + mForegroundRect = new Rect(); + + mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context); + mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap, + mCountX); + + addView(mShortcutsAndWidgets); + } + + static int widthInPortrait(Resources r, int numCells) { + // We use this method from Workspace to figure out how many rows/columns Launcher should + // have. We ignore the left/right padding on CellLayout because it turns out in our design + // the padding extends outside the visible screen size, but it looked fine anyway. + int cellWidth = r.getDimensionPixelSize(R.dimen.workspace_cell_width); + int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap), + r.getDimensionPixelSize(R.dimen.workspace_height_gap)); + + return minGap * (numCells - 1) + cellWidth * numCells; + } + + static int heightInLandscape(Resources r, int numCells) { + // We use this method from Workspace to figure out how many rows/columns Launcher should + // have. We ignore the left/right padding on CellLayout because it turns out in our design + // the padding extends outside the visible screen size, but it looked fine anyway. + int cellHeight = r.getDimensionPixelSize(R.dimen.workspace_cell_height); + int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap), + r.getDimensionPixelSize(R.dimen.workspace_height_gap)); + + return minGap * (numCells - 1) + cellHeight * numCells; + } + + public void enableHardwareLayers() { + mShortcutsAndWidgets.setLayerType(LAYER_TYPE_HARDWARE, sPaint); + } + + public void disableHardwareLayers() { + mShortcutsAndWidgets.setLayerType(LAYER_TYPE_NONE, sPaint); + } + + public void buildHardwareLayer() { + mShortcutsAndWidgets.buildLayer(); + } + + public float getChildrenScale() { + return mIsHotseat ? mHotseatScale : 1.0f; + } + + public void setGridSize(int x, int y) { + mCountX = x; + mCountY = y; + mOccupied = new boolean[mCountX][mCountY]; + mTmpOccupied = new boolean[mCountX][mCountY]; + mTempRectStack.clear(); + mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap, + mCountX); + requestLayout(); + } + + // Set whether or not to invert the layout horizontally if the layout is in RTL mode. + public void setInvertIfRtl(boolean invert) { + mShortcutsAndWidgets.setInvertIfRtl(invert); + } + + private void invalidateBubbleTextView(BubbleTextView icon) { + final int padding = icon.getPressedOrFocusedBackgroundPadding(); + invalidate(icon.getLeft() + getPaddingLeft() - padding, + icon.getTop() + getPaddingTop() - padding, + icon.getRight() + getPaddingLeft() + padding, + icon.getBottom() + getPaddingTop() + padding); + } + + void setOverScrollAmount(float r, boolean left) { + if (left && mOverScrollForegroundDrawable != mOverScrollLeft) { + mOverScrollForegroundDrawable = mOverScrollLeft; + } else if (!left && mOverScrollForegroundDrawable != mOverScrollRight) { + mOverScrollForegroundDrawable = mOverScrollRight; + } + + mForegroundAlpha = (int) Math.round((r * 255)); + mOverScrollForegroundDrawable.setAlpha(mForegroundAlpha); + invalidate(); + } + + void setPressedOrFocusedIcon(BubbleTextView icon) { + // We draw the pressed or focused BubbleTextView's background in CellLayout because it + // requires an expanded clip rect (due to the glow's blur radius) + BubbleTextView oldIcon = mPressedOrFocusedIcon; + mPressedOrFocusedIcon = icon; + if (oldIcon != null) { + invalidateBubbleTextView(oldIcon); + } + if (mPressedOrFocusedIcon != null) { + invalidateBubbleTextView(mPressedOrFocusedIcon); + } + } + + void setIsDragOverlapping(boolean isDragOverlapping) { + if (mIsDragOverlapping != isDragOverlapping) { + mIsDragOverlapping = isDragOverlapping; + invalidate(); + } + } + + boolean getIsDragOverlapping() { + return mIsDragOverlapping; + } + + protected void setOverscrollTransformsDirty(boolean dirty) { + mScrollingTransformsDirty = dirty; + } + + protected void resetOverscrollTransforms() { + if (mScrollingTransformsDirty) { + setOverscrollTransformsDirty(false); + setTranslationX(0); + setRotationY(0); + // It doesn't matter if we pass true or false here, the important thing is that we + // pass 0, which results in the overscroll drawable not being drawn any more. + setOverScrollAmount(0, false); + setPivotX(getMeasuredWidth() / 2); + setPivotY(getMeasuredHeight() / 2); + } + } + + public void scaleRect(Rect r, float scale) { + if (scale != 1.0f) { + r.left = (int) (r.left * scale + 0.5f); + r.top = (int) (r.top * scale + 0.5f); + r.right = (int) (r.right * scale + 0.5f); + r.bottom = (int) (r.bottom * scale + 0.5f); + } + } + + Rect temp = new Rect(); + void scaleRectAboutCenter(Rect in, Rect out, float scale) { + int cx = in.centerX(); + int cy = in.centerY(); + out.set(in); + out.offset(-cx, -cy); + scaleRect(out, scale); + out.offset(cx, cy); + } + + @Override + protected void onDraw(Canvas canvas) { + // When we're large, we are either drawn in a "hover" state (ie when dragging an item to + // a neighboring page) or with just a normal background (if backgroundAlpha > 0.0f) + // When we're small, we are either drawn normally or in the "accepts drops" state (during + // a drag). However, we also drag the mini hover background *over* one of those two + // backgrounds + if (mBackgroundAlpha > 0.0f) { + Drawable bg; + + if (mIsDragOverlapping) { + // In the mini case, we draw the active_glow bg *over* the active background + bg = mActiveGlowBackground; + } else { + bg = mNormalBackground; + } + + bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255)); + bg.setBounds(mBackgroundRect); + bg.draw(canvas); + } + + final Paint paint = mDragOutlinePaint; + for (int i = 0; i < mDragOutlines.length; i++) { + final float alpha = mDragOutlineAlphas[i]; + if (alpha > 0) { + final Rect r = mDragOutlines[i]; + scaleRectAboutCenter(r, temp, getChildrenScale()); + final Bitmap b = (Bitmap) mDragOutlineAnims[i].getTag(); + paint.setAlpha((int)(alpha + .5f)); + canvas.drawBitmap(b, null, temp, paint); + } + } + + // We draw the pressed or focused BubbleTextView's background in CellLayout because it + // requires an expanded clip rect (due to the glow's blur radius) + if (mPressedOrFocusedIcon != null) { + final int padding = mPressedOrFocusedIcon.getPressedOrFocusedBackgroundPadding(); + final Bitmap b = mPressedOrFocusedIcon.getPressedOrFocusedBackground(); + if (b != null) { + canvas.drawBitmap(b, + mPressedOrFocusedIcon.getLeft() + getPaddingLeft() - padding, + mPressedOrFocusedIcon.getTop() + getPaddingTop() - padding, + null); + } + } + + if (DEBUG_VISUALIZE_OCCUPIED) { + int[] pt = new int[2]; + ColorDrawable cd = new ColorDrawable(Color.RED); + cd.setBounds(0, 0, mCellWidth, mCellHeight); + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + if (mOccupied[i][j]) { + cellToPoint(i, j, pt); + canvas.save(); + canvas.translate(pt[0], pt[1]); + cd.draw(canvas); + canvas.restore(); + } + } + } + } + + int previewOffset = FolderRingAnimator.sPreviewSize; + + // The folder outer / inner ring image(s) + for (int i = 0; i < mFolderOuterRings.size(); i++) { + FolderRingAnimator fra = mFolderOuterRings.get(i); + + // Draw outer ring + Drawable d = FolderRingAnimator.sSharedOuterRingDrawable; + int width = (int) fra.getOuterRingSize(); + int height = width; + cellToPoint(fra.mCellX, fra.mCellY, mTempLocation); + + int centerX = mTempLocation[0] + mCellWidth / 2; + int centerY = mTempLocation[1] + previewOffset / 2; + + canvas.save(); + canvas.translate(centerX - width / 2, centerY - height / 2); + d.setBounds(0, 0, width, height); + d.draw(canvas); + canvas.restore(); + + // Draw inner ring + d = FolderRingAnimator.sSharedInnerRingDrawable; + width = (int) fra.getInnerRingSize(); + height = width; + cellToPoint(fra.mCellX, fra.mCellY, mTempLocation); + + centerX = mTempLocation[0] + mCellWidth / 2; + centerY = mTempLocation[1] + previewOffset / 2; + canvas.save(); + canvas.translate(centerX - width / 2, centerY - width / 2); + d.setBounds(0, 0, width, height); + d.draw(canvas); + canvas.restore(); + } + + if (mFolderLeaveBehindCell[0] >= 0 && mFolderLeaveBehindCell[1] >= 0) { + Drawable d = FolderIcon.sSharedFolderLeaveBehind; + int width = d.getIntrinsicWidth(); + int height = d.getIntrinsicHeight(); + + cellToPoint(mFolderLeaveBehindCell[0], mFolderLeaveBehindCell[1], mTempLocation); + int centerX = mTempLocation[0] + mCellWidth / 2; + int centerY = mTempLocation[1] + previewOffset / 2; + + canvas.save(); + canvas.translate(centerX - width / 2, centerY - width / 2); + d.setBounds(0, 0, width, height); + d.draw(canvas); + canvas.restore(); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (mForegroundAlpha > 0) { + mOverScrollForegroundDrawable.setBounds(mForegroundRect); + Paint p = ((NinePatchDrawable) mOverScrollForegroundDrawable).getPaint(); + p.setXfermode(sAddBlendMode); + mOverScrollForegroundDrawable.draw(canvas); + p.setXfermode(null); + } + } + + public void showFolderAccept(FolderRingAnimator fra) { + mFolderOuterRings.add(fra); + } + + public void hideFolderAccept(FolderRingAnimator fra) { + if (mFolderOuterRings.contains(fra)) { + mFolderOuterRings.remove(fra); + } + invalidate(); + } + + public void setFolderLeaveBehindCell(int x, int y) { + mFolderLeaveBehindCell[0] = x; + mFolderLeaveBehindCell[1] = y; + invalidate(); + } + + public void clearFolderLeaveBehind() { + mFolderLeaveBehindCell[0] = -1; + mFolderLeaveBehindCell[1] = -1; + invalidate(); + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + public void restoreInstanceState(SparseArray<Parcelable> states) { + dispatchRestoreInstanceState(states); + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + // Cancel long press for all children + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + child.cancelLongPress(); + } + } + + public void setOnInterceptTouchListener(View.OnTouchListener listener) { + mInterceptTouchListener = listener; + } + + int getCountX() { + return mCountX; + } + + int getCountY() { + return mCountY; + } + + public void setIsHotseat(boolean isHotseat) { + mIsHotseat = isHotseat; + } + + public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params, + boolean markCells) { + final LayoutParams lp = params; + + // Hotseat icons - remove text + if (child instanceof BubbleTextView) { + BubbleTextView bubbleChild = (BubbleTextView) child; + + Resources res = getResources(); + if (mIsHotseat) { + bubbleChild.setTextColor(res.getColor(android.R.color.transparent)); + } else { + bubbleChild.setTextColor(res.getColor(R.color.workspace_icon_text_color)); + } + } + + child.setScaleX(getChildrenScale()); + child.setScaleY(getChildrenScale()); + + // Generate an id for each view, this assumes we have at most 256x256 cells + // per workspace screen + if (lp.cellX >= 0 && lp.cellX <= mCountX - 1 && lp.cellY >= 0 && lp.cellY <= mCountY - 1) { + // If the horizontal or vertical span is set to -1, it is taken to + // mean that it spans the extent of the CellLayout + if (lp.cellHSpan < 0) lp.cellHSpan = mCountX; + if (lp.cellVSpan < 0) lp.cellVSpan = mCountY; + + child.setId(childId); + + mShortcutsAndWidgets.addView(child, index, lp); + + if (markCells) markCellsAsOccupiedForView(child); + + return true; + } + return false; + } + + @Override + public void removeAllViews() { + clearOccupiedCells(); + mShortcutsAndWidgets.removeAllViews(); + } + + @Override + public void removeAllViewsInLayout() { + if (mShortcutsAndWidgets.getChildCount() > 0) { + clearOccupiedCells(); + mShortcutsAndWidgets.removeAllViewsInLayout(); + } + } + + public void removeViewWithoutMarkingCells(View view) { + mShortcutsAndWidgets.removeView(view); + } + + @Override + public void removeView(View view) { + markCellsAsUnoccupiedForView(view); + mShortcutsAndWidgets.removeView(view); + } + + @Override + public void removeViewAt(int index) { + markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index)); + mShortcutsAndWidgets.removeViewAt(index); + } + + @Override + public void removeViewInLayout(View view) { + markCellsAsUnoccupiedForView(view); + mShortcutsAndWidgets.removeViewInLayout(view); + } + + @Override + public void removeViews(int start, int count) { + for (int i = start; i < start + count; i++) { + markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); + } + mShortcutsAndWidgets.removeViews(start, count); + } + + @Override + public void removeViewsInLayout(int start, int count) { + for (int i = start; i < start + count; i++) { + markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); + } + mShortcutsAndWidgets.removeViewsInLayout(start, count); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mCellInfo.screen = ((ViewGroup) getParent()).indexOfChild(this); + } + + public void setTagToCellInfoForPoint(int touchX, int touchY) { + final CellInfo cellInfo = mCellInfo; + Rect frame = mRect; + final int x = touchX + getScrollX(); + final int y = touchY + getScrollY(); + final int count = mShortcutsAndWidgets.getChildCount(); + + boolean found = false; + for (int i = count - 1; i >= 0; i--) { + final View child = mShortcutsAndWidgets.getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if ((child.getVisibility() == VISIBLE || child.getAnimation() != null) && + lp.isLockedToGrid) { + child.getHitRect(frame); + + float scale = child.getScaleX(); + frame = new Rect(child.getLeft(), child.getTop(), child.getRight(), + child.getBottom()); + // The child hit rect is relative to the CellLayoutChildren parent, so we need to + // offset that by this CellLayout's padding to test an (x,y) point that is relative + // to this view. + frame.offset(getPaddingLeft(), getPaddingTop()); + frame.inset((int) (frame.width() * (1f - scale) / 2), + (int) (frame.height() * (1f - scale) / 2)); + + if (frame.contains(x, y)) { + cellInfo.cell = child; + cellInfo.cellX = lp.cellX; + cellInfo.cellY = lp.cellY; + cellInfo.spanX = lp.cellHSpan; + cellInfo.spanY = lp.cellVSpan; + found = true; + break; + } + } + } + + mLastDownOnOccupiedCell = found; + + if (!found) { + final int cellXY[] = mTmpXY; + pointToCellExact(x, y, cellXY); + + cellInfo.cell = null; + cellInfo.cellX = cellXY[0]; + cellInfo.cellY = cellXY[1]; + cellInfo.spanX = 1; + cellInfo.spanY = 1; + } + setTag(cellInfo); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // First we clear the tag to ensure that on every touch down we start with a fresh slate, + // even in the case where we return early. Not clearing here was causing bugs whereby on + // long-press we'd end up picking up an item from a previous drag operation. + final int action = ev.getAction(); + + if (action == MotionEvent.ACTION_DOWN) { + clearTagCellInfo(); + } + + if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) { + return true; + } + + if (action == MotionEvent.ACTION_DOWN) { + setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY()); + } + + return false; + } + + private void clearTagCellInfo() { + final CellInfo cellInfo = mCellInfo; + cellInfo.cell = null; + cellInfo.cellX = -1; + cellInfo.cellY = -1; + cellInfo.spanX = 0; + cellInfo.spanY = 0; + setTag(cellInfo); + } + + public CellInfo getTag() { + return (CellInfo) super.getTag(); + } + + /** + * Given a point, return the cell that strictly encloses that point + * @param x X coordinate of the point + * @param y Y coordinate of the point + * @param result Array of 2 ints to hold the x and y coordinate of the cell + */ + void pointToCellExact(int x, int y, int[] result) { + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + + result[0] = (x - hStartPadding) / (mCellWidth + mWidthGap); + result[1] = (y - vStartPadding) / (mCellHeight + mHeightGap); + + final int xAxis = mCountX; + final int yAxis = mCountY; + + if (result[0] < 0) result[0] = 0; + if (result[0] >= xAxis) result[0] = xAxis - 1; + if (result[1] < 0) result[1] = 0; + if (result[1] >= yAxis) result[1] = yAxis - 1; + } + + /** + * Given a point, return the cell that most closely encloses that point + * @param x X coordinate of the point + * @param y Y coordinate of the point + * @param result Array of 2 ints to hold the x and y coordinate of the cell + */ + void pointToCellRounded(int x, int y, int[] result) { + pointToCellExact(x + (mCellWidth / 2), y + (mCellHeight / 2), result); + } + + /** + * Given a cell coordinate, return the point that represents the upper left corner of that cell + * + * @param cellX X coordinate of the cell + * @param cellY Y coordinate of the cell + * + * @param result Array of 2 ints to hold the x and y coordinate of the point + */ + void cellToPoint(int cellX, int cellY, int[] result) { + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + + result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap); + result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap); + } + + /** + * Given a cell coordinate, return the point that represents the center of the cell + * + * @param cellX X coordinate of the cell + * @param cellY Y coordinate of the cell + * + * @param result Array of 2 ints to hold the x and y coordinate of the point + */ + void cellToCenterPoint(int cellX, int cellY, int[] result) { + regionToCenterPoint(cellX, cellY, 1, 1, result); + } + + /** + * Given a cell coordinate and span return the point that represents the center of the regio + * + * @param cellX X coordinate of the cell + * @param cellY Y coordinate of the cell + * + * @param result Array of 2 ints to hold the x and y coordinate of the point + */ + void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) { + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap) + + (spanX * mCellWidth + (spanX - 1) * mWidthGap) / 2; + result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap) + + (spanY * mCellHeight + (spanY - 1) * mHeightGap) / 2; + } + + /** + * Given a cell coordinate and span fills out a corresponding pixel rect + * + * @param cellX X coordinate of the cell + * @param cellY Y coordinate of the cell + * @param result Rect in which to write the result + */ + void regionToRect(int cellX, int cellY, int spanX, int spanY, Rect result) { + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + final int left = hStartPadding + cellX * (mCellWidth + mWidthGap); + final int top = vStartPadding + cellY * (mCellHeight + mHeightGap); + result.set(left, top, left + (spanX * mCellWidth + (spanX - 1) * mWidthGap), + top + (spanY * mCellHeight + (spanY - 1) * mHeightGap)); + } + + public float getDistanceFromCell(float x, float y, int[] cell) { + cellToCenterPoint(cell[0], cell[1], mTmpPoint); + float distance = (float) Math.sqrt( Math.pow(x - mTmpPoint[0], 2) + + Math.pow(y - mTmpPoint[1], 2)); + return distance; + } + + int getCellWidth() { + return mCellWidth; + } + + int getCellHeight() { + return mCellHeight; + } + + int getWidthGap() { + return mWidthGap; + } + + int getHeightGap() { + return mHeightGap; + } + + Rect getContentRect(Rect r) { + if (r == null) { + r = new Rect(); + } + int left = getPaddingLeft(); + int top = getPaddingTop(); + int right = left + getWidth() - getPaddingLeft() - getPaddingRight(); + int bottom = top + getHeight() - getPaddingTop() - getPaddingBottom(); + r.set(left, top, right, bottom); + return r; + } + + static void getMetrics(Rect metrics, Resources res, int measureWidth, int measureHeight, + int countX, int countY, int orientation) { + int numWidthGaps = countX - 1; + int numHeightGaps = countY - 1; + + int widthGap; + int heightGap; + int cellWidth; + int cellHeight; + int paddingLeft; + int paddingRight; + int paddingTop; + int paddingBottom; + + int maxGap = res.getDimensionPixelSize(R.dimen.workspace_max_gap); + if (orientation == LANDSCAPE) { + cellWidth = res.getDimensionPixelSize(R.dimen.workspace_cell_width_land); + cellHeight = res.getDimensionPixelSize(R.dimen.workspace_cell_height_land); + widthGap = res.getDimensionPixelSize(R.dimen.workspace_width_gap_land); + heightGap = res.getDimensionPixelSize(R.dimen.workspace_height_gap_land); + paddingLeft = res.getDimensionPixelSize(R.dimen.cell_layout_left_padding_land); + paddingRight = res.getDimensionPixelSize(R.dimen.cell_layout_right_padding_land); + paddingTop = res.getDimensionPixelSize(R.dimen.cell_layout_top_padding_land); + paddingBottom = res.getDimensionPixelSize(R.dimen.cell_layout_bottom_padding_land); + } else { + // PORTRAIT + cellWidth = res.getDimensionPixelSize(R.dimen.workspace_cell_width_port); + cellHeight = res.getDimensionPixelSize(R.dimen.workspace_cell_height_port); + widthGap = res.getDimensionPixelSize(R.dimen.workspace_width_gap_port); + heightGap = res.getDimensionPixelSize(R.dimen.workspace_height_gap_port); + paddingLeft = res.getDimensionPixelSize(R.dimen.cell_layout_left_padding_port); + paddingRight = res.getDimensionPixelSize(R.dimen.cell_layout_right_padding_port); + paddingTop = res.getDimensionPixelSize(R.dimen.cell_layout_top_padding_port); + paddingBottom = res.getDimensionPixelSize(R.dimen.cell_layout_bottom_padding_port); + } + + if (widthGap < 0 || heightGap < 0) { + int hSpace = measureWidth - paddingLeft - paddingRight; + int vSpace = measureHeight - paddingTop - paddingBottom; + int hFreeSpace = hSpace - (countX * cellWidth); + int vFreeSpace = vSpace - (countY * cellHeight); + widthGap = Math.min(maxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0); + heightGap = Math.min(maxGap, numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0); + } + metrics.set(cellWidth, cellHeight, widthGap, heightGap); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { + throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); + } + + int numWidthGaps = mCountX - 1; + int numHeightGaps = mCountY - 1; + + if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) { + int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight(); + int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom(); + int hFreeSpace = hSpace - (mCountX * mCellWidth); + int vFreeSpace = vSpace - (mCountY * mCellHeight); + mWidthGap = Math.min(mMaxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0); + mHeightGap = Math.min(mMaxGap,numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0); + mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap, + mCountX); + } else { + mWidthGap = mOriginalWidthGap; + mHeightGap = mOriginalHeightGap; + } + + // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY + int newWidth = widthSpecSize; + int newHeight = heightSpecSize; + if (widthSpecMode == MeasureSpec.AT_MOST) { + newWidth = getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth) + + ((mCountX - 1) * mWidthGap); + newHeight = getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight) + + ((mCountY - 1) * mHeightGap); + setMeasuredDimension(newWidth, newHeight); + } + + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(newWidth - getPaddingLeft() - + getPaddingRight(), MeasureSpec.EXACTLY); + int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight - getPaddingTop() - + getPaddingBottom(), MeasureSpec.EXACTLY); + child.measure(childWidthMeasureSpec, childheightMeasureSpec); + } + setMeasuredDimension(newWidth, newHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + child.layout(getPaddingLeft(), getPaddingTop(), + r - l - getPaddingRight(), b - t - getPaddingBottom()); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mBackgroundRect.set(0, 0, w, h); + mForegroundRect.set(mForegroundPadding, mForegroundPadding, + w - mForegroundPadding, h - mForegroundPadding); + } + + @Override + protected void setChildrenDrawingCacheEnabled(boolean enabled) { + mShortcutsAndWidgets.setChildrenDrawingCacheEnabled(enabled); + } + + @Override + protected void setChildrenDrawnWithCacheEnabled(boolean enabled) { + mShortcutsAndWidgets.setChildrenDrawnWithCacheEnabled(enabled); + } + + public float getBackgroundAlpha() { + return mBackgroundAlpha; + } + + public void setBackgroundAlphaMultiplier(float multiplier) { + if (mBackgroundAlphaMultiplier != multiplier) { + mBackgroundAlphaMultiplier = multiplier; + invalidate(); + } + } + + public float getBackgroundAlphaMultiplier() { + return mBackgroundAlphaMultiplier; + } + + public void setBackgroundAlpha(float alpha) { + if (mBackgroundAlpha != alpha) { + mBackgroundAlpha = alpha; + invalidate(); + } + } + + public void setShortcutAndWidgetAlpha(float alpha) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).setAlpha(alpha); + } + } + + public ShortcutAndWidgetContainer getShortcutsAndWidgets() { + if (getChildCount() > 0) { + return (ShortcutAndWidgetContainer) getChildAt(0); + } + return null; + } + + public View getChildAt(int x, int y) { + return mShortcutsAndWidgets.getChildAt(x, y); + } + + public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration, + int delay, boolean permanent, boolean adjustOccupied) { + ShortcutAndWidgetContainer clc = getShortcutsAndWidgets(); + boolean[][] occupied = mOccupied; + if (!permanent) { + occupied = mTmpOccupied; + } + + if (clc.indexOfChild(child) != -1) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final ItemInfo info = (ItemInfo) child.getTag(); + + // We cancel any existing animations + if (mReorderAnimators.containsKey(lp)) { + mReorderAnimators.get(lp).cancel(); + mReorderAnimators.remove(lp); + } + + final int oldX = lp.x; + final int oldY = lp.y; + if (adjustOccupied) { + occupied[lp.cellX][lp.cellY] = false; + occupied[cellX][cellY] = true; + } + lp.isLockedToGrid = true; + if (permanent) { + lp.cellX = info.cellX = cellX; + lp.cellY = info.cellY = cellY; + } else { + lp.tmpCellX = cellX; + lp.tmpCellY = cellY; + } + clc.setupLp(lp); + lp.isLockedToGrid = false; + final int newX = lp.x; + final int newY = lp.y; + + lp.x = oldX; + lp.y = oldY; + + // Exit early if we're not actually moving the view + if (oldX == newX && oldY == newY) { + lp.isLockedToGrid = true; + return true; + } + + ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f); + va.setDuration(duration); + mReorderAnimators.put(lp, va); + + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float r = ((Float) animation.getAnimatedValue()).floatValue(); + lp.x = (int) ((1 - r) * oldX + r * newX); + lp.y = (int) ((1 - r) * oldY + r * newY); + child.requestLayout(); + } + }); + va.addListener(new AnimatorListenerAdapter() { + boolean cancelled = false; + public void onAnimationEnd(Animator animation) { + // If the animation was cancelled, it means that another animation + // has interrupted this one, and we don't want to lock the item into + // place just yet. + if (!cancelled) { + lp.isLockedToGrid = true; + child.requestLayout(); + } + if (mReorderAnimators.containsKey(lp)) { + mReorderAnimators.remove(lp); + } + } + public void onAnimationCancel(Animator animation) { + cancelled = true; + } + }); + va.setStartDelay(delay); + va.start(); + return true; + } + return false; + } + + /** + * Estimate where the top left cell of the dragged item will land if it is dropped. + * + * @param originX The X value of the top left corner of the item + * @param originY The Y value of the top left corner of the item + * @param spanX The number of horizontal cells that the item spans + * @param spanY The number of vertical cells that the item spans + * @param result The estimated drop cell X and Y. + */ + void estimateDropCell(int originX, int originY, int spanX, int spanY, int[] result) { + final int countX = mCountX; + final int countY = mCountY; + + // pointToCellRounded takes the top left of a cell but will pad that with + // cellWidth/2 and cellHeight/2 when finding the matching cell + pointToCellRounded(originX, originY, result); + + // If the item isn't fully on this screen, snap to the edges + int rightOverhang = result[0] + spanX - countX; + if (rightOverhang > 0) { + result[0] -= rightOverhang; // Snap to right + } + result[0] = Math.max(0, result[0]); // Snap to left + int bottomOverhang = result[1] + spanY - countY; + if (bottomOverhang > 0) { + result[1] -= bottomOverhang; // Snap to bottom + } + result[1] = Math.max(0, result[1]); // Snap to top + } + + void visualizeDropLocation(View v, Bitmap dragOutline, int originX, int originY, int cellX, + int cellY, int spanX, int spanY, boolean resize, Point dragOffset, Rect dragRegion) { + final int oldDragCellX = mDragCell[0]; + final int oldDragCellY = mDragCell[1]; + + if (v != null && dragOffset == null) { + mDragCenter.set(originX + (v.getWidth() / 2), originY + (v.getHeight() / 2)); + } else { + mDragCenter.set(originX, originY); + } + + if (dragOutline == null && v == null) { + return; + } + + if (cellX != oldDragCellX || cellY != oldDragCellY) { + mDragCell[0] = cellX; + mDragCell[1] = cellY; + // Find the top left corner of the rect the object will occupy + final int[] topLeft = mTmpPoint; + cellToPoint(cellX, cellY, topLeft); + + int left = topLeft[0]; + int top = topLeft[1]; + + if (v != null && dragOffset == null) { + // When drawing the drag outline, it did not account for margin offsets + // added by the view's parent. + MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams(); + left += lp.leftMargin; + top += lp.topMargin; + + // Offsets due to the size difference between the View and the dragOutline. + // There is a size difference to account for the outer blur, which may lie + // outside the bounds of the view. + top += (v.getHeight() - dragOutline.getHeight()) / 2; + // We center about the x axis + left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap) + - dragOutline.getWidth()) / 2; + } else { + if (dragOffset != null && dragRegion != null) { + // Center the drag region *horizontally* in the cell and apply a drag + // outline offset + left += dragOffset.x + ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap) + - dragRegion.width()) / 2; + top += dragOffset.y; + } else { + // Center the drag outline in the cell + left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap) + - dragOutline.getWidth()) / 2; + top += ((mCellHeight * spanY) + ((spanY - 1) * mHeightGap) + - dragOutline.getHeight()) / 2; + } + } + final int oldIndex = mDragOutlineCurrent; + mDragOutlineAnims[oldIndex].animateOut(); + mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length; + Rect r = mDragOutlines[mDragOutlineCurrent]; + r.set(left, top, left + dragOutline.getWidth(), top + dragOutline.getHeight()); + if (resize) { + cellToRect(cellX, cellY, spanX, spanY, r); + } + + mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline); + mDragOutlineAnims[mDragOutlineCurrent].animateIn(); + } + } + + public void clearDragOutlines() { + final int oldIndex = mDragOutlineCurrent; + mDragOutlineAnims[oldIndex].animateOut(); + mDragCell[0] = mDragCell[1] = -1; + } + + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location. Uses Euclidean distance to score multiple vacant areas. + * + * @param pixelX The X location at which you want to search for a vacant area. + * @param pixelY The Y location at which you want to search for a vacant area. + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param result Array in which to place the result, or null (in which case a new array will + * be allocated) + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, + int[] result) { + return findNearestVacantArea(pixelX, pixelY, spanX, spanY, null, result); + } + + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location. Uses Euclidean distance to score multiple vacant areas. + * + * @param pixelX The X location at which you want to search for a vacant area. + * @param pixelY The Y location at which you want to search for a vacant area. + * @param minSpanX The minimum horizontal span required + * @param minSpanY The minimum vertical span required + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param result Array in which to place the result, or null (in which case a new array will + * be allocated) + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, + int spanY, int[] result, int[] resultSpan) { + return findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null, + result, resultSpan); + } + + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location. Uses Euclidean distance to score multiple vacant areas. + * + * @param pixelX The X location at which you want to search for a vacant area. + * @param pixelY The Y location at which you want to search for a vacant area. + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param ignoreOccupied If true, the result can be an occupied cell + * @param result Array in which to place the result, or null (in which case a new array will + * be allocated) + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, View ignoreView, + boolean ignoreOccupied, int[] result) { + return findNearestArea(pixelX, pixelY, spanX, spanY, + spanX, spanY, ignoreView, ignoreOccupied, result, null, mOccupied); + } + + private final Stack<Rect> mTempRectStack = new Stack<Rect>(); + private void lazyInitTempRectStack() { + if (mTempRectStack.isEmpty()) { + for (int i = 0; i < mCountX * mCountY; i++) { + mTempRectStack.push(new Rect()); + } + } + } + + private void recycleTempRects(Stack<Rect> used) { + while (!used.isEmpty()) { + mTempRectStack.push(used.pop()); + } + } + + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location. Uses Euclidean distance to score multiple vacant areas. + * + * @param pixelX The X location at which you want to search for a vacant area. + * @param pixelY The Y location at which you want to search for a vacant area. + * @param minSpanX The minimum horizontal span required + * @param minSpanY The minimum vertical span required + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param ignoreOccupied If true, the result can be an occupied cell + * @param result Array in which to place the result, or null (in which case a new array will + * be allocated) + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + int[] findNearestArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, + View ignoreView, boolean ignoreOccupied, int[] result, int[] resultSpan, + boolean[][] occupied) { + lazyInitTempRectStack(); + // mark space take by ignoreView as available (method checks if ignoreView is null) + markCellsAsUnoccupiedForView(ignoreView, occupied); + + // For items with a spanX / spanY > 1, the passed in point (pixelX, pixelY) corresponds + // to the center of the item, but we are searching based on the top-left cell, so + // we translate the point over to correspond to the top-left. + pixelX -= (mCellWidth + mWidthGap) * (spanX - 1) / 2f; + pixelY -= (mCellHeight + mHeightGap) * (spanY - 1) / 2f; + + // Keep track of best-scoring drop area + final int[] bestXY = result != null ? result : new int[2]; + double bestDistance = Double.MAX_VALUE; + final Rect bestRect = new Rect(-1, -1, -1, -1); + final Stack<Rect> validRegions = new Stack<Rect>(); + + final int countX = mCountX; + final int countY = mCountY; + + if (minSpanX <= 0 || minSpanY <= 0 || spanX <= 0 || spanY <= 0 || + spanX < minSpanX || spanY < minSpanY) { + return bestXY; + } + + for (int y = 0; y < countY - (minSpanY - 1); y++) { + inner: + for (int x = 0; x < countX - (minSpanX - 1); x++) { + int ySize = -1; + int xSize = -1; + if (ignoreOccupied) { + // First, let's see if this thing fits anywhere + for (int i = 0; i < minSpanX; i++) { + for (int j = 0; j < minSpanY; j++) { + if (occupied[x + i][y + j]) { + continue inner; + } + } + } + xSize = minSpanX; + ySize = minSpanY; + + // We know that the item will fit at _some_ acceptable size, now let's see + // how big we can make it. We'll alternate between incrementing x and y spans + // until we hit a limit. + boolean incX = true; + boolean hitMaxX = xSize >= spanX; + boolean hitMaxY = ySize >= spanY; + while (!(hitMaxX && hitMaxY)) { + if (incX && !hitMaxX) { + for (int j = 0; j < ySize; j++) { + if (x + xSize > countX -1 || occupied[x + xSize][y + j]) { + // We can't move out horizontally + hitMaxX = true; + } + } + if (!hitMaxX) { + xSize++; + } + } else if (!hitMaxY) { + for (int i = 0; i < xSize; i++) { + if (y + ySize > countY - 1 || occupied[x + i][y + ySize]) { + // We can't move out vertically + hitMaxY = true; + } + } + if (!hitMaxY) { + ySize++; + } + } + hitMaxX |= xSize >= spanX; + hitMaxY |= ySize >= spanY; + incX = !incX; + } + incX = true; + hitMaxX = xSize >= spanX; + hitMaxY = ySize >= spanY; + } + final int[] cellXY = mTmpXY; + cellToCenterPoint(x, y, cellXY); + + // We verify that the current rect is not a sub-rect of any of our previous + // candidates. In this case, the current rect is disqualified in favour of the + // containing rect. + Rect currentRect = mTempRectStack.pop(); + currentRect.set(x, y, x + xSize, y + ySize); + boolean contained = false; + for (Rect r : validRegions) { + if (r.contains(currentRect)) { + contained = true; + break; + } + } + validRegions.push(currentRect); + double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2) + + Math.pow(cellXY[1] - pixelY, 2)); + + if ((distance <= bestDistance && !contained) || + currentRect.contains(bestRect)) { + bestDistance = distance; + bestXY[0] = x; + bestXY[1] = y; + if (resultSpan != null) { + resultSpan[0] = xSize; + resultSpan[1] = ySize; + } + bestRect.set(currentRect); + } + } + } + // re-mark space taken by ignoreView as occupied + markCellsAsOccupiedForView(ignoreView, occupied); + + // Return -1, -1 if no suitable location found + if (bestDistance == Double.MAX_VALUE) { + bestXY[0] = -1; + bestXY[1] = -1; + } + recycleTempRects(validRegions); + return bestXY; + } + + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location, and will also weigh in a suggested direction vector of the + * desired location. This method computers distance based on unit grid distances, + * not pixel distances. + * + * @param cellX The X cell nearest to which you want to search for a vacant area. + * @param cellY The Y cell nearest which you want to search for a vacant area. + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param direction The favored direction in which the views should move from x, y + * @param exactDirectionOnly If this parameter is true, then only solutions where the direction + * matches exactly. Otherwise we find the best matching direction. + * @param occoupied The array which represents which cells in the CellLayout are occupied + * @param blockOccupied The array which represents which cells in the specified block (cellX, + * cellY, spanX, spanY) are occupied. This is used when try to move a group of views. + * @param result Array in which to place the result, or null (in which case a new array will + * be allocated) + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction, + boolean[][] occupied, boolean blockOccupied[][], int[] result) { + // Keep track of best-scoring drop area + final int[] bestXY = result != null ? result : new int[2]; + float bestDistance = Float.MAX_VALUE; + int bestDirectionScore = Integer.MIN_VALUE; + + final int countX = mCountX; + final int countY = mCountY; + + for (int y = 0; y < countY - (spanY - 1); y++) { + inner: + for (int x = 0; x < countX - (spanX - 1); x++) { + // First, let's see if this thing fits anywhere + for (int i = 0; i < spanX; i++) { + for (int j = 0; j < spanY; j++) { + if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) { + continue inner; + } + } + } + + float distance = (float) + Math.sqrt((x - cellX) * (x - cellX) + (y - cellY) * (y - cellY)); + int[] curDirection = mTmpPoint; + computeDirectionVector(x - cellX, y - cellY, curDirection); + // The direction score is just the dot product of the two candidate direction + // and that passed in. + int curDirectionScore = direction[0] * curDirection[0] + + direction[1] * curDirection[1]; + boolean exactDirectionOnly = false; + boolean directionMatches = direction[0] == curDirection[0] && + direction[0] == curDirection[0]; + if ((directionMatches || !exactDirectionOnly) && + Float.compare(distance, bestDistance) < 0 || (Float.compare(distance, + bestDistance) == 0 && curDirectionScore > bestDirectionScore)) { + bestDistance = distance; + bestDirectionScore = curDirectionScore; + bestXY[0] = x; + bestXY[1] = y; + } + } + } + + // Return -1, -1 if no suitable location found + if (bestDistance == Float.MAX_VALUE) { + bestXY[0] = -1; + bestXY[1] = -1; + } + return bestXY; + } + + private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop, + int[] direction, ItemConfiguration currentState) { + CellAndSpan c = currentState.map.get(v); + boolean success = false; + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); + markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true); + + findNearestArea(c.x, c.y, c.spanX, c.spanY, direction, mTmpOccupied, null, mTempLocation); + + if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { + c.x = mTempLocation[0]; + c.y = mTempLocation[1]; + success = true; + } + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); + return success; + } + + /** + * This helper class defines a cluster of views. It helps with defining complex edges + * of the cluster and determining how those edges interact with other views. The edges + * essentially define a fine-grained boundary around the cluster of views -- like a more + * precise version of a bounding box. + */ + private class ViewCluster { + final static int LEFT = 0; + final static int TOP = 1; + final static int RIGHT = 2; + final static int BOTTOM = 3; + + ArrayList<View> views; + ItemConfiguration config; + Rect boundingRect = new Rect(); + + int[] leftEdge = new int[mCountY]; + int[] rightEdge = new int[mCountY]; + int[] topEdge = new int[mCountX]; + int[] bottomEdge = new int[mCountX]; + boolean leftEdgeDirty, rightEdgeDirty, topEdgeDirty, bottomEdgeDirty, boundingRectDirty; + + @SuppressWarnings("unchecked") + public ViewCluster(ArrayList<View> views, ItemConfiguration config) { + this.views = (ArrayList<View>) views.clone(); + this.config = config; + resetEdges(); + } + + void resetEdges() { + for (int i = 0; i < mCountX; i++) { + topEdge[i] = -1; + bottomEdge[i] = -1; + } + for (int i = 0; i < mCountY; i++) { + leftEdge[i] = -1; + rightEdge[i] = -1; + } + leftEdgeDirty = true; + rightEdgeDirty = true; + bottomEdgeDirty = true; + topEdgeDirty = true; + boundingRectDirty = true; + } + + void computeEdge(int which, int[] edge) { + int count = views.size(); + for (int i = 0; i < count; i++) { + CellAndSpan cs = config.map.get(views.get(i)); + switch (which) { + case LEFT: + int left = cs.x; + for (int j = cs.y; j < cs.y + cs.spanY; j++) { + if (left < edge[j] || edge[j] < 0) { + edge[j] = left; + } + } + break; + case RIGHT: + int right = cs.x + cs.spanX; + for (int j = cs.y; j < cs.y + cs.spanY; j++) { + if (right > edge[j]) { + edge[j] = right; + } + } + break; + case TOP: + int top = cs.y; + for (int j = cs.x; j < cs.x + cs.spanX; j++) { + if (top < edge[j] || edge[j] < 0) { + edge[j] = top; + } + } + break; + case BOTTOM: + int bottom = cs.y + cs.spanY; + for (int j = cs.x; j < cs.x + cs.spanX; j++) { + if (bottom > edge[j]) { + edge[j] = bottom; + } + } + break; + } + } + } + + boolean isViewTouchingEdge(View v, int whichEdge) { + CellAndSpan cs = config.map.get(v); + + int[] edge = getEdge(whichEdge); + + switch (whichEdge) { + case LEFT: + for (int i = cs.y; i < cs.y + cs.spanY; i++) { + if (edge[i] == cs.x + cs.spanX) { + return true; + } + } + break; + case RIGHT: + for (int i = cs.y; i < cs.y + cs.spanY; i++) { + if (edge[i] == cs.x) { + return true; + } + } + break; + case TOP: + for (int i = cs.x; i < cs.x + cs.spanX; i++) { + if (edge[i] == cs.y + cs.spanY) { + return true; + } + } + break; + case BOTTOM: + for (int i = cs.x; i < cs.x + cs.spanX; i++) { + if (edge[i] == cs.y) { + return true; + } + } + break; + } + return false; + } + + void shift(int whichEdge, int delta) { + for (View v: views) { + CellAndSpan c = config.map.get(v); + switch (whichEdge) { + case LEFT: + c.x -= delta; + break; + case RIGHT: + c.x += delta; + break; + case TOP: + c.y -= delta; + break; + case BOTTOM: + default: + c.y += delta; + break; + } + } + resetEdges(); + } + + public void addView(View v) { + views.add(v); + resetEdges(); + } + + public Rect getBoundingRect() { + if (boundingRectDirty) { + boolean first = true; + for (View v: views) { + CellAndSpan c = config.map.get(v); + if (first) { + boundingRect.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + first = false; + } else { + boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + } + } + } + return boundingRect; + } + + public int[] getEdge(int which) { + switch (which) { + case LEFT: + return getLeftEdge(); + case RIGHT: + return getRightEdge(); + case TOP: + return getTopEdge(); + case BOTTOM: + default: + return getBottomEdge(); + } + } + + public int[] getLeftEdge() { + if (leftEdgeDirty) { + computeEdge(LEFT, leftEdge); + } + return leftEdge; + } + + public int[] getRightEdge() { + if (rightEdgeDirty) { + computeEdge(RIGHT, rightEdge); + } + return rightEdge; + } + + public int[] getTopEdge() { + if (topEdgeDirty) { + computeEdge(TOP, topEdge); + } + return topEdge; + } + + public int[] getBottomEdge() { + if (bottomEdgeDirty) { + computeEdge(BOTTOM, bottomEdge); + } + return bottomEdge; + } + + PositionComparator comparator = new PositionComparator(); + class PositionComparator implements Comparator<View> { + int whichEdge = 0; + public int compare(View left, View right) { + CellAndSpan l = config.map.get(left); + CellAndSpan r = config.map.get(right); + switch (whichEdge) { + case LEFT: + return (r.x + r.spanX) - (l.x + l.spanX); + case RIGHT: + return l.x - r.x; + case TOP: + return (r.y + r.spanY) - (l.y + l.spanY); + case BOTTOM: + default: + return l.y - r.y; + } + } + } + + public void sortConfigurationForEdgePush(int edge) { + comparator.whichEdge = edge; + Collections.sort(config.sortedViews, comparator); + } + } + + private boolean pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop, + int[] direction, View dragView, ItemConfiguration currentState) { + + ViewCluster cluster = new ViewCluster(views, currentState); + Rect clusterRect = cluster.getBoundingRect(); + int whichEdge; + int pushDistance; + boolean fail = false; + + // Determine the edge of the cluster that will be leading the push and how far + // the cluster must be shifted. + if (direction[0] < 0) { + whichEdge = ViewCluster.LEFT; + pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left; + } else if (direction[0] > 0) { + whichEdge = ViewCluster.RIGHT; + pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left; + } else if (direction[1] < 0) { + whichEdge = ViewCluster.TOP; + pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top; + } else { + whichEdge = ViewCluster.BOTTOM; + pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top; + } + + // Break early for invalid push distance. + if (pushDistance <= 0) { + return false; + } + + // Mark the occupied state as false for the group of views we want to move. + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); + } + + // We save the current configuration -- if we fail to find a solution we will revert + // to the initial state. The process of finding a solution modifies the configuration + // in place, hence the need for revert in the failure case. + currentState.save(); + + // The pushing algorithm is simplified by considering the views in the order in which + // they would be pushed by the cluster. For example, if the cluster is leading with its + // left edge, we consider sort the views by their right edge, from right to left. + cluster.sortConfigurationForEdgePush(whichEdge); + + while (pushDistance > 0 && !fail) { + for (View v: currentState.sortedViews) { + // For each view that isn't in the cluster, we see if the leading edge of the + // cluster is contacting the edge of that view. If so, we add that view to the + // cluster. + if (!cluster.views.contains(v) && v != dragView) { + if (cluster.isViewTouchingEdge(v, whichEdge)) { + LayoutParams lp = (LayoutParams) v.getLayoutParams(); + if (!lp.canReorder) { + // The push solution includes the all apps button, this is not viable. + fail = true; + break; + } + cluster.addView(v); + CellAndSpan c = currentState.map.get(v); + + // Adding view to cluster, mark it as not occupied. + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); + } + } + } + pushDistance--; + + // The cluster has been completed, now we move the whole thing over in the appropriate + // direction. + cluster.shift(whichEdge, 1); + } + + boolean foundSolution = false; + clusterRect = cluster.getBoundingRect(); + + // Due to the nature of the algorithm, the only check required to verify a valid solution + // is to ensure that completed shifted cluster lies completely within the cell layout. + if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 && + clusterRect.bottom <= mCountY) { + foundSolution = true; + } else { + currentState.restore(); + } + + // In either case, we set the occupied array as marked for the location of the views + for (View v: cluster.views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); + } + + return foundSolution; + } + + private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop, + int[] direction, View dragView, ItemConfiguration currentState) { + if (views.size() == 0) return true; + + boolean success = false; + Rect boundingRect = null; + // We construct a rect which represents the entire group of views passed in + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + if (boundingRect == null) { + boundingRect = new Rect(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + } else { + boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + } + } + + // Mark the occupied state as false for the group of views we want to move. + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); + } + + boolean[][] blockOccupied = new boolean[boundingRect.width()][boundingRect.height()]; + int top = boundingRect.top; + int left = boundingRect.left; + // We mark more precisely which parts of the bounding rect are truly occupied, allowing + // for interlocking. + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x - left, c.y - top, c.spanX, c.spanY, blockOccupied, true); + } + + markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true); + + findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(), + boundingRect.height(), direction, mTmpOccupied, blockOccupied, mTempLocation); + + // If we successfuly found a location by pushing the block of views, we commit it + if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { + int deltaX = mTempLocation[0] - boundingRect.left; + int deltaY = mTempLocation[1] - boundingRect.top; + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + c.x += deltaX; + c.y += deltaY; + } + success = true; + } + + // In either case, we set the occupied array as marked for the location of the views + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); + } + return success; + } + + private void markCellsForRect(Rect r, boolean[][] occupied, boolean value) { + markCellsForView(r.left, r.top, r.width(), r.height(), occupied, value); + } + + // This method tries to find a reordering solution which satisfies the push mechanic by trying + // to push items in each of the cardinal directions, in an order based on the direction vector + // passed. + private boolean attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied, + int[] direction, View ignoreView, ItemConfiguration solution) { + if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) { + // If the direction vector has two non-zero components, we try pushing + // separately in each of the components. + int temp = direction[1]; + direction[1] = 0; + + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + direction[1] = temp; + temp = direction[0]; + direction[0] = 0; + + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Revert the direction + direction[0] = temp; + + // Now we try pushing in each component of the opposite direction + direction[0] *= -1; + direction[1] *= -1; + temp = direction[1]; + direction[1] = 0; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + + direction[1] = temp; + temp = direction[0]; + direction[0] = 0; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // revert the direction + direction[0] = temp; + direction[0] *= -1; + direction[1] *= -1; + + } else { + // If the direction vector has a single non-zero component, we push first in the + // direction of the vector + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Then we try the opposite direction + direction[0] *= -1; + direction[1] *= -1; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Switch the direction back + direction[0] *= -1; + direction[1] *= -1; + + // If we have failed to find a push solution with the above, then we try + // to find a solution by pushing along the perpendicular axis. + + // Swap the components + int temp = direction[1]; + direction[1] = direction[0]; + direction[0] = temp; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + + // Then we try the opposite direction + direction[0] *= -1; + direction[1] *= -1; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Switch the direction back + direction[0] *= -1; + direction[1] *= -1; + + // Swap the components back + temp = direction[1]; + direction[1] = direction[0]; + direction[0] = temp; + } + return false; + } + + private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction, + View ignoreView, ItemConfiguration solution) { + // Return early if get invalid cell positions + if (cellX < 0 || cellY < 0) return false; + + mIntersectingViews.clear(); + mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY); + + // Mark the desired location of the view currently being dragged. + if (ignoreView != null) { + CellAndSpan c = solution.map.get(ignoreView); + if (c != null) { + c.x = cellX; + c.y = cellY; + } + } + Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY); + Rect r1 = new Rect(); + for (View child: solution.map.keySet()) { + if (child == ignoreView) continue; + CellAndSpan c = solution.map.get(child); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + r1.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + if (Rect.intersects(r0, r1)) { + if (!lp.canReorder) { + return false; + } + mIntersectingViews.add(child); + } + } + + // First we try to find a solution which respects the push mechanic. That is, + // we try to find a solution such that no displaced item travels through another item + // without also displacing that item. + if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView, + solution)) { + return true; + } + + // Next we try moving the views as a block, but without requiring the push mechanic. + if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView, + solution)) { + return true; + } + + // Ok, they couldn't move as a block, let's move them individually + for (View v : mIntersectingViews) { + if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) { + return false; + } + } + return true; + } + + /* + * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between + * the provided point and the provided cell + */ + private void computeDirectionVector(float deltaX, float deltaY, int[] result) { + double angle = Math.atan(((float) deltaY) / deltaX); + + result[0] = 0; + result[1] = 0; + if (Math.abs(Math.cos(angle)) > 0.5f) { + result[0] = (int) Math.signum(deltaX); + } + if (Math.abs(Math.sin(angle)) > 0.5f) { + result[1] = (int) Math.signum(deltaY); + } + } + + private void copyOccupiedArray(boolean[][] occupied) { + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + occupied[i][j] = mOccupied[i][j]; + } + } + } + + ItemConfiguration simpleSwap(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, + int spanY, int[] direction, View dragView, boolean decX, ItemConfiguration solution) { + // Copy the current state into the solution. This solution will be manipulated as necessary. + copyCurrentStateToSolution(solution, false); + // Copy the current occupied array into the temporary occupied array. This array will be + // manipulated as necessary to find a solution. + copyOccupiedArray(mTmpOccupied); + + // We find the nearest cell into which we would place the dragged item, assuming there's + // nothing in its way. + int result[] = new int[2]; + result = findNearestArea(pixelX, pixelY, spanX, spanY, result); + + boolean success = false; + // First we try the exact nearest position of the item being dragged, + // we will then want to try to move this around to other neighbouring positions + success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView, + solution); + + if (!success) { + // We try shrinking the widget down to size in an alternating pattern, shrink 1 in + // x, then 1 in y etc. + if (spanX > minSpanX && (minSpanY == spanY || decX)) { + return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY, direction, + dragView, false, solution); + } else if (spanY > minSpanY) { + return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1, direction, + dragView, true, solution); + } + solution.isSolution = false; + } else { + solution.isSolution = true; + solution.dragViewX = result[0]; + solution.dragViewY = result[1]; + solution.dragViewSpanX = spanX; + solution.dragViewSpanY = spanY; + } + return solution; + } + + private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) { + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + CellAndSpan c; + if (temp) { + c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan); + } else { + c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan); + } + solution.add(child, c); + } + } + + private void copySolutionToTempState(ItemConfiguration solution, View dragView) { + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + mTmpOccupied[i][j] = false; + } + } + + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + if (child == dragView) continue; + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + CellAndSpan c = solution.map.get(child); + if (c != null) { + lp.tmpCellX = c.x; + lp.tmpCellY = c.y; + lp.cellHSpan = c.spanX; + lp.cellVSpan = c.spanY; + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); + } + } + markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX, + solution.dragViewSpanY, mTmpOccupied, true); + } + + private void animateItemsToSolution(ItemConfiguration solution, View dragView, boolean + commitDragView) { + + boolean[][] occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied; + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + occupied[i][j] = false; + } + } + + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + if (child == dragView) continue; + CellAndSpan c = solution.map.get(child); + if (c != null) { + animateChildToPosition(child, c.x, c.y, REORDER_ANIMATION_DURATION, 0, + DESTRUCTIVE_REORDER, false); + markCellsForView(c.x, c.y, c.spanX, c.spanY, occupied, true); + } + } + if (commitDragView) { + markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX, + solution.dragViewSpanY, occupied, true); + } + } + + // This method starts or changes the reorder hint animations + private void beginOrAdjustHintAnimations(ItemConfiguration solution, View dragView, int delay) { + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + if (child == dragView) continue; + CellAndSpan c = solution.map.get(child); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (c != null) { + ReorderHintAnimation rha = new ReorderHintAnimation(child, lp.cellX, lp.cellY, + c.x, c.y, c.spanX, c.spanY); + rha.animate(); + } + } + } + + // Class which represents the reorder hint animations. These animations show that an item is + // in a temporary state, and hint at where the item will return to. + class ReorderHintAnimation { + View child; + float finalDeltaX; + float finalDeltaY; + float initDeltaX; + float initDeltaY; + float finalScale; + float initScale; + private static final int DURATION = 300; + Animator a; + + public ReorderHintAnimation(View child, int cellX0, int cellY0, int cellX1, int cellY1, + int spanX, int spanY) { + regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint); + final int x0 = mTmpPoint[0]; + final int y0 = mTmpPoint[1]; + regionToCenterPoint(cellX1, cellY1, spanX, spanY, mTmpPoint); + final int x1 = mTmpPoint[0]; + final int y1 = mTmpPoint[1]; + final int dX = x1 - x0; + final int dY = y1 - y0; + finalDeltaX = 0; + finalDeltaY = 0; + if (dX == dY && dX == 0) { + } else { + if (dY == 0) { + finalDeltaX = - Math.signum(dX) * mReorderHintAnimationMagnitude; + } else if (dX == 0) { + finalDeltaY = - Math.signum(dY) * mReorderHintAnimationMagnitude; + } else { + double angle = Math.atan( (float) (dY) / dX); + finalDeltaX = (int) (- Math.signum(dX) * + Math.abs(Math.cos(angle) * mReorderHintAnimationMagnitude)); + finalDeltaY = (int) (- Math.signum(dY) * + Math.abs(Math.sin(angle) * mReorderHintAnimationMagnitude)); + } + } + initDeltaX = child.getTranslationX(); + initDeltaY = child.getTranslationY(); + finalScale = getChildrenScale() - 4.0f / child.getWidth(); + initScale = child.getScaleX(); + this.child = child; + } + + void animate() { + if (mShakeAnimators.containsKey(child)) { + ReorderHintAnimation oldAnimation = mShakeAnimators.get(child); + oldAnimation.cancel(); + mShakeAnimators.remove(child); + if (finalDeltaX == 0 && finalDeltaY == 0) { + completeAnimationImmediately(); + return; + } + } + if (finalDeltaX == 0 && finalDeltaY == 0) { + return; + } + ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f); + a = va; + va.setRepeatMode(ValueAnimator.REVERSE); + va.setRepeatCount(ValueAnimator.INFINITE); + va.setDuration(DURATION); + va.setStartDelay((int) (Math.random() * 60)); + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float r = ((Float) animation.getAnimatedValue()).floatValue(); + float x = r * finalDeltaX + (1 - r) * initDeltaX; + float y = r * finalDeltaY + (1 - r) * initDeltaY; + child.setTranslationX(x); + child.setTranslationY(y); + float s = r * finalScale + (1 - r) * initScale; + child.setScaleX(s); + child.setScaleY(s); + } + }); + va.addListener(new AnimatorListenerAdapter() { + public void onAnimationRepeat(Animator animation) { + // We make sure to end only after a full period + initDeltaX = 0; + initDeltaY = 0; + initScale = getChildrenScale(); + } + }); + mShakeAnimators.put(child, this); + va.start(); + } + + private void cancel() { + if (a != null) { + a.cancel(); + } + } + + private void completeAnimationImmediately() { + if (a != null) { + a.cancel(); + } + + AnimatorSet s = LauncherAnimUtils.createAnimatorSet(); + a = s; + s.playTogether( + LauncherAnimUtils.ofFloat(child, "scaleX", getChildrenScale()), + LauncherAnimUtils.ofFloat(child, "scaleY", getChildrenScale()), + LauncherAnimUtils.ofFloat(child, "translationX", 0f), + LauncherAnimUtils.ofFloat(child, "translationY", 0f) + ); + s.setDuration(REORDER_ANIMATION_DURATION); + s.setInterpolator(new android.view.animation.DecelerateInterpolator(1.5f)); + s.start(); + } + } + + private void completeAndClearReorderHintAnimations() { + for (ReorderHintAnimation a: mShakeAnimators.values()) { + a.completeAnimationImmediately(); + } + mShakeAnimators.clear(); + } + + private void commitTempPlacement() { + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + mOccupied[i][j] = mTmpOccupied[i][j]; + } + } + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo info = (ItemInfo) child.getTag(); + // We do a null check here because the item info can be null in the case of the + // AllApps button in the hotseat. + if (info != null) { + if (info.cellX != lp.tmpCellX || info.cellY != lp.tmpCellY || + info.spanX != lp.cellHSpan || info.spanY != lp.cellVSpan) { + info.requiresDbUpdate = true; + } + info.cellX = lp.cellX = lp.tmpCellX; + info.cellY = lp.cellY = lp.tmpCellY; + info.spanX = lp.cellHSpan; + info.spanY = lp.cellVSpan; + } + } + mLauncher.getWorkspace().updateItemLocationsInDatabase(this); + } + + public void setUseTempCoords(boolean useTempCoords) { + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + LayoutParams lp = (LayoutParams) mShortcutsAndWidgets.getChildAt(i).getLayoutParams(); + lp.useTmpCoords = useTempCoords; + } + } + + ItemConfiguration findConfigurationNoShuffle(int pixelX, int pixelY, int minSpanX, int minSpanY, + int spanX, int spanY, View dragView, ItemConfiguration solution) { + int[] result = new int[2]; + int[] resultSpan = new int[2]; + findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null, result, + resultSpan); + if (result[0] >= 0 && result[1] >= 0) { + copyCurrentStateToSolution(solution, false); + solution.dragViewX = result[0]; + solution.dragViewY = result[1]; + solution.dragViewSpanX = resultSpan[0]; + solution.dragViewSpanY = resultSpan[1]; + solution.isSolution = true; + } else { + solution.isSolution = false; + } + return solution; + } + + public void prepareChildForDrag(View child) { + markCellsAsUnoccupiedForView(child); + } + + /* This seems like it should be obvious and straight-forward, but when the direction vector + needs to match with the notion of the dragView pushing other views, we have to employ + a slightly more subtle notion of the direction vector. The question is what two points is + the vector between? The center of the dragView and its desired destination? Not quite, as + this doesn't necessarily coincide with the interaction of the dragView and items occupying + those cells. Instead we use some heuristics to often lock the vector to up, down, left + or right, which helps make pushing feel right. + */ + private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX, + int spanY, View dragView, int[] resultDirection) { + int[] targetDestination = new int[2]; + + findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination); + Rect dragRect = new Rect(); + regionToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect); + dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY()); + + Rect dropRegionRect = new Rect(); + getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY, + dragView, dropRegionRect, mIntersectingViews); + + int dropRegionSpanX = dropRegionRect.width(); + int dropRegionSpanY = dropRegionRect.height(); + + regionToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(), + dropRegionRect.height(), dropRegionRect); + + int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX; + int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY; + + if (dropRegionSpanX == mCountX || spanX == mCountX) { + deltaX = 0; + } + if (dropRegionSpanY == mCountY || spanY == mCountY) { + deltaY = 0; + } + + if (deltaX == 0 && deltaY == 0) { + // No idea what to do, give a random direction. + resultDirection[0] = 1; + resultDirection[1] = 0; + } else { + computeDirectionVector(deltaX, deltaY, resultDirection); + } + } + + // For a given cell and span, fetch the set of views intersecting the region. + private void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY, + View dragView, Rect boundingRect, ArrayList<View> intersectingViews) { + if (boundingRect != null) { + boundingRect.set(cellX, cellY, cellX + spanX, cellY + spanY); + } + intersectingViews.clear(); + Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY); + Rect r1 = new Rect(); + final int count = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < count; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + if (child == dragView) continue; + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + r1.set(lp.cellX, lp.cellY, lp.cellX + lp.cellHSpan, lp.cellY + lp.cellVSpan); + if (Rect.intersects(r0, r1)) { + mIntersectingViews.add(child); + if (boundingRect != null) { + boundingRect.union(r1); + } + } + } + } + + boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY, + View dragView, int[] result) { + result = findNearestArea(pixelX, pixelY, spanX, spanY, result); + getViewsIntersectingRegion(result[0], result[1], spanX, spanY, dragView, null, + mIntersectingViews); + return !mIntersectingViews.isEmpty(); + } + + void revertTempState() { + if (!isItemPlacementDirty() || DESTRUCTIVE_REORDER) return; + final int count = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < count; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY) { + lp.tmpCellX = lp.cellX; + lp.tmpCellY = lp.cellY; + animateChildToPosition(child, lp.cellX, lp.cellY, REORDER_ANIMATION_DURATION, + 0, false, false); + } + } + completeAndClearReorderHintAnimations(); + setItemPlacementDirty(false); + } + + boolean createAreaForResize(int cellX, int cellY, int spanX, int spanY, + View dragView, int[] direction, boolean commit) { + int[] pixelXY = new int[2]; + regionToCenterPoint(cellX, cellY, spanX, spanY, pixelXY); + + // First we determine if things have moved enough to cause a different layout + ItemConfiguration swapSolution = simpleSwap(pixelXY[0], pixelXY[1], spanX, spanY, + spanX, spanY, direction, dragView, true, new ItemConfiguration()); + + setUseTempCoords(true); + if (swapSolution != null && swapSolution.isSolution) { + // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother + // committing anything or animating anything as we just want to determine if a solution + // exists + copySolutionToTempState(swapSolution, dragView); + setItemPlacementDirty(true); + animateItemsToSolution(swapSolution, dragView, commit); + + if (commit) { + commitTempPlacement(); + completeAndClearReorderHintAnimations(); + setItemPlacementDirty(false); + } else { + beginOrAdjustHintAnimations(swapSolution, dragView, + REORDER_ANIMATION_DURATION); + } + mShortcutsAndWidgets.requestLayout(); + } + return swapSolution.isSolution; + } + + int[] createArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, + View dragView, int[] result, int resultSpan[], int mode) { + // First we determine if things have moved enough to cause a different layout + result = findNearestArea(pixelX, pixelY, spanX, spanY, result); + + if (resultSpan == null) { + resultSpan = new int[2]; + } + + // When we are checking drop validity or actually dropping, we don't recompute the + // direction vector, since we want the solution to match the preview, and it's possible + // that the exact position of the item has changed to result in a new reordering outcome. + if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP) + && mPreviousReorderDirection[0] != INVALID_DIRECTION) { + mDirectionVector[0] = mPreviousReorderDirection[0]; + mDirectionVector[1] = mPreviousReorderDirection[1]; + // We reset this vector after drop + if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) { + mPreviousReorderDirection[0] = INVALID_DIRECTION; + mPreviousReorderDirection[1] = INVALID_DIRECTION; + } + } else { + getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector); + mPreviousReorderDirection[0] = mDirectionVector[0]; + mPreviousReorderDirection[1] = mDirectionVector[1]; + } + + ItemConfiguration swapSolution = simpleSwap(pixelX, pixelY, minSpanX, minSpanY, + spanX, spanY, mDirectionVector, dragView, true, new ItemConfiguration()); + + // We attempt the approach which doesn't shuffle views at all + ItemConfiguration noShuffleSolution = findConfigurationNoShuffle(pixelX, pixelY, minSpanX, + minSpanY, spanX, spanY, dragView, new ItemConfiguration()); + + ItemConfiguration finalSolution = null; + if (swapSolution.isSolution && swapSolution.area() >= noShuffleSolution.area()) { + finalSolution = swapSolution; + } else if (noShuffleSolution.isSolution) { + finalSolution = noShuffleSolution; + } + + boolean foundSolution = true; + if (!DESTRUCTIVE_REORDER) { + setUseTempCoords(true); + } + + if (finalSolution != null) { + result[0] = finalSolution.dragViewX; + result[1] = finalSolution.dragViewY; + resultSpan[0] = finalSolution.dragViewSpanX; + resultSpan[1] = finalSolution.dragViewSpanY; + + // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother + // committing anything or animating anything as we just want to determine if a solution + // exists + if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) { + if (!DESTRUCTIVE_REORDER) { + copySolutionToTempState(finalSolution, dragView); + } + setItemPlacementDirty(true); + animateItemsToSolution(finalSolution, dragView, mode == MODE_ON_DROP); + + if (!DESTRUCTIVE_REORDER && + (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) { + commitTempPlacement(); + completeAndClearReorderHintAnimations(); + setItemPlacementDirty(false); + } else { + beginOrAdjustHintAnimations(finalSolution, dragView, + REORDER_ANIMATION_DURATION); + } + } + } else { + foundSolution = false; + result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1; + } + + if ((mode == MODE_ON_DROP || !foundSolution) && !DESTRUCTIVE_REORDER) { + setUseTempCoords(false); + } + + mShortcutsAndWidgets.requestLayout(); + return result; + } + + void setItemPlacementDirty(boolean dirty) { + mItemPlacementDirty = dirty; + } + boolean isItemPlacementDirty() { + return mItemPlacementDirty; + } + + private class ItemConfiguration { + HashMap<View, CellAndSpan> map = new HashMap<View, CellAndSpan>(); + private HashMap<View, CellAndSpan> savedMap = new HashMap<View, CellAndSpan>(); + ArrayList<View> sortedViews = new ArrayList<View>(); + boolean isSolution = false; + int dragViewX, dragViewY, dragViewSpanX, dragViewSpanY; + + void save() { + // Copy current state into savedMap + for (View v: map.keySet()) { + map.get(v).copy(savedMap.get(v)); + } + } + + void restore() { + // Restore current state from savedMap + for (View v: savedMap.keySet()) { + savedMap.get(v).copy(map.get(v)); + } + } + + void add(View v, CellAndSpan cs) { + map.put(v, cs); + savedMap.put(v, new CellAndSpan()); + sortedViews.add(v); + } + + int area() { + return dragViewSpanX * dragViewSpanY; + } + } + + private class CellAndSpan { + int x, y; + int spanX, spanY; + + public CellAndSpan() { + } + + public void copy(CellAndSpan copy) { + copy.x = x; + copy.y = y; + copy.spanX = spanX; + copy.spanY = spanY; + } + + public CellAndSpan(int x, int y, int spanX, int spanY) { + this.x = x; + this.y = y; + this.spanX = spanX; + this.spanY = spanY; + } + + public String toString() { + return "(" + x + ", " + y + ": " + spanX + ", " + spanY + ")"; + } + + } + + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location. Uses Euclidean distance to score multiple vacant areas. + * + * @param pixelX The X location at which you want to search for a vacant area. + * @param pixelY The Y location at which you want to search for a vacant area. + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param ignoreView Considers space occupied by this view as unoccupied + * @param result Previously returned value to possibly recycle. + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + int[] findNearestVacantArea( + int pixelX, int pixelY, int spanX, int spanY, View ignoreView, int[] result) { + return findNearestArea(pixelX, pixelY, spanX, spanY, ignoreView, true, result); + } + + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location. Uses Euclidean distance to score multiple vacant areas. + * + * @param pixelX The X location at which you want to search for a vacant area. + * @param pixelY The Y location at which you want to search for a vacant area. + * @param minSpanX The minimum horizontal span required + * @param minSpanY The minimum vertical span required + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param ignoreView Considers space occupied by this view as unoccupied + * @param result Previously returned value to possibly recycle. + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, + int spanX, int spanY, View ignoreView, int[] result, int[] resultSpan) { + return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, ignoreView, true, + result, resultSpan, mOccupied); + } + + /** + * Find a starting cell position that will fit the given bounds nearest the requested + * cell location. Uses Euclidean distance to score multiple vacant areas. + * + * @param pixelX The X location at which you want to search for a vacant area. + * @param pixelY The Y location at which you want to search for a vacant area. + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param ignoreView Considers space occupied by this view as unoccupied + * @param result Previously returned value to possibly recycle. + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + int[] findNearestArea( + int pixelX, int pixelY, int spanX, int spanY, int[] result) { + return findNearestArea(pixelX, pixelY, spanX, spanY, null, false, result); + } + + boolean existsEmptyCell() { + return findCellForSpan(null, 1, 1); + } + + /** + * Finds the upper-left coordinate of the first rectangle in the grid that can + * hold a cell of the specified dimensions. If intersectX and intersectY are not -1, + * then this method will only return coordinates for rectangles that contain the cell + * (intersectX, intersectY) + * + * @param cellXY The array that will contain the position of a vacant cell if such a cell + * can be found. + * @param spanX The horizontal span of the cell we want to find. + * @param spanY The vertical span of the cell we want to find. + * + * @return True if a vacant cell of the specified dimension was found, false otherwise. + */ + boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { + return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, null, mOccupied); + } + + /** + * Like above, but ignores any cells occupied by the item "ignoreView" + * + * @param cellXY The array that will contain the position of a vacant cell if such a cell + * can be found. + * @param spanX The horizontal span of the cell we want to find. + * @param spanY The vertical span of the cell we want to find. + * @param ignoreView The home screen item we should treat as not occupying any space + * @return + */ + boolean findCellForSpanIgnoring(int[] cellXY, int spanX, int spanY, View ignoreView) { + return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, + ignoreView, mOccupied); + } + + /** + * Like above, but if intersectX and intersectY are not -1, then this method will try to + * return coordinates for rectangles that contain the cell [intersectX, intersectY] + * + * @param spanX The horizontal span of the cell we want to find. + * @param spanY The vertical span of the cell we want to find. + * @param ignoreView The home screen item we should treat as not occupying any space + * @param intersectX The X coordinate of the cell that we should try to overlap + * @param intersectX The Y coordinate of the cell that we should try to overlap + * + * @return True if a vacant cell of the specified dimension was found, false otherwise. + */ + boolean findCellForSpanThatIntersects(int[] cellXY, int spanX, int spanY, + int intersectX, int intersectY) { + return findCellForSpanThatIntersectsIgnoring( + cellXY, spanX, spanY, intersectX, intersectY, null, mOccupied); + } + + /** + * The superset of the above two methods + */ + boolean findCellForSpanThatIntersectsIgnoring(int[] cellXY, int spanX, int spanY, + int intersectX, int intersectY, View ignoreView, boolean occupied[][]) { + // mark space take by ignoreView as available (method checks if ignoreView is null) + markCellsAsUnoccupiedForView(ignoreView, occupied); + + boolean foundCell = false; + while (true) { + int startX = 0; + if (intersectX >= 0) { + startX = Math.max(startX, intersectX - (spanX - 1)); + } + int endX = mCountX - (spanX - 1); + if (intersectX >= 0) { + endX = Math.min(endX, intersectX + (spanX - 1) + (spanX == 1 ? 1 : 0)); + } + int startY = 0; + if (intersectY >= 0) { + startY = Math.max(startY, intersectY - (spanY - 1)); + } + int endY = mCountY - (spanY - 1); + if (intersectY >= 0) { + endY = Math.min(endY, intersectY + (spanY - 1) + (spanY == 1 ? 1 : 0)); + } + + for (int y = startY; y < endY && !foundCell; y++) { + inner: + for (int x = startX; x < endX; x++) { + for (int i = 0; i < spanX; i++) { + for (int j = 0; j < spanY; j++) { + if (occupied[x + i][y + j]) { + // small optimization: we can skip to after the column we just found + // an occupied cell + x += i; + continue inner; + } + } + } + if (cellXY != null) { + cellXY[0] = x; + cellXY[1] = y; + } + foundCell = true; + break; + } + } + if (intersectX == -1 && intersectY == -1) { + break; + } else { + // if we failed to find anything, try again but without any requirements of + // intersecting + intersectX = -1; + intersectY = -1; + continue; + } + } + + // re-mark space taken by ignoreView as occupied + markCellsAsOccupiedForView(ignoreView, occupied); + return foundCell; + } + + /** + * A drag event has begun over this layout. + * It may have begun over this layout (in which case onDragChild is called first), + * or it may have begun on another layout. + */ + void onDragEnter() { + mDragEnforcer.onDragEnter(); + mDragging = true; + } + + /** + * Called when drag has left this CellLayout or has been completed (successfully or not) + */ + void onDragExit() { + mDragEnforcer.onDragExit(); + // This can actually be called when we aren't in a drag, e.g. when adding a new + // item to this layout via the customize drawer. + // Guard against that case. + if (mDragging) { + mDragging = false; + } + + // Invalidate the drag data + mDragCell[0] = mDragCell[1] = -1; + mDragOutlineAnims[mDragOutlineCurrent].animateOut(); + mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length; + revertTempState(); + setIsDragOverlapping(false); + } + + /** + * Mark a child as having been dropped. + * At the beginning of the drag operation, the child may have been on another + * screen, but it is re-parented before this method is called. + * + * @param child The child that is being dropped + */ + void onDropChild(View child) { + if (child != null) { + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.dropped = true; + child.requestLayout(); + } + } + + /** + * Computes a bounding rectangle for a range of cells + * + * @param cellX X coordinate of upper left corner expressed as a cell position + * @param cellY Y coordinate of upper left corner expressed as a cell position + * @param cellHSpan Width in cells + * @param cellVSpan Height in cells + * @param resultRect Rect into which to put the results + */ + public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect) { + final int cellWidth = mCellWidth; + final int cellHeight = mCellHeight; + final int widthGap = mWidthGap; + final int heightGap = mHeightGap; + + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + + int width = cellHSpan * cellWidth + ((cellHSpan - 1) * widthGap); + int height = cellVSpan * cellHeight + ((cellVSpan - 1) * heightGap); + + int x = hStartPadding + cellX * (cellWidth + widthGap); + int y = vStartPadding + cellY * (cellHeight + heightGap); + + resultRect.set(x, y, x + width, y + height); + } + + /** + * Computes the required horizontal and vertical cell spans to always + * fit the given rectangle. + * + * @param width Width in pixels + * @param height Height in pixels + * @param result An array of length 2 in which to store the result (may be null). + */ + public int[] rectToCell(int width, int height, int[] result) { + return rectToCell(getResources(), width, height, result); + } + + public static int[] rectToCell(Resources resources, int width, int height, int[] result) { + // Always assume we're working with the smallest span to make sure we + // reserve enough space in both orientations. + int actualWidth = resources.getDimensionPixelSize(R.dimen.workspace_cell_width); + int actualHeight = resources.getDimensionPixelSize(R.dimen.workspace_cell_height); + int smallerSize = Math.min(actualWidth, actualHeight); + + // Always round up to next largest cell + int spanX = (int) Math.ceil(width / (float) smallerSize); + int spanY = (int) Math.ceil(height / (float) smallerSize); + + if (result == null) { + return new int[] { spanX, spanY }; + } + result[0] = spanX; + result[1] = spanY; + return result; + } + + public int[] cellSpansToSize(int hSpans, int vSpans) { + int[] size = new int[2]; + size[0] = hSpans * mCellWidth + (hSpans - 1) * mWidthGap; + size[1] = vSpans * mCellHeight + (vSpans - 1) * mHeightGap; + return size; + } + + /** + * Calculate the grid spans needed to fit given item + */ + public void calculateSpans(ItemInfo info) { + final int minWidth; + final int minHeight; + + if (info instanceof LauncherAppWidgetInfo) { + minWidth = ((LauncherAppWidgetInfo) info).minWidth; + minHeight = ((LauncherAppWidgetInfo) info).minHeight; + } else if (info instanceof PendingAddWidgetInfo) { + minWidth = ((PendingAddWidgetInfo) info).minWidth; + minHeight = ((PendingAddWidgetInfo) info).minHeight; + } else { + // It's not a widget, so it must be 1x1 + info.spanX = info.spanY = 1; + return; + } + int[] spans = rectToCell(minWidth, minHeight, null); + info.spanX = spans[0]; + info.spanY = spans[1]; + } + + /** + * Find the first vacant cell, if there is one. + * + * @param vacant Holds the x and y coordinate of the vacant cell + * @param spanX Horizontal cell span. + * @param spanY Vertical cell span. + * + * @return True if a vacant cell was found + */ + public boolean getVacantCell(int[] vacant, int spanX, int spanY) { + + return findVacantCell(vacant, spanX, spanY, mCountX, mCountY, mOccupied); + } + + static boolean findVacantCell(int[] vacant, int spanX, int spanY, + int xCount, int yCount, boolean[][] occupied) { + + for (int y = 0; y < yCount; y++) { + for (int x = 0; x < xCount; x++) { + boolean available = !occupied[x][y]; +out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { + for (int j = y; j < y + spanY - 1 && y < yCount; j++) { + available = available && !occupied[i][j]; + if (!available) break out; + } + } + + if (available) { + vacant[0] = x; + vacant[1] = y; + return true; + } + } + } + + return false; + } + + private void clearOccupiedCells() { + for (int x = 0; x < mCountX; x++) { + for (int y = 0; y < mCountY; y++) { + mOccupied[x][y] = false; + } + } + } + + public void onMove(View view, int newCellX, int newCellY, int newSpanX, int newSpanY) { + markCellsAsUnoccupiedForView(view); + markCellsForView(newCellX, newCellY, newSpanX, newSpanY, mOccupied, true); + } + + public void markCellsAsOccupiedForView(View view) { + markCellsAsOccupiedForView(view, mOccupied); + } + public void markCellsAsOccupiedForView(View view, boolean[][] occupied) { + if (view == null || view.getParent() != mShortcutsAndWidgets) return; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, true); + } + + public void markCellsAsUnoccupiedForView(View view) { + markCellsAsUnoccupiedForView(view, mOccupied); + } + public void markCellsAsUnoccupiedForView(View view, boolean occupied[][]) { + if (view == null || view.getParent() != mShortcutsAndWidgets) return; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, false); + } + + private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean[][] occupied, + boolean value) { + if (cellX < 0 || cellY < 0) return; + for (int x = cellX; x < cellX + spanX && x < mCountX; x++) { + for (int y = cellY; y < cellY + spanY && y < mCountY; y++) { + occupied[x][y] = value; + } + } + } + + public int getDesiredWidth() { + return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth) + + (Math.max((mCountX - 1), 0) * mWidthGap); + } + + public int getDesiredHeight() { + return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight) + + (Math.max((mCountY - 1), 0) * mHeightGap); + } + + public boolean isOccupied(int x, int y) { + if (x < mCountX && y < mCountY) { + return mOccupied[x][y]; + } else { + throw new RuntimeException("Position exceeds the bound of this CellLayout"); + } + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new CellLayout.LayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof CellLayout.LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new CellLayout.LayoutParams(p); + } + + public static class CellLayoutAnimationController extends LayoutAnimationController { + public CellLayoutAnimationController(Animation animation, float delay) { + super(animation, delay); + } + + @Override + protected long getDelayForView(View view) { + return (int) (Math.random() * 150); + } + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + /** + * Horizontal location of the item in the grid. + */ + @ViewDebug.ExportedProperty + public int cellX; + + /** + * Vertical location of the item in the grid. + */ + @ViewDebug.ExportedProperty + public int cellY; + + /** + * Temporary horizontal location of the item in the grid during reorder + */ + public int tmpCellX; + + /** + * Temporary vertical location of the item in the grid during reorder + */ + public int tmpCellY; + + /** + * Indicates that the temporary coordinates should be used to layout the items + */ + public boolean useTmpCoords; + + /** + * Number of cells spanned horizontally by the item. + */ + @ViewDebug.ExportedProperty + public int cellHSpan; + + /** + * Number of cells spanned vertically by the item. + */ + @ViewDebug.ExportedProperty + public int cellVSpan; + + /** + * Indicates whether the item will set its x, y, width and height parameters freely, + * or whether these will be computed based on cellX, cellY, cellHSpan and cellVSpan. + */ + public boolean isLockedToGrid = true; + + /** + * Indicates whether this item can be reordered. Always true except in the case of the + * the AllApps button. + */ + public boolean canReorder = true; + + // X coordinate of the view in the layout. + @ViewDebug.ExportedProperty + int x; + // Y coordinate of the view in the layout. + @ViewDebug.ExportedProperty + int y; + + boolean dropped; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + cellHSpan = 1; + cellVSpan = 1; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + cellHSpan = 1; + cellVSpan = 1; + } + + public LayoutParams(LayoutParams source) { + super(source); + this.cellX = source.cellX; + this.cellY = source.cellY; + this.cellHSpan = source.cellHSpan; + this.cellVSpan = source.cellVSpan; + } + + public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) { + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + this.cellX = cellX; + this.cellY = cellY; + this.cellHSpan = cellHSpan; + this.cellVSpan = cellVSpan; + } + + public void setup(int cellWidth, int cellHeight, int widthGap, int heightGap, + boolean invertHorizontally, int colCount) { + if (isLockedToGrid) { + final int myCellHSpan = cellHSpan; + final int myCellVSpan = cellVSpan; + int myCellX = useTmpCoords ? tmpCellX : cellX; + int myCellY = useTmpCoords ? tmpCellY : cellY; + + if (invertHorizontally) { + myCellX = colCount - myCellX - cellHSpan; + } + + width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - + leftMargin - rightMargin; + height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - + topMargin - bottomMargin; + x = (int) (myCellX * (cellWidth + widthGap) + leftMargin); + y = (int) (myCellY * (cellHeight + heightGap) + topMargin); + } + } + + public String toString() { + return "(" + this.cellX + ", " + this.cellY + ")"; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getWidth() { + return width; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getHeight() { + return height; + } + + public void setX(int x) { + this.x = x; + } + + public int getX() { + return x; + } + + public void setY(int y) { + this.y = y; + } + + public int getY() { + return y; + } + } + + // This class stores info for two purposes: + // 1. When dragging items (mDragInfo in Workspace), we store the View, its cellX & cellY, + // its spanX, spanY, and the screen it is on + // 2. When long clicking on an empty cell in a CellLayout, we save information about the + // cellX and cellY coordinates and which page was clicked. We then set this as a tag on + // the CellLayout that was long clicked + static final class CellInfo { + View cell; + int cellX = -1; + int cellY = -1; + int spanX; + int spanY; + int screen; + long container; + + @Override + public String toString() { + return "Cell[view=" + (cell == null ? "null" : cell.getClass()) + + ", x=" + cellX + ", y=" + cellY + "]"; + } + } + + public boolean lastDownOnOccupiedCell() { + return mLastDownOnOccupiedCell; + } +} diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java new file mode 100644 index 000000000..7760f4e0c --- /dev/null +++ b/src/com/android/launcher3/CheckLongPressHelper.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012 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.view.View; + +public class CheckLongPressHelper { + private View mView; + private boolean mHasPerformedLongPress; + private CheckForLongPress mPendingCheckForLongPress; + + class CheckForLongPress implements Runnable { + public void run() { + if ((mView.getParent() != null) && mView.hasWindowFocus() + && !mHasPerformedLongPress) { + if (mView.performLongClick()) { + mView.setPressed(false); + mHasPerformedLongPress = true; + } + } + } + } + + public CheckLongPressHelper(View v) { + mView = v; + } + + public void postCheckForLongPress() { + mHasPerformedLongPress = false; + + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + mView.postDelayed(mPendingCheckForLongPress, LauncherApplication.getLongPressTimeout()); + } + + public void cancelLongPress() { + mHasPerformedLongPress = false; + if (mPendingCheckForLongPress != null) { + mView.removeCallbacks(mPendingCheckForLongPress); + mPendingCheckForLongPress = null; + } + } + + public boolean hasPerformedLongPress() { + return mHasPerformedLongPress; + } +} diff --git a/src/com/android/launcher3/Cling.java b/src/com/android/launcher3/Cling.java new file mode 100644 index 000000000..6bb183ce3 --- /dev/null +++ b/src/com/android/launcher3/Cling.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.FocusFinder; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.launcher3.R; + +public class Cling extends FrameLayout { + + static final String WORKSPACE_CLING_DISMISSED_KEY = "cling.workspace.dismissed"; + static final String ALLAPPS_CLING_DISMISSED_KEY = "cling.allapps.dismissed"; + static final String FOLDER_CLING_DISMISSED_KEY = "cling.folder.dismissed"; + + private static String WORKSPACE_PORTRAIT = "workspace_portrait"; + private static String WORKSPACE_LANDSCAPE = "workspace_landscape"; + private static String WORKSPACE_LARGE = "workspace_large"; + private static String WORKSPACE_CUSTOM = "workspace_custom"; + + private static String ALLAPPS_PORTRAIT = "all_apps_portrait"; + private static String ALLAPPS_LANDSCAPE = "all_apps_landscape"; + private static String ALLAPPS_LARGE = "all_apps_large"; + + private static String FOLDER_PORTRAIT = "folder_portrait"; + private static String FOLDER_LANDSCAPE = "folder_landscape"; + private static String FOLDER_LARGE = "folder_large"; + + private Launcher mLauncher; + private boolean mIsInitialized; + private String mDrawIdentifier; + private Drawable mBackground; + private Drawable mPunchThroughGraphic; + private Drawable mHandTouchGraphic; + private int mPunchThroughGraphicCenterRadius; + private int mAppIconSize; + private int mButtonBarHeight; + private float mRevealRadius; + private int[] mPositionData; + + private Paint mErasePaint; + + public Cling(Context context) { + this(context, null, 0); + } + + public Cling(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public Cling(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Cling, defStyle, 0); + mDrawIdentifier = a.getString(R.styleable.Cling_drawIdentifier); + a.recycle(); + + setClickable(true); + } + + void init(Launcher l, int[] positionData) { + if (!mIsInitialized) { + mLauncher = l; + mPositionData = positionData; + + Resources r = getContext().getResources(); + + mPunchThroughGraphic = r.getDrawable(R.drawable.cling); + mPunchThroughGraphicCenterRadius = + r.getDimensionPixelSize(R.dimen.clingPunchThroughGraphicCenterRadius); + mAppIconSize = r.getDimensionPixelSize(R.dimen.app_icon_size); + mRevealRadius = r.getDimensionPixelSize(R.dimen.reveal_radius) * 1f; + mButtonBarHeight = r.getDimensionPixelSize(R.dimen.button_bar_height); + + mErasePaint = new Paint(); + mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); + mErasePaint.setColor(0xFFFFFF); + mErasePaint.setAlpha(0); + + mIsInitialized = true; + } + } + + void cleanup() { + mBackground = null; + mPunchThroughGraphic = null; + mHandTouchGraphic = null; + mIsInitialized = false; + } + + public String getDrawIdentifier() { + return mDrawIdentifier; + } + + private int[] getPunchThroughPositions() { + if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT)) { + return new int[]{getMeasuredWidth() / 2, getMeasuredHeight() - (mButtonBarHeight / 2)}; + } else if (mDrawIdentifier.equals(WORKSPACE_LANDSCAPE)) { + return new int[]{getMeasuredWidth() - (mButtonBarHeight / 2), getMeasuredHeight() / 2}; + } else if (mDrawIdentifier.equals(WORKSPACE_LARGE)) { + final float scale = LauncherApplication.getScreenDensity(); + final int cornerXOffset = (int) (scale * 15); + final int cornerYOffset = (int) (scale * 10); + return new int[]{getMeasuredWidth() - cornerXOffset, cornerYOffset}; + } else if (mDrawIdentifier.equals(ALLAPPS_PORTRAIT) || + mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) || + mDrawIdentifier.equals(ALLAPPS_LARGE)) { + return mPositionData; + } + return new int[]{-1, -1}; + } + + @Override + public View focusSearch(int direction) { + return this.focusSearch(this, direction); + } + + @Override + public View focusSearch(View focused, int direction) { + return FocusFinder.getInstance().findNextFocus(this, focused, direction); + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + return (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) + || mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) + || mDrawIdentifier.equals(WORKSPACE_LARGE) + || mDrawIdentifier.equals(ALLAPPS_PORTRAIT) + || mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) + || mDrawIdentifier.equals(ALLAPPS_LARGE) + || mDrawIdentifier.equals(WORKSPACE_CUSTOM)); + } + + @Override + public boolean onTouchEvent(android.view.MotionEvent event) { + if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || + mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || + mDrawIdentifier.equals(WORKSPACE_LARGE) || + mDrawIdentifier.equals(ALLAPPS_PORTRAIT) || + mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) || + mDrawIdentifier.equals(ALLAPPS_LARGE)) { + + int[] positions = getPunchThroughPositions(); + for (int i = 0; i < positions.length; i += 2) { + double diff = Math.sqrt(Math.pow(event.getX() - positions[i], 2) + + Math.pow(event.getY() - positions[i + 1], 2)); + if (diff < mRevealRadius) { + return false; + } + } + } else if (mDrawIdentifier.equals(FOLDER_PORTRAIT) || + mDrawIdentifier.equals(FOLDER_LANDSCAPE) || + mDrawIdentifier.equals(FOLDER_LARGE)) { + Folder f = mLauncher.getWorkspace().getOpenFolder(); + if (f != null) { + Rect r = new Rect(); + f.getHitRect(r); + if (r.contains((int) event.getX(), (int) event.getY())) { + return false; + } + } + } + return true; + }; + + @Override + protected void dispatchDraw(Canvas canvas) { + if (mIsInitialized) { + DisplayMetrics metrics = new DisplayMetrics(); + mLauncher.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + // Initialize the draw buffer (to allow punching through) + Bitmap b = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + + // Draw the background + if (mBackground == null) { + if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || + mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || + mDrawIdentifier.equals(WORKSPACE_LARGE)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling1); + } else if (mDrawIdentifier.equals(ALLAPPS_PORTRAIT) || + mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) || + mDrawIdentifier.equals(ALLAPPS_LARGE)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling2); + } else if (mDrawIdentifier.equals(FOLDER_PORTRAIT) || + mDrawIdentifier.equals(FOLDER_LANDSCAPE)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling3); + } else if (mDrawIdentifier.equals(FOLDER_LARGE)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling4); + } else if (mDrawIdentifier.equals(WORKSPACE_CUSTOM)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling5); + } + } + if (mBackground != null) { + mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); + mBackground.draw(c); + } else { + c.drawColor(0x99000000); + } + + int cx = -1; + int cy = -1; + float scale = mRevealRadius / mPunchThroughGraphicCenterRadius; + int dw = (int) (scale * mPunchThroughGraphic.getIntrinsicWidth()); + int dh = (int) (scale * mPunchThroughGraphic.getIntrinsicHeight()); + + // Determine where to draw the punch through graphic + int[] positions = getPunchThroughPositions(); + for (int i = 0; i < positions.length; i += 2) { + cx = positions[i]; + cy = positions[i + 1]; + if (cx > -1 && cy > -1) { + c.drawCircle(cx, cy, mRevealRadius, mErasePaint); + mPunchThroughGraphic.setBounds(cx - dw/2, cy - dh/2, cx + dw/2, cy + dh/2); + mPunchThroughGraphic.draw(c); + } + } + + // Draw the hand graphic in All Apps + if (mDrawIdentifier.equals(ALLAPPS_PORTRAIT) || + mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) || + mDrawIdentifier.equals(ALLAPPS_LARGE)) { + if (mHandTouchGraphic == null) { + mHandTouchGraphic = getResources().getDrawable(R.drawable.hand); + } + int offset = mAppIconSize / 4; + mHandTouchGraphic.setBounds(cx + offset, cy + offset, + cx + mHandTouchGraphic.getIntrinsicWidth() + offset, + cy + mHandTouchGraphic.getIntrinsicHeight() + offset); + mHandTouchGraphic.draw(c); + } + + canvas.drawBitmap(b, 0, 0, null); + c.setBitmap(null); + b = null; + } + + // Draw the rest of the cling + super.dispatchDraw(canvas); + }; +} diff --git a/src/com/android/launcher3/DeferredHandler.java b/src/com/android/launcher3/DeferredHandler.java new file mode 100644 index 000000000..92ecf9643 --- /dev/null +++ b/src/com/android/launcher3/DeferredHandler.java @@ -0,0 +1,146 @@ +/* + * 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.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue; +import android.util.Pair; +import java.util.LinkedList; +import java.util.ListIterator; + +/** + * Queue of things to run on a looper thread. Items posted with {@link #post} will not + * be actually enqued on the handler until after the last one has run, to keep from + * starving the thread. + * + * This class is fifo. + */ +public class DeferredHandler { + private LinkedList<Pair<Runnable, Integer>> mQueue = new LinkedList<Pair<Runnable, Integer>>(); + private MessageQueue mMessageQueue = Looper.myQueue(); + private Impl mHandler = new Impl(); + + private class Impl extends Handler implements MessageQueue.IdleHandler { + public void handleMessage(Message msg) { + Pair<Runnable, Integer> p; + Runnable r; + synchronized (mQueue) { + if (mQueue.size() == 0) { + return; + } + p = mQueue.removeFirst(); + r = p.first; + } + r.run(); + synchronized (mQueue) { + scheduleNextLocked(); + } + } + + public boolean queueIdle() { + handleMessage(null); + return false; + } + } + + private class IdleRunnable implements Runnable { + Runnable mRunnable; + + IdleRunnable(Runnable r) { + mRunnable = r; + } + + public void run() { + mRunnable.run(); + } + } + + public DeferredHandler() { + } + + /** Schedule runnable to run after everything that's on the queue right now. */ + public void post(Runnable runnable) { + post(runnable, 0); + } + public void post(Runnable runnable, int type) { + synchronized (mQueue) { + mQueue.add(new Pair<Runnable, Integer>(runnable, type)); + if (mQueue.size() == 1) { + scheduleNextLocked(); + } + } + } + + /** Schedule runnable to run when the queue goes idle. */ + public void postIdle(final Runnable runnable) { + postIdle(runnable, 0); + } + public void postIdle(final Runnable runnable, int type) { + post(new IdleRunnable(runnable), type); + } + + public void cancelRunnable(Runnable runnable) { + synchronized (mQueue) { + while (mQueue.remove(runnable)) { } + } + } + public void cancelAllRunnablesOfType(int type) { + synchronized (mQueue) { + ListIterator<Pair<Runnable, Integer>> iter = mQueue.listIterator(); + Pair<Runnable, Integer> p; + while (iter.hasNext()) { + p = iter.next(); + if (p.second == type) { + iter.remove(); + } + } + } + } + + public void cancel() { + synchronized (mQueue) { + mQueue.clear(); + } + } + + /** Runs all queued Runnables from the calling thread. */ + public void flush() { + LinkedList<Pair<Runnable, Integer>> queue = new LinkedList<Pair<Runnable, Integer>>(); + synchronized (mQueue) { + queue.addAll(mQueue); + mQueue.clear(); + } + for (Pair<Runnable, Integer> p : queue) { + p.first.run(); + } + } + + void scheduleNextLocked() { + if (mQueue.size() > 0) { + Pair<Runnable, Integer> p = mQueue.getFirst(); + Runnable peek = p.first; + if (peek instanceof IdleRunnable) { + mMessageQueue.addIdleHandler(mHandler); + } else { + mHandler.sendEmptyMessage(1); + } + } + } +} + diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java new file mode 100644 index 000000000..eba154732 --- /dev/null +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -0,0 +1,437 @@ +/* + * Copyright (C) 2011 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.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.TransitionDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; + +import com.android.launcher3.R; + +public class DeleteDropTarget extends ButtonDropTarget { + private static int DELETE_ANIMATION_DURATION = 285; + private static int FLING_DELETE_ANIMATION_DURATION = 350; + private static float FLING_TO_DELETE_FRICTION = 0.035f; + private static int MODE_FLING_DELETE_TO_TRASH = 0; + private static int MODE_FLING_DELETE_ALONG_VECTOR = 1; + + private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR; + + private ColorStateList mOriginalTextColor; + private TransitionDrawable mUninstallDrawable; + private TransitionDrawable mRemoveDrawable; + private TransitionDrawable mCurrentDrawable; + + public DeleteDropTarget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DeleteDropTarget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + // Get the drawable + mOriginalTextColor = getTextColors(); + + // Get the hover color + Resources r = getResources(); + mHoverColor = r.getColor(R.color.delete_target_hover_tint); + mUninstallDrawable = (TransitionDrawable) + r.getDrawable(R.drawable.uninstall_target_selector); + mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector); + + mRemoveDrawable.setCrossFadeEnabled(true); + mUninstallDrawable.setCrossFadeEnabled(true); + + // The current drawable is set to either the remove drawable or the uninstall drawable + // and is initially set to the remove drawable, as set in the layout xml. + mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); + + // Remove the text in the Phone UI in landscape + int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + if (!LauncherApplication.isScreenLarge()) { + setText(""); + } + } + } + + private boolean isAllAppsApplication(DragSource source, Object info) { + return (source instanceof AppsCustomizePagedView) && (info instanceof ApplicationInfo); + } + private boolean isAllAppsWidget(DragSource source, Object info) { + if (source instanceof AppsCustomizePagedView) { + if (info instanceof PendingAddItemInfo) { + PendingAddItemInfo addInfo = (PendingAddItemInfo) info; + switch (addInfo.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + return true; + } + } + } + return false; + } + private boolean isDragSourceWorkspaceOrFolder(DragObject d) { + return (d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder); + } + private boolean isWorkspaceOrFolderApplication(DragObject d) { + return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof ShortcutInfo); + } + private boolean isWorkspaceOrFolderWidget(DragObject d) { + return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof LauncherAppWidgetInfo); + } + private boolean isWorkspaceFolder(DragObject d) { + return (d.dragSource instanceof Workspace) && (d.dragInfo instanceof FolderInfo); + } + + private void setHoverColor() { + mCurrentDrawable.startTransition(mTransitionDuration); + setTextColor(mHoverColor); + } + private void resetHoverColor() { + mCurrentDrawable.resetTransition(); + setTextColor(mOriginalTextColor); + } + + @Override + public boolean acceptDrop(DragObject d) { + // We can remove everything including App shortcuts, folders, widgets, etc. + return true; + } + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { + boolean isVisible = true; + boolean isUninstall = false; + + // If we are dragging a widget from AppsCustomize, hide the delete target + if (isAllAppsWidget(source, info)) { + isVisible = false; + } + + // If we are dragging an application from AppsCustomize, only show the control if we can + // delete the app (it was downloaded), and rename the string to "uninstall" in such a case + if (isAllAppsApplication(source, info)) { + ApplicationInfo appInfo = (ApplicationInfo) info; + if ((appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) != 0) { + isUninstall = true; + } else { + isVisible = false; + } + } + + if (isUninstall) { + setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null); + } else { + setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null); + } + mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); + + mActive = isVisible; + resetHoverColor(); + ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); + if (getText().length() > 0) { + setText(isUninstall ? R.string.delete_target_uninstall_label + : R.string.delete_target_label); + } + } + + @Override + public void onDragEnd() { + super.onDragEnd(); + mActive = false; + } + + public void onDragEnter(DragObject d) { + super.onDragEnter(d); + + setHoverColor(); + } + + public void onDragExit(DragObject d) { + super.onDragExit(d); + + if (!d.dragComplete) { + resetHoverColor(); + } else { + // Restore the hover color if we are deleting + d.dragView.setColor(mHoverColor); + } + } + + private void animateToTrashAndCompleteDrop(final DragObject d) { + DragLayer dragLayer = mLauncher.getDragLayer(); + Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, from); + Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); + float scale = (float) to.width() / from.width(); + + mSearchDropTargetBar.deferOnDragEnd(); + Runnable onAnimationEndRunnable = new Runnable() { + @Override + public void run() { + mSearchDropTargetBar.onDragEnd(); + mLauncher.exitSpringLoadedDragMode(); + completeDrop(d); + } + }; + dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, + DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2), + new LinearInterpolator(), onAnimationEndRunnable, + DragLayer.ANIMATION_END_DISAPPEAR, null); + } + + private void completeDrop(DragObject d) { + ItemInfo item = (ItemInfo) d.dragInfo; + + if (isAllAppsApplication(d.dragSource, item)) { + // Uninstall the application if it is being dragged from AppsCustomize + mLauncher.startApplicationUninstallActivity((ApplicationInfo) item); + } else if (isWorkspaceOrFolderApplication(d)) { + LauncherModel.deleteItemFromDatabase(mLauncher, item); + } else if (isWorkspaceFolder(d)) { + // Remove the folder from the workspace and delete the contents from launcher model + FolderInfo folderInfo = (FolderInfo) item; + mLauncher.removeFolder(folderInfo); + LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo); + } else if (isWorkspaceOrFolderWidget(d)) { + // Remove the widget from the workspace + mLauncher.removeAppWidget((LauncherAppWidgetInfo) item); + LauncherModel.deleteItemFromDatabase(mLauncher, item); + + final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; + final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); + if (appWidgetHost != null) { + // Deleting an app widget ID is a void call but writes to disk before returning + // to the caller... + new Thread("deleteAppWidgetId") { + public void run() { + appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId); + } + }.start(); + } + } + } + + public void onDrop(DragObject d) { + animateToTrashAndCompleteDrop(d); + } + + /** + * Creates an animation from the current drag view to the delete trash icon. + */ + private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer, + DragObject d, PointF vel, ViewConfiguration config) { + final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); + final Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, from); + + // Calculate how far along the velocity vector we should put the intermediate point on + // the bezier curve + float velocity = Math.abs(vel.length()); + float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f)); + int offsetY = (int) (-from.top * vp); + int offsetX = (int) (offsetY / (vel.y / vel.x)); + final float y2 = from.top + offsetY; // intermediate t/l + final float x2 = from.left + offsetX; + final float x1 = from.left; // drag view t/l + final float y1 = from.top; + final float x3 = to.left; // delete target t/l + final float y3 = to.top; + + final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() { + @Override + public float getInterpolation(float t) { + return t * t * t * t * t * t * t * t; + } + }; + return new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final DragView dragView = (DragView) dragLayer.getAnimatedView(); + float t = ((Float) animation.getAnimatedValue()).floatValue(); + float tp = scaleAlphaInterpolator.getInterpolation(t); + float initialScale = dragView.getInitialScale(); + float finalAlpha = 0.5f; + float scale = dragView.getScaleX(); + float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f; + float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f; + float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) + + (t * t) * x3; + float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) + + (t * t) * y3; + + dragView.setTranslationX(x); + dragView.setTranslationY(y); + dragView.setScaleX(initialScale * (1f - tp)); + dragView.setScaleY(initialScale * (1f - tp)); + dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp)); + } + }; + } + + /** + * Creates an animation from the current drag view along its current velocity vector. + * For this animation, the alpha runs for a fixed duration and we update the position + * progressively. + */ + private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { + private DragLayer mDragLayer; + private PointF mVelocity; + private Rect mFrom; + private long mPrevTime; + private boolean mHasOffsetForScale; + private float mFriction; + + private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); + + public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from, + long startTime, float friction) { + mDragLayer = dragLayer; + mVelocity = vel; + mFrom = from; + mPrevTime = startTime; + mFriction = 1f - (dragLayer.getResources().getDisplayMetrics().density * friction); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final DragView dragView = (DragView) mDragLayer.getAnimatedView(); + float t = ((Float) animation.getAnimatedValue()).floatValue(); + long curTime = AnimationUtils.currentAnimationTimeMillis(); + + if (!mHasOffsetForScale) { + mHasOffsetForScale = true; + float scale = dragView.getScaleX(); + float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f; + float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f; + + mFrom.left += xOffset; + mFrom.top += yOffset; + } + + mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); + mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); + + dragView.setTranslationX(mFrom.left); + dragView.setTranslationY(mFrom.top); + dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); + + mVelocity.x *= mFriction; + mVelocity.y *= mFriction; + mPrevTime = curTime; + } + }; + private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer, + DragObject d, PointF vel, final long startTime, final int duration, + ViewConfiguration config) { + final Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, from); + + return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime, + FLING_TO_DELETE_FRICTION); + } + + public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) { + final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView; + + // Don't highlight the icon as it's animating + d.dragView.setColor(0); + d.dragView.updateInitialScaleToCurrentScale(); + // Don't highlight the target if we are flinging from AllApps + if (isAllApps) { + resetHoverColor(); + } + + if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { + // Defer animating out the drop target if we are animating to it + mSearchDropTargetBar.deferOnDragEnd(); + mSearchDropTargetBar.finishAnimations(); + } + + final ViewConfiguration config = ViewConfiguration.get(mLauncher); + final DragLayer dragLayer = mLauncher.getDragLayer(); + final int duration = FLING_DELETE_ANIMATION_DURATION; + final long startTime = AnimationUtils.currentAnimationTimeMillis(); + + // NOTE: Because it takes time for the first frame of animation to actually be + // called and we expect the animation to be a continuation of the fling, we have + // to account for the time that has elapsed since the fling finished. And since + // we don't have a startDelay, we will always get call to update when we call + // start() (which we want to ignore). + final TimeInterpolator tInterpolator = new TimeInterpolator() { + private int mCount = -1; + private float mOffset = 0f; + + @Override + public float getInterpolation(float t) { + if (mCount < 0) { + mCount++; + } else if (mCount == 0) { + mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - + startTime) / duration); + mCount++; + } + return Math.min(1f, mOffset + t); + } + }; + AnimatorUpdateListener updateCb = null; + if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { + updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config); + } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) { + updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime, + duration, config); + } + Runnable onAnimationEndRunnable = new Runnable() { + @Override + public void run() { + mSearchDropTargetBar.onDragEnd(); + + // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up + // itself, otherwise, complete the drop to initiate the deletion process + if (!isAllApps) { + mLauncher.exitSpringLoadedDragMode(); + completeDrop(d); + } + mLauncher.getDragController().onDeferredEndFling(d); + } + }; + dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable, + DragLayer.ANIMATION_END_DISAPPEAR, null); + } +} diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java new file mode 100644 index 000000000..86355890e --- /dev/null +++ b/src/com/android/launcher3/DragController.java @@ -0,0 +1,821 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Handler; +import android.os.IBinder; +import android.os.Vibrator; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.inputmethod.InputMethodManager; + +import com.android.launcher3.R; + +import java.util.ArrayList; + +/** + * Class for initiating a drag within a view or across multiple views. + */ +public class DragController { + private static final String TAG = "Launcher.DragController"; + + /** Indicates the drag is a move. */ + public static int DRAG_ACTION_MOVE = 0; + + /** Indicates the drag is a copy. */ + public static int DRAG_ACTION_COPY = 1; + + private static final int SCROLL_DELAY = 500; + private static final int RESCROLL_DELAY = 750; + private static final int VIBRATE_DURATION = 15; + + private static final boolean PROFILE_DRAWING_DURING_DRAG = false; + + private static final int SCROLL_OUTSIDE_ZONE = 0; + private static final int SCROLL_WAITING_IN_ZONE = 1; + + static final int SCROLL_NONE = -1; + static final int SCROLL_LEFT = 0; + static final int SCROLL_RIGHT = 1; + + private static final float MAX_FLING_DEGREES = 35f; + + private Launcher mLauncher; + private Handler mHandler; + private final Vibrator mVibrator; + + // temporaries to avoid gc thrash + private Rect mRectTemp = new Rect(); + private final int[] mCoordinatesTemp = new int[2]; + + /** Whether or not we're dragging. */ + private boolean mDragging; + + /** X coordinate of the down event. */ + private int mMotionDownX; + + /** Y coordinate of the down event. */ + private int mMotionDownY; + + /** the area at the edge of the screen that makes the workspace go left + * or right while you're dragging. + */ + private int mScrollZone; + + private DropTarget.DragObject mDragObject; + + /** Who can receive drop events */ + private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>(); + private ArrayList<DragListener> mListeners = new ArrayList<DragListener>(); + private DropTarget mFlingToDeleteDropTarget; + + /** The window token used as the parent for the DragView. */ + private IBinder mWindowToken; + + /** The view that will be scrolled when dragging to the left and right edges of the screen. */ + private View mScrollView; + + private View mMoveTarget; + + private DragScroller mDragScroller; + private int mScrollState = SCROLL_OUTSIDE_ZONE; + private ScrollRunnable mScrollRunnable = new ScrollRunnable(); + + private DropTarget mLastDropTarget; + + private InputMethodManager mInputMethodManager; + + private int mLastTouch[] = new int[2]; + private long mLastTouchUpTime = -1; + private int mDistanceSinceScroll = 0; + + private int mTmpPoint[] = new int[2]; + private Rect mDragLayerRect = new Rect(); + + protected int mFlingToDeleteThresholdVelocity; + private VelocityTracker mVelocityTracker; + + /** + * Interface to receive notifications when a drag starts or stops + */ + interface DragListener { + + /** + * A drag has begun + * + * @param source An object representing where the drag originated + * @param info The data associated with the object that is being dragged + * @param dragAction The drag action: either {@link DragController#DRAG_ACTION_MOVE} + * or {@link DragController#DRAG_ACTION_COPY} + */ + void onDragStart(DragSource source, Object info, int dragAction); + + /** + * The drag has ended + */ + void onDragEnd(); + } + + /** + * Used to create a new DragLayer from XML. + * + * @param context The application's context. + */ + public DragController(Launcher launcher) { + Resources r = launcher.getResources(); + mLauncher = launcher; + mHandler = new Handler(); + mScrollZone = r.getDimensionPixelSize(R.dimen.scroll_zone); + mVelocityTracker = VelocityTracker.obtain(); + mVibrator = (Vibrator) launcher.getSystemService(Context.VIBRATOR_SERVICE); + + float density = r.getDisplayMetrics().density; + mFlingToDeleteThresholdVelocity = + (int) (r.getInteger(R.integer.config_flingToDeleteMinVelocity) * density); + } + + public boolean dragging() { + return mDragging; + } + + /** + * Starts a drag. + * + * @param v The view that is being dragged + * @param bmp The bitmap that represents the view being dragged + * @param source An object representing where the drag originated + * @param dragInfo The data associated with the object that is being dragged + * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or + * {@link #DRAG_ACTION_COPY} + * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. + * Makes dragging feel more precise, e.g. you can clip out a transparent border + */ + public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, int dragAction, + Point extraPadding, float initialDragViewScale) { + int[] loc = mCoordinatesTemp; + mLauncher.getDragLayer().getLocationInDragLayer(v, loc); + int viewExtraPaddingLeft = extraPadding != null ? extraPadding.x : 0; + int viewExtraPaddingTop = extraPadding != null ? extraPadding.y : 0; + int dragLayerX = loc[0] + v.getPaddingLeft() + viewExtraPaddingLeft + + (int) ((initialDragViewScale * bmp.getWidth() - bmp.getWidth()) / 2); + int dragLayerY = loc[1] + v.getPaddingTop() + viewExtraPaddingTop + + (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2); + + startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, + null, initialDragViewScale); + + if (dragAction == DRAG_ACTION_MOVE) { + v.setVisibility(View.GONE); + } + } + + /** + * Starts a drag. + * + * @param b The bitmap to display as the drag image. It will be re-scaled to the + * enlarged size. + * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. + * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. + * @param source An object representing where the drag originated + * @param dragInfo The data associated with the object that is being dragged + * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or + * {@link #DRAG_ACTION_COPY} + * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. + * Makes dragging feel more precise, e.g. you can clip out a transparent border + */ + public void startDrag(Bitmap b, int dragLayerX, int dragLayerY, + DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion, + float initialDragViewScale) { + if (PROFILE_DRAWING_DURING_DRAG) { + android.os.Debug.startMethodTracing("Launcher"); + } + + // Hide soft keyboard, if visible + if (mInputMethodManager == null) { + mInputMethodManager = (InputMethodManager) + mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE); + } + mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); + + for (DragListener listener : mListeners) { + listener.onDragStart(source, dragInfo, dragAction); + } + + final int registrationX = mMotionDownX - dragLayerX; + final int registrationY = mMotionDownY - dragLayerY; + + final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; + final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; + + mDragging = true; + + mDragObject = new DropTarget.DragObject(); + + mDragObject.dragComplete = false; + mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); + mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); + mDragObject.dragSource = source; + mDragObject.dragInfo = dragInfo; + + mVibrator.vibrate(VIBRATE_DURATION); + + final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, + registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale); + + if (dragOffset != null) { + dragView.setDragVisualizeOffset(new Point(dragOffset)); + } + if (dragRegion != null) { + dragView.setDragRegion(new Rect(dragRegion)); + } + + dragView.show(mMotionDownX, mMotionDownY); + handleMoveEvent(mMotionDownX, mMotionDownY); + } + + /** + * Draw the view into a bitmap. + */ + Bitmap getViewBitmap(View v) { + v.clearFocus(); + v.setPressed(false); + + boolean willNotCache = v.willNotCacheDrawing(); + v.setWillNotCacheDrawing(false); + + // Reset the drawing cache background color to fully transparent + // for the duration of this operation + int color = v.getDrawingCacheBackgroundColor(); + v.setDrawingCacheBackgroundColor(0); + float alpha = v.getAlpha(); + v.setAlpha(1.0f); + + if (color != 0) { + v.destroyDrawingCache(); + } + v.buildDrawingCache(); + Bitmap cacheBitmap = v.getDrawingCache(); + if (cacheBitmap == null) { + Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); + return null; + } + + Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); + + // Restore the view + v.destroyDrawingCache(); + v.setAlpha(alpha); + v.setWillNotCacheDrawing(willNotCache); + v.setDrawingCacheBackgroundColor(color); + + return bitmap; + } + + /** + * Call this from a drag source view like this: + * + * <pre> + * @Override + * public boolean dispatchKeyEvent(KeyEvent event) { + * return mDragController.dispatchKeyEvent(this, event) + * || super.dispatchKeyEvent(event); + * </pre> + */ + public boolean dispatchKeyEvent(KeyEvent event) { + return mDragging; + } + + public boolean isDragging() { + return mDragging; + } + + /** + * Stop dragging without dropping. + */ + public void cancelDrag() { + if (mDragging) { + if (mLastDropTarget != null) { + mLastDropTarget.onDragExit(mDragObject); + } + mDragObject.deferDragViewCleanupPostAnimation = false; + mDragObject.cancelled = true; + mDragObject.dragComplete = true; + mDragObject.dragSource.onDropCompleted(null, mDragObject, false, false); + } + endDrag(); + } + public void onAppsRemoved(ArrayList<ApplicationInfo> appInfos, Context context) { + // Cancel the current drag if we are removing an app that we are dragging + if (mDragObject != null) { + Object rawDragInfo = mDragObject.dragInfo; + if (rawDragInfo instanceof ShortcutInfo) { + ShortcutInfo dragInfo = (ShortcutInfo) rawDragInfo; + for (ApplicationInfo info : appInfos) { + // Added null checks to prevent NPE we've seen in the wild + if (dragInfo != null && + dragInfo.intent != null) { + boolean isSameComponent = + dragInfo.intent.getComponent().equals(info.componentName); + if (isSameComponent) { + cancelDrag(); + return; + } + } + } + } + } + } + + private void endDrag() { + if (mDragging) { + mDragging = false; + clearScrollRunnable(); + boolean isDeferred = false; + if (mDragObject.dragView != null) { + isDeferred = mDragObject.deferDragViewCleanupPostAnimation; + if (!isDeferred) { + mDragObject.dragView.remove(); + } + mDragObject.dragView = null; + } + + // Only end the drag if we are not deferred + if (!isDeferred) { + for (DragListener listener : mListeners) { + listener.onDragEnd(); + } + } + } + + releaseVelocityTracker(); + } + + /** + * This only gets called as a result of drag view cleanup being deferred in endDrag(); + */ + void onDeferredEndDrag(DragView dragView) { + dragView.remove(); + + // If we skipped calling onDragEnd() before, do it now + for (DragListener listener : mListeners) { + listener.onDragEnd(); + } + } + + void onDeferredEndFling(DropTarget.DragObject d) { + d.dragSource.onFlingToDeleteCompleted(); + } + + /** + * Clamps the position to the drag layer bounds. + */ + private int[] getClampedDragLayerPos(float x, float y) { + mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect); + mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1)); + mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1)); + return mTmpPoint; + } + + long getLastGestureUpTime() { + if (mDragging) { + return System.currentTimeMillis(); + } else { + return mLastTouchUpTime; + } + } + + void resetLastGestureUpTime() { + mLastTouchUpTime = -1; + } + + /** + * Call this from a drag source view. + */ + public boolean onInterceptTouchEvent(MotionEvent ev) { + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging=" + + mDragging); + } + + // Update the velocity tracker + acquireVelocityTrackerAndAddMovement(ev); + + final int action = ev.getAction(); + final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); + final int dragLayerX = dragLayerPos[0]; + final int dragLayerY = dragLayerPos[1]; + + switch (action) { + case MotionEvent.ACTION_MOVE: + break; + case MotionEvent.ACTION_DOWN: + // Remember location of down touch + mMotionDownX = dragLayerX; + mMotionDownY = dragLayerY; + mLastDropTarget = null; + break; + case MotionEvent.ACTION_UP: + mLastTouchUpTime = System.currentTimeMillis(); + if (mDragging) { + PointF vec = isFlingingToDelete(mDragObject.dragSource); + if (vec != null) { + dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec); + } else { + drop(dragLayerX, dragLayerY); + } + } + endDrag(); + break; + case MotionEvent.ACTION_CANCEL: + cancelDrag(); + break; + } + + return mDragging; + } + + /** + * Sets the view that should handle move events. + */ + void setMoveTarget(View view) { + mMoveTarget = view; + } + + public boolean dispatchUnhandledMove(View focused, int direction) { + return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); + } + + private void clearScrollRunnable() { + mHandler.removeCallbacks(mScrollRunnable); + if (mScrollState == SCROLL_WAITING_IN_ZONE) { + mScrollState = SCROLL_OUTSIDE_ZONE; + mScrollRunnable.setDirection(SCROLL_RIGHT); + mDragScroller.onExitScrollArea(); + mLauncher.getDragLayer().onExitScrollArea(); + } + } + + private void handleMoveEvent(int x, int y) { + mDragObject.dragView.move(x, y); + + // Drop on someone? + final int[] coordinates = mCoordinatesTemp; + DropTarget dropTarget = findDropTarget(x, y, coordinates); + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + checkTouchMove(dropTarget); + + // Check if we are hovering over the scroll areas + mDistanceSinceScroll += + Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); + mLastTouch[0] = x; + mLastTouch[1] = y; + checkScrollState(x, y); + } + + public void forceTouchMove() { + int[] dummyCoordinates = mCoordinatesTemp; + DropTarget dropTarget = findDropTarget(mLastTouch[0], mLastTouch[1], dummyCoordinates); + checkTouchMove(dropTarget); + } + + private void checkTouchMove(DropTarget dropTarget) { + if (dropTarget != null) { + DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject); + if (delegate != null) { + dropTarget = delegate; + } + + if (mLastDropTarget != dropTarget) { + if (mLastDropTarget != null) { + mLastDropTarget.onDragExit(mDragObject); + } + dropTarget.onDragEnter(mDragObject); + } + dropTarget.onDragOver(mDragObject); + } else { + if (mLastDropTarget != null) { + mLastDropTarget.onDragExit(mDragObject); + } + } + mLastDropTarget = dropTarget; + } + + private void checkScrollState(int x, int y) { + final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop(); + final int delay = mDistanceSinceScroll < slop ? RESCROLL_DELAY : SCROLL_DELAY; + final DragLayer dragLayer = mLauncher.getDragLayer(); + final boolean isRtl = (dragLayer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + final int forwardDirection = isRtl ? SCROLL_RIGHT : SCROLL_LEFT; + final int backwardsDirection = isRtl ? SCROLL_LEFT : SCROLL_RIGHT; + + if (x < mScrollZone) { + if (mScrollState == SCROLL_OUTSIDE_ZONE) { + mScrollState = SCROLL_WAITING_IN_ZONE; + if (mDragScroller.onEnterScrollArea(x, y, forwardDirection)) { + dragLayer.onEnterScrollArea(forwardDirection); + mScrollRunnable.setDirection(forwardDirection); + mHandler.postDelayed(mScrollRunnable, delay); + } + } + } else if (x > mScrollView.getWidth() - mScrollZone) { + if (mScrollState == SCROLL_OUTSIDE_ZONE) { + mScrollState = SCROLL_WAITING_IN_ZONE; + if (mDragScroller.onEnterScrollArea(x, y, backwardsDirection)) { + dragLayer.onEnterScrollArea(backwardsDirection); + mScrollRunnable.setDirection(backwardsDirection); + mHandler.postDelayed(mScrollRunnable, delay); + } + } + } else { + clearScrollRunnable(); + } + } + + /** + * Call this from a drag source view. + */ + public boolean onTouchEvent(MotionEvent ev) { + if (!mDragging) { + return false; + } + + // Update the velocity tracker + acquireVelocityTrackerAndAddMovement(ev); + + final int action = ev.getAction(); + final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); + final int dragLayerX = dragLayerPos[0]; + final int dragLayerY = dragLayerPos[1]; + + switch (action) { + case MotionEvent.ACTION_DOWN: + // Remember where the motion event started + mMotionDownX = dragLayerX; + mMotionDownY = dragLayerY; + + if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) { + mScrollState = SCROLL_WAITING_IN_ZONE; + mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); + } else { + mScrollState = SCROLL_OUTSIDE_ZONE; + } + break; + case MotionEvent.ACTION_MOVE: + handleMoveEvent(dragLayerX, dragLayerY); + break; + case MotionEvent.ACTION_UP: + // Ensure that we've processed a move event at the current pointer location. + handleMoveEvent(dragLayerX, dragLayerY); + mHandler.removeCallbacks(mScrollRunnable); + + if (mDragging) { + PointF vec = isFlingingToDelete(mDragObject.dragSource); + if (vec != null) { + dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec); + } else { + drop(dragLayerX, dragLayerY); + } + } + endDrag(); + break; + case MotionEvent.ACTION_CANCEL: + mHandler.removeCallbacks(mScrollRunnable); + cancelDrag(); + break; + } + + return true; + } + + /** + * Determines whether the user flung the current item to delete it. + * + * @return the vector at which the item was flung, or null if no fling was detected. + */ + private PointF isFlingingToDelete(DragSource source) { + if (mFlingToDeleteDropTarget == null) return null; + if (!source.supportsFlingToDelete()) return null; + + ViewConfiguration config = ViewConfiguration.get(mLauncher); + mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity()); + + if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) { + // Do a quick dot product test to ensure that we are flinging upwards + PointF vel = new PointF(mVelocityTracker.getXVelocity(), + mVelocityTracker.getYVelocity()); + PointF upVec = new PointF(0f, -1f); + float theta = (float) Math.acos(((vel.x * upVec.x) + (vel.y * upVec.y)) / + (vel.length() * upVec.length())); + if (theta <= Math.toRadians(MAX_FLING_DEGREES)) { + return vel; + } + } + return null; + } + + private void dropOnFlingToDeleteTarget(float x, float y, PointF vel) { + final int[] coordinates = mCoordinatesTemp; + + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + + // Clean up dragging on the target if it's not the current fling delete target otherwise, + // start dragging to it. + if (mLastDropTarget != null && mFlingToDeleteDropTarget != mLastDropTarget) { + mLastDropTarget.onDragExit(mDragObject); + } + + // Drop onto the fling-to-delete target + boolean accepted = false; + mFlingToDeleteDropTarget.onDragEnter(mDragObject); + // We must set dragComplete to true _only_ after we "enter" the fling-to-delete target for + // "drop" + mDragObject.dragComplete = true; + mFlingToDeleteDropTarget.onDragExit(mDragObject); + if (mFlingToDeleteDropTarget.acceptDrop(mDragObject)) { + mFlingToDeleteDropTarget.onFlingToDelete(mDragObject, mDragObject.x, mDragObject.y, + vel); + accepted = true; + } + mDragObject.dragSource.onDropCompleted((View) mFlingToDeleteDropTarget, mDragObject, true, + accepted); + } + + private void drop(float x, float y) { + final int[] coordinates = mCoordinatesTemp; + final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); + + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + boolean accepted = false; + if (dropTarget != null) { + mDragObject.dragComplete = true; + dropTarget.onDragExit(mDragObject); + if (dropTarget.acceptDrop(mDragObject)) { + dropTarget.onDrop(mDragObject); + accepted = true; + } + } + mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, false, accepted); + } + + private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { + final Rect r = mRectTemp; + + final ArrayList<DropTarget> dropTargets = mDropTargets; + final int count = dropTargets.size(); + for (int i=count-1; i>=0; i--) { + DropTarget target = dropTargets.get(i); + if (!target.isDropEnabled()) + continue; + + target.getHitRect(r); + + // Convert the hit rect to DragLayer coordinates + target.getLocationInDragLayer(dropCoordinates); + r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); + + mDragObject.x = x; + mDragObject.y = y; + if (r.contains(x, y)) { + DropTarget delegate = target.getDropTargetDelegate(mDragObject); + if (delegate != null) { + target = delegate; + target.getLocationInDragLayer(dropCoordinates); + } + + // Make dropCoordinates relative to the DropTarget + dropCoordinates[0] = x - dropCoordinates[0]; + dropCoordinates[1] = y - dropCoordinates[1]; + + return target; + } + } + return null; + } + + public void setDragScoller(DragScroller scroller) { + mDragScroller = scroller; + } + + public void setWindowToken(IBinder token) { + mWindowToken = token; + } + + /** + * Sets the drag listner which will be notified when a drag starts or ends. + */ + public void addDragListener(DragListener l) { + mListeners.add(l); + } + + /** + * Remove a previously installed drag listener. + */ + public void removeDragListener(DragListener l) { + mListeners.remove(l); + } + + /** + * Add a DropTarget to the list of potential places to receive drop events. + */ + public void addDropTarget(DropTarget target) { + mDropTargets.add(target); + } + + /** + * Don't send drop events to <em>target</em> any more. + */ + public void removeDropTarget(DropTarget target) { + mDropTargets.remove(target); + } + + /** + * Sets the current fling-to-delete drop target. + */ + public void setFlingToDeleteDropTarget(DropTarget target) { + mFlingToDeleteDropTarget = target; + } + + private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + } + + private void releaseVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * Set which view scrolls for touch events near the edge of the screen. + */ + public void setScrollView(View v) { + mScrollView = v; + } + + DragView getDragView() { + return mDragObject.dragView; + } + + private class ScrollRunnable implements Runnable { + private int mDirection; + + ScrollRunnable() { + } + + public void run() { + if (mDragScroller != null) { + if (mDirection == SCROLL_LEFT) { + mDragScroller.scrollLeft(); + } else { + mDragScroller.scrollRight(); + } + mScrollState = SCROLL_OUTSIDE_ZONE; + mDistanceSinceScroll = 0; + mDragScroller.onExitScrollArea(); + mLauncher.getDragLayer().onExitScrollArea(); + + if (isDragging()) { + // Check the scroll again so that we can requeue the scroller if necessary + checkScrollState(mLastTouch[0], mLastTouch[1]); + } + } + } + + void setDirection(int direction) { + mDirection = direction; + } + } +} diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java new file mode 100644 index 000000000..5a1b4ccd0 --- /dev/null +++ b/src/com/android/launcher3/DragLayer.java @@ -0,0 +1,804 @@ +/* + * 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.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.launcher3.R; + +import java.util.ArrayList; + +/** + * A ViewGroup that coordinates dragging across its descendants + */ +public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChangeListener { + private DragController mDragController; + private int[] mTmpXY = new int[2]; + + private int mXDown, mYDown; + private Launcher mLauncher; + + // Variables relating to resizing widgets + private final ArrayList<AppWidgetResizeFrame> mResizeFrames = + new ArrayList<AppWidgetResizeFrame>(); + private AppWidgetResizeFrame mCurrentResizeFrame; + + // Variables relating to animation of views after drop + private ValueAnimator mDropAnim = null; + private ValueAnimator mFadeOutAnim = null; + private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); + private DragView mDropView = null; + private int mAnchorViewInitialScrollX = 0; + private View mAnchorView = null; + + private boolean mHoverPointClosesFolder = false; + private Rect mHitRect = new Rect(); + private int mWorkspaceIndex = -1; + private int mQsbIndex = -1; + public static final int ANIMATION_END_DISAPPEAR = 0; + public static final int ANIMATION_END_FADE_OUT = 1; + public static final int ANIMATION_END_REMAIN_VISIBLE = 2; + + /** + * Used to create a new DragLayer from XML. + * + * @param context The application's context. + * @param attrs The attributes set containing the Workspace's customization values. + */ + public DragLayer(Context context, AttributeSet attrs) { + super(context, attrs); + + // Disable multitouch across the workspace/all apps/customize tray + setMotionEventSplittingEnabled(false); + setChildrenDrawingOrderEnabled(true); + setOnHierarchyChangeListener(this); + + mLeftHoverDrawable = getResources().getDrawable(R.drawable.page_hover_left_holo); + mRightHoverDrawable = getResources().getDrawable(R.drawable.page_hover_right_holo); + } + + public void setup(Launcher launcher, DragController controller) { + mLauncher = launcher; + mDragController = controller; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); + } + + private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) { + getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect); + if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { + return true; + } + return false; + } + + private boolean isEventOverFolder(Folder folder, MotionEvent ev) { + getDescendantRectRelativeToSelf(folder, mHitRect); + if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { + return true; + } + return false; + } + + private boolean handleTouchDown(MotionEvent ev, boolean intercept) { + Rect hitRect = new Rect(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + + for (AppWidgetResizeFrame child: mResizeFrames) { + child.getHitRect(hitRect); + if (hitRect.contains(x, y)) { + if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) { + mCurrentResizeFrame = child; + mXDown = x; + mYDown = y; + requestDisallowInterceptTouchEvent(true); + return true; + } + } + } + + Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); + if (currentFolder != null && !mLauncher.isFolderClingVisible() && intercept) { + if (currentFolder.isEditingName()) { + if (!isEventOverFolderTextRegion(currentFolder, ev)) { + currentFolder.dismissEditingName(); + return true; + } + } + + getDescendantRectRelativeToSelf(currentFolder, hitRect); + if (!isEventOverFolder(currentFolder, ev)) { + mLauncher.closeFolder(); + return true; + } + } + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if (handleTouchDown(ev, true)) { + return true; + } + } + clearAllResizeFrames(); + return mDragController.onInterceptTouchEvent(ev); + } + + @Override + public boolean onInterceptHoverEvent(MotionEvent ev) { + if (mLauncher == null || mLauncher.getWorkspace() == null) { + return false; + } + Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); + if (currentFolder == null) { + return false; + } else { + AccessibilityManager accessibilityManager = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.isTouchExplorationEnabled()) { + final int action = ev.getAction(); + boolean isOverFolder; + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: + isOverFolder = isEventOverFolder(currentFolder, ev); + if (!isOverFolder) { + sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); + mHoverPointClosesFolder = true; + return true; + } else if (isOverFolder) { + mHoverPointClosesFolder = false; + } else { + return true; + } + case MotionEvent.ACTION_HOVER_MOVE: + isOverFolder = isEventOverFolder(currentFolder, ev); + if (!isOverFolder && !mHoverPointClosesFolder) { + sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); + mHoverPointClosesFolder = true; + return true; + } else if (isOverFolder) { + mHoverPointClosesFolder = false; + } else { + return true; + } + } + } + } + return false; + } + + private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) { + AccessibilityManager accessibilityManager = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.isEnabled()) { + int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close; + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_VIEW_FOCUSED); + onInitializeAccessibilityEvent(event); + event.getText().add(getContext().getString(stringId)); + accessibilityManager.sendAccessibilityEvent(event); + } + } + + @Override + public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { + Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); + if (currentFolder != null) { + if (child == currentFolder) { + return super.onRequestSendAccessibilityEvent(child, event); + } + // Skip propagating onRequestSendAccessibilityEvent all for other children + // when a folder is open + return false; + } + return super.onRequestSendAccessibilityEvent(child, event); + } + + @Override + public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) { + Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); + if (currentFolder != null) { + // Only add the folder as a child for accessibility when it is open + childrenForAccessibility.add(currentFolder); + } else { + super.addChildrenForAccessibility(childrenForAccessibility); + } + } + + @Override + public boolean onHoverEvent(MotionEvent ev) { + // If we've received this, we've already done the necessary handling + // in onInterceptHoverEvent. Return true to consume the event. + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean handled = false; + int action = ev.getAction(); + + int x = (int) ev.getX(); + int y = (int) ev.getY(); + + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if (handleTouchDown(ev, false)) { + return true; + } + } + } + + if (mCurrentResizeFrame != null) { + handled = true; + switch (action) { + case MotionEvent.ACTION_MOVE: + mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown); + mCurrentResizeFrame.onTouchUp(); + mCurrentResizeFrame = null; + } + } + if (handled) return true; + return mDragController.onTouchEvent(ev); + } + + /** + * Determine the rect of the descendant in this DragLayer's coordinates + * + * @param descendant The descendant whose coordinates we want to find. + * @param r The rect into which to place the results. + * @return The factor by which this descendant is scaled relative to this DragLayer. + */ + public float getDescendantRectRelativeToSelf(View descendant, Rect r) { + mTmpXY[0] = 0; + mTmpXY[1] = 0; + float scale = getDescendantCoordRelativeToSelf(descendant, mTmpXY); + r.set(mTmpXY[0], mTmpXY[1], + mTmpXY[0] + descendant.getWidth(), mTmpXY[1] + descendant.getHeight()); + return scale; + } + + public float getLocationInDragLayer(View child, int[] loc) { + loc[0] = 0; + loc[1] = 0; + return getDescendantCoordRelativeToSelf(child, loc); + } + + /** + * Given a coordinate relative to the descendant, find the coordinate in this DragLayer's + * coordinates. + * + * @param descendant The descendant to which the passed coordinate is relative. + * @param coord The coordinate that we want mapped. + * @return The factor by which this descendant is scaled relative to this DragLayer. Caution + * this scale factor is assumed to be equal in X and Y, and so if at any point this + * assumption fails, we will need to return a pair of scale factors. + */ + public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) { + float scale = 1.0f; + float[] pt = {coord[0], coord[1]}; + descendant.getMatrix().mapPoints(pt); + scale *= descendant.getScaleX(); + pt[0] += descendant.getLeft(); + pt[1] += descendant.getTop(); + ViewParent viewParent = descendant.getParent(); + while (viewParent instanceof View && viewParent != this) { + final View view = (View)viewParent; + view.getMatrix().mapPoints(pt); + scale *= view.getScaleX(); + pt[0] += view.getLeft() - view.getScrollX(); + pt[1] += view.getTop() - view.getScrollY(); + viewParent = view.getParent(); + } + coord[0] = (int) Math.round(pt[0]); + coord[1] = (int) Math.round(pt[1]); + return scale; + } + + public void getViewRectRelativeToSelf(View v, Rect r) { + int[] loc = new int[2]; + getLocationInWindow(loc); + int x = loc[0]; + int y = loc[1]; + + v.getLocationInWindow(loc); + int vX = loc[0]; + int vY = loc[1]; + + int left = vX - x; + int top = vY - y; + r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight()); + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + return mDragController.dispatchUnhandledMove(focused, direction); + } + + public static class LayoutParams extends FrameLayout.LayoutParams { + public int x, y; + public boolean customPosition = false; + + /** + * {@inheritDoc} + */ + public LayoutParams(int width, int height) { + super(width, height); + } + + public void setWidth(int width) { + this.width = width; + } + + public int getWidth() { + return width; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getHeight() { + return height; + } + + public void setX(int x) { + this.x = x; + } + + public int getX() { + return x; + } + + public void setY(int y) { + this.y = y; + } + + public int getY() { + return y; + } + } + + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams(); + if (flp instanceof LayoutParams) { + final LayoutParams lp = (LayoutParams) flp; + if (lp.customPosition) { + child.layout(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height); + } + } + } + } + + public void clearAllResizeFrames() { + if (mResizeFrames.size() > 0) { + for (AppWidgetResizeFrame frame: mResizeFrames) { + frame.commitResize(); + removeView(frame); + } + mResizeFrames.clear(); + } + } + + public boolean hasResizeFrames() { + return mResizeFrames.size() > 0; + } + + public boolean isWidgetBeingResized() { + return mCurrentResizeFrame != null; + } + + public void addResizeFrame(ItemInfo itemInfo, LauncherAppWidgetHostView widget, + CellLayout cellLayout) { + AppWidgetResizeFrame resizeFrame = new AppWidgetResizeFrame(getContext(), + widget, cellLayout, this); + + LayoutParams lp = new LayoutParams(-1, -1); + lp.customPosition = true; + + addView(resizeFrame, lp); + mResizeFrames.add(resizeFrame); + + resizeFrame.snapToWidget(false); + } + + public void animateViewIntoPosition(DragView dragView, final View child) { + animateViewIntoPosition(dragView, child, null); + } + + public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, + float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, + int duration) { + Rect r = new Rect(); + getViewRectRelativeToSelf(dragView, r); + final int fromX = r.left; + final int fromY = r.top; + + animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY, + onFinishRunnable, animationEndStyle, duration, null); + } + + public void animateViewIntoPosition(DragView dragView, final View child, + final Runnable onFinishAnimationRunnable) { + animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable, null); + } + + public void animateViewIntoPosition(DragView dragView, final View child, int duration, + final Runnable onFinishAnimationRunnable, View anchorView) { + ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent(); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + parentChildren.measureChild(child); + + Rect r = new Rect(); + getViewRectRelativeToSelf(dragView, r); + + int coord[] = new int[2]; + float childScale = child.getScaleX(); + coord[0] = lp.x + (int) (child.getMeasuredWidth() * (1 - childScale) / 2); + coord[1] = lp.y + (int) (child.getMeasuredHeight() * (1 - childScale) / 2); + + // Since the child hasn't necessarily been laid out, we force the lp to be updated with + // the correct coordinates (above) and use these to determine the final location + float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord); + // We need to account for the scale of the child itself, as the above only accounts for + // for the scale in parents. + scale *= childScale; + int toX = coord[0]; + int toY = coord[1]; + if (child instanceof TextView) { + TextView tv = (TextView) child; + + // The child may be scaled (always about the center of the view) so to account for it, + // we have to offset the position by the scaled size. Once we do that, we can center + // the drag view about the scaled child view. + toY += Math.round(scale * tv.getPaddingTop()); + toY -= dragView.getMeasuredHeight() * (1 - scale) / 2; + toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; + } else if (child instanceof FolderIcon) { + // Account for holographic blur padding on the drag view + toY -= scale * Workspace.DRAG_BITMAP_PADDING / 2; + toY -= (1 - scale) * dragView.getMeasuredHeight() / 2; + // Center in the x coordinate about the target's drawable + toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; + } else { + toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2; + toX -= (Math.round(scale * (dragView.getMeasuredWidth() + - child.getMeasuredWidth()))) / 2; + } + + final int fromX = r.left; + final int fromY = r.top; + child.setVisibility(INVISIBLE); + Runnable onCompleteRunnable = new Runnable() { + public void run() { + child.setVisibility(VISIBLE); + if (onFinishAnimationRunnable != null) { + onFinishAnimationRunnable.run(); + } + } + }; + animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, scale, scale, + onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView); + } + + public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY, + final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY, + float finalScaleX, float finalScaleY, Runnable onCompleteRunnable, + int animationEndStyle, int duration, View anchorView) { + Rect from = new Rect(fromX, fromY, fromX + + view.getMeasuredWidth(), fromY + view.getMeasuredHeight()); + Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight()); + animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration, + null, null, onCompleteRunnable, animationEndStyle, anchorView); + } + + /** + * This method animates a view at the end of a drag and drop animation. + * + * @param view The view to be animated. This view is drawn directly into DragLayer, and so + * doesn't need to be a child of DragLayer. + * @param from The initial location of the view. Only the left and top parameters are used. + * @param to The final location of the view. Only the left and top parameters are used. This + * location doesn't account for scaling, and so should be centered about the desired + * final location (including scaling). + * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates. + * @param finalScale The final scale of the view. The view is scaled about its center. + * @param duration The duration of the animation. + * @param motionInterpolator The interpolator to use for the location of the view. + * @param alphaInterpolator The interpolator to use for the alpha of the view. + * @param onCompleteRunnable Optional runnable to run on animation completion. + * @param fadeOut Whether or not to fade out the view once the animation completes. If true, + * the runnable will execute after the view is faded out. + * @param anchorView If not null, this represents the view which the animated view stays + * anchored to in case scrolling is currently taking place. Note: currently this is + * only used for the X dimension for the case of the workspace. + */ + public void animateView(final DragView view, final Rect from, final Rect to, + final float finalAlpha, final float initScaleX, final float initScaleY, + final float finalScaleX, final float finalScaleY, int duration, + final Interpolator motionInterpolator, final Interpolator alphaInterpolator, + final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) { + + // Calculate the duration of the animation based on the object's distance + final float dist = (float) Math.sqrt(Math.pow(to.left - from.left, 2) + + Math.pow(to.top - from.top, 2)); + final Resources res = getResources(); + final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); + + // If duration < 0, this is a cue to compute the duration based on the distance + if (duration < 0) { + duration = res.getInteger(R.integer.config_dropAnimMaxDuration); + if (dist < maxDist) { + duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist); + } + duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration)); + } + + // Fall back to cubic ease out interpolator for the animation if none is specified + TimeInterpolator interpolator = null; + if (alphaInterpolator == null || motionInterpolator == null) { + interpolator = mCubicEaseOutInterpolator; + } + + // Animate the view + final float initAlpha = view.getAlpha(); + final float dropViewScale = view.getScaleX(); + AnimatorUpdateListener updateCb = new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final float percent = (Float) animation.getAnimatedValue(); + final int width = view.getMeasuredWidth(); + final int height = view.getMeasuredHeight(); + + float alphaPercent = alphaInterpolator == null ? percent : + alphaInterpolator.getInterpolation(percent); + float motionPercent = motionInterpolator == null ? percent : + motionInterpolator.getInterpolation(percent); + + float initialScaleX = initScaleX * dropViewScale; + float initialScaleY = initScaleY * dropViewScale; + float scaleX = finalScaleX * percent + initialScaleX * (1 - percent); + float scaleY = finalScaleY * percent + initialScaleY * (1 - percent); + float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent); + + float fromLeft = from.left + (initialScaleX - 1f) * width / 2; + float fromTop = from.top + (initialScaleY - 1f) * height / 2; + + int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent))); + int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent))); + + int xPos = x - mDropView.getScrollX() + (mAnchorView != null + ? (mAnchorViewInitialScrollX - mAnchorView.getScrollX()) : 0); + int yPos = y - mDropView.getScrollY(); + + mDropView.setTranslationX(xPos); + mDropView.setTranslationY(yPos); + mDropView.setScaleX(scaleX); + mDropView.setScaleY(scaleY); + mDropView.setAlpha(alpha); + } + }; + animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle, + anchorView); + } + + public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration, + TimeInterpolator interpolator, final Runnable onCompleteRunnable, + final int animationEndStyle, View anchorView) { + // Clean up the previous animations + if (mDropAnim != null) mDropAnim.cancel(); + if (mFadeOutAnim != null) mFadeOutAnim.cancel(); + + // Show the drop view if it was previously hidden + mDropView = view; + mDropView.cancelAnimation(); + mDropView.resetLayoutParams(); + + // Set the anchor view if the page is scrolling + if (anchorView != null) { + mAnchorViewInitialScrollX = anchorView.getScrollX(); + } + mAnchorView = anchorView; + + // Create and start the animation + mDropAnim = new ValueAnimator(); + mDropAnim.setInterpolator(interpolator); + mDropAnim.setDuration(duration); + mDropAnim.setFloatValues(0f, 1f); + mDropAnim.addUpdateListener(updateCb); + mDropAnim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + switch (animationEndStyle) { + case ANIMATION_END_DISAPPEAR: + clearAnimatedView(); + break; + case ANIMATION_END_FADE_OUT: + fadeOutDragView(); + break; + case ANIMATION_END_REMAIN_VISIBLE: + break; + } + } + }); + mDropAnim.start(); + } + + public void clearAnimatedView() { + if (mDropAnim != null) { + mDropAnim.cancel(); + } + if (mDropView != null) { + mDragController.onDeferredEndDrag(mDropView); + } + mDropView = null; + invalidate(); + } + + public View getAnimatedView() { + return mDropView; + } + + private void fadeOutDragView() { + mFadeOutAnim = new ValueAnimator(); + mFadeOutAnim.setDuration(150); + mFadeOutAnim.setFloatValues(0f, 1f); + mFadeOutAnim.removeAllUpdateListeners(); + mFadeOutAnim.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + final float percent = (Float) animation.getAnimatedValue(); + + float alpha = 1 - percent; + mDropView.setAlpha(alpha); + } + }); + mFadeOutAnim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + if (mDropView != null) { + mDragController.onDeferredEndDrag(mDropView); + } + mDropView = null; + invalidate(); + } + }); + mFadeOutAnim.start(); + } + + @Override + public void onChildViewAdded(View parent, View child) { + updateChildIndices(); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + updateChildIndices(); + } + + private void updateChildIndices() { + if (mLauncher != null) { + mWorkspaceIndex = indexOfChild(mLauncher.getWorkspace()); + mQsbIndex = indexOfChild(mLauncher.getSearchBar()); + } + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + // TODO: We have turned off this custom drawing order because it now effects touch + // dispatch order. We need to sort that issue out and then decide how to go about this. + if (true || LauncherApplication.isScreenLandscape(getContext()) || + mWorkspaceIndex == -1 || mQsbIndex == -1 || + mLauncher.getWorkspace().isDrawingBackgroundGradient()) { + return i; + } + + // This ensures that the workspace is drawn above the hotseat and qsb, + // except when the workspace is drawing a background gradient, in which + // case we want the workspace to stay behind these elements. + if (i == mQsbIndex) { + return mWorkspaceIndex; + } else if (i == mWorkspaceIndex) { + return mQsbIndex; + } else { + return i; + } + } + + private boolean mInScrollArea; + private Drawable mLeftHoverDrawable; + private Drawable mRightHoverDrawable; + + void onEnterScrollArea(int direction) { + mInScrollArea = true; + invalidate(); + } + + void onExitScrollArea() { + mInScrollArea = false; + invalidate(); + } + + /** + * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. + */ + private boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mInScrollArea && !LauncherApplication.isScreenLarge()) { + Workspace workspace = mLauncher.getWorkspace(); + int width = workspace.getWidth(); + Rect childRect = new Rect(); + getDescendantRectRelativeToSelf(workspace.getChildAt(0), childRect); + + int page = workspace.getNextPage(); + final boolean isRtl = isLayoutRtl(); + CellLayout leftPage = (CellLayout) workspace.getChildAt(isRtl ? page + 1 : page - 1); + CellLayout rightPage = (CellLayout) workspace.getChildAt(isRtl ? page - 1 : page + 1); + + if (leftPage != null && leftPage.getIsDragOverlapping()) { + mLeftHoverDrawable.setBounds(0, childRect.top, + mLeftHoverDrawable.getIntrinsicWidth(), childRect.bottom); + mLeftHoverDrawable.draw(canvas); + } else if (rightPage != null && rightPage.getIsDragOverlapping()) { + mRightHoverDrawable.setBounds(width - mRightHoverDrawable.getIntrinsicWidth(), + childRect.top, width, childRect.bottom); + mRightHoverDrawable.draw(canvas); + } + } + } +} diff --git a/src/com/android/launcher3/DragScroller.java b/src/com/android/launcher3/DragScroller.java new file mode 100644 index 000000000..e261f15d8 --- /dev/null +++ b/src/com/android/launcher3/DragScroller.java @@ -0,0 +1,40 @@ +/* + * 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; + +/** + * Handles scrolling while dragging + * + */ +public interface DragScroller { + void scrollLeft(); + void scrollRight(); + + /** + * The touch point has entered the scroll area; a scroll is imminent. + * This event will only occur while a drag is active. + * + * @param direction The scroll direction + */ + boolean onEnterScrollArea(int x, int y, int direction); + + /** + * The touch point has left the scroll area. + * NOTE: This may not be called, if a drop occurs inside the scroll area. + */ + boolean onExitScrollArea(); +} diff --git a/src/com/android/launcher3/DragSource.java b/src/com/android/launcher3/DragSource.java new file mode 100644 index 000000000..2ef99ae08 --- /dev/null +++ b/src/com/android/launcher3/DragSource.java @@ -0,0 +1,45 @@ +/* + * 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.view.View; + +import com.android.launcher3.DropTarget.DragObject; + +/** + * Interface defining an object that can originate a drag. + * + */ +public interface DragSource { + /** + * @return whether items dragged from this source supports + */ + boolean supportsFlingToDelete(); + + /** + * A callback specifically made back to the source after an item from this source has been flung + * to be deleted on a DropTarget. In such a situation, this method will be called after + * onDropCompleted, and more importantly, after the fling animation has completed. + */ + void onFlingToDeleteCompleted(); + + /** + * A callback made back to the source after an item from this source has been dropped on a + * DropTarget. + */ + void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, boolean success); +} diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java new file mode 100644 index 000000000..686cf62ff --- /dev/null +++ b/src/com/android/launcher3/DragView.java @@ -0,0 +1,295 @@ +/* + * 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.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.R; + +public class DragView extends View { + private static float sDragAlpha = 1f; + + private Bitmap mBitmap; + private Bitmap mCrossFadeBitmap; + private Paint mPaint; + private int mRegistrationX; + private int mRegistrationY; + + private Point mDragVisualizeOffset = null; + private Rect mDragRegion = null; + private DragLayer mDragLayer = null; + private boolean mHasDrawn = false; + private float mCrossFadeProgress = 0f; + + ValueAnimator mAnim; + private float mOffsetX = 0.0f; + private float mOffsetY = 0.0f; + private float mInitialScale = 1f; + + /** + * Construct the drag view. + * <p> + * The registration point is the point inside our view that the touch events should + * be centered upon. + * + * @param launcher The Launcher instance + * @param bitmap The view that we're dragging around. We scale it up when we draw it. + * @param registrationX The x coordinate of the registration point. + * @param registrationY The y coordinate of the registration point. + */ + public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, + int left, int top, int width, int height, final float initialScale) { + super(launcher); + mDragLayer = launcher.getDragLayer(); + mInitialScale = initialScale; + + final Resources res = getResources(); + final float offsetX = res.getDimensionPixelSize(R.dimen.dragViewOffsetX); + final float offsetY = res.getDimensionPixelSize(R.dimen.dragViewOffsetY); + final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale); + final float scale = (width + scaleDps) / width; + + // Set the initial scale to avoid any jumps + setScaleX(initialScale); + setScaleY(initialScale); + + // Animate the view into the correct position + mAnim = LauncherAnimUtils.ofFloat(this, 0f, 1f); + mAnim.setDuration(150); + mAnim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final float value = (Float) animation.getAnimatedValue(); + + final int deltaX = (int) ((value * offsetX) - mOffsetX); + final int deltaY = (int) ((value * offsetY) - mOffsetY); + + mOffsetX += deltaX; + mOffsetY += deltaY; + setScaleX(initialScale + (value * (scale - initialScale))); + setScaleY(initialScale + (value * (scale - initialScale))); + if (sDragAlpha != 1f) { + setAlpha(sDragAlpha * value + (1f - value)); + } + + if (getParent() == null) { + animation.cancel(); + } else { + setTranslationX(getTranslationX() + deltaX); + setTranslationY(getTranslationY() + deltaY); + } + } + }); + + mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height); + setDragRegion(new Rect(0, 0, width, height)); + + // The point in our scaled bitmap that the touch events are located + mRegistrationX = registrationX; + mRegistrationY = registrationY; + + // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass + int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + measure(ms, ms); + mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + } + + public float getOffsetY() { + return mOffsetY; + } + + public int getDragRegionLeft() { + return mDragRegion.left; + } + + public int getDragRegionTop() { + return mDragRegion.top; + } + + public int getDragRegionWidth() { + return mDragRegion.width(); + } + + public int getDragRegionHeight() { + return mDragRegion.height(); + } + + public void setDragVisualizeOffset(Point p) { + mDragVisualizeOffset = p; + } + + public Point getDragVisualizeOffset() { + return mDragVisualizeOffset; + } + + public void setDragRegion(Rect r) { + mDragRegion = r; + } + + public Rect getDragRegion() { + return mDragRegion; + } + + public float getInitialScale() { + return mInitialScale; + } + + public void updateInitialScaleToCurrentScale() { + mInitialScale = getScaleX(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight()); + } + + @Override + protected void onDraw(Canvas canvas) { + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + Paint p = new Paint(); + p.setStyle(Paint.Style.FILL); + p.setColor(0x66ffffff); + canvas.drawRect(0, 0, getWidth(), getHeight(), p); + } + + mHasDrawn = true; + boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null; + if (crossFade) { + int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255; + mPaint.setAlpha(alpha); + } + canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint); + if (crossFade) { + mPaint.setAlpha((int) (255 * mCrossFadeProgress)); + canvas.save(); + float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth(); + float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight(); + canvas.scale(sX, sY); + canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint); + canvas.restore(); + } + } + + public void setCrossFadeBitmap(Bitmap crossFadeBitmap) { + mCrossFadeBitmap = crossFadeBitmap; + } + + public void crossFade(int duration) { + ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1f); + va.setDuration(duration); + va.setInterpolator(new DecelerateInterpolator(1.5f)); + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mCrossFadeProgress = animation.getAnimatedFraction(); + } + }); + va.start(); + } + + public void setColor(int color) { + if (mPaint == null) { + mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + } + if (color != 0) { + mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); + } else { + mPaint.setColorFilter(null); + } + invalidate(); + } + + public boolean hasDrawn() { + return mHasDrawn; + } + + @Override + public void setAlpha(float alpha) { + super.setAlpha(alpha); + mPaint.setAlpha((int) (255 * alpha)); + invalidate(); + } + + /** + * Create a window containing this view and show it. + * + * @param windowToken obtained from v.getWindowToken() from one of your views + * @param touchX the x coordinate the user touched in DragLayer coordinates + * @param touchY the y coordinate the user touched in DragLayer coordinates + */ + public void show(int touchX, int touchY) { + mDragLayer.addView(this); + + // Start the pick-up animation + DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0); + lp.width = mBitmap.getWidth(); + lp.height = mBitmap.getHeight(); + lp.customPosition = true; + setLayoutParams(lp); + setTranslationX(touchX - mRegistrationX); + setTranslationY(touchY - mRegistrationY); + // Post the animation to skip other expensive work happening on the first frame + post(new Runnable() { + public void run() { + mAnim.start(); + } + }); + } + + public void cancelAnimation() { + if (mAnim != null && mAnim.isRunning()) { + mAnim.cancel(); + } + } + + public void resetLayoutParams() { + mOffsetX = mOffsetY = 0; + requestLayout(); + } + + /** + * Move the window containing this view. + * + * @param touchX the x coordinate the user touched in DragLayer coordinates + * @param touchY the y coordinate the user touched in DragLayer coordinates + */ + void move(int touchX, int touchY) { + setTranslationX(touchX - mRegistrationX + (int) mOffsetX); + setTranslationY(touchY - mRegistrationY + (int) mOffsetY); + } + + void remove() { + if (getParent() != null) { + mDragLayer.removeView(DragView.this); + } + } +} + diff --git a/src/com/android/launcher3/DrawableStateProxyView.java b/src/com/android/launcher3/DrawableStateProxyView.java new file mode 100644 index 000000000..196e2f2e1 --- /dev/null +++ b/src/com/android/launcher3/DrawableStateProxyView.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2012 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.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.launcher3.R; + +public class DrawableStateProxyView extends LinearLayout { + + private View mView; + private int mViewId; + + public DrawableStateProxyView(Context context) { + this(context, null); + } + + public DrawableStateProxyView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + + public DrawableStateProxyView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DrawableStateProxyView, + defStyle, 0); + mViewId = a.getResourceId(R.styleable.DrawableStateProxyView_sourceViewId, -1); + a.recycle(); + + setFocusable(false); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + if (mView == null) { + View parent = (View) getParent(); + mView = parent.findViewById(mViewId); + } + mView.setPressed(isPressed()); + mView.setHovered(isHovered()); + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + return false; + } +} diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java new file mode 100644 index 000000000..3ecb8ff08 --- /dev/null +++ b/src/com/android/launcher3/DropTarget.java @@ -0,0 +1,184 @@ +/* + * 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.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; +import android.util.Log; + +/** + * Interface defining an object that can receive a drag. + * + */ +public interface DropTarget { + + public static final String TAG = "DropTarget"; + + class DragObject { + public int x = -1; + public int y = -1; + + /** X offset from the upper-left corner of the cell to where we touched. */ + public int xOffset = -1; + + /** Y offset from the upper-left corner of the cell to where we touched. */ + public int yOffset = -1; + + /** This indicates whether a drag is in final stages, either drop or cancel. It + * differentiates onDragExit, since this is called when the drag is ending, above + * the current drag target, or when the drag moves off the current drag object. + */ + public boolean dragComplete = false; + + /** The view that moves around while you drag. */ + public DragView dragView = null; + + /** The data associated with the object being dragged */ + public Object dragInfo = null; + + /** Where the drag originated */ + public DragSource dragSource = null; + + /** Post drag animation runnable */ + public Runnable postAnimationRunnable = null; + + /** Indicates that the drag operation was cancelled */ + public boolean cancelled = false; + + /** Defers removing the DragView from the DragLayer until after the drop animation. */ + public boolean deferDragViewCleanupPostAnimation = true; + + public DragObject() { + } + } + + public static class DragEnforcer implements DragController.DragListener { + int dragParity = 0; + + public DragEnforcer(Context context) { + Launcher launcher = (Launcher) context; + launcher.getDragController().addDragListener(this); + } + + void onDragEnter() { + dragParity++; + if (dragParity != 1) { + Log.e(TAG, "onDragEnter: Drag contract violated: " + dragParity); + } + } + + void onDragExit() { + dragParity--; + if (dragParity != 0) { + Log.e(TAG, "onDragExit: Drag contract violated: " + dragParity); + } + } + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { + if (dragParity != 0) { + Log.e(TAG, "onDragEnter: Drag contract violated: " + dragParity); + } + } + + @Override + public void onDragEnd() { + if (dragParity != 0) { + Log.e(TAG, "onDragExit: Drag contract violated: " + dragParity); + } + } + } + + /** + * Used to temporarily disable certain drop targets + * + * @return boolean specifying whether this drop target is currently enabled + */ + boolean isDropEnabled(); + + /** + * Handle an object being dropped on the DropTarget + * + * @param source DragSource where the drag started + * @param x X coordinate of the drop location + * @param y Y coordinate of the drop location + * @param xOffset Horizontal offset with the object being dragged where the original + * touch happened + * @param yOffset Vertical offset with the object being dragged where the original + * touch happened + * @param dragView The DragView that's being dragged around on screen. + * @param dragInfo Data associated with the object being dragged + * + */ + void onDrop(DragObject dragObject); + + void onDragEnter(DragObject dragObject); + + void onDragOver(DragObject dragObject); + + void onDragExit(DragObject dragObject); + + /** + * Handle an object being dropped as a result of flinging to delete and will be called in place + * of onDrop(). (This is only called on objects that are set as the DragController's + * fling-to-delete target. + */ + void onFlingToDelete(DragObject dragObject, int x, int y, PointF vec); + + /** + * Allows a DropTarget to delegate drag and drop events to another object. + * + * Most subclasses will should just return null from this method. + * + * @param source DragSource where the drag started + * @param x X coordinate of the drop location + * @param y Y coordinate of the drop location + * @param xOffset Horizontal offset with the object being dragged where the original + * touch happened + * @param yOffset Vertical offset with the object being dragged where the original + * touch happened + * @param dragView The DragView that's being dragged around on screen. + * @param dragInfo Data associated with the object being dragged + * + * @return The DropTarget to delegate to, or null to not delegate to another object. + */ + DropTarget getDropTargetDelegate(DragObject dragObject); + + /** + * Check if a drop action can occur at, or near, the requested location. + * This will be called just before onDrop. + * + * @param source DragSource where the drag started + * @param x X coordinate of the drop location + * @param y Y coordinate of the drop location + * @param xOffset Horizontal offset with the object being dragged where the + * original touch happened + * @param yOffset Vertical offset with the object being dragged where the + * original touch happened + * @param dragView The DragView that's being dragged around on screen. + * @param dragInfo Data associated with the object being dragged + * @return True if the drop will be accepted, false otherwise. + */ + boolean acceptDrop(DragObject dragObject); + + // These methods are implemented in Views + void getHitRect(Rect outRect); + void getLocationInDragLayer(int[] loc); + int getLeft(); + int getTop(); +} diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java new file mode 100644 index 000000000..14760c7b6 --- /dev/null +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -0,0 +1,109 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +class FastBitmapDrawable extends Drawable { + private Bitmap mBitmap; + private int mAlpha; + private int mWidth; + private int mHeight; + private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + + FastBitmapDrawable(Bitmap b) { + mAlpha = 255; + mBitmap = b; + if (b != null) { + mWidth = mBitmap.getWidth(); + mHeight = mBitmap.getHeight(); + } else { + mWidth = mHeight = 0; + } + } + + @Override + public void draw(Canvas canvas) { + final Rect r = getBounds(); + // Draw the bitmap into the bounding rect + canvas.drawBitmap(mBitmap, null, r, mPaint); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + mAlpha = alpha; + mPaint.setAlpha(alpha); + } + + public void setFilterBitmap(boolean filterBitmap) { + mPaint.setFilterBitmap(filterBitmap); + } + + public int getAlpha() { + return mAlpha; + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + + @Override + public int getMinimumWidth() { + return mWidth; + } + + @Override + public int getMinimumHeight() { + return mHeight; + } + + public void setBitmap(Bitmap b) { + mBitmap = b; + if (b != null) { + mWidth = mBitmap.getWidth(); + mHeight = mBitmap.getHeight(); + } else { + mWidth = mHeight = 0; + } + } + + public Bitmap getBitmap() { + return mBitmap; + } +} diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java new file mode 100644 index 000000000..78fdadd4f --- /dev/null +++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2013 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.ValueAnimator; +import android.util.Log; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.ViewTreeObserver; + +/* + * This is a helper class that listens to updates from the corresponding animation. + * For the first two frames, it adjusts the current play time of the animation to + * prevent jank at the beginning of the animation + */ +public class FirstFrameAnimatorHelper extends AnimatorListenerAdapter + implements ValueAnimator.AnimatorUpdateListener { + private static final boolean DEBUG = false; + private static final int MAX_DELAY = 1000; + private static final int IDEAL_FRAME_DURATION = 16; + private View mTarget; + private long mStartFrame; + private long mStartTime = -1; + private boolean mHandlingOnAnimationUpdate; + private boolean mAdjustedSecondFrameTime; + + private static ViewTreeObserver.OnDrawListener sGlobalDrawListener; + private static long sGlobalFrameCounter; + private static boolean sVisible; + + public FirstFrameAnimatorHelper(ValueAnimator animator, View target) { + mTarget = target; + animator.addUpdateListener(this); + } + + public FirstFrameAnimatorHelper(ViewPropertyAnimator vpa, View target) { + mTarget = target; + vpa.setListener(this); + } + + // only used for ViewPropertyAnimators + public void onAnimationStart(Animator animation) { + final ValueAnimator va = (ValueAnimator) animation; + va.addUpdateListener(FirstFrameAnimatorHelper.this); + onAnimationUpdate(va); + } + + public static void setIsVisible(boolean visible) { + sVisible = visible; + } + + public static void initializeDrawListener(View view) { + if (sGlobalDrawListener != null) { + view.getViewTreeObserver().removeOnDrawListener(sGlobalDrawListener); + } + sGlobalDrawListener = new ViewTreeObserver.OnDrawListener() { + private long mTime = System.currentTimeMillis(); + public void onDraw() { + sGlobalFrameCounter++; + if (DEBUG) { + long newTime = System.currentTimeMillis(); + Log.d("FirstFrameAnimatorHelper", "TICK " + (newTime - mTime)); + mTime = newTime; + } + } + }; + view.getViewTreeObserver().addOnDrawListener(sGlobalDrawListener); + sVisible = true; + } + + public void onAnimationUpdate(final ValueAnimator animation) { + final long currentTime = System.currentTimeMillis(); + if (mStartTime == -1) { + mStartFrame = sGlobalFrameCounter; + mStartTime = currentTime; + } + + if (!mHandlingOnAnimationUpdate && + sVisible && + // If the current play time exceeds the duration, the animation + // will get finished, even if we call setCurrentPlayTime -- therefore + // don't adjust the animation in that case + animation.getCurrentPlayTime() < animation.getDuration()) { + mHandlingOnAnimationUpdate = true; + long frameNum = sGlobalFrameCounter - mStartFrame; + // If we haven't drawn our first frame, reset the time to t = 0 + // (give up after MAX_DELAY ms of waiting though - might happen, for example, if we + // are no longer in the foreground and no frames are being rendered ever) + if (frameNum == 0 && currentTime < mStartTime + MAX_DELAY) { + // The first frame on animations doesn't always trigger an invalidate... + // force an invalidate here to make sure the animation continues to advance + mTarget.getRootView().invalidate(); + animation.setCurrentPlayTime(0); + + // For the second frame, if the first frame took more than 16ms, + // adjust the start time and pretend it took only 16ms anyway. This + // prevents a large jump in the animation due to an expensive first frame + } else if (frameNum == 1 && currentTime < mStartTime + MAX_DELAY && + !mAdjustedSecondFrameTime && + currentTime > mStartTime + IDEAL_FRAME_DURATION) { + animation.setCurrentPlayTime(IDEAL_FRAME_DURATION); + mAdjustedSecondFrameTime = true; + } else { + if (frameNum > 1) { + mTarget.post(new Runnable() { + public void run() { + animation.removeUpdateListener(FirstFrameAnimatorHelper.this); + } + }); + } + if (DEBUG) print(animation); + } + mHandlingOnAnimationUpdate = false; + } else { + if (DEBUG) print(animation); + } + } + + public void print(ValueAnimator animation) { + float flatFraction = animation.getCurrentPlayTime() / (float) animation.getDuration(); + Log.d("FirstFrameAnimatorHelper", sGlobalFrameCounter + + "(" + (sGlobalFrameCounter - mStartFrame) + ") " + mTarget + " dirty? " + + mTarget.isDirty() + " " + flatFraction + " " + this + " " + animation); + } +} diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java new file mode 100644 index 000000000..94c5820ce --- /dev/null +++ b/src/com/android/launcher3/FocusHelper.java @@ -0,0 +1,898 @@ +/* + * Copyright (C) 2011 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.content.res.Configuration; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.TabHost; +import android.widget.TabWidget; + +import com.android.launcher3.R; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * A keyboard listener we set on all the workspace icons. + */ +class IconKeyEventListener implements View.OnKeyListener { + public boolean onKey(View v, int keyCode, KeyEvent event) { + return FocusHelper.handleIconKeyEvent(v, keyCode, event); + } +} + +/** + * A keyboard listener we set on all the workspace icons. + */ +class FolderKeyEventListener implements View.OnKeyListener { + public boolean onKey(View v, int keyCode, KeyEvent event) { + return FocusHelper.handleFolderKeyEvent(v, keyCode, event); + } +} + +/** + * A keyboard listener we set on all the hotseat buttons. + */ +class HotseatIconKeyEventListener implements View.OnKeyListener { + public boolean onKey(View v, int keyCode, KeyEvent event) { + final Configuration configuration = v.getResources().getConfiguration(); + return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation); + } +} + +/** + * A keyboard listener we set on the last tab button in AppsCustomize to jump to then + * market icon and vice versa. + */ +class AppsCustomizeTabKeyEventListener implements View.OnKeyListener { + public boolean onKey(View v, int keyCode, KeyEvent event) { + return FocusHelper.handleAppsCustomizeTabKeyEvent(v, keyCode, event); + } +} + +public class FocusHelper { + /** + * Private helper to get the parent TabHost in the view hiearchy. + */ + private static TabHost findTabHostParent(View v) { + ViewParent p = v.getParent(); + while (p != null && !(p instanceof TabHost)) { + p = p.getParent(); + } + return (TabHost) p; + } + + /** + * Handles key events in a AppsCustomize tab between the last tab view and the shop button. + */ + static boolean handleAppsCustomizeTabKeyEvent(View v, int keyCode, KeyEvent e) { + final TabHost tabHost = findTabHostParent(v); + final ViewGroup contents = tabHost.getTabContentView(); + final View shop = tabHost.findViewById(R.id.market_button); + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the shop button if we aren't on it + if (v != shop) { + shop.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the content view (down is handled by the tab key handler otherwise) + if (v == shop) { + contents.requestFocus(); + wasHandled = true; + } + } + break; + default: break; + } + return wasHandled; + } + + /** + * Returns the Viewgroup containing page contents for the page at the index specified. + */ + private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) { + ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index); + if (page instanceof PagedViewCellLayout) { + // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren + page = (ViewGroup) page.getChildAt(0); + } + return page; + } + + /** + * Handles key events in a PageViewExtendedLayout containing PagedViewWidgets. + */ + static boolean handlePagedViewGridLayoutWidgetKeyEvent(PagedViewWidget w, int keyCode, + KeyEvent e) { + + final PagedViewGridLayout parent = (PagedViewGridLayout) w.getParent(); + final PagedView container = (PagedView) parent.getParent(); + final TabHost tabHost = findTabHostParent(container); + final TabWidget tabs = tabHost.getTabWidget(); + final int widgetIndex = parent.indexOfChild(w); + final int widgetCount = parent.getChildCount(); + final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parent)); + final int pageCount = container.getChildCount(); + final int cellCountX = parent.getCellCountX(); + final int cellCountY = parent.getCellCountY(); + final int x = widgetIndex % cellCountX; + final int y = widgetIndex / cellCountX; + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + ViewGroup newParent = null; + // Now that we load items in the bg asynchronously, we can't just focus + // child siblings willy-nilly + View child = null; + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous widget or the last widget on the previous page + if (widgetIndex > 0) { + parent.getChildAt(widgetIndex - 1).requestFocus(); + } else { + if (pageIndex > 0) { + newParent = getAppsCustomizePage(container, pageIndex - 1); + if (newParent != null) { + child = newParent.getChildAt(newParent.getChildCount() - 1); + if (child != null) child.requestFocus(); + } + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next widget or the first widget on the next page + if (widgetIndex < (widgetCount - 1)) { + parent.getChildAt(widgetIndex + 1).requestFocus(); + } else { + if (pageIndex < (pageCount - 1)) { + newParent = getAppsCustomizePage(container, pageIndex + 1); + if (newParent != null) { + child = newParent.getChildAt(0); + if (child != null) child.requestFocus(); + } + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (handleKeyEvent) { + // Select the closest icon in the previous row, otherwise select the tab bar + if (y > 0) { + int newWidgetIndex = ((y - 1) * cellCountX) + x; + child = parent.getChildAt(newWidgetIndex); + if (child != null) child.requestFocus(); + } else { + tabs.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the closest icon in the previous row, otherwise do nothing + if (y < (cellCountY - 1)) { + int newWidgetIndex = Math.min(widgetCount - 1, ((y + 1) * cellCountX) + x); + child = parent.getChildAt(newWidgetIndex); + if (child != null) child.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (handleKeyEvent) { + // Simulate a click on the widget + View.OnClickListener clickListener = (View.OnClickListener) container; + clickListener.onClick(w); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_UP: + if (handleKeyEvent) { + // Select the first item on the previous page, or the first item on this page + // if there is no previous page + if (pageIndex > 0) { + newParent = getAppsCustomizePage(container, pageIndex - 1); + if (newParent != null) { + child = newParent.getChildAt(0); + } + } else { + child = parent.getChildAt(0); + } + if (child != null) child.requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + if (handleKeyEvent) { + // Select the first item on the next page, or the last item on this page + // if there is no next page + if (pageIndex < (pageCount - 1)) { + newParent = getAppsCustomizePage(container, pageIndex + 1); + if (newParent != null) { + child = newParent.getChildAt(0); + } + } else { + child = parent.getChildAt(widgetCount - 1); + } + if (child != null) child.requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_HOME: + if (handleKeyEvent) { + // Select the first item on this page + child = parent.getChildAt(0); + if (child != null) child.requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_END: + if (handleKeyEvent) { + // Select the last item on this page + parent.getChildAt(widgetCount - 1).requestFocus(); + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Handles key events in a PageViewCellLayout containing PagedViewIcons. + */ + static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) { + ViewGroup parentLayout; + ViewGroup itemContainer; + int countX; + int countY; + if (v.getParent() instanceof PagedViewCellLayoutChildren) { + itemContainer = (ViewGroup) v.getParent(); + parentLayout = (ViewGroup) itemContainer.getParent(); + countX = ((PagedViewCellLayout) parentLayout).getCellCountX(); + countY = ((PagedViewCellLayout) parentLayout).getCellCountY(); + } else { + itemContainer = parentLayout = (ViewGroup) v.getParent(); + countX = ((PagedViewGridLayout) parentLayout).getCellCountX(); + countY = ((PagedViewGridLayout) parentLayout).getCellCountY(); + } + + // Note we have an extra parent because of the + // PagedViewCellLayout/PagedViewCellLayoutChildren relationship + final PagedView container = (PagedView) parentLayout.getParent(); + final TabHost tabHost = findTabHostParent(container); + final TabWidget tabs = tabHost.getTabWidget(); + final int iconIndex = itemContainer.indexOfChild(v); + final int itemCount = itemContainer.getChildCount(); + final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parentLayout)); + final int pageCount = container.getChildCount(); + + final int x = iconIndex % countX; + final int y = iconIndex / countX; + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + ViewGroup newParent = null; + // Side pages do not always load synchronously, so check before focusing child siblings + // willy-nilly + View child = null; + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous icon or the last icon on the previous page + if (iconIndex > 0) { + itemContainer.getChildAt(iconIndex - 1).requestFocus(); + } else { + if (pageIndex > 0) { + newParent = getAppsCustomizePage(container, pageIndex - 1); + if (newParent != null) { + container.snapToPage(pageIndex - 1); + child = newParent.getChildAt(newParent.getChildCount() - 1); + if (child != null) child.requestFocus(); + } + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next icon or the first icon on the next page + if (iconIndex < (itemCount - 1)) { + itemContainer.getChildAt(iconIndex + 1).requestFocus(); + } else { + if (pageIndex < (pageCount - 1)) { + newParent = getAppsCustomizePage(container, pageIndex + 1); + if (newParent != null) { + container.snapToPage(pageIndex + 1); + child = newParent.getChildAt(0); + if (child != null) child.requestFocus(); + } + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (handleKeyEvent) { + // Select the closest icon in the previous row, otherwise select the tab bar + if (y > 0) { + int newiconIndex = ((y - 1) * countX) + x; + itemContainer.getChildAt(newiconIndex).requestFocus(); + } else { + tabs.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the closest icon in the previous row, otherwise do nothing + if (y < (countY - 1)) { + int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x); + itemContainer.getChildAt(newiconIndex).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (handleKeyEvent) { + // Simulate a click on the icon + View.OnClickListener clickListener = (View.OnClickListener) container; + clickListener.onClick(v); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_UP: + if (handleKeyEvent) { + // Select the first icon on the previous page, or the first icon on this page + // if there is no previous page + if (pageIndex > 0) { + newParent = getAppsCustomizePage(container, pageIndex - 1); + if (newParent != null) { + container.snapToPage(pageIndex - 1); + child = newParent.getChildAt(0); + if (child != null) child.requestFocus(); + } + } else { + itemContainer.getChildAt(0).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + if (handleKeyEvent) { + // Select the first icon on the next page, or the last icon on this page + // if there is no next page + if (pageIndex < (pageCount - 1)) { + newParent = getAppsCustomizePage(container, pageIndex + 1); + if (newParent != null) { + container.snapToPage(pageIndex + 1); + child = newParent.getChildAt(0); + if (child != null) child.requestFocus(); + } + } else { + itemContainer.getChildAt(itemCount - 1).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_HOME: + if (handleKeyEvent) { + // Select the first icon on this page + itemContainer.getChildAt(0).requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_END: + if (handleKeyEvent) { + // Select the last icon on this page + itemContainer.getChildAt(itemCount - 1).requestFocus(); + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Handles key events in the tab widget. + */ + static boolean handleTabKeyEvent(AccessibleTabView v, int keyCode, KeyEvent e) { + if (!LauncherApplication.isScreenLarge()) return false; + + final FocusOnlyTabWidget parent = (FocusOnlyTabWidget) v.getParent(); + final TabHost tabHost = findTabHostParent(parent); + final ViewGroup contents = tabHost.getTabContentView(); + final int tabCount = parent.getTabCount(); + final int tabIndex = parent.getChildTabIndex(v); + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous tab + if (tabIndex > 0) { + parent.getChildTabViewAt(tabIndex - 1).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next tab, or if the last tab has a focus right id, select that + if (tabIndex < (tabCount - 1)) { + parent.getChildTabViewAt(tabIndex + 1).requestFocus(); + } else { + if (v.getNextFocusRightId() != View.NO_ID) { + tabHost.findViewById(v.getNextFocusRightId()).requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + // Do nothing + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the content view + contents.requestFocus(); + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Handles key events in the workspace hotseat (bottom of the screen). + */ + static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) { + final ViewGroup parent = (ViewGroup) v.getParent(); + final ViewGroup launcher = (ViewGroup) parent.getParent(); + final Workspace workspace = (Workspace) launcher.findViewById(R.id.workspace); + final int buttonIndex = parent.indexOfChild(v); + final int buttonCount = parent.getChildCount(); + final int pageIndex = workspace.getCurrentPage(); + + // NOTE: currently we don't special case for the phone UI in different + // orientations, even though the hotseat is on the side in landscape mode. This + // is to ensure that accessibility consistency is maintained across rotations. + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous button, otherwise snap to the previous page + if (buttonIndex > 0) { + parent.getChildAt(buttonIndex - 1).requestFocus(); + } else { + workspace.snapToPage(pageIndex - 1); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next button, otherwise snap to the next page + if (buttonIndex < (buttonCount - 1)) { + parent.getChildAt(buttonIndex + 1).requestFocus(); + } else { + workspace.snapToPage(pageIndex + 1); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (handleKeyEvent) { + // Select the first bubble text view in the current page of the workspace + final CellLayout layout = (CellLayout) workspace.getChildAt(pageIndex); + final ShortcutAndWidgetContainer children = layout.getShortcutsAndWidgets(); + final View newIcon = getIconInDirection(layout, children, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + workspace.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + // Do nothing + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Private helper method to get the CellLayoutChildren given a CellLayout index. + */ + private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( + ViewGroup container, int i) { + ViewGroup parent = (ViewGroup) container.getChildAt(i); + return (ShortcutAndWidgetContainer) parent.getChildAt(0); + } + + /** + * Private helper method to sort all the CellLayout children in order of their (x,y) spatially + * from top left to bottom right. + */ + private static ArrayList<View> getCellLayoutChildrenSortedSpatially(CellLayout layout, + ViewGroup parent) { + // First we order each the CellLayout children by their x,y coordinates + final int cellCountX = layout.getCountX(); + final int count = parent.getChildCount(); + ArrayList<View> views = new ArrayList<View>(); + for (int j = 0; j < count; ++j) { + views.add(parent.getChildAt(j)); + } + Collections.sort(views, new Comparator<View>() { + @Override + public int compare(View lhs, View rhs) { + CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams(); + CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams(); + int lvIndex = (llp.cellY * cellCountX) + llp.cellX; + int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX; + return lvIndex - rvIndex; + } + }); + return views; + } + /** + * Private helper method to find the index of the next BubbleTextView or FolderIcon in the + * direction delta. + * + * @param delta either -1 or 1 depending on the direction we want to search + */ + private static View findIndexOfIcon(ArrayList<View> views, int i, int delta) { + // Then we find the next BubbleTextView offset by delta from i + final int count = views.size(); + int newI = i + delta; + while (0 <= newI && newI < count) { + View newV = views.get(newI); + if (newV instanceof BubbleTextView || newV instanceof FolderIcon) { + return newV; + } + newI += delta; + } + return null; + } + private static View getIconInDirection(CellLayout layout, ViewGroup parent, int i, + int delta) { + final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); + return findIndexOfIcon(views, i, delta); + } + private static View getIconInDirection(CellLayout layout, ViewGroup parent, View v, + int delta) { + final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); + return findIndexOfIcon(views, views.indexOf(v), delta); + } + /** + * Private helper method to find the next closest BubbleTextView or FolderIcon in the direction + * delta on the next line. + * + * @param delta either -1 or 1 depending on the line and direction we want to search + */ + private static View getClosestIconOnLine(CellLayout layout, ViewGroup parent, View v, + int lineDelta) { + final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); + final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); + final int cellCountY = layout.getCountY(); + final int row = lp.cellY; + final int newRow = row + lineDelta; + if (0 <= newRow && newRow < cellCountY) { + float closestDistance = Float.MAX_VALUE; + int closestIndex = -1; + int index = views.indexOf(v); + int endIndex = (lineDelta < 0) ? -1 : views.size(); + while (index != endIndex) { + View newV = views.get(index); + CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams(); + boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row); + if (satisfiesRow && + (newV instanceof BubbleTextView || newV instanceof FolderIcon)) { + float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) + + Math.pow(tmpLp.cellY - lp.cellY, 2)); + if (tmpDistance < closestDistance) { + closestIndex = index; + closestDistance = tmpDistance; + } + } + if (index <= endIndex) { + ++index; + } else { + --index; + } + } + if (closestIndex > -1) { + return views.get(closestIndex); + } + } + return null; + } + + /** + * Handles key events in a Workspace containing. + */ + static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { + ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout layout = (CellLayout) parent.getParent(); + final Workspace workspace = (Workspace) layout.getParent(); + final ViewGroup launcher = (ViewGroup) workspace.getParent(); + final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.qsb_bar); + final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat); + int pageIndex = workspace.indexOfChild(layout); + int pageCount = workspace.getChildCount(); + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous icon or the last icon on the previous page if possible + View newIcon = getIconInDirection(layout, parent, v, -1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + if (pageIndex > 0) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = getIconInDirection(layout, parent, + parent.getChildCount(), -1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + // Snap to the previous page + workspace.snapToPage(pageIndex - 1); + } + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next icon or the first icon on the next page if possible + View newIcon = getIconInDirection(layout, parent, v, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + if (pageIndex < (pageCount - 1)) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = getIconInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + // Snap to the next page + workspace.snapToPage(pageIndex + 1); + } + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (handleKeyEvent) { + // Select the closest icon in the previous line, otherwise select the tab bar + View newIcon = getClosestIconOnLine(layout, parent, v, -1); + if (newIcon != null) { + newIcon.requestFocus(); + wasHandled = true; + } else { + tabs.requestFocus(); + } + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the closest icon in the next line, otherwise select the button bar + View newIcon = getClosestIconOnLine(layout, parent, v, 1); + if (newIcon != null) { + newIcon.requestFocus(); + wasHandled = true; + } else if (hotseat != null) { + hotseat.requestFocus(); + } + } + break; + case KeyEvent.KEYCODE_PAGE_UP: + if (handleKeyEvent) { + // Select the first icon on the previous page or the first icon on this page + // if there is no previous page + if (pageIndex > 0) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + View newIcon = getIconInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + // Snap to the previous page + workspace.snapToPage(pageIndex - 1); + } + } else { + View newIcon = getIconInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + if (handleKeyEvent) { + // Select the first icon on the next page or the last icon on this page + // if there is no previous page + if (pageIndex < (pageCount - 1)) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + View newIcon = getIconInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + // Snap to the next page + workspace.snapToPage(pageIndex + 1); + } + } else { + View newIcon = getIconInDirection(layout, parent, + parent.getChildCount(), -1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_HOME: + if (handleKeyEvent) { + // Select the first icon on this page + View newIcon = getIconInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_END: + if (handleKeyEvent) { + // Select the last icon on this page + View newIcon = getIconInDirection(layout, parent, + parent.getChildCount(), -1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Handles key events for items in a Folder. + */ + static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) { + ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout layout = (CellLayout) parent.getParent(); + final Folder folder = (Folder) layout.getParent(); + View title = folder.mFolderName; + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous icon + View newIcon = getIconInDirection(layout, parent, v, -1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next icon + View newIcon = getIconInDirection(layout, parent, v, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + title.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (handleKeyEvent) { + // Select the closest icon in the previous line + View newIcon = getClosestIconOnLine(layout, parent, v, -1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the closest icon in the next line + View newIcon = getClosestIconOnLine(layout, parent, v, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + title.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_HOME: + if (handleKeyEvent) { + // Select the first icon on this page + View newIcon = getIconInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_END: + if (handleKeyEvent) { + // Select the last icon on this page + View newIcon = getIconInDirection(layout, parent, + parent.getChildCount(), -1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } +} diff --git a/src/com/android/launcher3/FocusOnlyTabWidget.java b/src/com/android/launcher3/FocusOnlyTabWidget.java new file mode 100644 index 000000000..08fc311bc --- /dev/null +++ b/src/com/android/launcher3/FocusOnlyTabWidget.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TabWidget; + +public class FocusOnlyTabWidget extends TabWidget { + public FocusOnlyTabWidget(Context context) { + super(context); + } + + public FocusOnlyTabWidget(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FocusOnlyTabWidget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public View getSelectedTab() { + final int count = getTabCount(); + for (int i = 0; i < count; ++i) { + View v = getChildTabViewAt(i); + if (v.isSelected()) { + return v; + } + } + return null; + } + + public int getChildTabIndex(View v) { + final int tabCount = getTabCount(); + for (int i = 0; i < tabCount; ++i) { + if (getChildTabViewAt(i) == v) { + return i; + } + } + return -1; + } + + public void setCurrentTabToFocusedTab() { + View tab = null; + int index = -1; + final int count = getTabCount(); + for (int i = 0; i < count; ++i) { + View v = getChildTabViewAt(i); + if (v.hasFocus()) { + tab = v; + index = i; + break; + } + } + if (index > -1) { + super.setCurrentTab(index); + super.onFocusChange(tab, true); + } + } + public void superOnFocusChange(View v, boolean hasFocus) { + super.onFocusChange(v, hasFocus); + } + + @Override + public void onFocusChange(android.view.View v, boolean hasFocus) { + if (v == this && hasFocus && getTabCount() > 0) { + getSelectedTab().requestFocus(); + return; + } + } +} diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java new file mode 100644 index 000000000..a7b5c5c54 --- /dev/null +++ b/src/com/android/launcher3/Folder.java @@ -0,0 +1,1114 @@ +/* + * 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.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.InputType; +import android.text.Selection; +import android.text.Spannable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ActionMode; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.R; +import com.android.launcher3.FolderInfo.FolderListener; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * 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 { + private static final String TAG = "Launcher.Folder"; + + protected DragController mDragController; + protected Launcher mLauncher; + protected FolderInfo mInfo; + + static final int STATE_NONE = -1; + static final int STATE_SMALL = 0; + static final int STATE_ANIMATING = 1; + static final int STATE_OPEN = 2; + + private int mExpandDuration; + protected CellLayout mContent; + private final LayoutInflater mInflater; + private final IconCache mIconCache; + private int mState = STATE_NONE; + private static final int REORDER_ANIMATION_DURATION = 230; + private static final int ON_EXIT_CLOSE_DELAY = 800; + private boolean mRearrangeOnClose = false; + private FolderIcon mFolderIcon; + private int mMaxCountX; + private int mMaxCountY; + private int mMaxNumItems; + private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); + private Drawable mIconDrawable; + boolean mItemsInvalidated = false; + private ShortcutInfo mCurrentDragInfo; + private View mCurrentDragView; + boolean mSuppressOnAdd = false; + private int[] mTargetCell = new int[2]; + private int[] mPreviousTargetCell = new int[2]; + private int[] mEmptyCell = new int[2]; + private Alarm mReorderAlarm = new Alarm(); + private Alarm mOnExitAlarm = new Alarm(); + private int mFolderNameHeight; + private Rect mTempRect = new Rect(); + private boolean mDragInProgress = false; + private boolean mDeleteFolderOnDropCompleted = false; + private boolean mSuppressFolderDeletion = false; + private boolean mItemAddedBackToSelfViaIcon = false; + FolderEditText mFolderName; + private float mFolderIconPivotX; + private float mFolderIconPivotY; + + private boolean mIsEditingName = false; + private InputMethodManager mInputMethodManager; + + private static String sDefaultFolderName; + private static String sHintText; + + private boolean mDestroyed; + + /** + * Used to inflate the Workspace from XML. + * + * @param context The application's context. + * @param attrs The attribtues set containing the Workspace's customization values. + */ + public Folder(Context context, AttributeSet attrs) { + super(context, attrs); + setAlwaysDrawnWithCacheEnabled(false); + mInflater = LayoutInflater.from(context); + mIconCache = ((LauncherApplication)context.getApplicationContext()).getIconCache(); + + Resources res = getResources(); + mMaxCountX = res.getInteger(R.integer.folder_max_count_x); + mMaxCountY = res.getInteger(R.integer.folder_max_count_y); + mMaxNumItems = res.getInteger(R.integer.folder_max_num_items); + if (mMaxCountX < 0 || mMaxCountY < 0 || mMaxNumItems < 0) { + mMaxCountX = LauncherModel.getCellCountX(); + mMaxCountY = LauncherModel.getCellCountY(); + mMaxNumItems = mMaxCountX * mMaxCountY; + } + + mInputMethodManager = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + mExpandDuration = res.getInteger(R.integer.config_folderAnimDuration); + + 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 behvior when clicking the text field (since it will always gain focus on click). + setFocusableInTouchMode(true); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mContent = (CellLayout) findViewById(R.id.folder_content); + mContent.setGridSize(0, 0); + mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); + mContent.setInvertIfRtl(true); + mFolderName = (FolderEditText) findViewById(R.id.folder_name); + mFolderName.setFolder(this); + mFolderName.setOnFocusChangeListener(this); + + // We find out how tall the text view wants to be (it is set to wrap_content), so that + // we can allocate the appropriate amount of space for it. + int measureSpec = MeasureSpec.UNSPECIFIED; + mFolderName.measure(measureSpec, measureSpec); + mFolderNameHeight = mFolderName.getMeasuredHeight(); + + // 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); + } + + 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) { + // refactor this code from Folder + ShortcutInfo item = (ShortcutInfo) tag; + int[] pos = new int[2]; + v.getLocationOnScreen(pos); + item.intent.setSourceBounds(new Rect(pos[0], pos[1], + pos[0] + v.getWidth(), pos[1] + v.getHeight())); + + mLauncher.startActivitySafely(v, item.intent, item); + } + } + + public boolean onLongClick(View v) { + // Return if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return true; + + Object tag = v.getTag(); + if (tag instanceof ShortcutInfo) { + ShortcutInfo item = (ShortcutInfo) tag; + if (!v.isInTouchMode()) { + return false; + } + + mLauncher.dismissFolderCling(null); + + mLauncher.getWorkspace().onDragStartedWithItem(v); + mLauncher.getWorkspace().beginDragShared(v, this); + mIconDrawable = ((TextView) v).getCompoundDrawables()[1]; + + mCurrentDragInfo = item; + mEmptyCell[0] = item.cellX; + mEmptyCell[1] = item.cellY; + mCurrentDragView = v; + + mContent.removeView(mCurrentDragView); + mInfo.remove(mCurrentDragInfo); + mDragInProgress = true; + mItemAddedBackToSelfViaIcon = false; + } + return true; + } + + 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(); + 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; + } + + 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; + } + + public Drawable getDragDrawable() { + return mIconDrawable; + } + + /** + * We need to handle touch events to prevent them from falling through to the workspace below. + */ + @Override + public boolean onTouchEvent(MotionEvent ev) { + return true; + } + + public void setDragController(DragController dragController) { + mDragController = dragController; + } + + 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 + */ + FolderInfo getInfo() { + return mInfo; + } + + private class GridComparator implements Comparator<ShortcutInfo> { + int mNumCols; + public GridComparator(int numCols) { + mNumCols = numCols; + } + + @Override + public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { + int lhIndex = lhs.cellY * mNumCols + lhs.cellX; + int rhIndex = rhs.cellY * mNumCols + rhs.cellX; + return (lhIndex - rhIndex); + } + } + + private void placeInReadingOrder(ArrayList<ShortcutInfo> items) { + int maxX = 0; + int count = items.size(); + for (int i = 0; i < count; i++) { + ShortcutInfo item = items.get(i); + if (item.cellX > maxX) { + maxX = item.cellX; + } + } + + GridComparator gridComparator = new GridComparator(maxX + 1); + Collections.sort(items, gridComparator); + final int countX = mContent.getCountX(); + for (int i = 0; i < count; i++) { + int x = i % countX; + int y = i / countX; + ShortcutInfo item = items.get(i); + item.cellX = x; + item.cellY = y; + } + } + + void bind(FolderInfo info) { + mInfo = info; + ArrayList<ShortcutInfo> children = info.contents; + ArrayList<ShortcutInfo> overflow = new ArrayList<ShortcutInfo>(); + setupContentForNumItems(children.size()); + placeInReadingOrder(children); + int count = 0; + for (int i = 0; i < children.size(); i++) { + ShortcutInfo child = (ShortcutInfo) children.get(i); + if (!createAndAddShortcut(child)) { + overflow.add(child); + } else { + count++; + } + } + + // We rearrange the items in case there are any empty gaps + setupContentForNumItems(count); + + // 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); + + if (!sDefaultFolderName.contentEquals(mInfo.title)) { + mFolderName.setText(mInfo.title); + } else { + mFolderName.setText(""); + } + updateItemLocationsInDatabase(); + } + + /** + * Creates a new UserFolder, inflated from R.layout.user_folder. + * + * @param context The application's context. + * + * @return A new UserFolder. + */ + static Folder fromXml(Context context) { + return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null); + } + + /** + * 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; + } + + public void animateOpen() { + positionAndSizeAsIcon(); + + if (!(getParent() instanceof DragLayer)) return; + centerAboutIcon(); + 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.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + String.format(getContext().getString(R.string.folder_opened), + mContent.getCountX(), mContent.getCountY())); + mState = STATE_ANIMATING; + } + @Override + public void onAnimationEnd(Animator animation) { + mState = STATE_OPEN; + setLayerType(LAYER_TYPE_NONE, null); + Cling cling = mLauncher.showFirstRunFoldersCling(); + if (cling != null) { + cling.bringToFront(); + } + setFocusOnFirstChild(); + } + }); + oa.setDuration(mExpandDuration); + setLayerType(LAYER_TYPE_HARDWARE, null); + oa.start(); + } + + private 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); + } + } + + private void setFocusOnFirstChild() { + View firstChild = mContent.getChildAt(0, 0); + if (firstChild != null) { + firstChild.requestFocus(); + } + } + + public void animateClosed() { + if (!(getParent() instanceof DragLayer)) return; + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); + final ObjectAnimator oa = + LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); + + oa.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onCloseComplete(); + setLayerType(LAYER_TYPE_NONE, null); + mState = STATE_SMALL; + } + @Override + public void onAnimationStart(Animator animation) { + sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + getContext().getString(R.string.folder_closed)); + mState = STATE_ANIMATING; + } + }); + oa.setDuration(mExpandDuration); + setLayerType(LAYER_TYPE_HARDWARE, null); + oa.start(); + } + + void notifyDataSetChanged() { + // recreate all the children if the data set changes under us. We may want to do this more + // intelligently (ie just removing the views that should no longer exist) + mContent.removeAllViewsInLayout(); + bind(mInfo); + } + + 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()); + } + + protected boolean findAndSetEmptyCells(ShortcutInfo item) { + int[] emptyCell = new int[2]; + if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) { + item.cellX = emptyCell[0]; + item.cellY = emptyCell[1]; + return true; + } else { + return false; + } + } + + protected boolean createAndAddShortcut(ShortcutInfo item) { + final TextView textView = + (TextView) mInflater.inflate(R.layout.application, this, false); + textView.setCompoundDrawablesWithIntrinsicBounds(null, + new FastBitmapDrawable(item.getIcon(mIconCache)), null, null); + textView.setText(item.title); + textView.setTag(item); + + textView.setOnClickListener(this); + textView.setOnLongClickListener(this); + + // We need to check here to verify that the given item's location isn't already occupied + // by another item. + if (mContent.getChildAt(item.cellX, item.cellY) != null || item.cellX < 0 || item.cellY < 0 + || item.cellX >= mContent.getCountX() || item.cellY >= mContent.getCountY()) { + // This shouldn't happen, log it. + Log.e(TAG, "Folder order not properly persisted during bind"); + if (!findAndSetEmptyCells(item)) { + return false; + } + } + + CellLayout.LayoutParams lp = + new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); + boolean insert = false; + textView.setOnKeyListener(new FolderKeyEventListener()); + mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true); + return true; + } + + public void onDragEnter(DragObject d) { + mPreviousTargetCell[0] = -1; + mPreviousTargetCell[1] = -1; + mOnExitAlarm.cancelAlarm(); + } + + OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { + public void onAlarm(Alarm alarm) { + realTimeReorder(mEmptyCell, mTargetCell); + } + }; + + boolean readingOrderGreaterThan(int[] v1, int[] v2) { + if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) { + return true; + } else { + return false; + } + } + + private void realTimeReorder(int[] empty, int[] target) { + boolean wrap; + int startX; + int endX; + int startY; + int delay = 0; + float delayAmount = 30; + if (readingOrderGreaterThan(target, empty)) { + wrap = empty[0] >= mContent.getCountX() - 1; + startY = wrap ? empty[1] + 1 : empty[1]; + for (int y = startY; y <= target[1]; y++) { + startX = y == empty[1] ? empty[0] + 1 : 0; + endX = y < target[1] ? mContent.getCountX() - 1 : target[0]; + for (int x = startX; x <= endX; x++) { + View v = mContent.getChildAt(x,y); + if (mContent.animateChildToPosition(v, empty[0], empty[1], + REORDER_ANIMATION_DURATION, delay, true, true)) { + empty[0] = x; + empty[1] = y; + delay += delayAmount; + delayAmount *= 0.9; + } + } + } + } else { + wrap = empty[0] == 0; + startY = wrap ? empty[1] - 1 : empty[1]; + for (int y = startY; y >= target[1]; y--) { + startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1; + endX = y > target[1] ? 0 : target[0]; + for (int x = startX; x >= endX; x--) { + View v = mContent.getChildAt(x,y); + if (mContent.animateChildToPosition(v, empty[0], empty[1], + REORDER_ANIMATION_DURATION, delay, true, true)) { + empty[0] = x; + empty[1] = y; + delay += delayAmount; + delayAmount *= 0.9; + } + } + } + } + } + + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + public void onDragOver(DragObject d) { + float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null); + mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1], 1, 1, mTargetCell); + + if (isLayoutRtl()) { + mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1; + } + + if (mTargetCell[0] != mPreviousTargetCell[0] || mTargetCell[1] != mPreviousTargetCell[1]) { + mReorderAlarm.cancelAlarm(); + mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); + mReorderAlarm.setAlarm(150); + mPreviousTargetCell[0] = mTargetCell[0]; + mPreviousTargetCell[1] = mTargetCell[1]; + } + } + + // This is used to compute the visual center of the dragView. The idea is that + // the visual center represents the user's interpretation of where the item is, and hence + // is the appropriate point to use when determining drop location. + private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, + DragView dragView, float[] recycle) { + float res[]; + if (recycle == null) { + res = new float[2]; + } else { + res = recycle; + } + + // These represent the visual top and left of drag view if a dragRect was provided. + // If a dragRect was not provided, then they correspond to the actual view left and + // top, as the dragRect is in that case taken to be the entire dragView. + // R.dimen.dragViewOffsetY. + int left = x - xOffset; + int top = y - yOffset; + + // In order to find the visual center, we shift by half the dragRect + res[0] = left + dragView.getDragRegion().width() / 2; + res[1] = top + dragView.getDragRegion().height() / 2; + + return res; + } + + OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { + public void onAlarm(Alarm alarm) { + completeDragExit(); + } + }; + + public void completeDragExit() { + mLauncher.closeFolder(); + mCurrentDragInfo = null; + mCurrentDragView = null; + mSuppressOnAdd = false; + mRearrangeOnClose = true; + } + + 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(); + } + + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + if (success) { + if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon) { + replaceFolderWithFinalItem(); + } + } else { + setupContentForNumItems(getItemCount()); + // The drag failed, we need to return the item to the folder + mFolderIcon.onDrop(d); + } + + if (target != this) { + if (mOnExitAlarm.alarmPending()) { + mOnExitAlarm.cancelAlarm(); + if (!success) { + mSuppressFolderDeletion = true; + } + completeDragExit(); + } + } + + 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. + updateItemLocationsInDatabase(); + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + + @Override + public void onFlingToDeleteCompleted() { + // Do nothing + } + + private void updateItemLocationsInDatabase() { + ArrayList<View> list = getItemsInReadingOrder(); + for (int i = 0; i < list.size(); i++) { + View v = list.get(i); + ItemInfo info = (ItemInfo) v.getTag(); + LauncherModel.moveItemInDatabase(mLauncher, info, mInfo.id, 0, + info.cellX, info.cellY); + } + } + + public void notifyDrop() { + if (mDragInProgress) { + mItemAddedBackToSelfViaIcon = true; + } + } + + public boolean isDropEnabled() { + return true; + } + + public DropTarget getDropTargetDelegate(DragObject d) { + return null; + } + + private void setupContentDimensions(int count) { + ArrayList<View> list = getItemsInReadingOrder(); + + int countX = mContent.getCountX(); + int countY = mContent.getCountY(); + boolean done = false; + + while (!done) { + int oldCountX = countX; + int oldCountY = countY; + if (countX * countY < count) { + // Current grid is too small, expand it + if ((countX <= countY || countY == mMaxCountY) && countX < mMaxCountX) { + countX++; + } else if (countY < mMaxCountY) { + countY++; + } + if (countY == 0) countY++; + } else if ((countY - 1) * countX >= count && countY >= countX) { + countY = Math.max(0, countY - 1); + } else if ((countX - 1) * countY >= count) { + countX = Math.max(0, countX - 1); + } + done = countX == oldCountX && countY == oldCountY; + } + mContent.setGridSize(countX, countY); + arrangeChildren(list); + } + + public boolean isFull() { + return getItemCount() >= mMaxNumItems; + } + + private void centerAboutIcon() { + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + + int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight() + + mFolderNameHeight; + DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer); + + float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect); + + int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2); + int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2); + int centeredLeft = centerX - width / 2; + int centeredTop = centerY - height / 2; + + int currentPage = mLauncher.getWorkspace().getCurrentPage(); + // In case the workspace is scrolling, we need to use the final scroll to compute + // the folders bounds. + mLauncher.getWorkspace().setFinalScrollForPageChange(currentPage); + // 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); + // We reset the workspaces scroll + mLauncher.getWorkspace().resetFinalScrollForPageChange(currentPage); + + // We need to bound the folder to the currently visible CellLayoutChildren + int left = Math.min(Math.max(bounds.left, centeredLeft), + bounds.left + bounds.width() - width); + int top = Math.min(Math.max(bounds.top, centeredTop), + bounds.top + bounds.height() - height); + // If the folder doesn't fit within the bounds, center it about the desired bounds + if (width >= bounds.width()) { + left = bounds.left + (bounds.width() - width) / 2; + } + if (height >= bounds.height()) { + top = bounds.top + (bounds.height() - height) / 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)); + + lp.width = width; + lp.height = height; + lp.x = left; + lp.y = top; + } + + float getPivotXForIconAnimation() { + return mFolderIconPivotX; + } + float getPivotYForIconAnimation() { + return mFolderIconPivotY; + } + + private void setupContentForNumItems(int count) { + setupContentDimensions(count); + + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + if (lp == null) { + lp = new DragLayer.LayoutParams(0, 0); + lp.customPosition = true; + setLayoutParams(lp); + } + centerAboutIcon(); + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight() + + mFolderNameHeight; + + int contentWidthSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredWidth(), + MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredHeight(), + MeasureSpec.EXACTLY); + mContent.measure(contentWidthSpec, contentHeightSpec); + + mFolderName.measure(contentWidthSpec, + MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY)); + setMeasuredDimension(width, height); + } + + private void arrangeChildren(ArrayList<View> list) { + int[] vacant = new int[2]; + if (list == null) { + list = getItemsInReadingOrder(); + } + mContent.removeAllViews(); + + for (int i = 0; i < list.size(); i++) { + View v = list.get(i); + mContent.getVacantCell(vacant, 1, 1); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); + lp.cellX = vacant[0]; + lp.cellY = vacant[1]; + ItemInfo info = (ItemInfo) v.getTag(); + if (info.cellX != vacant[0] || info.cellY != vacant[1]) { + info.cellX = vacant[0]; + info.cellY = vacant[1]; + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0, + info.cellX, info.cellY); + } + boolean insert = false; + mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true); + } + mItemsInvalidated = true; + } + + public int getItemCount() { + return mContent.getShortcutsAndWidgets().getChildCount(); + } + + public View getItemAt(int index) { + return mContent.getShortcutsAndWidgets().getChildAt(index); + } + + private void onCloseComplete() { + DragLayer parent = (DragLayer) getParent(); + if (parent != null) { + parent.removeView(this); + } + mDragController.removeDropTarget((DropTarget) this); + clearFocus(); + mFolderIcon.requestFocus(); + + if (mRearrangeOnClose) { + setupContentForNumItems(getItemCount()); + mRearrangeOnClose = false; + } + if (getItemCount() <= 1) { + if (!mDragInProgress && !mSuppressFolderDeletion) { + replaceFolderWithFinalItem(); + } else if (mDragInProgress) { + mDeleteFolderOnDropCompleted = true; + } + } + mSuppressFolderDeletion = false; + } + + private 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.screen); + + View child = null; + // Move the item from the folder to the workspace, in the position of the folder + if (getItemCount() == 1) { + ShortcutInfo finalItem = mInfo.contents.get(0); + child = mLauncher.createShortcut(R.layout.application, cellLayout, + finalItem); + LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, + mInfo.screen, mInfo.cellX, mInfo.cellY); + } + if (getItemCount() <= 1) { + // Remove the folder + LauncherModel.deleteItemFromDatabase(mLauncher, mInfo); + 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. + if (child != null) { + mLauncher.getWorkspace().addInScreen(child, mInfo.container, mInfo.screen, + mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY); + } + } + }; + View finalChild = getItemAt(0); + if (finalChild != null) { + mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable); + } + mDestroyed = true; + } + + boolean isDestroyed() { + return mDestroyed; + } + + // This method keeps track of the last item in the folder for the purposes + // of keyboard focus + private void updateTextViewFocus() { + View lastChild = getItemAt(getItemCount() - 1); + getItemAt(getItemCount() - 1); + if (lastChild != null) { + mFolderName.setNextFocusDownId(lastChild.getId()); + mFolderName.setNextFocusRightId(lastChild.getId()); + mFolderName.setNextFocusLeftId(lastChild.getId()); + mFolderName.setNextFocusUpId(lastChild.getId()); + } + } + + public void onDrop(DragObject d) { + ShortcutInfo item; + if (d.dragInfo instanceof ApplicationInfo) { + // Came from all apps -- make a copy + item = ((ApplicationInfo) d.dragInfo).makeShortcut(); + item.spanX = 1; + item.spanY = 1; + } else { + item = (ShortcutInfo) d.dragInfo; + } + // Dragged from self onto self, currently this is the only path possible, however + // we keep this as a distinct code path. + if (item == mCurrentDragInfo) { + ShortcutInfo si = (ShortcutInfo) mCurrentDragView.getTag(); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mCurrentDragView.getLayoutParams(); + si.cellX = lp.cellX = mEmptyCell[0]; + si.cellX = lp.cellY = mEmptyCell[1]; + mContent.addViewToCellLayout(mCurrentDragView, -1, (int)item.id, lp, true); + if (d.dragView.hasDrawn()) { + mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, mCurrentDragView); + } else { + d.deferDragViewCleanupPostAnimation = false; + mCurrentDragView.setVisibility(VISIBLE); + } + mItemsInvalidated = true; + setupContentDimensions(getItemCount()); + mSuppressOnAdd = true; + } + mInfo.add(item); + } + + // 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); + } + + public void onAdd(ShortcutInfo item) { + mItemsInvalidated = true; + // 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; + if (!findAndSetEmptyCells(item)) { + // The current layout is full, can we expand it? + setupContentForNumItems(getItemCount() + 1); + findAndSetEmptyCells(item); + } + createAndAddShortcut(item); + 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; + View v = getViewForInfo(item); + mContent.removeView(v); + if (mState == STATE_ANIMATING) { + mRearrangeOnClose = true; + } else { + setupContentForNumItems(getItemCount()); + } + if (getItemCount() <= 1) { + replaceFolderWithFinalItem(); + } + } + + private View getViewForInfo(ShortcutInfo item) { + for (int j = 0; j < mContent.getCountY(); j++) { + for (int i = 0; i < mContent.getCountX(); i++) { + View v = mContent.getChildAt(i, j); + if (v.getTag() == item) { + return v; + } + } + } + return null; + } + + public void onItemsChanged() { + updateTextViewFocus(); + } + + public void onTitleChanged(CharSequence title) { + } + + public ArrayList<View> getItemsInReadingOrder() { + if (mItemsInvalidated) { + mItemsInReadingOrder.clear(); + for (int j = 0; j < mContent.getCountY(); j++) { + for (int i = 0; i < mContent.getCountX(); i++) { + View v = mContent.getChildAt(i, j); + if (v != null) { + mItemsInReadingOrder.add(v); + } + } + } + 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(); + } + } +} diff --git a/src/com/android/launcher3/FolderEditText.java b/src/com/android/launcher3/FolderEditText.java new file mode 100644 index 000000000..c31100899 --- /dev/null +++ b/src/com/android/launcher3/FolderEditText.java @@ -0,0 +1,36 @@ +package com.android.launcher3; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + +public class FolderEditText extends EditText { + + private Folder mFolder; + + public FolderEditText(Context context) { + super(context); + } + + public FolderEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FolderEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setFolder(Folder folder) { + mFolder = folder; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + // Catch the back button on the soft keyboard so that we can just close the activity + if (event.getKeyCode() == android.view.KeyEvent.KEYCODE_BACK) { + mFolder.doneEditingFolderName(true); + } + return super.onKeyPreIme(keyCode, event); + } +} diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java new file mode 100644 index 000000000..e11d7d18a --- /dev/null +++ b/src/com/android/launcher3/FolderIcon.java @@ -0,0 +1,667 @@ +/* + * 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.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.R; +import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.FolderInfo.FolderListener; + +import java.util.ArrayList; + +/** + * An icon that can appear on in the workspace representing an {@link UserFolder}. + */ +public class FolderIcon extends LinearLayout implements FolderListener { + private Launcher mLauncher; + private Folder mFolder; + private FolderInfo mInfo; + private static boolean sStaticValuesDirty = true; + + private CheckLongPressHelper mLongPressHelper; + + // The number of icons to display in the + private static final int NUM_ITEMS_IN_PREVIEW = 3; + private static final int CONSUMPTION_ANIMATION_DURATION = 100; + private static final int DROP_IN_ANIMATION_DURATION = 400; + private static final int INITIAL_ITEM_ANIMATION_DURATION = 350; + private static final int FINAL_ITEM_ANIMATION_DURATION = 200; + + // The degree to which the inner ring grows when accepting drop + private static final float INNER_RING_GROWTH_FACTOR = 0.15f; + + // The degree to which the outer ring is scaled in its natural state + private static final float OUTER_RING_GROWTH_FACTOR = 0.3f; + + // The amount of vertical spread between items in the stack [0...1] + private static final float PERSPECTIVE_SHIFT_FACTOR = 0.24f; + + // The degree to which the item in the back of the stack is scaled [0...1] + // (0 means it's not scaled at all, 1 means it's scaled to nothing) + private static final float PERSPECTIVE_SCALE_FACTOR = 0.35f; + + public static Drawable sSharedFolderLeaveBehind = null; + + private ImageView mPreviewBackground; + private BubbleTextView mFolderName; + + FolderRingAnimator mFolderRingAnimator = null; + + // These variables are all associated with the drawing of the preview; they are stored + // as member variables for shared usage and to avoid computation on each frame + private int mIntrinsicIconSize; + private float mBaselineIconScale; + private int mBaselineIconSize; + private int mAvailableSpaceInPreview; + private int mTotalWidth = -1; + private int mPreviewOffsetX; + private int mPreviewOffsetY; + private float mMaxPerspectiveShift; + boolean mAnimating = false; + + private PreviewItemDrawingParams mParams = new PreviewItemDrawingParams(0, 0, 0, 0); + private PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0); + private ArrayList<ShortcutInfo> mHiddenItems = new ArrayList<ShortcutInfo>(); + + public FolderIcon(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public FolderIcon(Context context) { + super(context); + init(); + } + + private void init() { + mLongPressHelper = new CheckLongPressHelper(this); + } + + public boolean isDropEnabled() { + final ViewGroup cellLayoutChildren = (ViewGroup) getParent(); + final ViewGroup cellLayout = (ViewGroup) cellLayoutChildren.getParent(); + final Workspace workspace = (Workspace) cellLayout.getParent(); + return !workspace.isSmall(); + } + + static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group, + FolderInfo folderInfo, IconCache iconCache) { + @SuppressWarnings("all") // suppress dead code warning + final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION; + if (error) { + throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " + + "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + + "is dependent on this"); + } + + FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false); + + icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name); + icon.mFolderName.setText(folderInfo.title); + icon.mPreviewBackground = (ImageView) icon.findViewById(R.id.preview_background); + + icon.setTag(folderInfo); + icon.setOnClickListener(launcher); + icon.mInfo = folderInfo; + icon.mLauncher = launcher; + icon.setContentDescription(String.format(launcher.getString(R.string.folder_name_format), + folderInfo.title)); + Folder folder = Folder.fromXml(launcher); + folder.setDragController(launcher.getDragController()); + folder.setFolderIcon(icon); + folder.bind(folderInfo); + icon.mFolder = folder; + + icon.mFolderRingAnimator = new FolderRingAnimator(launcher, icon); + folderInfo.addListener(icon); + + return icon; + } + + @Override + protected Parcelable onSaveInstanceState() { + sStaticValuesDirty = true; + return super.onSaveInstanceState(); + } + + public static class FolderRingAnimator { + public int mCellX; + public int mCellY; + private CellLayout mCellLayout; + public float mOuterRingSize; + public float mInnerRingSize; + public FolderIcon mFolderIcon = null; + public Drawable mOuterRingDrawable = null; + public Drawable mInnerRingDrawable = null; + public static Drawable sSharedOuterRingDrawable = null; + public static Drawable sSharedInnerRingDrawable = null; + public static int sPreviewSize = -1; + public static int sPreviewPadding = -1; + + private ValueAnimator mAcceptAnimator; + private ValueAnimator mNeutralAnimator; + + public FolderRingAnimator(Launcher launcher, FolderIcon folderIcon) { + mFolderIcon = folderIcon; + Resources res = launcher.getResources(); + mOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo); + mInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_holo); + + // We need to reload the static values when configuration changes in case they are + // different in another configuration + if (sStaticValuesDirty) { + sPreviewSize = res.getDimensionPixelSize(R.dimen.folder_preview_size); + sPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding); + sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo); + sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_holo); + sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest); + sStaticValuesDirty = false; + } + } + + public void animateToAcceptState() { + if (mNeutralAnimator != null) { + mNeutralAnimator.cancel(); + } + mAcceptAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f); + mAcceptAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION); + + final int previewSize = sPreviewSize; + mAcceptAnimator.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + final float percent = (Float) animation.getAnimatedValue(); + mOuterRingSize = (1 + percent * OUTER_RING_GROWTH_FACTOR) * previewSize; + mInnerRingSize = (1 + percent * INNER_RING_GROWTH_FACTOR) * previewSize; + if (mCellLayout != null) { + mCellLayout.invalidate(); + } + } + }); + mAcceptAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (mFolderIcon != null) { + mFolderIcon.mPreviewBackground.setVisibility(INVISIBLE); + } + } + }); + mAcceptAnimator.start(); + } + + public void animateToNaturalState() { + if (mAcceptAnimator != null) { + mAcceptAnimator.cancel(); + } + mNeutralAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f); + mNeutralAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION); + + final int previewSize = sPreviewSize; + mNeutralAnimator.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + final float percent = (Float) animation.getAnimatedValue(); + mOuterRingSize = (1 + (1 - percent) * OUTER_RING_GROWTH_FACTOR) * previewSize; + mInnerRingSize = (1 + (1 - percent) * INNER_RING_GROWTH_FACTOR) * previewSize; + if (mCellLayout != null) { + mCellLayout.invalidate(); + } + } + }); + mNeutralAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mCellLayout != null) { + mCellLayout.hideFolderAccept(FolderRingAnimator.this); + } + if (mFolderIcon != null) { + mFolderIcon.mPreviewBackground.setVisibility(VISIBLE); + } + } + }); + mNeutralAnimator.start(); + } + + // Location is expressed in window coordinates + public void getCell(int[] loc) { + loc[0] = mCellX; + loc[1] = mCellY; + } + + // Location is expressed in window coordinates + public void setCell(int x, int y) { + mCellX = x; + mCellY = y; + } + + public void setCellLayout(CellLayout layout) { + mCellLayout = layout; + } + + public float getOuterRingSize() { + return mOuterRingSize; + } + + public float getInnerRingSize() { + return mInnerRingSize; + } + } + + Folder getFolder() { + return mFolder; + } + + FolderInfo getFolderInfo() { + return mInfo; + } + + private boolean willAcceptItem(ItemInfo item) { + final int itemType = item.itemType; + return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || + itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && + !mFolder.isFull() && item != mInfo && !mInfo.opened); + } + + public boolean acceptDrop(Object dragInfo) { + final ItemInfo item = (ItemInfo) dragInfo; + return !mFolder.isDestroyed() && willAcceptItem(item); + } + + public void addItem(ShortcutInfo item) { + mInfo.add(item); + } + + public void onDragEnter(Object dragInfo) { + if (mFolder.isDestroyed() || !willAcceptItem((ItemInfo) dragInfo)) return; + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); + CellLayout layout = (CellLayout) getParent().getParent(); + mFolderRingAnimator.setCell(lp.cellX, lp.cellY); + mFolderRingAnimator.setCellLayout(layout); + mFolderRingAnimator.animateToAcceptState(); + layout.showFolderAccept(mFolderRingAnimator); + } + + public void onDragOver(Object dragInfo) { + } + + public void performCreateAnimation(final ShortcutInfo destInfo, final View destView, + final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect, + float scaleRelativeToDragLayer, Runnable postAnimationRunnable) { + + // These correspond two the drawable and view that the icon was dropped _onto_ + Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1]; + computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), + destView.getMeasuredWidth()); + + // This will animate the first item from it's position as an icon into its + // position as the first item in the preview + animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION, false, null); + addItem(destInfo); + + // This will animate the dragView (srcView) into the new folder + onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable, null); + } + + public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) { + Drawable animateDrawable = ((TextView) finalView).getCompoundDrawables()[1]; + computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), + finalView.getMeasuredWidth()); + + // This will animate the first item from it's position as an icon into its + // position as the first item in the preview + animateFirstItem(animateDrawable, FINAL_ITEM_ANIMATION_DURATION, true, + onCompleteRunnable); + } + + public void onDragExit(Object dragInfo) { + onDragExit(); + } + + public void onDragExit() { + mFolderRingAnimator.animateToNaturalState(); + } + + private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect, + float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable, + DragObject d) { + item.cellX = -1; + item.cellY = -1; + + // Typically, the animateView corresponds to the DragView; however, if this is being done + // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we + // will not have a view to animate + if (animateView != null) { + DragLayer dragLayer = mLauncher.getDragLayer(); + Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(animateView, from); + Rect to = finalRect; + if (to == null) { + to = new Rect(); + Workspace workspace = mLauncher.getWorkspace(); + // Set cellLayout and this to it's final state to compute final animation locations + workspace.setFinalTransitionTransform((CellLayout) getParent().getParent()); + float scaleX = getScaleX(); + float scaleY = getScaleY(); + setScaleX(1.0f); + setScaleY(1.0f); + scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to); + // Finished computing final animation locations, restore current state + setScaleX(scaleX); + setScaleY(scaleY); + workspace.resetTransitionTransform((CellLayout) getParent().getParent()); + } + + int[] center = new int[2]; + float scale = getLocalCenterForIndex(index, center); + center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]); + center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]); + + to.offset(center[0] - animateView.getMeasuredWidth() / 2, + center[1] - animateView.getMeasuredHeight() / 2); + + float finalAlpha = index < NUM_ITEMS_IN_PREVIEW ? 0.5f : 0f; + + float finalScale = scale * scaleRelativeToDragLayer; + dragLayer.animateView(animateView, from, to, finalAlpha, + 1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION, + new DecelerateInterpolator(2), new AccelerateInterpolator(2), + postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null); + addItem(item); + mHiddenItems.add(item); + mFolder.hideItem(item); + postDelayed(new Runnable() { + public void run() { + mHiddenItems.remove(item); + mFolder.showItem(item); + invalidate(); + } + }, DROP_IN_ANIMATION_DURATION); + } else { + addItem(item); + } + } + + public void onDrop(DragObject d) { + ShortcutInfo item; + if (d.dragInfo instanceof ApplicationInfo) { + // Came from all apps -- make a copy + item = ((ApplicationInfo) d.dragInfo).makeShortcut(); + } else { + item = (ShortcutInfo) d.dragInfo; + } + mFolder.notifyDrop(); + onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable, d); + } + + public DropTarget getDropTargetDelegate(DragObject d) { + return null; + } + + private void computePreviewDrawingParams(int drawableSize, int totalSize) { + if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize) { + mIntrinsicIconSize = drawableSize; + mTotalWidth = totalSize; + + final int previewSize = FolderRingAnimator.sPreviewSize; + final int previewPadding = FolderRingAnimator.sPreviewPadding; + + mAvailableSpaceInPreview = (previewSize - 2 * previewPadding); + // cos(45) = 0.707 + ~= 0.1) = 0.8f + int adjustedAvailableSpace = (int) ((mAvailableSpaceInPreview / 2) * (1 + 0.8f)); + + int unscaledHeight = (int) (mIntrinsicIconSize * (1 + PERSPECTIVE_SHIFT_FACTOR)); + mBaselineIconScale = (1.0f * adjustedAvailableSpace / unscaledHeight); + + mBaselineIconSize = (int) (mIntrinsicIconSize * mBaselineIconScale); + mMaxPerspectiveShift = mBaselineIconSize * PERSPECTIVE_SHIFT_FACTOR; + + mPreviewOffsetX = (mTotalWidth - mAvailableSpaceInPreview) / 2; + mPreviewOffsetY = previewPadding; + } + } + + private void computePreviewDrawingParams(Drawable d) { + computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth()); + } + + class PreviewItemDrawingParams { + PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) { + this.transX = transX; + this.transY = transY; + this.scale = scale; + this.overlayAlpha = overlayAlpha; + } + float transX; + float transY; + float scale; + int overlayAlpha; + Drawable drawable; + } + + private float getLocalCenterForIndex(int index, int[] center) { + mParams = computePreviewItemDrawingParams(Math.min(NUM_ITEMS_IN_PREVIEW, index), mParams); + + mParams.transX += mPreviewOffsetX; + mParams.transY += mPreviewOffsetY; + float offsetX = mParams.transX + (mParams.scale * mIntrinsicIconSize) / 2; + float offsetY = mParams.transY + (mParams.scale * mIntrinsicIconSize) / 2; + + center[0] = (int) Math.round(offsetX); + center[1] = (int) Math.round(offsetY); + return mParams.scale; + } + + private PreviewItemDrawingParams computePreviewItemDrawingParams(int index, + PreviewItemDrawingParams params) { + index = NUM_ITEMS_IN_PREVIEW - index - 1; + float r = (index * 1.0f) / (NUM_ITEMS_IN_PREVIEW - 1); + float scale = (1 - PERSPECTIVE_SCALE_FACTOR * (1 - r)); + + float offset = (1 - r) * mMaxPerspectiveShift; + float scaledSize = scale * mBaselineIconSize; + float scaleOffsetCorrection = (1 - scale) * mBaselineIconSize; + + // We want to imagine our coordinates from the bottom left, growing up and to the + // right. This is natural for the x-axis, but for the y-axis, we have to invert things. + float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection); + float transX = offset + scaleOffsetCorrection; + float totalScale = mBaselineIconScale * scale; + final int overlayAlpha = (int) (80 * (1 - r)); + + if (params == null) { + params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha); + } else { + params.transX = transX; + params.transY = transY; + params.scale = totalScale; + params.overlayAlpha = overlayAlpha; + } + return params; + } + + private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) { + canvas.save(); + canvas.translate(params.transX + mPreviewOffsetX, params.transY + mPreviewOffsetY); + canvas.scale(params.scale, params.scale); + Drawable d = params.drawable; + + if (d != null) { + d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize); + d.setFilterBitmap(true); + d.setColorFilter(Color.argb(params.overlayAlpha, 0, 0, 0), PorterDuff.Mode.SRC_ATOP); + d.draw(canvas); + d.clearColorFilter(); + d.setFilterBitmap(false); + } + canvas.restore(); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mFolder == null) return; + if (mFolder.getItemCount() == 0 && !mAnimating) return; + + ArrayList<View> items = mFolder.getItemsInReadingOrder(); + Drawable d; + TextView v; + + // Update our drawing parameters if necessary + if (mAnimating) { + computePreviewDrawingParams(mAnimParams.drawable); + } else { + v = (TextView) items.get(0); + d = v.getCompoundDrawables()[1]; + computePreviewDrawingParams(d); + } + + int nItemsInPreview = Math.min(items.size(), NUM_ITEMS_IN_PREVIEW); + if (!mAnimating) { + for (int i = nItemsInPreview - 1; i >= 0; i--) { + v = (TextView) items.get(i); + if (!mHiddenItems.contains(v.getTag())) { + d = v.getCompoundDrawables()[1]; + mParams = computePreviewItemDrawingParams(i, mParams); + mParams.drawable = d; + drawPreviewItem(canvas, mParams); + } + } + } else { + drawPreviewItem(canvas, mAnimParams); + } + } + + private void animateFirstItem(final Drawable d, int duration, final boolean reverse, + final Runnable onCompleteRunnable) { + final PreviewItemDrawingParams finalParams = computePreviewItemDrawingParams(0, null); + + final float scale0 = 1.0f; + final float transX0 = (mAvailableSpaceInPreview - d.getIntrinsicWidth()) / 2; + final float transY0 = (mAvailableSpaceInPreview - d.getIntrinsicHeight()) / 2; + mAnimParams.drawable = d; + + ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1.0f); + va.addUpdateListener(new AnimatorUpdateListener(){ + public void onAnimationUpdate(ValueAnimator animation) { + float progress = (Float) animation.getAnimatedValue(); + if (reverse) { + progress = 1 - progress; + mPreviewBackground.setAlpha(progress); + } + + mAnimParams.transX = transX0 + progress * (finalParams.transX - transX0); + mAnimParams.transY = transY0 + progress * (finalParams.transY - transY0); + mAnimParams.scale = scale0 + progress * (finalParams.scale - scale0); + invalidate(); + } + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mAnimating = true; + } + @Override + public void onAnimationEnd(Animator animation) { + mAnimating = false; + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + }); + va.setDuration(duration); + va.start(); + } + + public void setTextVisible(boolean visible) { + if (visible) { + mFolderName.setVisibility(VISIBLE); + } else { + mFolderName.setVisibility(INVISIBLE); + } + } + + public boolean getTextVisible() { + return mFolderName.getVisibility() == VISIBLE; + } + + public void onItemsChanged() { + invalidate(); + requestLayout(); + } + + public void onAdd(ShortcutInfo item) { + invalidate(); + requestLayout(); + } + + public void onRemove(ShortcutInfo item) { + invalidate(); + requestLayout(); + } + + public void onTitleChanged(CharSequence title) { + mFolderName.setText(title.toString()); + setContentDescription(String.format(getContext().getString(R.string.folder_name_format), + title)); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Call the superclass onTouchEvent first, because sometimes it changes the state to + // isPressed() on an ACTION_UP + boolean result = super.onTouchEvent(event); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mLongPressHelper.postCheckForLongPress(); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mLongPressHelper.cancelLongPress(); + break; + } + return result; + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + mLongPressHelper.cancelLongPress(); + } +} diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java new file mode 100644 index 000000000..6d45e59ce --- /dev/null +++ b/src/com/android/launcher3/FolderInfo.java @@ -0,0 +1,111 @@ +/* + * 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 java.util.ArrayList; + +import android.content.ContentValues; + +/** + * Represents a folder containing shortcuts or apps. + */ +class FolderInfo extends ItemInfo { + + /** + * Whether this folder has been opened + */ + boolean opened; + + /** + * The apps and shortcuts + */ + ArrayList<ShortcutInfo> contents = new ArrayList<ShortcutInfo>(); + + ArrayList<FolderListener> listeners = new ArrayList<FolderListener>(); + + FolderInfo() { + itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; + } + + /** + * Add an app or shortcut + * + * @param item + */ + public void add(ShortcutInfo item) { + contents.add(item); + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onAdd(item); + } + itemsChanged(); + } + + /** + * Remove an app or shortcut. Does not change the DB. + * + * @param item + */ + public void remove(ShortcutInfo item) { + contents.remove(item); + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onRemove(item); + } + itemsChanged(); + } + + public void setTitle(CharSequence title) { + this.title = title; + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onTitleChanged(title); + } + } + + @Override + void onAddToDatabase(ContentValues values) { + super.onAddToDatabase(values); + values.put(LauncherSettings.Favorites.TITLE, title.toString()); + } + + void addListener(FolderListener listener) { + listeners.add(listener); + } + + void removeListener(FolderListener listener) { + if (listeners.contains(listener)) { + listeners.remove(listener); + } + } + + void itemsChanged() { + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onItemsChanged(); + } + } + + @Override + void unbind() { + super.unbind(); + listeners.clear(); + } + + interface FolderListener { + public void onAdd(ShortcutInfo item); + public void onRemove(ShortcutInfo item); + public void onTitleChanged(CharSequence title); + public void onItemsChanged(); + } +} diff --git a/src/com/android/launcher3/HandleView.java b/src/com/android/launcher3/HandleView.java new file mode 100644 index 000000000..6cb51da31 --- /dev/null +++ b/src/com/android/launcher3/HandleView.java @@ -0,0 +1,76 @@ +/* + * 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.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; + +import com.android.launcher3.R; + +public class HandleView extends ImageView { + private static final int ORIENTATION_HORIZONTAL = 1; + + private Launcher mLauncher; + private int mOrientation = ORIENTATION_HORIZONTAL; + + public HandleView(Context context) { + super(context); + } + + public HandleView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HandleView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HandleView, defStyle, 0); + mOrientation = a.getInt(R.styleable.HandleView_direction, ORIENTATION_HORIZONTAL); + a.recycle(); + + setContentDescription(context.getString(R.string.all_apps_button_label)); + } + + @Override + public View focusSearch(int direction) { + View newFocus = super.focusSearch(direction); + if (newFocus == null && !mLauncher.isAllAppsVisible()) { + final Workspace workspace = mLauncher.getWorkspace(); + workspace.dispatchUnhandledMove(null, direction); + return (mOrientation == ORIENTATION_HORIZONTAL && direction == FOCUS_DOWN) ? + this : workspace; + } + return newFocus; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN && mLauncher.isAllAppsVisible()) { + return false; + } + return super.onTouchEvent(ev); + } + + void setLauncher(Launcher launcher) { + mLauncher = launcher; + } +} diff --git a/src/com/android/launcher3/HideFromAccessibilityHelper.java b/src/com/android/launcher3/HideFromAccessibilityHelper.java new file mode 100644 index 000000000..33adf773c --- /dev/null +++ b/src/com/android/launcher3/HideFromAccessibilityHelper.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2012 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.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.OnHierarchyChangeListener; + +import java.util.HashMap; + +public class HideFromAccessibilityHelper implements OnHierarchyChangeListener { + private HashMap<View, Integer> mPreviousValues; + boolean mHide; + boolean mOnlyAllApps; + + public HideFromAccessibilityHelper() { + mPreviousValues = new HashMap<View, Integer>(); + mHide = false; + } + + public void setImportantForAccessibilityToNo(View v, boolean onlyAllApps) { + mOnlyAllApps = onlyAllApps; + setImportantForAccessibilityToNoHelper(v); + mHide = true; + } + + private void setImportantForAccessibilityToNoHelper(View v) { + mPreviousValues.put(v, v.getImportantForAccessibility()); + v.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + + // Call method on children recursively + if (v instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) v; + vg.setOnHierarchyChangeListener(this); + for (int i = 0; i < vg.getChildCount(); i++) { + View child = vg.getChildAt(i); + + if (includeView(child)) { + setImportantForAccessibilityToNoHelper(child); + } + } + } + } + + public void restoreImportantForAccessibility(View v) { + if (mHide) { + restoreImportantForAccessibilityHelper(v); + } + mHide = false; + } + + private void restoreImportantForAccessibilityHelper(View v) { + v.setImportantForAccessibility(mPreviousValues.get(v)); + mPreviousValues.remove(v); + + // Call method on children recursively + if (v instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) v; + + // We assume if a class implements OnHierarchyChangeListener, it listens + // to changes to any of its children (happens to be the case in Launcher) + if (vg instanceof OnHierarchyChangeListener) { + vg.setOnHierarchyChangeListener((OnHierarchyChangeListener) vg); + } else { + vg.setOnHierarchyChangeListener(null); + } + for (int i = 0; i < vg.getChildCount(); i++) { + View child = vg.getChildAt(i); + if (includeView(child)) { + restoreImportantForAccessibilityHelper(child); + } + } + } + } + + public void onChildViewAdded(View parent, View child) { + if (mHide && includeView(child)) { + setImportantForAccessibilityToNoHelper(child); + } + } + + public void onChildViewRemoved(View parent, View child) { + if (mHide && includeView(child)) { + restoreImportantForAccessibilityHelper(child); + } + } + + private boolean includeView(View v) { + return !hasAncestorOfType(v, Cling.class) && + (!mOnlyAllApps || hasAncestorOfType(v, AppsCustomizeTabHost.class)); + } + + private boolean hasAncestorOfType(View v, Class c) { + return v != null && + (v.getClass().equals(c) || + (v.getParent() instanceof ViewGroup && + hasAncestorOfType((ViewGroup) v.getParent(), c))); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/HolographicImageView.java b/src/com/android/launcher3/HolographicImageView.java new file mode 100644 index 000000000..0ad82a70c --- /dev/null +++ b/src/com/android/launcher3/HolographicImageView.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class HolographicImageView extends ImageView { + + private final HolographicViewHelper mHolographicHelper; + + public HolographicImageView(Context context) { + this(context, null); + } + + public HolographicImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HolographicImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mHolographicHelper = new HolographicViewHelper(context); + } + + void invalidatePressedFocusedStates() { + mHolographicHelper.invalidatePressedFocusedStates(this); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // One time call to generate the pressed/focused state -- must be called after + // measure/layout + mHolographicHelper.generatePressedFocusedStates(this); + } +} diff --git a/src/com/android/launcher3/HolographicLinearLayout.java b/src/com/android/launcher3/HolographicLinearLayout.java new file mode 100644 index 000000000..73d4c3a01 --- /dev/null +++ b/src/com/android/launcher3/HolographicLinearLayout.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.android.launcher3.R; + +public class HolographicLinearLayout extends LinearLayout { + + private final HolographicViewHelper mHolographicHelper; + private ImageView mImageView; + private int mImageViewId; + + public HolographicLinearLayout(Context context) { + this(context, null); + } + + public HolographicLinearLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HolographicLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HolographicLinearLayout, + defStyle, 0); + mImageViewId = a.getResourceId(R.styleable.HolographicLinearLayout_sourceImageViewId, -1); + a.recycle(); + + setWillNotDraw(false); + mHolographicHelper = new HolographicViewHelper(context); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + if (mImageView != null) { + Drawable d = mImageView.getDrawable(); + if (d instanceof StateListDrawable) { + StateListDrawable sld = (StateListDrawable) d; + sld.setState(getDrawableState()); + } + } + } + + void invalidatePressedFocusedStates() { + mHolographicHelper.invalidatePressedFocusedStates(mImageView); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // One time call to generate the pressed/focused state -- must be called after + // measure/layout + if (mImageView == null) { + mImageView = (ImageView) findViewById(mImageViewId); + } + mHolographicHelper.generatePressedFocusedStates(mImageView); + } +} diff --git a/src/com/android/launcher3/HolographicOutlineHelper.java b/src/com/android/launcher3/HolographicOutlineHelper.java new file mode 100644 index 000000000..2decc3d22 --- /dev/null +++ b/src/com/android/launcher3/HolographicOutlineHelper.java @@ -0,0 +1,221 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; + +public class HolographicOutlineHelper { + private final Paint mHolographicPaint = new Paint(); + private final Paint mBlurPaint = new Paint(); + private final Paint mErasePaint = new Paint(); + + public static final int MAX_OUTER_BLUR_RADIUS; + public static final int MIN_OUTER_BLUR_RADIUS; + + private static final BlurMaskFilter sExtraThickOuterBlurMaskFilter; + private static final BlurMaskFilter sThickOuterBlurMaskFilter; + private static final BlurMaskFilter sMediumOuterBlurMaskFilter; + private static final BlurMaskFilter sThinOuterBlurMaskFilter; + private static final BlurMaskFilter sThickInnerBlurMaskFilter; + private static final BlurMaskFilter sExtraThickInnerBlurMaskFilter; + private static final BlurMaskFilter sMediumInnerBlurMaskFilter; + + private static final int THICK = 0; + private static final int MEDIUM = 1; + private static final int EXTRA_THICK = 2; + + static { + final float scale = LauncherApplication.getScreenDensity(); + + MIN_OUTER_BLUR_RADIUS = (int) (scale * 1.0f); + MAX_OUTER_BLUR_RADIUS = (int) (scale * 12.0f); + + sExtraThickOuterBlurMaskFilter = new BlurMaskFilter(scale * 12.0f, BlurMaskFilter.Blur.OUTER); + sThickOuterBlurMaskFilter = new BlurMaskFilter(scale * 6.0f, BlurMaskFilter.Blur.OUTER); + sMediumOuterBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.OUTER); + sThinOuterBlurMaskFilter = new BlurMaskFilter(scale * 1.0f, BlurMaskFilter.Blur.OUTER); + sExtraThickInnerBlurMaskFilter = new BlurMaskFilter(scale * 6.0f, BlurMaskFilter.Blur.NORMAL); + sThickInnerBlurMaskFilter = new BlurMaskFilter(scale * 4.0f, BlurMaskFilter.Blur.NORMAL); + sMediumInnerBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.NORMAL); + } + + HolographicOutlineHelper() { + mHolographicPaint.setFilterBitmap(true); + mHolographicPaint.setAntiAlias(true); + mBlurPaint.setFilterBitmap(true); + mBlurPaint.setAntiAlias(true); + mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + mErasePaint.setFilterBitmap(true); + mErasePaint.setAntiAlias(true); + } + + /** + * Returns the interpolated holographic highlight alpha for the effect we want when scrolling + * pages. + */ + public static float highlightAlphaInterpolator(float r) { + float maxAlpha = 0.6f; + return (float) Math.pow(maxAlpha * (1.0f - r), 1.5f); + } + + /** + * Returns the interpolated view alpha for the effect we want when scrolling pages. + */ + public static float viewAlphaInterpolator(float r) { + final float pivot = 0.95f; + if (r < pivot) { + return (float) Math.pow(r / pivot, 1.5f); + } else { + return 1.0f; + } + } + + /** + * Applies a more expensive and accurate outline to whatever is currently drawn in a specified + * bitmap. + */ + void applyExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor, int thickness) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, true, + thickness); + } + void applyExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor, boolean clipAlpha, int thickness) { + + // We start by removing most of the alpha channel so as to ignore shadows, and + // other types of partial transparency when defining the shape of the object + if (clipAlpha) { + int[] srcBuffer = new int[srcDst.getWidth() * srcDst.getHeight()]; + srcDst.getPixels(srcBuffer, + 0, srcDst.getWidth(), 0, 0, srcDst.getWidth(), srcDst.getHeight()); + for (int i = 0; i < srcBuffer.length; i++) { + final int alpha = srcBuffer[i] >>> 24; + if (alpha < 188) { + srcBuffer[i] = 0; + } + } + srcDst.setPixels(srcBuffer, + 0, srcDst.getWidth(), 0, 0, srcDst.getWidth(), srcDst.getHeight()); + } + Bitmap glowShape = srcDst.extractAlpha(); + + // calculate the outer blur first + BlurMaskFilter outerBlurMaskFilter; + switch (thickness) { + case EXTRA_THICK: + outerBlurMaskFilter = sExtraThickOuterBlurMaskFilter; + break; + case THICK: + outerBlurMaskFilter = sThickOuterBlurMaskFilter; + break; + case MEDIUM: + outerBlurMaskFilter = sMediumOuterBlurMaskFilter; + break; + default: + throw new RuntimeException("Invalid blur thickness"); + } + mBlurPaint.setMaskFilter(outerBlurMaskFilter); + int[] outerBlurOffset = new int[2]; + Bitmap thickOuterBlur = glowShape.extractAlpha(mBlurPaint, outerBlurOffset); + if (thickness == EXTRA_THICK) { + mBlurPaint.setMaskFilter(sMediumOuterBlurMaskFilter); + } else { + mBlurPaint.setMaskFilter(sThinOuterBlurMaskFilter); + } + + int[] brightOutlineOffset = new int[2]; + Bitmap brightOutline = glowShape.extractAlpha(mBlurPaint, brightOutlineOffset); + + // calculate the inner blur + srcDstCanvas.setBitmap(glowShape); + srcDstCanvas.drawColor(0xFF000000, PorterDuff.Mode.SRC_OUT); + BlurMaskFilter innerBlurMaskFilter; + switch (thickness) { + case EXTRA_THICK: + innerBlurMaskFilter = sExtraThickInnerBlurMaskFilter; + break; + case THICK: + innerBlurMaskFilter = sThickInnerBlurMaskFilter; + break; + case MEDIUM: + innerBlurMaskFilter = sMediumInnerBlurMaskFilter; + break; + default: + throw new RuntimeException("Invalid blur thickness"); + } + mBlurPaint.setMaskFilter(innerBlurMaskFilter); + int[] thickInnerBlurOffset = new int[2]; + Bitmap thickInnerBlur = glowShape.extractAlpha(mBlurPaint, thickInnerBlurOffset); + + // mask out the inner blur + srcDstCanvas.setBitmap(thickInnerBlur); + srcDstCanvas.drawBitmap(glowShape, -thickInnerBlurOffset[0], + -thickInnerBlurOffset[1], mErasePaint); + srcDstCanvas.drawRect(0, 0, -thickInnerBlurOffset[0], thickInnerBlur.getHeight(), + mErasePaint); + srcDstCanvas.drawRect(0, 0, thickInnerBlur.getWidth(), -thickInnerBlurOffset[1], + mErasePaint); + + // draw the inner and outer blur + srcDstCanvas.setBitmap(srcDst); + srcDstCanvas.drawColor(0, PorterDuff.Mode.CLEAR); + mHolographicPaint.setColor(color); + srcDstCanvas.drawBitmap(thickInnerBlur, thickInnerBlurOffset[0], thickInnerBlurOffset[1], + mHolographicPaint); + srcDstCanvas.drawBitmap(thickOuterBlur, outerBlurOffset[0], outerBlurOffset[1], + mHolographicPaint); + + // draw the bright outline + mHolographicPaint.setColor(outlineColor); + srcDstCanvas.drawBitmap(brightOutline, brightOutlineOffset[0], brightOutlineOffset[1], + mHolographicPaint); + + // cleanup + srcDstCanvas.setBitmap(null); + brightOutline.recycle(); + thickOuterBlur.recycle(); + thickInnerBlur.recycle(); + glowShape.recycle(); + } + + void applyExtraThickExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, EXTRA_THICK); + } + + void applyThickExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, THICK); + } + + void applyMediumExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor, boolean clipAlpha) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, clipAlpha, + MEDIUM); + } + + void applyMediumExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, MEDIUM); + } + +} diff --git a/src/com/android/launcher3/HolographicViewHelper.java b/src/com/android/launcher3/HolographicViewHelper.java new file mode 100644 index 000000000..9d3ad70a8 --- /dev/null +++ b/src/com/android/launcher3/HolographicViewHelper.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.widget.ImageView; + +public class HolographicViewHelper { + + private final Canvas mTempCanvas = new Canvas(); + + private boolean mStatesUpdated; + private int mHighlightColor; + + public HolographicViewHelper(Context context) { + Resources res = context.getResources(); + mHighlightColor = res.getColor(android.R.color.holo_blue_light); + } + + /** + * Generate the pressed/focused states if necessary. + */ + void generatePressedFocusedStates(ImageView v) { + if (!mStatesUpdated && v != null) { + mStatesUpdated = true; + Bitmap original = createOriginalImage(v, mTempCanvas); + Bitmap outline = createPressImage(v, mTempCanvas); + FastBitmapDrawable originalD = new FastBitmapDrawable(original); + FastBitmapDrawable outlineD = new FastBitmapDrawable(outline); + + StateListDrawable states = new StateListDrawable(); + states.addState(new int[] {android.R.attr.state_pressed}, outlineD); + states.addState(new int[] {android.R.attr.state_focused}, outlineD); + states.addState(new int[] {}, originalD); + v.setImageDrawable(states); + } + } + + /** + * Invalidates the pressed/focused states. + */ + void invalidatePressedFocusedStates(ImageView v) { + mStatesUpdated = false; + if (v != null) { + v.invalidate(); + } + } + + /** + * Creates a copy of the original image. + */ + private Bitmap createOriginalImage(ImageView v, Canvas canvas) { + final Drawable d = v.getDrawable(); + final Bitmap b = Bitmap.createBitmap( + d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + canvas.save(); + d.draw(canvas); + canvas.restore(); + canvas.setBitmap(null); + + return b; + } + + /** + * Creates a new press state image which is the old image with a blue overlay. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createPressImage(ImageView v, Canvas canvas) { + final Drawable d = v.getDrawable(); + final Bitmap b = Bitmap.createBitmap( + d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + canvas.save(); + d.draw(canvas); + canvas.restore(); + canvas.drawColor(mHighlightColor, PorterDuff.Mode.SRC_IN); + canvas.setBitmap(null); + + return b; + } +} diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java new file mode 100644 index 000000000..2844d245e --- /dev/null +++ b/src/com/android/launcher3/Hotseat.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.launcher3.R; + +public class Hotseat extends FrameLayout { + @SuppressWarnings("unused") + private static final String TAG = "Hotseat"; + + private Launcher mLauncher; + private CellLayout mContent; + + private int mCellCountX; + private int mCellCountY; + private int mAllAppsButtonRank; + + private boolean mTransposeLayoutWithOrientation; + private boolean mIsLandscape; + + public Hotseat(Context context) { + this(context, null); + } + + public Hotseat(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public Hotseat(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.Hotseat, defStyle, 0); + Resources r = context.getResources(); + mCellCountX = a.getInt(R.styleable.Hotseat_cellCountX, -1); + mCellCountY = a.getInt(R.styleable.Hotseat_cellCountY, -1); + mAllAppsButtonRank = r.getInteger(R.integer.hotseat_all_apps_index); + mTransposeLayoutWithOrientation = + r.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); + mIsLandscape = context.getResources().getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE; + } + + public void setup(Launcher launcher) { + mLauncher = launcher; + setOnKeyListener(new HotseatIconKeyEventListener()); + } + + CellLayout getLayout() { + return mContent; + } + + private boolean hasVerticalHotseat() { + return (mIsLandscape && mTransposeLayoutWithOrientation); + } + + /* Get the orientation invariant order of the item in the hotseat for persistence. */ + int getOrderInHotseat(int x, int y) { + return hasVerticalHotseat() ? (mContent.getCountY() - y - 1) : x; + } + /* Get the orientation specific coordinates given an invariant order in the hotseat. */ + int getCellXFromOrder(int rank) { + return hasVerticalHotseat() ? 0 : rank; + } + int getCellYFromOrder(int rank) { + return hasVerticalHotseat() ? (mContent.getCountY() - (rank + 1)) : 0; + } + public boolean isAllAppsButtonRank(int rank) { + return rank == mAllAppsButtonRank; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mCellCountX < 0) mCellCountX = LauncherModel.getCellCountX(); + if (mCellCountY < 0) mCellCountY = LauncherModel.getCellCountY(); + mContent = (CellLayout) findViewById(R.id.layout); + mContent.setGridSize(mCellCountX, mCellCountY); + mContent.setIsHotseat(true); + + resetLayout(); + } + + void resetLayout() { + mContent.removeAllViewsInLayout(); + + // Add the Apps button + Context context = getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + BubbleTextView allAppsButton = (BubbleTextView) + inflater.inflate(R.layout.application, mContent, false); + allAppsButton.setCompoundDrawablesWithIntrinsicBounds(null, + context.getResources().getDrawable(R.drawable.all_apps_button_icon), null, null); + allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label)); + allAppsButton.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mLauncher != null && + (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { + mLauncher.onTouchDownAllAppsButton(v); + } + return false; + } + }); + + allAppsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(android.view.View v) { + if (mLauncher != null) { + mLauncher.onClickAllAppsButton(v); + } + } + }); + + // Note: We do this to ensure that the hotseat is always laid out in the orientation of + // the hotseat in order regardless of which orientation they were added + int x = getCellXFromOrder(mAllAppsButtonRank); + int y = getCellYFromOrder(mAllAppsButtonRank); + CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1); + lp.canReorder = false; + mContent.addViewToCellLayout(allAppsButton, -1, 0, lp, true); + } +} diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java new file mode 100644 index 000000000..774f27e1c --- /dev/null +++ b/src/com/android/launcher3/IconCache.java @@ -0,0 +1,229 @@ +/* + * 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.app.ActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; + +import java.util.HashMap; + +/** + * Cache of application icons. Icons can be made from any thread. + */ +public class IconCache { + @SuppressWarnings("unused") + private static final String TAG = "Launcher.IconCache"; + + private static final int INITIAL_ICON_CACHE_CAPACITY = 50; + + private static class CacheEntry { + public Bitmap icon; + public String title; + } + + private final Bitmap mDefaultIcon; + private final LauncherApplication mContext; + private final PackageManager mPackageManager; + private final HashMap<ComponentName, CacheEntry> mCache = + new HashMap<ComponentName, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); + private int mIconDpi; + + public IconCache(LauncherApplication context) { + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + mContext = context; + mPackageManager = context.getPackageManager(); + mIconDpi = activityManager.getLauncherLargeIconDensity(); + + // need to set mIconDpi before getting default icon + mDefaultIcon = makeDefaultIcon(); + } + + public Drawable getFullResDefaultActivityIcon() { + return getFullResIcon(Resources.getSystem(), + android.R.mipmap.sym_def_app_icon); + } + + public Drawable getFullResIcon(Resources resources, int iconId) { + Drawable d; + try { + d = resources.getDrawableForDensity(iconId, mIconDpi); + } catch (Resources.NotFoundException e) { + d = null; + } + + return (d != null) ? d : getFullResDefaultActivityIcon(); + } + + public Drawable getFullResIcon(String packageName, int iconId) { + Resources resources; + try { + resources = mPackageManager.getResourcesForApplication(packageName); + } catch (PackageManager.NameNotFoundException e) { + resources = null; + } + if (resources != null) { + if (iconId != 0) { + return getFullResIcon(resources, iconId); + } + } + return getFullResDefaultActivityIcon(); + } + + public Drawable getFullResIcon(ResolveInfo info) { + return getFullResIcon(info.activityInfo); + } + + public Drawable getFullResIcon(ActivityInfo info) { + + Resources resources; + try { + resources = mPackageManager.getResourcesForApplication( + info.applicationInfo); + } catch (PackageManager.NameNotFoundException e) { + resources = null; + } + if (resources != null) { + int iconId = info.getIconResource(); + if (iconId != 0) { + return getFullResIcon(resources, iconId); + } + } + return getFullResDefaultActivityIcon(); + } + + private Bitmap makeDefaultIcon() { + Drawable d = getFullResDefaultActivityIcon(); + Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1), + Math.max(d.getIntrinsicHeight(), 1), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + d.setBounds(0, 0, b.getWidth(), b.getHeight()); + d.draw(c); + c.setBitmap(null); + return b; + } + + /** + * Remove any records for the supplied ComponentName. + */ + public void remove(ComponentName componentName) { + synchronized (mCache) { + mCache.remove(componentName); + } + } + + /** + * Empty out the cache. + */ + public void flush() { + synchronized (mCache) { + mCache.clear(); + } + } + + /** + * Fill in "application" with the icon and label for "info." + */ + public void getTitleAndIcon(ApplicationInfo application, ResolveInfo info, + HashMap<Object, CharSequence> labelCache) { + synchronized (mCache) { + CacheEntry entry = cacheLocked(application.componentName, info, labelCache); + + application.title = entry.title; + application.iconBitmap = entry.icon; + } + } + + public Bitmap getIcon(Intent intent) { + synchronized (mCache) { + final ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, 0); + ComponentName component = intent.getComponent(); + + if (resolveInfo == null || component == null) { + return mDefaultIcon; + } + + CacheEntry entry = cacheLocked(component, resolveInfo, null); + return entry.icon; + } + } + + public Bitmap getIcon(ComponentName component, ResolveInfo resolveInfo, + HashMap<Object, CharSequence> labelCache) { + synchronized (mCache) { + if (resolveInfo == null || component == null) { + return null; + } + + CacheEntry entry = cacheLocked(component, resolveInfo, labelCache); + return entry.icon; + } + } + + public boolean isDefaultIcon(Bitmap icon) { + return mDefaultIcon == icon; + } + + private CacheEntry cacheLocked(ComponentName componentName, ResolveInfo info, + HashMap<Object, CharSequence> labelCache) { + CacheEntry entry = mCache.get(componentName); + if (entry == null) { + entry = new CacheEntry(); + + mCache.put(componentName, entry); + + ComponentName key = LauncherModel.getComponentNameFromResolveInfo(info); + if (labelCache != null && labelCache.containsKey(key)) { + entry.title = labelCache.get(key).toString(); + } else { + entry.title = info.loadLabel(mPackageManager).toString(); + if (labelCache != null) { + labelCache.put(key, entry.title); + } + } + if (entry.title == null) { + entry.title = info.activityInfo.name; + } + + entry.icon = Utilities.createIconBitmap( + getFullResIcon(info), mContext); + } + return entry; + } + + public HashMap<ComponentName,Bitmap> getAllIcons() { + synchronized (mCache) { + HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>(); + for (ComponentName cn : mCache.keySet()) { + final CacheEntry e = mCache.get(cn); + set.put(cn, e.icon); + } + return set; + } + } +} diff --git a/src/com/android/launcher3/InfoDropTarget.java b/src/com/android/launcher3/InfoDropTarget.java new file mode 100644 index 000000000..9f1b0169c --- /dev/null +++ b/src/com/android/launcher3/InfoDropTarget.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2011 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.content.ComponentName; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.TransitionDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.R; + +public class InfoDropTarget extends ButtonDropTarget { + + private ColorStateList mOriginalTextColor; + private TransitionDrawable mDrawable; + + public InfoDropTarget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public InfoDropTarget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mOriginalTextColor = getTextColors(); + + // Get the hover color + Resources r = getResources(); + mHoverColor = r.getColor(R.color.info_target_hover_tint); + mDrawable = (TransitionDrawable) getCurrentDrawable(); + if (null != mDrawable) { + mDrawable.setCrossFadeEnabled(true); + } + + // Remove the text in the Phone UI in landscape + int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + if (!LauncherApplication.isScreenLarge()) { + setText(""); + } + } + } + + private boolean isFromAllApps(DragSource source) { + return (source instanceof AppsCustomizePagedView); + } + + @Override + public boolean acceptDrop(DragObject d) { + // acceptDrop is called just before onDrop. We do the work here, rather than + // in onDrop, because it allows us to reject the drop (by returning false) + // so that the object being dragged isn't removed from the drag source. + ComponentName componentName = null; + if (d.dragInfo instanceof ApplicationInfo) { + componentName = ((ApplicationInfo) d.dragInfo).componentName; + } else if (d.dragInfo instanceof ShortcutInfo) { + componentName = ((ShortcutInfo) d.dragInfo).intent.getComponent(); + } else if (d.dragInfo instanceof PendingAddItemInfo) { + componentName = ((PendingAddItemInfo) d.dragInfo).componentName; + } + if (componentName != null) { + mLauncher.startApplicationDetailsActivity(componentName); + } + + // There is no post-drop animation, so clean up the DragView now + d.deferDragViewCleanupPostAnimation = false; + return false; + } + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { + boolean isVisible = true; + + // Hide this button unless we are dragging something from AllApps + if (!isFromAllApps(source)) { + isVisible = false; + } + + mActive = isVisible; + mDrawable.resetTransition(); + setTextColor(mOriginalTextColor); + ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); + } + + @Override + public void onDragEnd() { + super.onDragEnd(); + mActive = false; + } + + public void onDragEnter(DragObject d) { + super.onDragEnter(d); + + mDrawable.startTransition(mTransitionDuration); + setTextColor(mHoverColor); + } + + public void onDragExit(DragObject d) { + super.onDragExit(d); + + if (!d.dragComplete) { + mDrawable.resetTransition(); + setTextColor(mOriginalTextColor); + } + } +} diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java new file mode 100644 index 000000000..a0ea93b72 --- /dev/null +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -0,0 +1,363 @@ +/* + * 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Base64; +import android.util.Log; +import android.widget.Toast; + +import com.android.launcher3.R; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.json.*; + +public class InstallShortcutReceiver extends BroadcastReceiver { + public static final String ACTION_INSTALL_SHORTCUT = + "com.android.launcher3.action.INSTALL_SHORTCUT"; + public static final String NEW_APPS_PAGE_KEY = "apps.new.page"; + public static final String NEW_APPS_LIST_KEY = "apps.new.list"; + + public static final String DATA_INTENT_KEY = "intent.data"; + public static final String LAUNCH_INTENT_KEY = "intent.launch"; + public static final String NAME_KEY = "name"; + public static final String ICON_KEY = "icon"; + public static final String ICON_RESOURCE_NAME_KEY = "iconResource"; + public static final String ICON_RESOURCE_PACKAGE_NAME_KEY = "iconResourcePackage"; + // The set of shortcuts that are pending install + public static final String APPS_PENDING_INSTALL = "apps_to_install"; + + public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450; + public static final int NEW_SHORTCUT_STAGGER_DELAY = 75; + + private static final int INSTALL_SHORTCUT_SUCCESSFUL = 0; + private static final int INSTALL_SHORTCUT_IS_DUPLICATE = -1; + private static final int INSTALL_SHORTCUT_NO_SPACE = -2; + + // A mime-type representing shortcut data + public static final String SHORTCUT_MIMETYPE = + "com.android.launcher3/shortcut"; + + private static Object sLock = new Object(); + + private static void addToStringSet(SharedPreferences sharedPrefs, + SharedPreferences.Editor editor, String key, String value) { + Set<String> strings = sharedPrefs.getStringSet(key, null); + if (strings == null) { + strings = new HashSet<String>(0); + } else { + strings = new HashSet<String>(strings); + } + strings.add(value); + editor.putStringSet(key, strings); + } + + private static void addToInstallQueue( + SharedPreferences sharedPrefs, PendingInstallShortcutInfo info) { + synchronized(sLock) { + try { + JSONStringer json = new JSONStringer() + .object() + .key(DATA_INTENT_KEY).value(info.data.toUri(0)) + .key(LAUNCH_INTENT_KEY).value(info.launchIntent.toUri(0)) + .key(NAME_KEY).value(info.name); + if (info.icon != null) { + byte[] iconByteArray = ItemInfo.flattenBitmap(info.icon); + json = json.key(ICON_KEY).value( + Base64.encodeToString( + iconByteArray, 0, iconByteArray.length, Base64.DEFAULT)); + } + if (info.iconResource != null) { + json = json.key(ICON_RESOURCE_NAME_KEY).value(info.iconResource.resourceName); + json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY) + .value(info.iconResource.packageName); + } + json = json.endObject(); + SharedPreferences.Editor editor = sharedPrefs.edit(); + addToStringSet(sharedPrefs, editor, APPS_PENDING_INSTALL, json.toString()); + editor.commit(); + } catch (org.json.JSONException e) { + Log.d("InstallShortcutReceiver", "Exception when adding shortcut: " + e); + } + } + } + + private static ArrayList<PendingInstallShortcutInfo> getAndClearInstallQueue( + SharedPreferences sharedPrefs) { + synchronized(sLock) { + Set<String> strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null); + if (strings == null) { + return new ArrayList<PendingInstallShortcutInfo>(); + } + ArrayList<PendingInstallShortcutInfo> infos = + new ArrayList<PendingInstallShortcutInfo>(); + for (String json : strings) { + try { + JSONObject object = (JSONObject) new JSONTokener(json).nextValue(); + Intent data = Intent.parseUri(object.getString(DATA_INTENT_KEY), 0); + Intent launchIntent = Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0); + String name = object.getString(NAME_KEY); + String iconBase64 = object.optString(ICON_KEY); + String iconResourceName = object.optString(ICON_RESOURCE_NAME_KEY); + String iconResourcePackageName = + object.optString(ICON_RESOURCE_PACKAGE_NAME_KEY); + if (iconBase64 != null && !iconBase64.isEmpty()) { + byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT); + Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length); + data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b); + } else if (iconResourceName != null && !iconResourceName.isEmpty()) { + Intent.ShortcutIconResource iconResource = + new Intent.ShortcutIconResource(); + iconResource.resourceName = iconResourceName; + iconResource.packageName = iconResourcePackageName; + data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); + } + data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent); + PendingInstallShortcutInfo info = + new PendingInstallShortcutInfo(data, name, launchIntent); + infos.add(info); + } catch (org.json.JSONException e) { + Log.d("InstallShortcutReceiver", "Exception reading shortcut to add: " + e); + } catch (java.net.URISyntaxException e) { + Log.d("InstallShortcutReceiver", "Exception reading shortcut to add: " + e); + } + } + sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet<String>()).commit(); + return infos; + } + } + + // Determines whether to defer installing shortcuts immediately until + // processAllPendingInstalls() is called. + private static boolean mUseInstallQueue = false; + + private static class PendingInstallShortcutInfo { + Intent data; + Intent launchIntent; + String name; + Bitmap icon; + Intent.ShortcutIconResource iconResource; + + public PendingInstallShortcutInfo(Intent rawData, String shortcutName, + Intent shortcutIntent) { + data = rawData; + name = shortcutName; + launchIntent = shortcutIntent; + } + } + + public void onReceive(Context context, Intent data) { + if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) { + return; + } + + Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); + if (intent == null) { + return; + } + // This name is only used for comparisons and notifications, so fall back to activity name + // if not supplied + String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); + if (name == null) { + try { + PackageManager pm = context.getPackageManager(); + ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0); + name = info.loadLabel(pm).toString(); + } catch (PackageManager.NameNotFoundException nnfe) { + return; + } + } + Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON); + Intent.ShortcutIconResource iconResource = + data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE); + + // Queue the item up for adding if launcher has not loaded properly yet + boolean launcherNotLoaded = LauncherModel.getCellCountX() <= 0 || + LauncherModel.getCellCountY() <= 0; + + PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, name, intent); + info.icon = icon; + info.iconResource = iconResource; + if (mUseInstallQueue || launcherNotLoaded) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + addToInstallQueue(sp, info); + } else { + processInstallShortcut(context, info); + } + } + + static void enableInstallQueue() { + mUseInstallQueue = true; + } + static void disableAndFlushInstallQueue(Context context) { + mUseInstallQueue = false; + flushInstallQueue(context); + } + static void flushInstallQueue(Context context) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + ArrayList<PendingInstallShortcutInfo> installQueue = getAndClearInstallQueue(sp); + Iterator<PendingInstallShortcutInfo> iter = installQueue.iterator(); + while (iter.hasNext()) { + processInstallShortcut(context, iter.next()); + } + } + + private static void processInstallShortcut(Context context, + PendingInstallShortcutInfo pendingInfo) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + + final Intent data = pendingInfo.data; + final Intent intent = pendingInfo.launchIntent; + final String name = pendingInfo.name; + + // Lock on the app so that we don't try and get the items while apps are being added + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + final int[] result = {INSTALL_SHORTCUT_SUCCESSFUL}; + boolean found = false; + synchronized (app) { + // Flush the LauncherModel worker thread, so that if we just did another + // processInstallShortcut, we give it time for its shortcut to get added to the + // database (getItemsInLocalCoordinates reads the database) + app.getModel().flushWorkerThread(); + final ArrayList<ItemInfo> items = LauncherModel.getItemsInLocalCoordinates(context); + final boolean exists = LauncherModel.shortcutExists(context, name, intent); + + // Try adding to the workspace screens incrementally, starting at the default or center + // screen and alternating between +1, -1, +2, -2, etc. (using ~ ceil(i/2f)*(-1)^(i-1)) + final int screen = Launcher.DEFAULT_SCREEN; + for (int i = 0; i < (2 * Launcher.SCREEN_COUNT) + 1 && !found; ++i) { + int si = screen + (int) ((i / 2f) + 0.5f) * ((i % 2 == 1) ? 1 : -1); + if (0 <= si && si < Launcher.SCREEN_COUNT) { + found = installShortcut(context, data, items, name, intent, si, exists, sp, + result); + } + } + } + + // We only report error messages (duplicate shortcut or out of space) as the add-animation + // will provide feedback otherwise + if (!found) { + if (result[0] == INSTALL_SHORTCUT_NO_SPACE) { + Toast.makeText(context, context.getString(R.string.completely_out_of_space), + Toast.LENGTH_SHORT).show(); + } else if (result[0] == INSTALL_SHORTCUT_IS_DUPLICATE) { + Toast.makeText(context, context.getString(R.string.shortcut_duplicate, name), + Toast.LENGTH_SHORT).show(); + } + } + } + + private static boolean installShortcut(Context context, Intent data, ArrayList<ItemInfo> items, + String name, final Intent intent, final int screen, boolean shortcutExists, + final SharedPreferences sharedPrefs, int[] result) { + int[] tmpCoordinates = new int[2]; + if (findEmptyCell(context, items, tmpCoordinates, screen)) { + if (intent != null) { + if (intent.getAction() == null) { + intent.setAction(Intent.ACTION_VIEW); + } else if (intent.getAction().equals(Intent.ACTION_MAIN) && + intent.getCategories() != null && + intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + } + + // By default, we allow for duplicate entries (located in + // different places) + boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); + if (duplicate || !shortcutExists) { + new Thread("setNewAppsThread") { + public void run() { + synchronized (sLock) { + // If the new app is going to fall into the same page as before, + // then just continue adding to the current page + final int newAppsScreen = sharedPrefs.getInt( + NEW_APPS_PAGE_KEY, screen); + SharedPreferences.Editor editor = sharedPrefs.edit(); + if (newAppsScreen == -1 || newAppsScreen == screen) { + addToStringSet(sharedPrefs, + editor, NEW_APPS_LIST_KEY, intent.toUri(0)); + } + editor.putInt(NEW_APPS_PAGE_KEY, screen); + editor.commit(); + } + } + }.start(); + + // Update the Launcher db + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + ShortcutInfo info = app.getModel().addShortcut(context, data, + LauncherSettings.Favorites.CONTAINER_DESKTOP, screen, + tmpCoordinates[0], tmpCoordinates[1], true); + if (info == null) { + return false; + } + } else { + result[0] = INSTALL_SHORTCUT_IS_DUPLICATE; + } + + return true; + } + } else { + result[0] = INSTALL_SHORTCUT_NO_SPACE; + } + + return false; + } + + private static boolean findEmptyCell(Context context, ArrayList<ItemInfo> items, int[] xy, + int screen) { + final int xCount = LauncherModel.getCellCountX(); + final int yCount = LauncherModel.getCellCountY(); + boolean[][] occupied = new boolean[xCount][yCount]; + + ItemInfo item = null; + int cellX, cellY, spanX, spanY; + for (int i = 0; i < items.size(); ++i) { + item = items.get(i); + if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + if (item.screen == screen) { + cellX = item.cellX; + cellY = item.cellY; + spanX = item.spanX; + spanY = item.spanY; + for (int x = cellX; 0 <= x && x < cellX + spanX && x < xCount; x++) { + for (int y = cellY; 0 <= y && y < cellY + spanY && y < yCount; y++) { + occupied[x][y] = true; + } + } + } + } + } + + return CellLayout.findVacantCell(xy, 1, 1, xCount, yCount, occupied); + } +} diff --git a/src/com/android/launcher3/InstallWidgetReceiver.java b/src/com/android/launcher3/InstallWidgetReceiver.java new file mode 100644 index 000000000..d802df279 --- /dev/null +++ b/src/com/android/launcher3/InstallWidgetReceiver.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2010 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 java.util.List; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ClipData; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import com.android.launcher3.R; + + +/** + * We will likely flesh this out later, to handle allow external apps to place widgets, but for now, + * we just want to expose the action around for checking elsewhere. + */ +public class InstallWidgetReceiver { + public static final String ACTION_INSTALL_WIDGET = + "com.android.launcher3.action.INSTALL_WIDGET"; + public static final String ACTION_SUPPORTS_CLIPDATA_MIMETYPE = + "com.android.launcher3.action.SUPPORTS_CLIPDATA_MIMETYPE"; + + // Currently not exposed. Put into Intent when we want to make it public. + // TEMP: Should we call this "EXTRA_APPWIDGET_PROVIDER"? + public static final String EXTRA_APPWIDGET_COMPONENT = + "com.android.launcher3.extra.widget.COMPONENT"; + public static final String EXTRA_APPWIDGET_CONFIGURATION_DATA_MIME_TYPE = + "com.android.launcher3.extra.widget.CONFIGURATION_DATA_MIME_TYPE"; + public static final String EXTRA_APPWIDGET_CONFIGURATION_DATA = + "com.android.launcher3.extra.widget.CONFIGURATION_DATA"; + + /** + * A simple data class that contains per-item information that the adapter below can reference. + */ + public static class WidgetMimeTypeHandlerData { + public ResolveInfo resolveInfo; + public AppWidgetProviderInfo widgetInfo; + + public WidgetMimeTypeHandlerData(ResolveInfo rInfo, AppWidgetProviderInfo wInfo) { + resolveInfo = rInfo; + widgetInfo = wInfo; + } + } + + /** + * The ListAdapter which presents all the valid widgets that can be created for a given drop. + */ + public static class WidgetListAdapter implements ListAdapter, DialogInterface.OnClickListener { + private LayoutInflater mInflater; + private Launcher mLauncher; + private String mMimeType; + private ClipData mClipData; + private List<WidgetMimeTypeHandlerData> mActivities; + private CellLayout mTargetLayout; + private int mTargetLayoutScreen; + private int[] mTargetLayoutPos; + + public WidgetListAdapter(Launcher l, String mimeType, ClipData data, + List<WidgetMimeTypeHandlerData> list, CellLayout target, + int targetScreen, int[] targetPos) { + mLauncher = l; + mMimeType = mimeType; + mClipData = data; + mActivities = list; + mTargetLayout = target; + mTargetLayoutScreen = targetScreen; + mTargetLayoutPos = targetPos; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + } + + @Override + public int getCount() { + return mActivities.size(); + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Context context = parent.getContext(); + final PackageManager packageManager = context.getPackageManager(); + + // Lazy-create inflater + if (mInflater == null) { + mInflater = LayoutInflater.from(context); + } + + // Use the convert-view where possible + if (convertView == null) { + convertView = mInflater.inflate(R.layout.external_widget_drop_list_item, parent, + false); + } + + final WidgetMimeTypeHandlerData data = mActivities.get(position); + final ResolveInfo resolveInfo = data.resolveInfo; + final AppWidgetProviderInfo widgetInfo = data.widgetInfo; + + // Set the icon + Drawable d = resolveInfo.loadIcon(packageManager); + ImageView i = (ImageView) convertView.findViewById(R.id.provider_icon); + i.setImageDrawable(d); + + // Set the text + final CharSequence component = resolveInfo.loadLabel(packageManager); + final int[] widgetSpan = new int[2]; + mTargetLayout.rectToCell(widgetInfo.minWidth, widgetInfo.minHeight, widgetSpan); + TextView t = (TextView) convertView.findViewById(R.id.provider); + t.setText(context.getString(R.string.external_drop_widget_pick_format, + component, widgetSpan[0], widgetSpan[1])); + + return convertView; + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return mActivities.isEmpty(); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final AppWidgetProviderInfo widgetInfo = mActivities.get(which).widgetInfo; + + final PendingAddWidgetInfo createInfo = new PendingAddWidgetInfo(widgetInfo, mMimeType, + mClipData); + mLauncher.addAppWidgetFromDrop(createInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, + mTargetLayoutScreen, null, null, mTargetLayoutPos); + } + } +} diff --git a/src/com/android/launcher3/InterruptibleInOutAnimator.java b/src/com/android/launcher3/InterruptibleInOutAnimator.java new file mode 100644 index 000000000..2898b347d --- /dev/null +++ b/src/com/android/launcher3/InterruptibleInOutAnimator.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2010 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.ValueAnimator; +import android.view.View; + +/** + * A convenience class for two-way animations, e.g. a fadeIn/fadeOut animation. + * With a regular ValueAnimator, if you call reverse to show the 'out' animation, you'll get + * a frame-by-frame mirror of the 'in' animation -- i.e., the interpolated values will + * be exactly reversed. Using this class, both the 'in' and the 'out' animation use the + * interpolator in the same direction. + */ +public class InterruptibleInOutAnimator { + private long mOriginalDuration; + private float mOriginalFromValue; + private float mOriginalToValue; + private ValueAnimator mAnimator; + + private boolean mFirstRun = true; + + private Object mTag = null; + + private static final int STOPPED = 0; + private static final int IN = 1; + private static final int OUT = 2; + + // TODO: This isn't really necessary, but is here to help diagnose a bug in the drag viz + private int mDirection = STOPPED; + + public InterruptibleInOutAnimator(View view, long duration, float fromValue, float toValue) { + mAnimator = LauncherAnimUtils.ofFloat(view, fromValue, toValue).setDuration(duration); + mOriginalDuration = duration; + mOriginalFromValue = fromValue; + mOriginalToValue = toValue; + + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDirection = STOPPED; + } + }); + } + + private void animate(int direction) { + final long currentPlayTime = mAnimator.getCurrentPlayTime(); + final float toValue = (direction == IN) ? mOriginalToValue : mOriginalFromValue; + final float startValue = mFirstRun ? mOriginalFromValue : + ((Float) mAnimator.getAnimatedValue()).floatValue(); + + // Make sure it's stopped before we modify any values + cancel(); + + // TODO: We don't really need to do the animation if startValue == toValue, but + // somehow that doesn't seem to work, possibly a quirk of the animation framework + mDirection = direction; + + // Ensure we don't calculate a non-sensical duration + long duration = mOriginalDuration - currentPlayTime; + mAnimator.setDuration(Math.max(0, Math.min(duration, mOriginalDuration))); + + mAnimator.setFloatValues(startValue, toValue); + mAnimator.start(); + mFirstRun = false; + } + + public void cancel() { + mAnimator.cancel(); + mDirection = STOPPED; + } + + public void end() { + mAnimator.end(); + mDirection = STOPPED; + } + + /** + * Return true when the animation is not running and it hasn't even been started. + */ + public boolean isStopped() { + return mDirection == STOPPED; + } + + /** + * This is the equivalent of calling Animator.start(), except that it can be called when + * the animation is running in the opposite direction, in which case we reverse + * direction and animate for a correspondingly shorter duration. + */ + public void animateIn() { + animate(IN); + } + + /** + * This is the roughly the equivalent of calling Animator.reverse(), except that it uses the + * same interpolation curve as animateIn(), rather than mirroring it. Also, like animateIn(), + * if the animation is currently running in the opposite direction, we reverse + * direction and animate for a correspondingly shorter duration. + */ + public void animateOut() { + animate(OUT); + } + + public void setTag(Object tag) { + mTag = tag; + } + + public Object getTag() { + return mTag; + } + + public ValueAnimator getAnimator() { + return mAnimator; + } +} diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java new file mode 100644 index 000000000..fb4183423 --- /dev/null +++ b/src/com/android/launcher3/ItemInfo.java @@ -0,0 +1,194 @@ +/* + * 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.content.ContentValues; +import android.content.Intent; +import android.graphics.Bitmap; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Represents an item in the launcher. + */ +class ItemInfo { + + static final int NO_ID = -1; + + /** + * The id in the settings database for this item + */ + long id = NO_ID; + + /** + * One of {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION}, + * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT}, + * {@link LauncherSettings.Favorites#ITEM_TYPE_FOLDER}, or + * {@link LauncherSettings.Favorites#ITEM_TYPE_APPWIDGET}. + */ + int itemType; + + /** + * The id of the container that holds this item. For the desktop, this will be + * {@link LauncherSettings.Favorites#CONTAINER_DESKTOP}. For the all applications folder it + * will be {@link #NO_ID} (since it is not stored in the settings DB). For user folders + * it will be the id of the folder. + */ + long container = NO_ID; + + /** + * Iindicates the screen in which the shortcut appears. + */ + int screen = -1; + + /** + * Indicates the X position of the associated cell. + */ + int cellX = -1; + + /** + * Indicates the Y position of the associated cell. + */ + int cellY = -1; + + /** + * Indicates the X cell span. + */ + int spanX = 1; + + /** + * Indicates the Y cell span. + */ + int spanY = 1; + + /** + * Indicates the minimum X cell span. + */ + int minSpanX = 1; + + /** + * Indicates the minimum Y cell span. + */ + int minSpanY = 1; + + /** + * Indicates that this item needs to be updated in the db + */ + boolean requiresDbUpdate = false; + + /** + * Title of the item + */ + CharSequence title; + + /** + * The position of the item in a drag-and-drop operation. + */ + int[] dropPos = null; + + ItemInfo() { + } + + ItemInfo(ItemInfo info) { + id = info.id; + cellX = info.cellX; + cellY = info.cellY; + spanX = info.spanX; + spanY = info.spanY; + screen = info.screen; + itemType = info.itemType; + container = info.container; + // tempdebug: + LauncherModel.checkItemInfo(this); + } + + /** Returns the package name that the intent will resolve to, or an empty string if + * none exists. */ + static String getPackageName(Intent intent) { + if (intent != null) { + String packageName = intent.getPackage(); + if (packageName == null && intent.getComponent() != null) { + packageName = intent.getComponent().getPackageName(); + } + if (packageName != null) { + return packageName; + } + } + return ""; + } + + /** + * Write the fields of this item to the DB + * + * @param values + */ + void onAddToDatabase(ContentValues values) { + values.put(LauncherSettings.BaseLauncherColumns.ITEM_TYPE, itemType); + values.put(LauncherSettings.Favorites.CONTAINER, container); + values.put(LauncherSettings.Favorites.SCREEN, screen); + values.put(LauncherSettings.Favorites.CELLX, cellX); + values.put(LauncherSettings.Favorites.CELLY, cellY); + values.put(LauncherSettings.Favorites.SPANX, spanX); + values.put(LauncherSettings.Favorites.SPANY, spanY); + } + + void updateValuesWithCoordinates(ContentValues values, int cellX, int cellY) { + values.put(LauncherSettings.Favorites.CELLX, cellX); + values.put(LauncherSettings.Favorites.CELLY, cellY); + } + + static byte[] flattenBitmap(Bitmap bitmap) { + // Try go guesstimate how much space the icon will take when serialized + // to avoid unnecessary allocations/copies during the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w("Favorite", "Could not write icon"); + return null; + } + } + + static void writeBitmap(ContentValues values, Bitmap bitmap) { + if (bitmap != null) { + byte[] data = flattenBitmap(bitmap); + values.put(LauncherSettings.Favorites.ICON, data); + } + } + + /** + * It is very important that sub-classes implement this if they contain any references + * to the activity (anything in the view hierarchy etc.). If not, leaks can result since + * ItemInfo objects persist across rotation and can hence leak by holding stale references + * to the old view hierarchy / activity. + */ + void unbind() { + } + + @Override + public String toString() { + return "Item(id=" + this.id + " type=" + this.itemType + " container=" + this.container + + " screen=" + screen + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + + " spanY=" + spanY + " dropPos=" + dropPos + ")"; + } +} diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java new file mode 100644 index 000000000..3f487cd21 --- /dev/null +++ b/src/com/android/launcher3/Launcher.java @@ -0,0 +1,4070 @@ + +/* + * 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.accounts.Account; +import android.accounts.AccountManager; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.SearchManager; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.ComponentCallbacks2; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.os.StrictMode; +import android.os.SystemClock; +import android.provider.Settings; +import android.speech.RecognizerIntent; +import android.text.Selection; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.method.TextKeyListener; +import android.util.Log; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.inputmethod.InputMethodManager; +import android.widget.Advanceable; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.common.Search; +import com.android.launcher3.R; +import com.android.launcher3.DropTarget.DragObject; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Default launcher application. + */ +public final class Launcher extends Activity + implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, + View.OnTouchListener { + static final String TAG = "Launcher"; + static final boolean LOGD = false; + + static final boolean PROFILE_STARTUP = false; + static final boolean DEBUG_WIDGETS = false; + static final boolean DEBUG_STRICT_MODE = false; + static final boolean DEBUG_RESUME_TIME = false; + + private static final int MENU_GROUP_WALLPAPER = 1; + private static final int MENU_WALLPAPER_SETTINGS = Menu.FIRST + 1; + private static final int MENU_MANAGE_APPS = MENU_WALLPAPER_SETTINGS + 1; + private static final int MENU_SYSTEM_SETTINGS = MENU_MANAGE_APPS + 1; + private static final int MENU_HELP = MENU_SYSTEM_SETTINGS + 1; + + private static final int REQUEST_CREATE_SHORTCUT = 1; + private static final int REQUEST_CREATE_APPWIDGET = 5; + private static final int REQUEST_PICK_APPLICATION = 6; + private static final int REQUEST_PICK_SHORTCUT = 7; + private static final int REQUEST_PICK_APPWIDGET = 9; + private static final int REQUEST_PICK_WALLPAPER = 10; + + private static final int REQUEST_BIND_APPWIDGET = 11; + + static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate"; + + static final int SCREEN_COUNT = 5; + static final int DEFAULT_SCREEN = 2; + + private static final String PREFERENCES = "launcher.preferences"; + // To turn on these properties, type + // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] + static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; + static final String DUMP_STATE_PROPERTY = "launcher_dump_state"; + + // The Intent extra that defines whether to ignore the launch animation + static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = + "com.android.launcher3.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION"; + + // Type: int + private static final String RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen"; + // Type: int + private static final String RUNTIME_STATE = "launcher.state"; + // Type: int + private static final String RUNTIME_STATE_PENDING_ADD_CONTAINER = "launcher.add_container"; + // Type: int + private static final String RUNTIME_STATE_PENDING_ADD_SCREEN = "launcher.add_screen"; + // Type: int + private static final String RUNTIME_STATE_PENDING_ADD_CELL_X = "launcher.add_cell_x"; + // Type: int + private static final String RUNTIME_STATE_PENDING_ADD_CELL_Y = "launcher.add_cell_y"; + // Type: boolean + private static final String RUNTIME_STATE_PENDING_FOLDER_RENAME = "launcher.rename_folder"; + // Type: long + private static final String RUNTIME_STATE_PENDING_FOLDER_RENAME_ID = "launcher.rename_folder_id"; + // Type: int + private static final String RUNTIME_STATE_PENDING_ADD_SPAN_X = "launcher.add_span_x"; + // Type: int + private static final String RUNTIME_STATE_PENDING_ADD_SPAN_Y = "launcher.add_span_y"; + // Type: parcelable + private static final String RUNTIME_STATE_PENDING_ADD_WIDGET_INFO = "launcher.add_widget_info"; + + private static final String TOOLBAR_ICON_METADATA_NAME = "com.android.launcher3.toolbar_icon"; + private static final String TOOLBAR_SEARCH_ICON_METADATA_NAME = + "com.android.launcher3.toolbar_search_icon"; + private static final String TOOLBAR_VOICE_SEARCH_ICON_METADATA_NAME = + "com.android.launcher3.toolbar_voice_search_icon"; + + /** The different states that Launcher can be in. */ + private enum State { NONE, WORKSPACE, APPS_CUSTOMIZE, APPS_CUSTOMIZE_SPRING_LOADED }; + private State mState = State.WORKSPACE; + private AnimatorSet mStateAnimation; + private AnimatorSet mDividerAnimator; + + static final int APPWIDGET_HOST_ID = 1024; + private static final int EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT = 300; + private static final int EXIT_SPRINGLOADED_MODE_LONG_TIMEOUT = 600; + private static final int SHOW_CLING_DURATION = 550; + private static final int DISMISS_CLING_DURATION = 250; + + private static final Object sLock = new Object(); + private static int sScreen = DEFAULT_SCREEN; + + // How long to wait before the new-shortcut animation automatically pans the workspace + private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 10; + + private final BroadcastReceiver mCloseSystemDialogsReceiver + = new CloseSystemDialogsIntentReceiver(); + private final ContentObserver mWidgetObserver = new AppWidgetResetObserver(); + + private LayoutInflater mInflater; + + private Workspace mWorkspace; + private View mQsbDivider; + private View mDockDivider; + private View mLauncherView; + private DragLayer mDragLayer; + private DragController mDragController; + + private AppWidgetManager mAppWidgetManager; + private LauncherAppWidgetHost mAppWidgetHost; + + private ItemInfo mPendingAddInfo = new ItemInfo(); + private AppWidgetProviderInfo mPendingAddWidgetInfo; + + private int[] mTmpAddItemCellCoordinates = new int[2]; + + private FolderInfo mFolderInfo; + + private Hotseat mHotseat; + private View mAllAppsButton; + + private SearchDropTargetBar mSearchDropTargetBar; + private AppsCustomizeTabHost mAppsCustomizeTabHost; + private AppsCustomizePagedView mAppsCustomizeContent; + private boolean mAutoAdvanceRunning = false; + + private Bundle mSavedState; + // We set the state in both onCreate and then onNewIntent in some cases, which causes both + // scroll issues (because the workspace may not have been measured yet) and extra work. + // Instead, just save the state that we need to restore Launcher to, and commit it in onResume. + private State mOnResumeState = State.NONE; + + private SpannableStringBuilder mDefaultKeySsb = null; + + private boolean mWorkspaceLoading = true; + + private boolean mPaused = true; + private boolean mRestoring; + private boolean mWaitingForResult; + private boolean mOnResumeNeedsLoad; + + private ArrayList<Runnable> mOnResumeCallbacks = new ArrayList<Runnable>(); + + // Keep track of whether the user has left launcher + private static boolean sPausedFromUserAction = false; + + private Bundle mSavedInstanceState; + + private LauncherModel mModel; + private IconCache mIconCache; + private boolean mUserPresent = true; + private boolean mVisible = false; + private boolean mAttached = false; + + private static LocaleConfiguration sLocaleConfiguration = null; + + private static HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>(); + + private Intent mAppMarketIntent = null; + + // Related to the auto-advancing of widgets + private final int ADVANCE_MSG = 1; + private final int mAdvanceInterval = 20000; + private final int mAdvanceStagger = 250; + private long mAutoAdvanceSentTime; + private long mAutoAdvanceTimeLeft = -1; + private HashMap<View, AppWidgetProviderInfo> mWidgetsToAdvance = + new HashMap<View, AppWidgetProviderInfo>(); + + // Determines how long to wait after a rotation before restoring the screen orientation to + // match the sensor state. + private final int mRestoreScreenOrientationDelay = 500; + + // External icons saved in case of resource changes, orientation, etc. + private static Drawable.ConstantState[] sGlobalSearchIcon = new Drawable.ConstantState[2]; + private static Drawable.ConstantState[] sVoiceSearchIcon = new Drawable.ConstantState[2]; + private static Drawable.ConstantState[] sAppMarketIcon = new Drawable.ConstantState[2]; + + private Drawable mWorkspaceBackgroundDrawable; + + private final ArrayList<Integer> mSynchronouslyBoundPages = new ArrayList<Integer>(); + + static final ArrayList<String> sDumpLogs = new ArrayList<String>(); + + // We only want to get the SharedPreferences once since it does an FS stat each time we get + // it from the context. + private SharedPreferences mSharedPrefs; + + // Holds the page that we need to animate to, and the icon views that we need to animate up + // when we scroll to that page on resume. + private int mNewShortcutAnimatePage = -1; + private ArrayList<View> mNewShortcutAnimateViews = new ArrayList<View>(); + private ImageView mFolderIconImageView; + private Bitmap mFolderIconBitmap; + private Canvas mFolderIconCanvas; + private Rect mRectForFolderAnimation = new Rect(); + + private BubbleTextView mWaitingForResume; + + private HideFromAccessibilityHelper mHideFromAccessibilityHelper + = new HideFromAccessibilityHelper(); + + private Runnable mBuildLayersRunnable = new Runnable() { + public void run() { + if (mWorkspace != null) { + mWorkspace.buildPageHardwareLayers(); + } + } + }; + + private static ArrayList<PendingAddArguments> sPendingAddList + = new ArrayList<PendingAddArguments>(); + + private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); + + private static class PendingAddArguments { + int requestCode; + Intent intent; + long container; + int screen; + int cellX; + int cellY; + } + + private static boolean isPropertyEnabled(String propertyName) { + return Log.isLoggable(propertyName, Log.VERBOSE); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (DEBUG_STRICT_MODE) { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() // or .detectAll() for all detectable problems + .penaltyLog() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .penaltyDeath() + .build()); + } + + super.onCreate(savedInstanceState); + LauncherApplication app = ((LauncherApplication)getApplication()); + mSharedPrefs = getSharedPreferences(LauncherApplication.getSharedPreferencesKey(), + Context.MODE_PRIVATE); + mModel = app.setLauncher(this); + mIconCache = app.getIconCache(); + mDragController = new DragController(this); + mInflater = getLayoutInflater(); + + mAppWidgetManager = AppWidgetManager.getInstance(this); + mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID); + mAppWidgetHost.startListening(); + + // If we are getting an onCreate, we can actually preempt onResume and unset mPaused here, + // this also ensures that any synchronous binding below doesn't re-trigger another + // LauncherModel load. + mPaused = false; + + if (PROFILE_STARTUP) { + android.os.Debug.startMethodTracing( + Environment.getExternalStorageDirectory() + "/launcher"); + } + + checkForLocaleChange(); + setContentView(R.layout.launcher); + setupViews(); + showFirstRunWorkspaceCling(); + + registerContentObservers(); + + lockAllApps(); + + mSavedState = savedInstanceState; + restoreState(mSavedState); + + // Update customization drawer _after_ restoring the states + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.onPackagesUpdated( + LauncherModel.getSortedWidgetsAndShortcuts(this)); + } + + if (PROFILE_STARTUP) { + android.os.Debug.stopMethodTracing(); + } + + if (!mRestoring) { + if (sPausedFromUserAction) { + // If the user leaves launcher, then we should just load items asynchronously when + // they return. + mModel.startLoader(true, -1); + } else { + // We only load the page synchronously if the user rotates (or triggers a + // configuration change) while launcher is in the foreground + mModel.startLoader(true, mWorkspace.getCurrentPage()); + } + } + + if (!mModel.isAllAppsLoaded()) { + ViewGroup appsCustomizeContentParent = (ViewGroup) mAppsCustomizeContent.getParent(); + mInflater.inflate(R.layout.apps_customize_progressbar, appsCustomizeContentParent); + } + + // For handling default keys + mDefaultKeySsb = new SpannableStringBuilder(); + Selection.setSelection(mDefaultKeySsb, 0); + + IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + registerReceiver(mCloseSystemDialogsReceiver, filter); + + updateGlobalIcons(); + + // On large interfaces, we want the screen to auto-rotate based on the current orientation + unlockScreenOrientation(true); + } + + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + sPausedFromUserAction = true; + } + + private void updateGlobalIcons() { + boolean searchVisible = false; + boolean voiceVisible = false; + // If we have a saved version of these external icons, we load them up immediately + int coi = getCurrentOrientationIndexForGlobalIcons(); + if (sGlobalSearchIcon[coi] == null || sVoiceSearchIcon[coi] == null || + sAppMarketIcon[coi] == null) { + updateAppMarketIcon(); + searchVisible = updateGlobalSearchIcon(); + voiceVisible = updateVoiceSearchIcon(searchVisible); + } + if (sGlobalSearchIcon[coi] != null) { + updateGlobalSearchIcon(sGlobalSearchIcon[coi]); + searchVisible = true; + } + if (sVoiceSearchIcon[coi] != null) { + updateVoiceSearchIcon(sVoiceSearchIcon[coi]); + voiceVisible = true; + } + if (sAppMarketIcon[coi] != null) { + updateAppMarketIcon(sAppMarketIcon[coi]); + } + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.onSearchPackagesChanged(searchVisible, voiceVisible); + } + } + + private void checkForLocaleChange() { + if (sLocaleConfiguration == null) { + new AsyncTask<Void, Void, LocaleConfiguration>() { + @Override + protected LocaleConfiguration doInBackground(Void... unused) { + LocaleConfiguration localeConfiguration = new LocaleConfiguration(); + readConfiguration(Launcher.this, localeConfiguration); + return localeConfiguration; + } + + @Override + protected void onPostExecute(LocaleConfiguration result) { + sLocaleConfiguration = result; + checkForLocaleChange(); // recursive, but now with a locale configuration + } + }.execute(); + return; + } + + final Configuration configuration = getResources().getConfiguration(); + + final String previousLocale = sLocaleConfiguration.locale; + final String locale = configuration.locale.toString(); + + final int previousMcc = sLocaleConfiguration.mcc; + final int mcc = configuration.mcc; + + final int previousMnc = sLocaleConfiguration.mnc; + final int mnc = configuration.mnc; + + boolean localeChanged = !locale.equals(previousLocale) || mcc != previousMcc || mnc != previousMnc; + + if (localeChanged) { + sLocaleConfiguration.locale = locale; + sLocaleConfiguration.mcc = mcc; + sLocaleConfiguration.mnc = mnc; + + mIconCache.flush(); + + final LocaleConfiguration localeConfiguration = sLocaleConfiguration; + new Thread("WriteLocaleConfiguration") { + @Override + public void run() { + writeConfiguration(Launcher.this, localeConfiguration); + } + }.start(); + } + } + + private static class LocaleConfiguration { + public String locale; + public int mcc = -1; + public int mnc = -1; + } + + private static void readConfiguration(Context context, LocaleConfiguration configuration) { + DataInputStream in = null; + try { + in = new DataInputStream(context.openFileInput(PREFERENCES)); + configuration.locale = in.readUTF(); + configuration.mcc = in.readInt(); + configuration.mnc = in.readInt(); + } catch (FileNotFoundException e) { + // Ignore + } catch (IOException e) { + // Ignore + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + + private static void writeConfiguration(Context context, LocaleConfiguration configuration) { + DataOutputStream out = null; + try { + out = new DataOutputStream(context.openFileOutput(PREFERENCES, MODE_PRIVATE)); + out.writeUTF(configuration.locale); + out.writeInt(configuration.mcc); + out.writeInt(configuration.mnc); + out.flush(); + } catch (FileNotFoundException e) { + // Ignore + } catch (IOException e) { + //noinspection ResultOfMethodCallIgnored + context.getFileStreamPath(PREFERENCES).delete(); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + + public DragLayer getDragLayer() { + return mDragLayer; + } + + boolean isDraggingEnabled() { + // We prevent dragging when we are loading the workspace as it is possible to pick up a view + // that is subsequently removed from the workspace in startBinding(). + return !mModel.isLoadingWorkspace(); + } + + static int getScreen() { + synchronized (sLock) { + return sScreen; + } + } + + static void setScreen(int screen) { + synchronized (sLock) { + sScreen = screen; + } + } + + /** + * Returns whether we should delay spring loaded mode -- for shortcuts and widgets that have + * a configuration step, this allows the proper animations to run after other transitions. + */ + private boolean completeAdd(PendingAddArguments args) { + boolean result = false; + switch (args.requestCode) { + case REQUEST_PICK_APPLICATION: + completeAddApplication(args.intent, args.container, args.screen, args.cellX, + args.cellY); + break; + case REQUEST_PICK_SHORTCUT: + processShortcut(args.intent); + break; + case REQUEST_CREATE_SHORTCUT: + completeAddShortcut(args.intent, args.container, args.screen, args.cellX, + args.cellY); + result = true; + break; + case REQUEST_CREATE_APPWIDGET: + int appWidgetId = args.intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + completeAddAppWidget(appWidgetId, args.container, args.screen, null, null); + result = true; + break; + case REQUEST_PICK_WALLPAPER: + // We just wanted the activity result here so we can clear mWaitingForResult + break; + } + // Before adding this resetAddInfo(), after a shortcut was added to a workspace screen, + // if you turned the screen off and then back while in All Apps, Launcher would not + // return to the workspace. Clearing mAddInfo.container here fixes this issue + resetAddInfo(); + return result; + } + + @Override + protected void onActivityResult( + final int requestCode, final int resultCode, final Intent data) { + if (requestCode == REQUEST_BIND_APPWIDGET) { + int appWidgetId = data != null ? + data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1; + if (resultCode == RESULT_CANCELED) { + completeTwoStageWidgetDrop(RESULT_CANCELED, appWidgetId); + } else if (resultCode == RESULT_OK) { + addAppWidgetImpl(appWidgetId, mPendingAddInfo, null, mPendingAddWidgetInfo); + } + return; + } + boolean delayExitSpringLoadedMode = false; + boolean isWidgetDrop = (requestCode == REQUEST_PICK_APPWIDGET || + requestCode == REQUEST_CREATE_APPWIDGET); + mWaitingForResult = false; + + // We have special handling for widgets + if (isWidgetDrop) { + int appWidgetId = data != null ? + data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1; + if (appWidgetId < 0) { + Log.e(TAG, "Error: appWidgetId (EXTRA_APPWIDGET_ID) was not returned from the \\" + + "widget configuration activity."); + completeTwoStageWidgetDrop(RESULT_CANCELED, appWidgetId); + } else { + completeTwoStageWidgetDrop(resultCode, appWidgetId); + } + return; + } + + // The pattern used here is that a user PICKs a specific application, + // which, depending on the target, might need to CREATE the actual target. + + // For example, the user would PICK_SHORTCUT for "Music playlist", and we + // launch over to the Music app to actually CREATE_SHORTCUT. + if (resultCode == RESULT_OK && mPendingAddInfo.container != ItemInfo.NO_ID) { + final PendingAddArguments args = new PendingAddArguments(); + args.requestCode = requestCode; + args.intent = data; + args.container = mPendingAddInfo.container; + args.screen = mPendingAddInfo.screen; + args.cellX = mPendingAddInfo.cellX; + args.cellY = mPendingAddInfo.cellY; + if (isWorkspaceLocked()) { + sPendingAddList.add(args); + } else { + delayExitSpringLoadedMode = completeAdd(args); + } + } + mDragLayer.clearAnimatedView(); + // Exit spring loaded mode if necessary after cancelling the configuration of a widget + exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), delayExitSpringLoadedMode, + null); + } + + private void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { + CellLayout cellLayout = + (CellLayout) mWorkspace.getChildAt(mPendingAddInfo.screen); + Runnable onCompleteRunnable = null; + int animationType = 0; + + AppWidgetHostView boundWidget = null; + if (resultCode == RESULT_OK) { + animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION; + final AppWidgetHostView layout = mAppWidgetHost.createView(this, appWidgetId, + mPendingAddWidgetInfo); + boundWidget = layout; + onCompleteRunnable = new Runnable() { + @Override + public void run() { + completeAddAppWidget(appWidgetId, mPendingAddInfo.container, + mPendingAddInfo.screen, layout, null); + exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), false, + null); + } + }; + } else if (resultCode == RESULT_CANCELED) { + animationType = Workspace.CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION; + onCompleteRunnable = new Runnable() { + @Override + public void run() { + exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), false, + null); + } + }; + } + if (mDragLayer.getAnimatedView() != null) { + mWorkspace.animateWidgetDrop(mPendingAddInfo, cellLayout, + (DragView) mDragLayer.getAnimatedView(), onCompleteRunnable, + animationType, boundWidget, true); + } else { + // The animated view may be null in the case of a rotation during widget configuration + onCompleteRunnable.run(); + } + } + + @Override + protected void onStop() { + super.onStop(); + FirstFrameAnimatorHelper.setIsVisible(false); + } + + @Override + protected void onStart() { + super.onStart(); + FirstFrameAnimatorHelper.setIsVisible(true); + } + + @Override + protected void onResume() { + long startTime = 0; + if (DEBUG_RESUME_TIME) { + startTime = System.currentTimeMillis(); + } + super.onResume(); + + // Restore the previous launcher state + if (mOnResumeState == State.WORKSPACE) { + showWorkspace(false); + } else if (mOnResumeState == State.APPS_CUSTOMIZE) { + showAllApps(false); + } + mOnResumeState = State.NONE; + + // Background was set to gradient in onPause(), restore to black if in all apps. + setWorkspaceBackground(mState == State.WORKSPACE); + + // Process any items that were added while Launcher was away + InstallShortcutReceiver.flushInstallQueue(this); + + mPaused = false; + sPausedFromUserAction = false; + if (mRestoring || mOnResumeNeedsLoad) { + mWorkspaceLoading = true; + mModel.startLoader(true, -1); + mRestoring = false; + mOnResumeNeedsLoad = false; + } + if (mOnResumeCallbacks.size() > 0) { + // We might have postponed some bind calls until onResume (see waitUntilResume) -- + // execute them here + long startTimeCallbacks = 0; + if (DEBUG_RESUME_TIME) { + startTimeCallbacks = System.currentTimeMillis(); + } + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.setBulkBind(true); + } + for (int i = 0; i < mOnResumeCallbacks.size(); i++) { + mOnResumeCallbacks.get(i).run(); + } + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.setBulkBind(false); + } + mOnResumeCallbacks.clear(); + if (DEBUG_RESUME_TIME) { + Log.d(TAG, "Time spent processing callbacks in onResume: " + + (System.currentTimeMillis() - startTimeCallbacks)); + } + } + + // Reset the pressed state of icons that were locked in the press state while activities + // were launching + if (mWaitingForResume != null) { + // Resets the previous workspace icon press state + mWaitingForResume.setStayPressed(false); + } + if (mAppsCustomizeContent != null) { + // Resets the previous all apps icon press state + mAppsCustomizeContent.resetDrawableState(); + } + // It is possible that widgets can receive updates while launcher is not in the foreground. + // Consequently, the widgets will be inflated in the orientation of the foreground activity + // (framework issue). On resuming, we ensure that any widgets are inflated for the current + // orientation. + getWorkspace().reinflateWidgetsIfNecessary(); + + // Again, as with the above scenario, it's possible that one or more of the global icons + // were updated in the wrong orientation. + updateGlobalIcons(); + if (DEBUG_RESUME_TIME) { + Log.d(TAG, "Time spent in onResume: " + (System.currentTimeMillis() - startTime)); + } + } + + @Override + protected void onPause() { + // NOTE: We want all transitions from launcher to act as if the wallpaper were enabled + // to be consistent. So re-enable the flag here, and we will re-disable it as necessary + // when Launcher resumes and we are still in AllApps. + updateWallpaperVisibility(true); + + super.onPause(); + mPaused = true; + mDragController.cancelDrag(); + mDragController.resetLastGestureUpTime(); + } + + @Override + public Object onRetainNonConfigurationInstance() { + // Flag the loader to stop early before switching + mModel.stopLoader(); + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.surrender(); + } + return Boolean.TRUE; + } + + // We can't hide the IME if it was forced open. So don't bother + /* + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + if (hasFocus) { + final InputMethodManager inputManager = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + WindowManager.LayoutParams lp = getWindow().getAttributes(); + inputManager.hideSoftInputFromWindow(lp.token, 0, new android.os.ResultReceiver(new + android.os.Handler()) { + protected void onReceiveResult(int resultCode, Bundle resultData) { + Log.d(TAG, "ResultReceiver got resultCode=" + resultCode); + } + }); + Log.d(TAG, "called hideSoftInputFromWindow from onWindowFocusChanged"); + } + } + */ + + private boolean acceptFilter() { + final InputMethodManager inputManager = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + return !inputManager.isFullscreenMode(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + final int uniChar = event.getUnicodeChar(); + final boolean handled = super.onKeyDown(keyCode, event); + final boolean isKeyNotWhitespace = uniChar > 0 && !Character.isWhitespace(uniChar); + if (!handled && acceptFilter() && isKeyNotWhitespace) { + boolean gotKey = TextKeyListener.getInstance().onKeyDown(mWorkspace, mDefaultKeySsb, + keyCode, event); + if (gotKey && mDefaultKeySsb != null && mDefaultKeySsb.length() > 0) { + // something usable has been typed - start a search + // the typed text will be retrieved and cleared by + // showSearchDialog() + // If there are multiple keystrokes before the search dialog takes focus, + // onSearchRequested() will be called for every keystroke, + // but it is idempotent, so it's fine. + return onSearchRequested(); + } + } + + // Eat the long press event so the keyboard doesn't come up. + if (keyCode == KeyEvent.KEYCODE_MENU && event.isLongPress()) { + return true; + } + + return handled; + } + + private String getTypedText() { + return mDefaultKeySsb.toString(); + } + + private void clearTypedText() { + mDefaultKeySsb.clear(); + mDefaultKeySsb.clearSpans(); + Selection.setSelection(mDefaultKeySsb, 0); + } + + /** + * Given the integer (ordinal) value of a State enum instance, convert it to a variable of type + * State + */ + private static State intToState(int stateOrdinal) { + State state = State.WORKSPACE; + final State[] stateValues = State.values(); + for (int i = 0; i < stateValues.length; i++) { + if (stateValues[i].ordinal() == stateOrdinal) { + state = stateValues[i]; + break; + } + } + return state; + } + + /** + * Restores the previous state, if it exists. + * + * @param savedState The previous state. + */ + private void restoreState(Bundle savedState) { + if (savedState == null) { + return; + } + + State state = intToState(savedState.getInt(RUNTIME_STATE, State.WORKSPACE.ordinal())); + if (state == State.APPS_CUSTOMIZE) { + mOnResumeState = State.APPS_CUSTOMIZE; + } + + int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN, -1); + if (currentScreen > -1) { + mWorkspace.setCurrentPage(currentScreen); + } + + final long pendingAddContainer = savedState.getLong(RUNTIME_STATE_PENDING_ADD_CONTAINER, -1); + final int pendingAddScreen = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SCREEN, -1); + + if (pendingAddContainer != ItemInfo.NO_ID && pendingAddScreen > -1) { + mPendingAddInfo.container = pendingAddContainer; + mPendingAddInfo.screen = pendingAddScreen; + mPendingAddInfo.cellX = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_X); + mPendingAddInfo.cellY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_Y); + mPendingAddInfo.spanX = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_X); + mPendingAddInfo.spanY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y); + mPendingAddWidgetInfo = savedState.getParcelable(RUNTIME_STATE_PENDING_ADD_WIDGET_INFO); + mWaitingForResult = true; + mRestoring = true; + } + + + boolean renameFolder = savedState.getBoolean(RUNTIME_STATE_PENDING_FOLDER_RENAME, false); + if (renameFolder) { + long id = savedState.getLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID); + mFolderInfo = mModel.getFolderById(this, sFolders, id); + mRestoring = true; + } + + + // Restore the AppsCustomize tab + if (mAppsCustomizeTabHost != null) { + String curTab = savedState.getString("apps_customize_currentTab"); + if (curTab != null) { + mAppsCustomizeTabHost.setContentTypeImmediate( + mAppsCustomizeTabHost.getContentTypeForTabTag(curTab)); + mAppsCustomizeContent.loadAssociatedPages( + mAppsCustomizeContent.getCurrentPage()); + } + + int currentIndex = savedState.getInt("apps_customize_currentIndex"); + mAppsCustomizeContent.restorePageForIndex(currentIndex); + } + } + + /** + * Finds all the views we need and configure them properly. + */ + private void setupViews() { + final DragController dragController = mDragController; + + mLauncherView = findViewById(R.id.launcher); + mDragLayer = (DragLayer) findViewById(R.id.drag_layer); + mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace); + mQsbDivider = findViewById(R.id.qsb_divider); + mDockDivider = findViewById(R.id.dock_divider); + + mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + mWorkspaceBackgroundDrawable = getResources().getDrawable(R.drawable.workspace_bg); + + // Setup the drag layer + mDragLayer.setup(this, dragController); + + // Setup the hotseat + mHotseat = (Hotseat) findViewById(R.id.hotseat); + if (mHotseat != null) { + mHotseat.setup(this); + } + + // Setup the workspace + mWorkspace.setHapticFeedbackEnabled(false); + mWorkspace.setOnLongClickListener(this); + mWorkspace.setup(dragController); + dragController.addDragListener(mWorkspace); + + // Get the search/delete bar + mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.qsb_bar); + + // Setup AppsCustomize + mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane); + mAppsCustomizeContent = (AppsCustomizePagedView) + mAppsCustomizeTabHost.findViewById(R.id.apps_customize_pane_content); + mAppsCustomizeContent.setup(this, dragController); + + // Setup the drag controller (drop targets have to be added in reverse order in priority) + dragController.setDragScoller(mWorkspace); + dragController.setScrollView(mDragLayer); + dragController.setMoveTarget(mWorkspace); + dragController.addDropTarget(mWorkspace); + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.setup(this, dragController); + } + } + + /** + * Creates a view representing a shortcut. + * + * @param info The data structure describing the shortcut. + * + * @return A View inflated from R.layout.application. + */ + View createShortcut(ShortcutInfo info) { + return createShortcut(R.layout.application, + (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); + } + + /** + * Creates a view representing a shortcut inflated from the specified resource. + * + * @param layoutResId The id of the XML layout used to create the shortcut. + * @param parent The group the shortcut belongs to. + * @param info The data structure describing the shortcut. + * + * @return A View inflated from layoutResId. + */ + View createShortcut(int layoutResId, ViewGroup parent, ShortcutInfo info) { + BubbleTextView favorite = (BubbleTextView) mInflater.inflate(layoutResId, parent, false); + favorite.applyFromShortcutInfo(info, mIconCache); + favorite.setOnClickListener(this); + return favorite; + } + + /** + * Add an application shortcut to the workspace. + * + * @param data The intent describing the application. + * @param cellInfo The position on screen where to create the shortcut. + */ + void completeAddApplication(Intent data, long container, int screen, int cellX, int cellY) { + final int[] cellXY = mTmpAddItemCellCoordinates; + final CellLayout layout = getCellLayout(container, screen); + + // First we check if we already know the exact location where we want to add this item. + if (cellX >= 0 && cellY >= 0) { + cellXY[0] = cellX; + cellXY[1] = cellY; + } else if (!layout.findCellForSpan(cellXY, 1, 1)) { + showOutOfSpaceMessage(isHotseatLayout(layout)); + return; + } + + final ShortcutInfo info = mModel.getShortcutInfo(getPackageManager(), data, this); + + if (info != null) { + info.setActivity(data.getComponent(), Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + info.container = ItemInfo.NO_ID; + mWorkspace.addApplicationShortcut(info, layout, container, screen, cellXY[0], cellXY[1], + isWorkspaceLocked(), cellX, cellY); + } else { + Log.e(TAG, "Couldn't find ActivityInfo for selected application: " + data); + } + } + + /** + * Add a shortcut to the workspace. + * + * @param data The intent describing the shortcut. + * @param cellInfo The position on screen where to create the shortcut. + */ + private void completeAddShortcut(Intent data, long container, int screen, int cellX, + int cellY) { + int[] cellXY = mTmpAddItemCellCoordinates; + int[] touchXY = mPendingAddInfo.dropPos; + CellLayout layout = getCellLayout(container, screen); + + boolean foundCellSpan = false; + + ShortcutInfo info = mModel.infoFromShortcutIntent(this, data, null); + if (info == null) { + return; + } + final View view = createShortcut(info); + + // First we check if we already know the exact location where we want to add this item. + if (cellX >= 0 && cellY >= 0) { + cellXY[0] = cellX; + cellXY[1] = cellY; + foundCellSpan = true; + + // If appropriate, either create a folder or add to an existing folder + if (mWorkspace.createUserFolderIfNecessary(view, container, layout, cellXY, 0, + true, null,null)) { + return; + } + DragObject dragObject = new DragObject(); + dragObject.dragInfo = info; + if (mWorkspace.addToExistingFolderIfNecessary(view, layout, cellXY, 0, dragObject, + true)) { + return; + } + } else if (touchXY != null) { + // when dragging and dropping, just find the closest free spot + int[] result = layout.findNearestVacantArea(touchXY[0], touchXY[1], 1, 1, cellXY); + foundCellSpan = (result != null); + } else { + foundCellSpan = layout.findCellForSpan(cellXY, 1, 1); + } + + if (!foundCellSpan) { + showOutOfSpaceMessage(isHotseatLayout(layout)); + return; + } + + LauncherModel.addItemToDatabase(this, info, container, screen, cellXY[0], cellXY[1], false); + + if (!mRestoring) { + mWorkspace.addInScreen(view, container, screen, cellXY[0], cellXY[1], 1, 1, + isWorkspaceLocked()); + } + } + + static int[] getSpanForWidget(Context context, ComponentName component, int minWidth, + int minHeight) { + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(context, component, null); + // We want to account for the extra amount of padding that we are adding to the widget + // to ensure that it gets the full amount of space that it has requested + int requiredWidth = minWidth + padding.left + padding.right; + int requiredHeight = minHeight + padding.top + padding.bottom; + return CellLayout.rectToCell(context.getResources(), requiredWidth, requiredHeight, null); + } + + static int[] getSpanForWidget(Context context, AppWidgetProviderInfo info) { + return getSpanForWidget(context, info.provider, info.minWidth, info.minHeight); + } + + static int[] getMinSpanForWidget(Context context, AppWidgetProviderInfo info) { + return getSpanForWidget(context, info.provider, info.minResizeWidth, info.minResizeHeight); + } + + static int[] getSpanForWidget(Context context, PendingAddWidgetInfo info) { + return getSpanForWidget(context, info.componentName, info.minWidth, info.minHeight); + } + + static int[] getMinSpanForWidget(Context context, PendingAddWidgetInfo info) { + return getSpanForWidget(context, info.componentName, info.minResizeWidth, + info.minResizeHeight); + } + + /** + * Add a widget to the workspace. + * + * @param appWidgetId The app widget id + * @param cellInfo The position on screen where to create the widget. + */ + private void completeAddAppWidget(final int appWidgetId, long container, int screen, + AppWidgetHostView hostView, AppWidgetProviderInfo appWidgetInfo) { + if (appWidgetInfo == null) { + appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); + } + + // Calculate the grid spans needed to fit this widget + CellLayout layout = getCellLayout(container, screen); + + int[] minSpanXY = getMinSpanForWidget(this, appWidgetInfo); + int[] spanXY = getSpanForWidget(this, appWidgetInfo); + + // Try finding open space on Launcher screen + // We have saved the position to which the widget was dragged-- this really only matters + // if we are placing widgets on a "spring-loaded" screen + int[] cellXY = mTmpAddItemCellCoordinates; + int[] touchXY = mPendingAddInfo.dropPos; + int[] finalSpan = new int[2]; + boolean foundCellSpan = false; + if (mPendingAddInfo.cellX >= 0 && mPendingAddInfo.cellY >= 0) { + cellXY[0] = mPendingAddInfo.cellX; + cellXY[1] = mPendingAddInfo.cellY; + spanXY[0] = mPendingAddInfo.spanX; + spanXY[1] = mPendingAddInfo.spanY; + foundCellSpan = true; + } else if (touchXY != null) { + // when dragging and dropping, just find the closest free spot + int[] result = layout.findNearestVacantArea( + touchXY[0], touchXY[1], minSpanXY[0], minSpanXY[1], spanXY[0], + spanXY[1], cellXY, finalSpan); + spanXY[0] = finalSpan[0]; + spanXY[1] = finalSpan[1]; + foundCellSpan = (result != null); + } else { + foundCellSpan = layout.findCellForSpan(cellXY, minSpanXY[0], minSpanXY[1]); + } + + if (!foundCellSpan) { + if (appWidgetId != -1) { + // Deleting an app widget ID is a void call but writes to disk before returning + // to the caller... + new Thread("deleteAppWidgetId") { + public void run() { + mAppWidgetHost.deleteAppWidgetId(appWidgetId); + } + }.start(); + } + showOutOfSpaceMessage(isHotseatLayout(layout)); + return; + } + + // Build Launcher-specific widget info and save to database + LauncherAppWidgetInfo launcherInfo = new LauncherAppWidgetInfo(appWidgetId, + appWidgetInfo.provider); + launcherInfo.spanX = spanXY[0]; + launcherInfo.spanY = spanXY[1]; + launcherInfo.minSpanX = mPendingAddInfo.minSpanX; + launcherInfo.minSpanY = mPendingAddInfo.minSpanY; + + LauncherModel.addItemToDatabase(this, launcherInfo, + container, screen, cellXY[0], cellXY[1], false); + + if (!mRestoring) { + if (hostView == null) { + // Perform actual inflation because we're live + launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo); + } else { + // The AppWidgetHostView has already been inflated and instantiated + launcherInfo.hostView = hostView; + } + + launcherInfo.hostView.setTag(launcherInfo); + launcherInfo.hostView.setVisibility(View.VISIBLE); + launcherInfo.notifyWidgetSizeChanged(this); + + mWorkspace.addInScreen(launcherInfo.hostView, container, screen, cellXY[0], cellXY[1], + launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); + + addWidgetToAutoAdvanceIfNeeded(launcherInfo.hostView, appWidgetInfo); + } + resetAddInfo(); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_SCREEN_OFF.equals(action)) { + mUserPresent = false; + mDragLayer.clearAllResizeFrames(); + updateRunning(); + + // Reset AllApps to its initial state only if we are not in the middle of + // processing a multi-step drop + if (mAppsCustomizeTabHost != null && mPendingAddInfo.container == ItemInfo.NO_ID) { + mAppsCustomizeTabHost.reset(); + showWorkspace(false); + } + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + mUserPresent = true; + updateRunning(); + } + } + }; + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + // Listen for broadcasts related to user-presence + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + registerReceiver(mReceiver, filter); + FirstFrameAnimatorHelper.initializeDrawListener(getWindow().getDecorView()); + mAttached = true; + mVisible = true; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mVisible = false; + + if (mAttached) { + unregisterReceiver(mReceiver); + mAttached = false; + } + updateRunning(); + } + + public void onWindowVisibilityChanged(int visibility) { + mVisible = visibility == View.VISIBLE; + updateRunning(); + // The following code used to be in onResume, but it turns out onResume is called when + // you're in All Apps and click home to go to the workspace. onWindowVisibilityChanged + // is a more appropriate event to handle + if (mVisible) { + mAppsCustomizeTabHost.onWindowVisible(); + if (!mWorkspaceLoading) { + final ViewTreeObserver observer = mWorkspace.getViewTreeObserver(); + // We want to let Launcher draw itself at least once before we force it to build + // layers on all the workspace pages, so that transitioning to Launcher from other + // apps is nice and speedy. + observer.addOnDrawListener(new ViewTreeObserver.OnDrawListener() { + private boolean mStarted = false; + public void onDraw() { + if (mStarted) return; + mStarted = true; + // We delay the layer building a bit in order to give + // other message processing a time to run. In particular + // this avoids a delay in hiding the IME if it was + // currently shown, because doing that may involve + // some communication back with the app. + mWorkspace.postDelayed(mBuildLayersRunnable, 500); + final ViewTreeObserver.OnDrawListener listener = this; + mWorkspace.post(new Runnable() { + public void run() { + if (mWorkspace != null && + mWorkspace.getViewTreeObserver() != null) { + mWorkspace.getViewTreeObserver(). + removeOnDrawListener(listener); + } + } + }); + return; + } + }); + } + // When Launcher comes back to foreground, a different Activity might be responsible for + // the app market intent, so refresh the icon + updateAppMarketIcon(); + clearTypedText(); + } + } + + private void sendAdvanceMessage(long delay) { + mHandler.removeMessages(ADVANCE_MSG); + Message msg = mHandler.obtainMessage(ADVANCE_MSG); + mHandler.sendMessageDelayed(msg, delay); + mAutoAdvanceSentTime = System.currentTimeMillis(); + } + + private void updateRunning() { + boolean autoAdvanceRunning = mVisible && mUserPresent && !mWidgetsToAdvance.isEmpty(); + if (autoAdvanceRunning != mAutoAdvanceRunning) { + mAutoAdvanceRunning = autoAdvanceRunning; + if (autoAdvanceRunning) { + long delay = mAutoAdvanceTimeLeft == -1 ? mAdvanceInterval : mAutoAdvanceTimeLeft; + sendAdvanceMessage(delay); + } else { + if (!mWidgetsToAdvance.isEmpty()) { + mAutoAdvanceTimeLeft = Math.max(0, mAdvanceInterval - + (System.currentTimeMillis() - mAutoAdvanceSentTime)); + } + mHandler.removeMessages(ADVANCE_MSG); + mHandler.removeMessages(0); // Remove messages sent using postDelayed() + } + } + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == ADVANCE_MSG) { + int i = 0; + for (View key: mWidgetsToAdvance.keySet()) { + final View v = key.findViewById(mWidgetsToAdvance.get(key).autoAdvanceViewId); + final int delay = mAdvanceStagger * i; + if (v instanceof Advanceable) { + postDelayed(new Runnable() { + public void run() { + ((Advanceable) v).advance(); + } + }, delay); + } + i++; + } + sendAdvanceMessage(mAdvanceInterval); + } + } + }; + + void addWidgetToAutoAdvanceIfNeeded(View hostView, AppWidgetProviderInfo appWidgetInfo) { + if (appWidgetInfo == null || appWidgetInfo.autoAdvanceViewId == -1) return; + View v = hostView.findViewById(appWidgetInfo.autoAdvanceViewId); + if (v instanceof Advanceable) { + mWidgetsToAdvance.put(hostView, appWidgetInfo); + ((Advanceable) v).fyiWillBeAdvancedByHostKThx(); + updateRunning(); + } + } + + void removeWidgetToAutoAdvance(View hostView) { + if (mWidgetsToAdvance.containsKey(hostView)) { + mWidgetsToAdvance.remove(hostView); + updateRunning(); + } + } + + public void removeAppWidget(LauncherAppWidgetInfo launcherInfo) { + removeWidgetToAutoAdvance(launcherInfo.hostView); + launcherInfo.hostView = null; + } + + void showOutOfSpaceMessage(boolean isHotseatLayout) { + int strId = (isHotseatLayout ? R.string.hotseat_out_of_space : R.string.out_of_space); + Toast.makeText(this, getString(strId), Toast.LENGTH_SHORT).show(); + } + + public LauncherAppWidgetHost getAppWidgetHost() { + return mAppWidgetHost; + } + + public LauncherModel getModel() { + return mModel; + } + + void closeSystemDialogs() { + getWindow().closeAllPanels(); + + // Whatever we were doing is hereby canceled. + mWaitingForResult = false; + } + + @Override + protected void onNewIntent(Intent intent) { + long startTime = 0; + if (DEBUG_RESUME_TIME) { + startTime = System.currentTimeMillis(); + } + super.onNewIntent(intent); + + // Close the menu + if (Intent.ACTION_MAIN.equals(intent.getAction())) { + // also will cancel mWaitingForResult. + closeSystemDialogs(); + + final boolean alreadyOnHome = + ((intent.getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) + != Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); + + Runnable processIntent = new Runnable() { + public void run() { + if (mWorkspace == null) { + // Can be cases where mWorkspace is null, this prevents a NPE + return; + } + Folder openFolder = mWorkspace.getOpenFolder(); + // In all these cases, only animate if we're already on home + mWorkspace.exitWidgetResizeMode(); + if (alreadyOnHome && mState == State.WORKSPACE && !mWorkspace.isTouchActive() && + openFolder == null) { + mWorkspace.moveToDefaultScreen(true); + } + + closeFolder(); + exitSpringLoadedDragMode(); + + // If we are already on home, then just animate back to the workspace, + // otherwise, just wait until onResume to set the state back to Workspace + if (alreadyOnHome) { + showWorkspace(true); + } else { + mOnResumeState = State.WORKSPACE; + } + + final View v = getWindow().peekDecorView(); + if (v != null && v.getWindowToken() != null) { + InputMethodManager imm = (InputMethodManager)getSystemService( + INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + + // Reset AllApps to its initial state + if (!alreadyOnHome && mAppsCustomizeTabHost != null) { + mAppsCustomizeTabHost.reset(); + } + } + }; + + if (alreadyOnHome && !mWorkspace.hasWindowFocus()) { + // Delay processing of the intent to allow the status bar animation to finish + // first in order to avoid janky animations. + mWorkspace.postDelayed(processIntent, 350); + } else { + // Process the intent immediately. + processIntent.run(); + } + + } + if (DEBUG_RESUME_TIME) { + Log.d(TAG, "Time spent in onNewIntent: " + (System.currentTimeMillis() - startTime)); + } + } + + @Override + public void onRestoreInstanceState(Bundle state) { + super.onRestoreInstanceState(state); + for (int page: mSynchronouslyBoundPages) { + mWorkspace.restoreInstanceStateForChild(page); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putInt(RUNTIME_STATE_CURRENT_SCREEN, mWorkspace.getNextPage()); + super.onSaveInstanceState(outState); + + outState.putInt(RUNTIME_STATE, mState.ordinal()); + // We close any open folder since it will not be re-opened, and we need to make sure + // this state is reflected. + closeFolder(); + + if (mPendingAddInfo.container != ItemInfo.NO_ID && mPendingAddInfo.screen > -1 && + mWaitingForResult) { + outState.putLong(RUNTIME_STATE_PENDING_ADD_CONTAINER, mPendingAddInfo.container); + outState.putInt(RUNTIME_STATE_PENDING_ADD_SCREEN, mPendingAddInfo.screen); + outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_X, mPendingAddInfo.cellX); + outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_Y, mPendingAddInfo.cellY); + outState.putInt(RUNTIME_STATE_PENDING_ADD_SPAN_X, mPendingAddInfo.spanX); + outState.putInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y, mPendingAddInfo.spanY); + outState.putParcelable(RUNTIME_STATE_PENDING_ADD_WIDGET_INFO, mPendingAddWidgetInfo); + } + + if (mFolderInfo != null && mWaitingForResult) { + outState.putBoolean(RUNTIME_STATE_PENDING_FOLDER_RENAME, true); + outState.putLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID, mFolderInfo.id); + } + + // Save the current AppsCustomize tab + if (mAppsCustomizeTabHost != null) { + String currentTabTag = mAppsCustomizeTabHost.getCurrentTabTag(); + if (currentTabTag != null) { + outState.putString("apps_customize_currentTab", currentTabTag); + } + int currentIndex = mAppsCustomizeContent.getSaveInstanceStateIndex(); + outState.putInt("apps_customize_currentIndex", currentIndex); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + // Remove all pending runnables + mHandler.removeMessages(ADVANCE_MSG); + mHandler.removeMessages(0); + mWorkspace.removeCallbacks(mBuildLayersRunnable); + + // Stop callbacks from LauncherModel + LauncherApplication app = ((LauncherApplication) getApplication()); + mModel.stopLoader(); + app.setLauncher(null); + + try { + mAppWidgetHost.stopListening(); + } catch (NullPointerException ex) { + Log.w(TAG, "problem while stopping AppWidgetHost during Launcher destruction", ex); + } + mAppWidgetHost = null; + + mWidgetsToAdvance.clear(); + + TextKeyListener.getInstance().release(); + + // Disconnect any of the callbacks and drawables associated with ItemInfos on the workspace + // to prevent leaking Launcher activities on orientation change. + if (mModel != null) { + mModel.unbindItemInfosAndClearQueuedBindRunnables(); + } + + getContentResolver().unregisterContentObserver(mWidgetObserver); + unregisterReceiver(mCloseSystemDialogsReceiver); + + mDragLayer.clearAllResizeFrames(); + ((ViewGroup) mWorkspace.getParent()).removeAllViews(); + mWorkspace.removeAllViews(); + mWorkspace = null; + mDragController = null; + + LauncherAnimUtils.onDestroyActivity(); + } + + public DragController getDragController() { + return mDragController; + } + + @Override + public void startActivityForResult(Intent intent, int requestCode) { + if (requestCode >= 0) mWaitingForResult = true; + super.startActivityForResult(intent, requestCode); + } + + /** + * Indicates that we want global search for this activity by setting the globalSearch + * argument for {@link #startSearch} to true. + */ + @Override + public void startSearch(String initialQuery, boolean selectInitialQuery, + Bundle appSearchData, boolean globalSearch) { + + showWorkspace(true); + + if (initialQuery == null) { + // Use any text typed in the launcher as the initial query + initialQuery = getTypedText(); + } + if (appSearchData == null) { + appSearchData = new Bundle(); + appSearchData.putString(Search.SOURCE, "launcher-search"); + } + Rect sourceBounds = new Rect(); + if (mSearchDropTargetBar != null) { + sourceBounds = mSearchDropTargetBar.getSearchBarBounds(); + } + + startGlobalSearch(initialQuery, selectInitialQuery, + appSearchData, sourceBounds); + } + + /** + * Starts the global search activity. This code is a copied from SearchManager + */ + public void startGlobalSearch(String initialQuery, + boolean selectInitialQuery, Bundle appSearchData, Rect sourceBounds) { + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + ComponentName globalSearchActivity = searchManager.getGlobalSearchActivity(); + if (globalSearchActivity == null) { + Log.w(TAG, "No global search activity found."); + return; + } + Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setComponent(globalSearchActivity); + // Make sure that we have a Bundle to put source in + if (appSearchData == null) { + appSearchData = new Bundle(); + } else { + appSearchData = new Bundle(appSearchData); + } + // Set source to package name of app that starts global search, if not set already. + if (!appSearchData.containsKey("source")) { + appSearchData.putString("source", getPackageName()); + } + intent.putExtra(SearchManager.APP_DATA, appSearchData); + if (!TextUtils.isEmpty(initialQuery)) { + intent.putExtra(SearchManager.QUERY, initialQuery); + } + if (selectInitialQuery) { + intent.putExtra(SearchManager.EXTRA_SELECT_QUERY, selectInitialQuery); + } + intent.setSourceBounds(sourceBounds); + try { + startActivity(intent); + } catch (ActivityNotFoundException ex) { + Log.e(TAG, "Global search activity not found: " + globalSearchActivity); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (isWorkspaceLocked()) { + return false; + } + + super.onCreateOptionsMenu(menu); + + Intent manageApps = new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS); + manageApps.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + Intent settings = new Intent(android.provider.Settings.ACTION_SETTINGS); + settings.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + String helpUrl = getString(R.string.help_url); + Intent help = new Intent(Intent.ACTION_VIEW, Uri.parse(helpUrl)); + help.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + + menu.add(MENU_GROUP_WALLPAPER, MENU_WALLPAPER_SETTINGS, 0, R.string.menu_wallpaper) + .setIcon(android.R.drawable.ic_menu_gallery) + .setAlphabeticShortcut('W'); + menu.add(0, MENU_MANAGE_APPS, 0, R.string.menu_manage_apps) + .setIcon(android.R.drawable.ic_menu_manage) + .setIntent(manageApps) + .setAlphabeticShortcut('M'); + menu.add(0, MENU_SYSTEM_SETTINGS, 0, R.string.menu_settings) + .setIcon(android.R.drawable.ic_menu_preferences) + .setIntent(settings) + .setAlphabeticShortcut('P'); + if (!helpUrl.isEmpty()) { + menu.add(0, MENU_HELP, 0, R.string.menu_help) + .setIcon(android.R.drawable.ic_menu_help) + .setIntent(help) + .setAlphabeticShortcut('H'); + } + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + if (mAppsCustomizeTabHost.isTransitioning()) { + return false; + } + boolean allAppsVisible = (mAppsCustomizeTabHost.getVisibility() == View.VISIBLE); + menu.setGroupVisible(MENU_GROUP_WALLPAPER, !allAppsVisible); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_WALLPAPER_SETTINGS: + startWallpaper(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onSearchRequested() { + startSearch(null, false, null, true); + // Use a custom animation for launching search + return true; + } + + public boolean isWorkspaceLocked() { + return mWorkspaceLoading || mWaitingForResult; + } + + private void resetAddInfo() { + mPendingAddInfo.container = ItemInfo.NO_ID; + mPendingAddInfo.screen = -1; + mPendingAddInfo.cellX = mPendingAddInfo.cellY = -1; + mPendingAddInfo.spanX = mPendingAddInfo.spanY = -1; + mPendingAddInfo.minSpanX = mPendingAddInfo.minSpanY = -1; + mPendingAddInfo.dropPos = null; + } + + void addAppWidgetImpl(final int appWidgetId, ItemInfo info, AppWidgetHostView boundWidget, + AppWidgetProviderInfo appWidgetInfo) { + if (appWidgetInfo.configure != null) { + mPendingAddWidgetInfo = appWidgetInfo; + + // Launch over to configure widget, if needed + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); + intent.setComponent(appWidgetInfo.configure); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + startActivityForResultSafely(intent, REQUEST_CREATE_APPWIDGET); + } else { + // Otherwise just add it + completeAddAppWidget(appWidgetId, info.container, info.screen, boundWidget, + appWidgetInfo); + // Exit spring loaded mode if necessary after adding the widget + exitSpringLoadedDragModeDelayed(true, false, null); + } + } + + /** + * Process a shortcut drop. + * + * @param componentName The name of the component + * @param screen The screen where it should be added + * @param cell The cell it should be added to, optional + * @param position The location on the screen where it was dropped, optional + */ + void processShortcutFromDrop(ComponentName componentName, long container, int screen, + int[] cell, int[] loc) { + resetAddInfo(); + mPendingAddInfo.container = container; + mPendingAddInfo.screen = screen; + mPendingAddInfo.dropPos = loc; + + if (cell != null) { + mPendingAddInfo.cellX = cell[0]; + mPendingAddInfo.cellY = cell[1]; + } + + Intent createShortcutIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); + createShortcutIntent.setComponent(componentName); + processShortcut(createShortcutIntent); + } + + /** + * Process a widget drop. + * + * @param info The PendingAppWidgetInfo of the widget being added. + * @param screen The screen where it should be added + * @param cell The cell it should be added to, optional + * @param position The location on the screen where it was dropped, optional + */ + void addAppWidgetFromDrop(PendingAddWidgetInfo info, long container, int screen, + int[] cell, int[] span, int[] loc) { + resetAddInfo(); + mPendingAddInfo.container = info.container = container; + mPendingAddInfo.screen = info.screen = screen; + mPendingAddInfo.dropPos = loc; + mPendingAddInfo.minSpanX = info.minSpanX; + mPendingAddInfo.minSpanY = info.minSpanY; + + if (cell != null) { + mPendingAddInfo.cellX = cell[0]; + mPendingAddInfo.cellY = cell[1]; + } + if (span != null) { + mPendingAddInfo.spanX = span[0]; + mPendingAddInfo.spanY = span[1]; + } + + AppWidgetHostView hostView = info.boundWidget; + int appWidgetId; + if (hostView != null) { + appWidgetId = hostView.getAppWidgetId(); + addAppWidgetImpl(appWidgetId, info, hostView, info.info); + } else { + // In this case, we either need to start an activity to get permission to bind + // the widget, or we need to start an activity to configure the widget, or both. + appWidgetId = getAppWidgetHost().allocateAppWidgetId(); + Bundle options = info.bindOptions; + + boolean success = false; + if (options != null) { + success = mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, + info.componentName, options); + } else { + success = mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, + info.componentName); + } + if (success) { + addAppWidgetImpl(appWidgetId, info, null, info.info); + } else { + mPendingAddWidgetInfo = info.info; + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.componentName); + // TODO: we need to make sure that this accounts for the options bundle. + // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options); + startActivityForResult(intent, REQUEST_BIND_APPWIDGET); + } + } + } + + void processShortcut(Intent intent) { + // Handle case where user selected "Applications" + String applicationName = getResources().getString(R.string.group_applications); + String shortcutName = intent.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); + + if (applicationName != null && applicationName.equals(shortcutName)) { + Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + + Intent pickIntent = new Intent(Intent.ACTION_PICK_ACTIVITY); + pickIntent.putExtra(Intent.EXTRA_INTENT, mainIntent); + pickIntent.putExtra(Intent.EXTRA_TITLE, getText(R.string.title_select_application)); + startActivityForResultSafely(pickIntent, REQUEST_PICK_APPLICATION); + } else { + startActivityForResultSafely(intent, REQUEST_CREATE_SHORTCUT); + } + } + + void processWallpaper(Intent intent) { + startActivityForResult(intent, REQUEST_PICK_WALLPAPER); + } + + FolderIcon addFolder(CellLayout layout, long container, final int screen, int cellX, + int cellY) { + final FolderInfo folderInfo = new FolderInfo(); + folderInfo.title = getText(R.string.folder_name); + + // Update the model + LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screen, cellX, cellY, + false); + sFolders.put(folderInfo.id, folderInfo); + + // Create the view + FolderIcon newFolder = + FolderIcon.fromXml(R.layout.folder_icon, this, layout, folderInfo, mIconCache); + mWorkspace.addInScreen(newFolder, container, screen, cellX, cellY, 1, 1, + isWorkspaceLocked()); + return newFolder; + } + + void removeFolder(FolderInfo folder) { + sFolders.remove(folder.id); + } + + private void startWallpaper() { + showWorkspace(true); + final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER); + Intent chooser = Intent.createChooser(pickWallpaper, + getText(R.string.chooser_wallpaper)); + // NOTE: Adds a configure option to the chooser if the wallpaper supports it + // Removed in Eclair MR1 +// WallpaperManager wm = (WallpaperManager) +// getSystemService(Context.WALLPAPER_SERVICE); +// WallpaperInfo wi = wm.getWallpaperInfo(); +// if (wi != null && wi.getSettingsActivity() != null) { +// LabeledIntent li = new LabeledIntent(getPackageName(), +// R.string.configure_wallpaper, 0); +// li.setClassName(wi.getPackageName(), wi.getSettingsActivity()); +// chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { li }); +// } + startActivityForResult(chooser, REQUEST_PICK_WALLPAPER); + } + + /** + * Registers various content observers. The current implementation registers + * only a favorites observer to keep track of the favorites applications. + */ + private void registerContentObservers() { + ContentResolver resolver = getContentResolver(); + resolver.registerContentObserver(LauncherProvider.CONTENT_APPWIDGET_RESET_URI, + true, mWidgetObserver); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_HOME: + return true; + case KeyEvent.KEYCODE_VOLUME_DOWN: + if (isPropertyEnabled(DUMP_STATE_PROPERTY)) { + dumpState(); + return true; + } + break; + } + } else if (event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_HOME: + return true; + } + } + + return super.dispatchKeyEvent(event); + } + + @Override + public void onBackPressed() { + if (isAllAppsVisible()) { + showWorkspace(true); + } else if (mWorkspace.getOpenFolder() != null) { + Folder openFolder = mWorkspace.getOpenFolder(); + if (openFolder.isEditingName()) { + openFolder.dismissEditingName(); + } else { + closeFolder(); + } + } else { + mWorkspace.exitWidgetResizeMode(); + + // Back button is a no-op here, but give at least some feedback for the button press + mWorkspace.showOutlinesTemporarily(); + } + } + + /** + * Re-listen when widgets are reset. + */ + private void onAppWidgetReset() { + if (mAppWidgetHost != null) { + mAppWidgetHost.startListening(); + } + } + + /** + * Launches the intent referred by the clicked shortcut. + * + * @param v The view representing the clicked shortcut. + */ + public void onClick(View v) { + // Make sure that rogue clicks don't get through while allapps is launching, or after the + // view has detached (it's possible for this to happen if the view is removed mid touch). + if (v.getWindowToken() == null) { + return; + } + + if (!mWorkspace.isFinishedSwitchingState()) { + return; + } + + Object tag = v.getTag(); + if (tag instanceof ShortcutInfo) { + // Open shortcut + final Intent intent = ((ShortcutInfo) tag).intent; + int[] pos = new int[2]; + v.getLocationOnScreen(pos); + intent.setSourceBounds(new Rect(pos[0], pos[1], + pos[0] + v.getWidth(), pos[1] + v.getHeight())); + + boolean success = startActivitySafely(v, intent, tag); + + if (success && v instanceof BubbleTextView) { + mWaitingForResume = (BubbleTextView) v; + mWaitingForResume.setStayPressed(true); + } + } else if (tag instanceof FolderInfo) { + if (v instanceof FolderIcon) { + FolderIcon fi = (FolderIcon) v; + handleFolderClick(fi); + } + } else if (v == mAllAppsButton) { + if (isAllAppsVisible()) { + showWorkspace(true); + } else { + onClickAllAppsButton(v); + } + } + } + + public boolean onTouch(View v, MotionEvent event) { + // this is an intercepted event being forwarded from mWorkspace; + // clicking anywhere on the workspace causes the customization drawer to slide down + showWorkspace(true); + return false; + } + + /** + * Event handler for the search button + * + * @param v The view that was clicked. + */ + public void onClickSearchButton(View v) { + v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + + onSearchRequested(); + } + + /** + * Event handler for the voice button + * + * @param v The view that was clicked. + */ + public void onClickVoiceButton(View v) { + v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + + try { + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + ComponentName activityName = searchManager.getGlobalSearchActivity(); + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (activityName != null) { + intent.setPackage(activityName.getPackageName()); + } + startActivity(null, intent, "onClickVoiceButton"); + } catch (ActivityNotFoundException e) { + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivitySafely(null, intent, "onClickVoiceButton"); + } + } + + /** + * Event handler for the "grid" button that appears on the home screen, which + * enters all apps mode. + * + * @param v The view that was clicked. + */ + public void onClickAllAppsButton(View v) { + showAllApps(true); + } + + public void onTouchDownAllAppsButton(View v) { + // Provide the same haptic feedback that the system offers for virtual keys. + v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + } + + public void onClickAppMarketButton(View v) { + if (mAppMarketIntent != null) { + startActivitySafely(v, mAppMarketIntent, "app market"); + } else { + Log.e(TAG, "Invalid app market intent."); + } + } + + void startApplicationDetailsActivity(ComponentName componentName) { + String packageName = componentName.getPackageName(); + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", packageName, null)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + startActivitySafely(null, intent, "startApplicationDetailsActivity"); + } + + void startApplicationUninstallActivity(ApplicationInfo appInfo) { + if ((appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) == 0) { + // System applications cannot be installed. For now, show a toast explaining that. + // We may give them the option of disabling apps this way. + int messageId = R.string.uninstall_system_app_text; + Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show(); + } else { + String packageName = appInfo.componentName.getPackageName(); + String className = appInfo.componentName.getClassName(); + Intent intent = new Intent( + Intent.ACTION_DELETE, Uri.fromParts("package", packageName, className)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + startActivity(intent); + } + } + + boolean startActivity(View v, Intent intent, Object tag) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + try { + // Only launch using the new animation if the shortcut has not opted out (this is a + // private contract between launcher and may be ignored in the future). + boolean useLaunchAnimation = (v != null) && + !intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION); + if (useLaunchAnimation) { + ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, + v.getMeasuredWidth(), v.getMeasuredHeight()); + + startActivity(intent, opts.toBundle()); + } else { + startActivity(intent); + } + return true; + } catch (SecurityException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Launcher does not have the permission to launch " + intent + + ". Make sure to create a MAIN intent-filter for the corresponding activity " + + "or use the exported attribute for this activity. " + + "tag="+ tag + " intent=" + intent, e); + } + return false; + } + + boolean startActivitySafely(View v, Intent intent, Object tag) { + boolean success = false; + try { + success = startActivity(v, intent, tag); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Unable to launch. tag=" + tag + " intent=" + intent, e); + } + return success; + } + + void startActivityForResultSafely(Intent intent, int requestCode) { + try { + startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + } catch (SecurityException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Launcher does not have the permission to launch " + intent + + ". Make sure to create a MAIN intent-filter for the corresponding activity " + + "or use the exported attribute for this activity.", e); + } + } + + private void handleFolderClick(FolderIcon folderIcon) { + final FolderInfo info = folderIcon.getFolderInfo(); + Folder openFolder = mWorkspace.getFolderForTag(info); + + // If the folder info reports that the associated folder is open, then verify that + // it is actually opened. There have been a few instances where this gets out of sync. + if (info.opened && openFolder == null) { + Log.d(TAG, "Folder info marked as open, but associated folder is not open. Screen: " + + info.screen + " (" + info.cellX + ", " + info.cellY + ")"); + info.opened = false; + } + + if (!info.opened && !folderIcon.getFolder().isDestroyed()) { + // Close any open folder + closeFolder(); + // Open the requested folder + openFolder(folderIcon); + } else { + // Find the open folder... + int folderScreen; + if (openFolder != null) { + folderScreen = mWorkspace.getPageForView(openFolder); + // .. and close it + closeFolder(openFolder); + if (folderScreen != mWorkspace.getCurrentPage()) { + // Close any folder open on the current screen + closeFolder(); + // Pull the folder onto this screen + openFolder(folderIcon); + } + } + } + } + + /** + * This method draws the FolderIcon to an ImageView and then adds and positions that ImageView + * in the DragLayer in the exact absolute location of the original FolderIcon. + */ + private void copyFolderIconToImage(FolderIcon fi) { + final int width = fi.getMeasuredWidth(); + final int height = fi.getMeasuredHeight(); + + // Lazy load ImageView, Bitmap and Canvas + if (mFolderIconImageView == null) { + mFolderIconImageView = new ImageView(this); + } + if (mFolderIconBitmap == null || mFolderIconBitmap.getWidth() != width || + mFolderIconBitmap.getHeight() != height) { + mFolderIconBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + mFolderIconCanvas = new Canvas(mFolderIconBitmap); + } + + DragLayer.LayoutParams lp; + if (mFolderIconImageView.getLayoutParams() instanceof DragLayer.LayoutParams) { + lp = (DragLayer.LayoutParams) mFolderIconImageView.getLayoutParams(); + } else { + lp = new DragLayer.LayoutParams(width, height); + } + + // The layout from which the folder is being opened may be scaled, adjust the starting + // view size by this scale factor. + float scale = mDragLayer.getDescendantRectRelativeToSelf(fi, mRectForFolderAnimation); + lp.customPosition = true; + lp.x = mRectForFolderAnimation.left; + lp.y = mRectForFolderAnimation.top; + lp.width = (int) (scale * width); + lp.height = (int) (scale * height); + + mFolderIconCanvas.drawColor(0, PorterDuff.Mode.CLEAR); + fi.draw(mFolderIconCanvas); + mFolderIconImageView.setImageBitmap(mFolderIconBitmap); + if (fi.getFolder() != null) { + mFolderIconImageView.setPivotX(fi.getFolder().getPivotXForIconAnimation()); + mFolderIconImageView.setPivotY(fi.getFolder().getPivotYForIconAnimation()); + } + // Just in case this image view is still in the drag layer from a previous animation, + // we remove it and re-add it. + if (mDragLayer.indexOfChild(mFolderIconImageView) != -1) { + mDragLayer.removeView(mFolderIconImageView); + } + mDragLayer.addView(mFolderIconImageView, lp); + if (fi.getFolder() != null) { + fi.getFolder().bringToFront(); + } + } + + private void growAndFadeOutFolderIcon(FolderIcon fi) { + if (fi == null) return; + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.5f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.5f); + + FolderInfo info = (FolderInfo) fi.getTag(); + if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + CellLayout cl = (CellLayout) fi.getParent().getParent(); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) fi.getLayoutParams(); + cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY); + } + + // Push an ImageView copy of the FolderIcon into the DragLayer and hide the original + copyFolderIconToImage(fi); + fi.setVisibility(View.INVISIBLE); + + ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(mFolderIconImageView, alpha, + scaleX, scaleY); + oa.setDuration(getResources().getInteger(R.integer.config_folderAnimDuration)); + oa.start(); + } + + private void shrinkAndFadeInFolderIcon(final FolderIcon fi) { + if (fi == null) return; + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); + + final CellLayout cl = (CellLayout) fi.getParent().getParent(); + + // We remove and re-draw the FolderIcon in-case it has changed + mDragLayer.removeView(mFolderIconImageView); + copyFolderIconToImage(fi); + ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(mFolderIconImageView, alpha, + scaleX, scaleY); + oa.setDuration(getResources().getInteger(R.integer.config_folderAnimDuration)); + oa.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (cl != null) { + cl.clearFolderLeaveBehind(); + // Remove the ImageView copy of the FolderIcon and make the original visible. + mDragLayer.removeView(mFolderIconImageView); + fi.setVisibility(View.VISIBLE); + } + } + }); + oa.start(); + } + + /** + * Opens the user folder described by the specified tag. The opening of the folder + * is animated relative to the specified View. If the View is null, no animation + * is played. + * + * @param folderInfo The FolderInfo describing the folder to open. + */ + public void openFolder(FolderIcon folderIcon) { + Folder folder = folderIcon.getFolder(); + FolderInfo info = folder.mInfo; + + info.opened = true; + + // Just verify that the folder hasn't already been added to the DragLayer. + // There was a one-off crash where the folder had a parent already. + if (folder.getParent() == null) { + mDragLayer.addView(folder); + mDragController.addDropTarget((DropTarget) folder); + } else { + Log.w(TAG, "Opening folder (" + folder + ") which already has a parent (" + + folder.getParent() + ")."); + } + folder.animateOpen(); + growAndFadeOutFolderIcon(folderIcon); + + // Notify the accessibility manager that this folder "window" has appeared and occluded + // the workspace items + folder.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + + public void closeFolder() { + Folder folder = mWorkspace.getOpenFolder(); + if (folder != null) { + if (folder.isEditingName()) { + folder.dismissEditingName(); + } + closeFolder(folder); + + // Dismiss the folder cling + dismissFolderCling(null); + } + } + + void closeFolder(Folder folder) { + folder.getInfo().opened = false; + + ViewGroup parent = (ViewGroup) folder.getParent().getParent(); + if (parent != null) { + FolderIcon fi = (FolderIcon) mWorkspace.getViewForTag(folder.mInfo); + shrinkAndFadeInFolderIcon(fi); + } + folder.animateClosed(); + + // Notify the accessibility manager that this folder "window" has disappeard and no + // longer occludeds the workspace items + getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + public boolean onLongClick(View v) { + if (!isDraggingEnabled()) return false; + if (isWorkspaceLocked()) return false; + if (mState != State.WORKSPACE) return false; + + if (!(v instanceof CellLayout)) { + v = (View) v.getParent().getParent(); + } + + resetAddInfo(); + CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag(); + // This happens when long clicking an item with the dpad/trackball + if (longClickCellInfo == null) { + return true; + } + + // The hotseat touch handling does not go through Workspace, and we always allow long press + // on hotseat items. + final View itemUnderLongClick = longClickCellInfo.cell; + boolean allowLongPress = isHotseatLayout(v) || mWorkspace.allowLongPress(); + if (allowLongPress && !mDragController.isDragging()) { + if (itemUnderLongClick == null) { + // User long pressed on empty space + mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + startWallpaper(); + } else { + if (!(itemUnderLongClick instanceof Folder)) { + // User long pressed on an item + mWorkspace.startDrag(longClickCellInfo); + } + } + } + return true; + } + + boolean isHotseatLayout(View layout) { + return mHotseat != null && layout != null && + (layout instanceof CellLayout) && (layout == mHotseat.getLayout()); + } + Hotseat getHotseat() { + return mHotseat; + } + SearchDropTargetBar getSearchBar() { + return mSearchDropTargetBar; + } + + /** + * Returns the CellLayout of the specified container at the specified screen. + */ + CellLayout getCellLayout(long container, int screen) { + if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + if (mHotseat != null) { + return mHotseat.getLayout(); + } else { + return null; + } + } else { + return (CellLayout) mWorkspace.getChildAt(screen); + } + } + + Workspace getWorkspace() { + return mWorkspace; + } + + // Now a part of LauncherModel.Callbacks. Used to reorder loading steps. + @Override + public boolean isAllAppsVisible() { + return (mState == State.APPS_CUSTOMIZE) || (mOnResumeState == State.APPS_CUSTOMIZE); + } + + @Override + public boolean isAllAppsButtonRank(int rank) { + return mHotseat.isAllAppsButtonRank(rank); + } + + /** + * Helper method for the cameraZoomIn/cameraZoomOut animations + * @param view The view being animated + * @param scaleFactor The scale factor used for the zoom + */ + private void setPivotsForZoom(View view, float scaleFactor) { + view.setPivotX(view.getWidth() / 2.0f); + view.setPivotY(view.getHeight() / 2.0f); + } + + void disableWallpaperIfInAllApps() { + // Only disable it if we are in all apps + if (isAllAppsVisible()) { + if (mAppsCustomizeTabHost != null && + !mAppsCustomizeTabHost.isTransitioning()) { + updateWallpaperVisibility(false); + } + } + } + + private void setWorkspaceBackground(boolean workspace) { + mLauncherView.setBackground(workspace ? + mWorkspaceBackgroundDrawable : null); + } + + void updateWallpaperVisibility(boolean visible) { + int wpflags = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0; + int curflags = getWindow().getAttributes().flags + & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + if (wpflags != curflags) { + getWindow().setFlags(wpflags, WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER); + } + setWorkspaceBackground(visible); + } + + private void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionPrepare(this, animated, toWorkspace); + } + } + + private void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStart(this, animated, toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 0f); + } + + private void dispatchOnLauncherTransitionStep(View v, float t) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStep(this, t); + } + } + + private void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionEnd(this, animated, toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 1f); + } + + /** + * Things to test when changing the following seven functions. + * - Home from workspace + * - from center screen + * - from other screens + * - Home from all apps + * - from center screen + * - from other screens + * - Back from all apps + * - from center screen + * - from other screens + * - Launch app from workspace and quit + * - with back + * - with home + * - Launch app from all apps and quit + * - with back + * - with home + * - Go to a screen that's not the default, then all + * apps, and launch and app, and go back + * - with back + * -with home + * - On workspace, long press power and go back + * - with back + * - with home + * - On all apps, long press power and go back + * - with back + * - with home + * - On workspace, power off + * - On all apps, power off + * - Launch an app and turn off the screen while in that app + * - Go back with home key + * - Go back with back key TODO: make this not go to workspace + * - From all apps + * - From workspace + * - Enter and exit car mode (becuase it causes an extra configuration changed) + * - From all apps + * - From the center workspace + * - From another workspace + */ + + /** + * Zoom the camera out from the workspace to reveal 'toView'. + * Assumes that the view to show is anchored at either the very top or very bottom + * of the screen. + */ + private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded) { + if (mStateAnimation != null) { + mStateAnimation.setDuration(0); + mStateAnimation.cancel(); + mStateAnimation = null; + } + final Resources res = getResources(); + + final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime); + final int fadeDuration = res.getInteger(R.integer.config_appsCustomizeFadeInTime); + final float scale = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); + final View fromView = mWorkspace; + final AppsCustomizeTabHost toView = mAppsCustomizeTabHost; + final int startDelay = + res.getInteger(R.integer.config_workspaceAppsCustomizeAnimationStagger); + + setPivotsForZoom(toView, scale); + + // Shrink workspaces away if going to AppsCustomize from workspace + Animator workspaceAnim = + mWorkspace.getChangeStateAnimation(Workspace.State.SMALL, animated); + + if (animated) { + toView.setScaleX(scale); + toView.setScaleY(scale); + final LauncherViewPropertyAnimator scaleAnim = new LauncherViewPropertyAnimator(toView); + scaleAnim. + scaleX(1f).scaleY(1f). + setDuration(duration). + setInterpolator(new Workspace.ZoomOutInterpolator()); + + toView.setVisibility(View.VISIBLE); + toView.setAlpha(0f); + final ObjectAnimator alphaAnim = LauncherAnimUtils + .ofFloat(toView, "alpha", 0f, 1f) + .setDuration(fadeDuration); + alphaAnim.setInterpolator(new DecelerateInterpolator(1.5f)); + alphaAnim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (animation == null) { + throw new RuntimeException("animation is null"); + } + float t = (Float) animation.getAnimatedValue(); + dispatchOnLauncherTransitionStep(fromView, t); + dispatchOnLauncherTransitionStep(toView, t); + } + }); + + // toView should appear right at the end of the workspace shrink + // animation + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + mStateAnimation.play(scaleAnim).after(startDelay); + mStateAnimation.play(alphaAnim).after(startDelay); + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + boolean animationCancelled = false; + + @Override + public void onAnimationStart(Animator animation) { + updateWallpaperVisibility(true); + // Prepare the position + toView.setTranslationX(0.0f); + toView.setTranslationY(0.0f); + toView.setVisibility(View.VISIBLE); + toView.bringToFront(); + } + @Override + public void onAnimationEnd(Animator animation) { + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); + + if (mWorkspace != null && !springLoaded && !LauncherApplication.isScreenLarge()) { + // Hide the workspace scrollbar + mWorkspace.hideScrollingIndicator(true); + hideDockDivider(); + } + if (!animationCancelled) { + updateWallpaperVisibility(false); + } + + // Hide the search bar + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.hideSearchBar(false); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + animationCancelled = true; + } + }); + + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + + boolean delayAnim = false; + + dispatchOnLauncherTransitionPrepare(fromView, animated, false); + dispatchOnLauncherTransitionPrepare(toView, animated, false); + + // If any of the objects being animated haven't been measured/laid out + // yet, delay the animation until we get a layout pass + if ((((LauncherTransitionable) toView).getContent().getMeasuredWidth() == 0) || + (mWorkspace.getMeasuredWidth() == 0) || + (toView.getMeasuredWidth() == 0)) { + delayAnim = true; + } + + final AnimatorSet stateAnimation = mStateAnimation; + final Runnable startAnimRunnable = new Runnable() { + public void run() { + // Check that mStateAnimation hasn't changed while + // we waited for a layout/draw pass + if (mStateAnimation != stateAnimation) + return; + setPivotsForZoom(toView, scale); + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView); + } + }; + if (delayAnim) { + final ViewTreeObserver observer = toView.getViewTreeObserver(); + observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + public void onGlobalLayout() { + startAnimRunnable.run(); + toView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + } else { + startAnimRunnable.run(); + } + } else { + toView.setTranslationX(0.0f); + toView.setTranslationY(0.0f); + toView.setScaleX(1.0f); + toView.setScaleY(1.0f); + toView.setVisibility(View.VISIBLE); + toView.bringToFront(); + + if (!springLoaded && !LauncherApplication.isScreenLarge()) { + // Hide the workspace scrollbar + mWorkspace.hideScrollingIndicator(true); + hideDockDivider(); + + // Hide the search bar + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.hideSearchBar(false); + } + } + dispatchOnLauncherTransitionPrepare(fromView, animated, false); + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionPrepare(toView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); + updateWallpaperVisibility(false); + } + } + + /** + * Zoom the camera back into the workspace, hiding 'fromView'. + * This is the opposite of showAppsCustomizeHelper. + * @param animated If true, the transition will be animated. + */ + private void hideAppsCustomizeHelper(State toState, final boolean animated, + final boolean springLoaded, final Runnable onCompleteRunnable) { + + if (mStateAnimation != null) { + mStateAnimation.setDuration(0); + mStateAnimation.cancel(); + mStateAnimation = null; + } + Resources res = getResources(); + + final int duration = res.getInteger(R.integer.config_appsCustomizeZoomOutTime); + final int fadeOutDuration = + res.getInteger(R.integer.config_appsCustomizeFadeOutTime); + final float scaleFactor = (float) + res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); + final View fromView = mAppsCustomizeTabHost; + final View toView = mWorkspace; + Animator workspaceAnim = null; + + if (toState == State.WORKSPACE) { + int stagger = res.getInteger(R.integer.config_appsCustomizeWorkspaceAnimationStagger); + workspaceAnim = mWorkspace.getChangeStateAnimation( + Workspace.State.NORMAL, animated, stagger); + } else if (toState == State.APPS_CUSTOMIZE_SPRING_LOADED) { + workspaceAnim = mWorkspace.getChangeStateAnimation( + Workspace.State.SPRING_LOADED, animated); + } + + setPivotsForZoom(fromView, scaleFactor); + updateWallpaperVisibility(true); + showHotseat(animated); + if (animated) { + final LauncherViewPropertyAnimator scaleAnim = + new LauncherViewPropertyAnimator(fromView); + scaleAnim. + scaleX(scaleFactor).scaleY(scaleFactor). + setDuration(duration). + setInterpolator(new Workspace.ZoomInInterpolator()); + + final ObjectAnimator alphaAnim = LauncherAnimUtils + .ofFloat(fromView, "alpha", 1f, 0f) + .setDuration(fadeOutDuration); + alphaAnim.setInterpolator(new AccelerateDecelerateInterpolator()); + alphaAnim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = 1f - (Float) animation.getAnimatedValue(); + dispatchOnLauncherTransitionStep(fromView, t); + dispatchOnLauncherTransitionStep(toView, t); + } + }); + + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + mAppsCustomizeContent.pauseScrolling(); + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + updateWallpaperVisibility(true); + fromView.setVisibility(View.GONE); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); + if (mWorkspace != null) { + mWorkspace.hideScrollingIndicator(false); + } + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + mAppsCustomizeContent.updateCurrentPageScroll(); + mAppsCustomizeContent.resumeScrolling(); + } + }); + + mStateAnimation.playTogether(scaleAnim, alphaAnim); + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + dispatchOnLauncherTransitionStart(fromView, animated, true); + dispatchOnLauncherTransitionStart(toView, animated, true); + LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView); + } else { + fromView.setVisibility(View.GONE); + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionStart(fromView, animated, true); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + dispatchOnLauncherTransitionStart(toView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); + mWorkspace.hideScrollingIndicator(false); + } + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { + mAppsCustomizeTabHost.onTrimMemory(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (!hasFocus) { + // When another window occludes launcher (like the notification shade, or recents), + // ensure that we enable the wallpaper flag so that transitions are done correctly. + updateWallpaperVisibility(true); + } else { + // When launcher has focus again, disable the wallpaper if we are in AllApps + mWorkspace.postDelayed(new Runnable() { + @Override + public void run() { + disableWallpaperIfInAllApps(); + } + }, 500); + } + } + + void showWorkspace(boolean animated) { + showWorkspace(animated, null); + } + + void showWorkspace(boolean animated, Runnable onCompleteRunnable) { + if (mState != State.WORKSPACE) { + boolean wasInSpringLoadedMode = (mState == State.APPS_CUSTOMIZE_SPRING_LOADED); + mWorkspace.setVisibility(View.VISIBLE); + hideAppsCustomizeHelper(State.WORKSPACE, animated, false, onCompleteRunnable); + + // Show the search bar (only animate if we were showing the drop target bar in spring + // loaded mode) + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.showSearchBar(wasInSpringLoadedMode); + } + + // We only need to animate in the dock divider if we're going from spring loaded mode + showDockDivider(animated && wasInSpringLoadedMode); + + // Set focus to the AppsCustomize button + if (mAllAppsButton != null) { + mAllAppsButton.requestFocus(); + } + } + + mWorkspace.flashScrollingIndicator(animated); + + // Change the state *after* we've called all the transition code + mState = State.WORKSPACE; + + // Resume the auto-advance of widgets + mUserPresent = true; + updateRunning(); + + // Send an accessibility event to announce the context change + getWindow().getDecorView() + .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void showAllApps(boolean animated) { + if (mState != State.WORKSPACE) return; + + showAppsCustomizeHelper(animated, false); + mAppsCustomizeTabHost.requestFocus(); + + // Change the state *after* we've called all the transition code + mState = State.APPS_CUSTOMIZE; + + // Pause the auto-advance of widgets until we are out of AllApps + mUserPresent = false; + updateRunning(); + closeFolder(); + + // Send an accessibility event to announce the context change + getWindow().getDecorView() + .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void enterSpringLoadedDragMode() { + if (isAllAppsVisible()) { + hideAppsCustomizeHelper(State.APPS_CUSTOMIZE_SPRING_LOADED, true, true, null); + hideDockDivider(); + mState = State.APPS_CUSTOMIZE_SPRING_LOADED; + } + } + + void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, boolean extendedDelay, + final Runnable onCompleteRunnable) { + if (mState != State.APPS_CUSTOMIZE_SPRING_LOADED) return; + + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (successfulDrop) { + // Before we show workspace, hide all apps again because + // exitSpringLoadedDragMode made it visible. This is a bit hacky; we should + // clean up our state transition functions + mAppsCustomizeTabHost.setVisibility(View.GONE); + showWorkspace(true, onCompleteRunnable); + } else { + exitSpringLoadedDragMode(); + } + } + }, (extendedDelay ? + EXIT_SPRINGLOADED_MODE_LONG_TIMEOUT : + EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT)); + } + + void exitSpringLoadedDragMode() { + if (mState == State.APPS_CUSTOMIZE_SPRING_LOADED) { + final boolean animated = true; + final boolean springLoaded = true; + showAppsCustomizeHelper(animated, springLoaded); + mState = State.APPS_CUSTOMIZE; + } + // Otherwise, we are not in spring loaded mode, so don't do anything. + } + + void hideDockDivider() { + if (mQsbDivider != null && mDockDivider != null) { + mQsbDivider.setVisibility(View.INVISIBLE); + mDockDivider.setVisibility(View.INVISIBLE); + } + } + + void showDockDivider(boolean animated) { + if (mQsbDivider != null && mDockDivider != null) { + mQsbDivider.setVisibility(View.VISIBLE); + mDockDivider.setVisibility(View.VISIBLE); + if (mDividerAnimator != null) { + mDividerAnimator.cancel(); + mQsbDivider.setAlpha(1f); + mDockDivider.setAlpha(1f); + mDividerAnimator = null; + } + if (animated) { + mDividerAnimator = LauncherAnimUtils.createAnimatorSet(); + mDividerAnimator.playTogether(LauncherAnimUtils.ofFloat(mQsbDivider, "alpha", 1f), + LauncherAnimUtils.ofFloat(mDockDivider, "alpha", 1f)); + int duration = 0; + if (mSearchDropTargetBar != null) { + duration = mSearchDropTargetBar.getTransitionInDuration(); + } + mDividerAnimator.setDuration(duration); + mDividerAnimator.start(); + } + } + } + + void lockAllApps() { + // TODO + } + + void unlockAllApps() { + // TODO + } + + /** + * Shows the hotseat area. + */ + void showHotseat(boolean animated) { + if (!LauncherApplication.isScreenLarge()) { + if (animated) { + if (mHotseat.getAlpha() != 1f) { + int duration = 0; + if (mSearchDropTargetBar != null) { + duration = mSearchDropTargetBar.getTransitionInDuration(); + } + mHotseat.animate().alpha(1f).setDuration(duration); + } + } else { + mHotseat.setAlpha(1f); + } + } + } + + /** + * Hides the hotseat area. + */ + void hideHotseat(boolean animated) { + if (!LauncherApplication.isScreenLarge()) { + if (animated) { + if (mHotseat.getAlpha() != 0f) { + int duration = 0; + if (mSearchDropTargetBar != null) { + duration = mSearchDropTargetBar.getTransitionOutDuration(); + } + mHotseat.animate().alpha(0f).setDuration(duration); + } + } else { + mHotseat.setAlpha(0f); + } + } + } + + /** + * Add an item from all apps or customize onto the given workspace screen. + * If layout is null, add to the current screen. + */ + void addExternalItemToScreen(ItemInfo itemInfo, final CellLayout layout) { + if (!mWorkspace.addExternalItemToScreen(itemInfo, layout)) { + showOutOfSpaceMessage(isHotseatLayout(layout)); + } + } + + /** Maps the current orientation to an index for referencing orientation correct global icons */ + private int getCurrentOrientationIndexForGlobalIcons() { + // default - 0, landscape - 1 + switch (getResources().getConfiguration().orientation) { + case Configuration.ORIENTATION_LANDSCAPE: + return 1; + default: + return 0; + } + } + + private Drawable getExternalPackageToolbarIcon(ComponentName activityName, String resourceName) { + try { + PackageManager packageManager = getPackageManager(); + // Look for the toolbar icon specified in the activity meta-data + Bundle metaData = packageManager.getActivityInfo( + activityName, PackageManager.GET_META_DATA).metaData; + if (metaData != null) { + int iconResId = metaData.getInt(resourceName); + if (iconResId != 0) { + Resources res = packageManager.getResourcesForActivity(activityName); + return res.getDrawable(iconResId); + } + } + } catch (NameNotFoundException e) { + // This can happen if the activity defines an invalid drawable + Log.w(TAG, "Failed to load toolbar icon; " + activityName.flattenToShortString() + + " not found", e); + } catch (Resources.NotFoundException nfe) { + // This can happen if the activity defines an invalid drawable + Log.w(TAG, "Failed to load toolbar icon from " + activityName.flattenToShortString(), + nfe); + } + return null; + } + + // if successful in getting icon, return it; otherwise, set button to use default drawable + private Drawable.ConstantState updateTextButtonWithIconFromExternalActivity( + int buttonId, ComponentName activityName, int fallbackDrawableId, + String toolbarResourceName) { + Drawable toolbarIcon = getExternalPackageToolbarIcon(activityName, toolbarResourceName); + Resources r = getResources(); + int w = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_width); + int h = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_height); + + TextView button = (TextView) findViewById(buttonId); + // If we were unable to find the icon via the meta-data, use a generic one + if (toolbarIcon == null) { + toolbarIcon = r.getDrawable(fallbackDrawableId); + toolbarIcon.setBounds(0, 0, w, h); + if (button != null) { + button.setCompoundDrawables(toolbarIcon, null, null, null); + } + return null; + } else { + toolbarIcon.setBounds(0, 0, w, h); + if (button != null) { + button.setCompoundDrawables(toolbarIcon, null, null, null); + } + return toolbarIcon.getConstantState(); + } + } + + // if successful in getting icon, return it; otherwise, set button to use default drawable + private Drawable.ConstantState updateButtonWithIconFromExternalActivity( + int buttonId, ComponentName activityName, int fallbackDrawableId, + String toolbarResourceName) { + ImageView button = (ImageView) findViewById(buttonId); + Drawable toolbarIcon = getExternalPackageToolbarIcon(activityName, toolbarResourceName); + + if (button != null) { + // If we were unable to find the icon via the meta-data, use a + // generic one + if (toolbarIcon == null) { + button.setImageResource(fallbackDrawableId); + } else { + button.setImageDrawable(toolbarIcon); + } + } + + return toolbarIcon != null ? toolbarIcon.getConstantState() : null; + + } + + private void updateTextButtonWithDrawable(int buttonId, Drawable d) { + TextView button = (TextView) findViewById(buttonId); + button.setCompoundDrawables(d, null, null, null); + } + + private void updateButtonWithDrawable(int buttonId, Drawable.ConstantState d) { + ImageView button = (ImageView) findViewById(buttonId); + button.setImageDrawable(d.newDrawable(getResources())); + } + + private void invalidatePressedFocusedStates(View container, View button) { + if (container instanceof HolographicLinearLayout) { + HolographicLinearLayout layout = (HolographicLinearLayout) container; + layout.invalidatePressedFocusedStates(); + } else if (button instanceof HolographicImageView) { + HolographicImageView view = (HolographicImageView) button; + view.invalidatePressedFocusedStates(); + } + } + + private boolean updateGlobalSearchIcon() { + final View searchButtonContainer = findViewById(R.id.search_button_container); + final ImageView searchButton = (ImageView) findViewById(R.id.search_button); + final View voiceButtonContainer = findViewById(R.id.voice_button_container); + final View voiceButton = findViewById(R.id.voice_button); + final View voiceButtonProxy = findViewById(R.id.voice_button_proxy); + + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + ComponentName activityName = searchManager.getGlobalSearchActivity(); + if (activityName != null) { + int coi = getCurrentOrientationIndexForGlobalIcons(); + sGlobalSearchIcon[coi] = updateButtonWithIconFromExternalActivity( + R.id.search_button, activityName, R.drawable.ic_home_search_normal_holo, + TOOLBAR_SEARCH_ICON_METADATA_NAME); + if (sGlobalSearchIcon[coi] == null) { + sGlobalSearchIcon[coi] = updateButtonWithIconFromExternalActivity( + R.id.search_button, activityName, R.drawable.ic_home_search_normal_holo, + TOOLBAR_ICON_METADATA_NAME); + } + + if (searchButtonContainer != null) searchButtonContainer.setVisibility(View.VISIBLE); + searchButton.setVisibility(View.VISIBLE); + invalidatePressedFocusedStates(searchButtonContainer, searchButton); + return true; + } else { + // We disable both search and voice search when there is no global search provider + if (searchButtonContainer != null) searchButtonContainer.setVisibility(View.GONE); + if (voiceButtonContainer != null) voiceButtonContainer.setVisibility(View.GONE); + searchButton.setVisibility(View.GONE); + voiceButton.setVisibility(View.GONE); + if (voiceButtonProxy != null) { + voiceButtonProxy.setVisibility(View.GONE); + } + return false; + } + } + + private void updateGlobalSearchIcon(Drawable.ConstantState d) { + final View searchButtonContainer = findViewById(R.id.search_button_container); + final View searchButton = (ImageView) findViewById(R.id.search_button); + updateButtonWithDrawable(R.id.search_button, d); + invalidatePressedFocusedStates(searchButtonContainer, searchButton); + } + + private boolean updateVoiceSearchIcon(boolean searchVisible) { + final View voiceButtonContainer = findViewById(R.id.voice_button_container); + final View voiceButton = findViewById(R.id.voice_button); + final View voiceButtonProxy = findViewById(R.id.voice_button_proxy); + + // We only show/update the voice search icon if the search icon is enabled as well + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + ComponentName globalSearchActivity = searchManager.getGlobalSearchActivity(); + + ComponentName activityName = null; + if (globalSearchActivity != null) { + // Check if the global search activity handles voice search + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + intent.setPackage(globalSearchActivity.getPackageName()); + activityName = intent.resolveActivity(getPackageManager()); + } + + if (activityName == null) { + // Fallback: check if an activity other than the global search activity + // resolves this + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + activityName = intent.resolveActivity(getPackageManager()); + } + if (searchVisible && activityName != null) { + int coi = getCurrentOrientationIndexForGlobalIcons(); + sVoiceSearchIcon[coi] = updateButtonWithIconFromExternalActivity( + R.id.voice_button, activityName, R.drawable.ic_home_voice_search_holo, + TOOLBAR_VOICE_SEARCH_ICON_METADATA_NAME); + if (sVoiceSearchIcon[coi] == null) { + sVoiceSearchIcon[coi] = updateButtonWithIconFromExternalActivity( + R.id.voice_button, activityName, R.drawable.ic_home_voice_search_holo, + TOOLBAR_ICON_METADATA_NAME); + } + if (voiceButtonContainer != null) voiceButtonContainer.setVisibility(View.VISIBLE); + voiceButton.setVisibility(View.VISIBLE); + if (voiceButtonProxy != null) { + voiceButtonProxy.setVisibility(View.VISIBLE); + } + invalidatePressedFocusedStates(voiceButtonContainer, voiceButton); + return true; + } else { + if (voiceButtonContainer != null) voiceButtonContainer.setVisibility(View.GONE); + voiceButton.setVisibility(View.GONE); + if (voiceButtonProxy != null) { + voiceButtonProxy.setVisibility(View.GONE); + } + return false; + } + } + + private void updateVoiceSearchIcon(Drawable.ConstantState d) { + final View voiceButtonContainer = findViewById(R.id.voice_button_container); + final View voiceButton = findViewById(R.id.voice_button); + updateButtonWithDrawable(R.id.voice_button, d); + invalidatePressedFocusedStates(voiceButtonContainer, voiceButton); + } + + /** + * Sets the app market icon + */ + private void updateAppMarketIcon() { + final View marketButton = findViewById(R.id.market_button); + Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_APP_MARKET); + // Find the app market activity by resolving an intent. + // (If multiple app markets are installed, it will return the ResolverActivity.) + ComponentName activityName = intent.resolveActivity(getPackageManager()); + if (activityName != null) { + int coi = getCurrentOrientationIndexForGlobalIcons(); + mAppMarketIntent = intent; + sAppMarketIcon[coi] = updateTextButtonWithIconFromExternalActivity( + R.id.market_button, activityName, R.drawable.ic_launcher_market_holo, + TOOLBAR_ICON_METADATA_NAME); + marketButton.setVisibility(View.VISIBLE); + } else { + // We should hide and disable the view so that we don't try and restore the visibility + // of it when we swap between drag & normal states from IconDropTarget subclasses. + marketButton.setVisibility(View.GONE); + marketButton.setEnabled(false); + } + } + + private void updateAppMarketIcon(Drawable.ConstantState d) { + // Ensure that the new drawable we are creating has the approprate toolbar icon bounds + Resources r = getResources(); + Drawable marketIconDrawable = d.newDrawable(r); + int w = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_width); + int h = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_height); + marketIconDrawable.setBounds(0, 0, w, h); + + updateTextButtonWithDrawable(R.id.market_button, marketIconDrawable); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + final boolean result = super.dispatchPopulateAccessibilityEvent(event); + final List<CharSequence> text = event.getText(); + text.clear(); + // Populate event with a fake title based on the current state. + if (mState == State.APPS_CUSTOMIZE) { + text.add(getString(R.string.all_apps_button_label)); + } else { + text.add(getString(R.string.all_apps_home_button_label)); + } + return result; + } + + /** + * Receives notifications when system dialogs are to be closed. + */ + private class CloseSystemDialogsIntentReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + closeSystemDialogs(); + } + } + + /** + * Receives notifications whenever the appwidgets are reset. + */ + private class AppWidgetResetObserver extends ContentObserver { + public AppWidgetResetObserver() { + super(new Handler()); + } + + @Override + public void onChange(boolean selfChange) { + onAppWidgetReset(); + } + } + + /** + * If the activity is currently paused, signal that we need to run the passed Runnable + * in onResume. + * + * This needs to be called from incoming places where resources might have been loaded + * while we are paused. That is becaues the Configuration might be wrong + * when we're not running, and if it comes back to what it was when we + * were paused, we are not restarted. + * + * Implementation of the method from LauncherModel.Callbacks. + * + * @return true if we are currently paused. The caller might be able to + * skip some work in that case since we will come back again. + */ + private boolean waitUntilResume(Runnable run, boolean deletePreviousRunnables) { + if (mPaused) { + Log.i(TAG, "Deferring update until onResume"); + if (deletePreviousRunnables) { + while (mOnResumeCallbacks.remove(run)) { + } + } + mOnResumeCallbacks.add(run); + return true; + } else { + return false; + } + } + + private boolean waitUntilResume(Runnable run) { + return waitUntilResume(run, false); + } + + /** + * If the activity is currently paused, signal that we need to re-run the loader + * in onResume. + * + * This needs to be called from incoming places where resources might have been loaded + * while we are paused. That is becaues the Configuration might be wrong + * when we're not running, and if it comes back to what it was when we + * were paused, we are not restarted. + * + * Implementation of the method from LauncherModel.Callbacks. + * + * @return true if we are currently paused. The caller might be able to + * skip some work in that case since we will come back again. + */ + public boolean setLoadOnResume() { + if (mPaused) { + Log.i(TAG, "setLoadOnResume"); + mOnResumeNeedsLoad = true; + return true; + } else { + return false; + } + } + + /** + * Implementation of the method from LauncherModel.Callbacks. + */ + public int getCurrentWorkspaceScreen() { + if (mWorkspace != null) { + return mWorkspace.getCurrentPage(); + } else { + return SCREEN_COUNT / 2; + } + } + + /** + * Refreshes the shortcuts shown on the workspace. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void startBinding() { + // If we're starting binding all over again, clear any bind calls we'd postponed in + // the past (see waitUntilResume) -- we don't need them since we're starting binding + // from scratch again + mOnResumeCallbacks.clear(); + + final Workspace workspace = mWorkspace; + mNewShortcutAnimatePage = -1; + mNewShortcutAnimateViews.clear(); + mWorkspace.clearDropTargets(); + int count = workspace.getChildCount(); + for (int i = 0; i < count; i++) { + // Use removeAllViewsInLayout() to avoid an extra requestLayout() and invalidate(). + final CellLayout layoutParent = (CellLayout) workspace.getChildAt(i); + layoutParent.removeAllViewsInLayout(); + } + mWidgetsToAdvance.clear(); + if (mHotseat != null) { + mHotseat.resetLayout(); + } + } + + /** + * Bind the items start-end from the list. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindItems(final ArrayList<ItemInfo> shortcuts, final int start, final int end) { + if (waitUntilResume(new Runnable() { + public void run() { + bindItems(shortcuts, start, end); + } + })) { + return; + } + + // Get the list of added shortcuts and intersect them with the set of shortcuts here + Set<String> newApps = new HashSet<String>(); + newApps = mSharedPrefs.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, newApps); + + Workspace workspace = mWorkspace; + for (int i = start; i < end; i++) { + final ItemInfo item = shortcuts.get(i); + + // Short circuit if we are loading dock items for a configuration which has no dock + if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT && + mHotseat == null) { + continue; + } + + switch (item.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + ShortcutInfo info = (ShortcutInfo) item; + String uri = info.intent.toUri(0).toString(); + View shortcut = createShortcut(info); + workspace.addInScreen(shortcut, item.container, item.screen, item.cellX, + item.cellY, 1, 1, false); + boolean animateIconUp = false; + synchronized (newApps) { + if (newApps.contains(uri)) { + animateIconUp = newApps.remove(uri); + } + } + if (animateIconUp) { + // Prepare the view to be animated up + shortcut.setAlpha(0f); + shortcut.setScaleX(0f); + shortcut.setScaleY(0f); + mNewShortcutAnimatePage = item.screen; + if (!mNewShortcutAnimateViews.contains(shortcut)) { + mNewShortcutAnimateViews.add(shortcut); + } + } + break; + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, + (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), + (FolderInfo) item, mIconCache); + workspace.addInScreen(newFolder, item.container, item.screen, item.cellX, + item.cellY, 1, 1, false); + break; + } + } + + workspace.requestLayout(); + } + + /** + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindFolders(final HashMap<Long, FolderInfo> folders) { + if (waitUntilResume(new Runnable() { + public void run() { + bindFolders(folders); + } + })) { + return; + } + sFolders.clear(); + sFolders.putAll(folders); + } + + /** + * Add the views for a widget to the workspace. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindAppWidget(final LauncherAppWidgetInfo item) { + if (waitUntilResume(new Runnable() { + public void run() { + bindAppWidget(item); + } + })) { + return; + } + + final long start = DEBUG_WIDGETS ? SystemClock.uptimeMillis() : 0; + if (DEBUG_WIDGETS) { + Log.d(TAG, "bindAppWidget: " + item); + } + final Workspace workspace = mWorkspace; + + final int appWidgetId = item.appWidgetId; + final AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); + if (DEBUG_WIDGETS) { + Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + appWidgetInfo.provider); + } + + item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + + item.hostView.setTag(item); + item.onBindAppWidget(this); + + workspace.addInScreen(item.hostView, item.container, item.screen, item.cellX, + item.cellY, item.spanX, item.spanY, false); + addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); + + workspace.requestLayout(); + + if (DEBUG_WIDGETS) { + Log.d(TAG, "bound widget id="+item.appWidgetId+" in " + + (SystemClock.uptimeMillis()-start) + "ms"); + } + } + + public void onPageBoundSynchronously(int page) { + mSynchronouslyBoundPages.add(page); + } + + /** + * Callback saying that there aren't any more items to bind. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void finishBindingItems() { + if (waitUntilResume(new Runnable() { + public void run() { + finishBindingItems(); + } + })) { + return; + } + if (mSavedState != null) { + if (!mWorkspace.hasFocus()) { + mWorkspace.getChildAt(mWorkspace.getCurrentPage()).requestFocus(); + } + mSavedState = null; + } + + mWorkspace.restoreInstanceStateForRemainingPages(); + + // If we received the result of any pending adds while the loader was running (e.g. the + // widget configuration forced an orientation change), process them now. + for (int i = 0; i < sPendingAddList.size(); i++) { + completeAdd(sPendingAddList.get(i)); + } + sPendingAddList.clear(); + + // Update the market app icon as necessary (the other icons will be managed in response to + // package changes in bindSearchablesChanged() + updateAppMarketIcon(); + + // Animate up any icons as necessary + if (mVisible || mWorkspaceLoading) { + Runnable newAppsRunnable = new Runnable() { + @Override + public void run() { + runNewAppsAnimation(false); + } + }; + + boolean willSnapPage = mNewShortcutAnimatePage > -1 && + mNewShortcutAnimatePage != mWorkspace.getCurrentPage(); + if (canRunNewAppsAnimation()) { + // If the user has not interacted recently, then either snap to the new page to show + // the new-apps animation or just run them if they are to appear on the current page + if (willSnapPage) { + mWorkspace.snapToPage(mNewShortcutAnimatePage, newAppsRunnable); + } else { + runNewAppsAnimation(false); + } + } else { + // If the user has interacted recently, then just add the items in place if they + // are on another page (or just normally if they are added to the current page) + runNewAppsAnimation(willSnapPage); + } + } + + mWorkspaceLoading = false; + } + + private boolean canRunNewAppsAnimation() { + long diff = System.currentTimeMillis() - mDragController.getLastGestureUpTime(); + return diff > (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000); + } + + /** + * Runs a new animation that scales up icons that were added while Launcher was in the + * background. + * + * @param immediate whether to run the animation or show the results immediately + */ + private void runNewAppsAnimation(boolean immediate) { + AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); + Collection<Animator> bounceAnims = new ArrayList<Animator>(); + + // Order these new views spatially so that they animate in order + Collections.sort(mNewShortcutAnimateViews, new Comparator<View>() { + @Override + public int compare(View a, View b) { + CellLayout.LayoutParams alp = (CellLayout.LayoutParams) a.getLayoutParams(); + CellLayout.LayoutParams blp = (CellLayout.LayoutParams) b.getLayoutParams(); + int cellCountX = LauncherModel.getCellCountX(); + return (alp.cellY * cellCountX + alp.cellX) - (blp.cellY * cellCountX + blp.cellX); + } + }); + + // Animate each of the views in place (or show them immediately if requested) + if (immediate) { + for (View v : mNewShortcutAnimateViews) { + v.setAlpha(1f); + v.setScaleX(1f); + v.setScaleY(1f); + } + } else { + for (int i = 0; i < mNewShortcutAnimateViews.size(); ++i) { + View v = mNewShortcutAnimateViews.get(i); + ValueAnimator bounceAnim = LauncherAnimUtils.ofPropertyValuesHolder(v, + PropertyValuesHolder.ofFloat("alpha", 1f), + PropertyValuesHolder.ofFloat("scaleX", 1f), + PropertyValuesHolder.ofFloat("scaleY", 1f)); + bounceAnim.setDuration(InstallShortcutReceiver.NEW_SHORTCUT_BOUNCE_DURATION); + bounceAnim.setStartDelay(i * InstallShortcutReceiver.NEW_SHORTCUT_STAGGER_DELAY); + bounceAnim.setInterpolator(new SmoothPagedView.OvershootInterpolator()); + bounceAnims.add(bounceAnim); + } + anim.playTogether(bounceAnims); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mWorkspace != null) { + mWorkspace.postDelayed(mBuildLayersRunnable, 500); + } + } + }); + anim.start(); + } + + // Clean up + mNewShortcutAnimatePage = -1; + mNewShortcutAnimateViews.clear(); + new Thread("clearNewAppsThread") { + public void run() { + mSharedPrefs.edit() + .putInt(InstallShortcutReceiver.NEW_APPS_PAGE_KEY, -1) + .putStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, null) + .commit(); + } + }.start(); + } + + @Override + public void bindSearchablesChanged() { + boolean searchVisible = updateGlobalSearchIcon(); + boolean voiceVisible = updateVoiceSearchIcon(searchVisible); + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.onSearchPackagesChanged(searchVisible, voiceVisible); + } + } + + /** + * Add the icons for all apps. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindAllApplications(final ArrayList<ApplicationInfo> apps) { + Runnable setAllAppsRunnable = new Runnable() { + public void run() { + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.setApps(apps); + } + } + }; + + // Remove the progress bar entirely; we could also make it GONE + // but better to remove it since we know it's not going to be used + View progressBar = mAppsCustomizeTabHost. + findViewById(R.id.apps_customize_progress_bar); + if (progressBar != null) { + ((ViewGroup)progressBar.getParent()).removeView(progressBar); + + // We just post the call to setApps so the user sees the progress bar + // disappear-- otherwise, it just looks like the progress bar froze + // which doesn't look great + mAppsCustomizeTabHost.post(setAllAppsRunnable); + } else { + // If we did not initialize the spinner in onCreate, then we can directly set the + // list of applications without waiting for any progress bars views to be hidden. + setAllAppsRunnable.run(); + } + } + + /** + * A package was installed. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindAppsAdded(final ArrayList<ApplicationInfo> apps) { + if (waitUntilResume(new Runnable() { + public void run() { + bindAppsAdded(apps); + } + })) { + return; + } + + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.addApps(apps); + } + } + + /** + * A package was updated. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindAppsUpdated(final ArrayList<ApplicationInfo> apps) { + if (waitUntilResume(new Runnable() { + public void run() { + bindAppsUpdated(apps); + } + })) { + return; + } + + if (mWorkspace != null) { + mWorkspace.updateShortcuts(apps); + } + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.updateApps(apps); + } + } + + /** + * A package was uninstalled. We take both the super set of packageNames + * in addition to specific applications to remove, the reason being that + * this can be called when a package is updated as well. In that scenario, + * we only remove specific components from the workspace, where as + * package-removal should clear all items by package name. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindComponentsRemoved(final ArrayList<String> packageNames, + final ArrayList<ApplicationInfo> appInfos, + final boolean matchPackageNamesOnly) { + if (waitUntilResume(new Runnable() { + public void run() { + bindComponentsRemoved(packageNames, appInfos, matchPackageNamesOnly); + } + })) { + return; + } + + if (matchPackageNamesOnly) { + mWorkspace.removeItemsByPackageName(packageNames); + } else { + mWorkspace.removeItemsByApplicationInfo(appInfos); + } + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.removeApps(appInfos); + } + + // Notify the drag controller + mDragController.onAppsRemoved(appInfos, this); + } + + /** + * A number of packages were updated. + */ + + private ArrayList<Object> mWidgetsAndShortcuts; + private Runnable mBindPackagesUpdatedRunnable = new Runnable() { + public void run() { + bindPackagesUpdated(mWidgetsAndShortcuts); + mWidgetsAndShortcuts = null; + } + }; + + public void bindPackagesUpdated(final ArrayList<Object> widgetsAndShortcuts) { + if (waitUntilResume(mBindPackagesUpdatedRunnable, true)) { + mWidgetsAndShortcuts = widgetsAndShortcuts; + return; + } + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.onPackagesUpdated(widgetsAndShortcuts); + } + } + + private int mapConfigurationOriActivityInfoOri(int configOri) { + final Display d = getWindowManager().getDefaultDisplay(); + int naturalOri = Configuration.ORIENTATION_LANDSCAPE; + switch (d.getRotation()) { + case Surface.ROTATION_0: + case Surface.ROTATION_180: + // We are currently in the same basic orientation as the natural orientation + naturalOri = configOri; + break; + case Surface.ROTATION_90: + case Surface.ROTATION_270: + // We are currently in the other basic orientation to the natural orientation + naturalOri = (configOri == Configuration.ORIENTATION_LANDSCAPE) ? + Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; + break; + } + + int[] oriMap = { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + }; + // Since the map starts at portrait, we need to offset if this device's natural orientation + // is landscape. + int indexOffset = 0; + if (naturalOri == Configuration.ORIENTATION_LANDSCAPE) { + indexOffset = 1; + } + return oriMap[(d.getRotation() + indexOffset) % 4]; + } + + public boolean isRotationEnabled() { + boolean enableRotation = sForceEnableRotation || + getResources().getBoolean(R.bool.allow_rotation); + return enableRotation; + } + public void lockScreenOrientation() { + if (isRotationEnabled()) { + setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources() + .getConfiguration().orientation)); + } + } + public void unlockScreenOrientation(boolean immediate) { + if (isRotationEnabled()) { + if (immediate) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } else { + mHandler.postDelayed(new Runnable() { + public void run() { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + }, mRestoreScreenOrientationDelay); + } + } + } + + /* Cling related */ + private boolean isClingsEnabled() { + // disable clings when running in a test harness + if(ActivityManager.isRunningInTestHarness()) return false; + + // Restricted secondary users (child mode) will potentially have very few apps + // seeded when they start up for the first time. Clings won't work well with that +// boolean supportsLimitedUsers = +// android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; +// Account[] accounts = AccountManager.get(this).getAccounts(); +// if (supportsLimitedUsers && accounts.length == 0) { +// UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); +// Bundle restrictions = um.getUserRestrictions(); +// if (restrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false)) { +// return false; +// } +// } + return true; + } + + private Cling initCling(int clingId, int[] positionData, boolean animate, int delay) { + final Cling cling = (Cling) findViewById(clingId); + if (cling != null) { + cling.init(this, positionData); + cling.setVisibility(View.VISIBLE); + cling.setLayerType(View.LAYER_TYPE_HARDWARE, null); + if (animate) { + cling.buildLayer(); + cling.setAlpha(0f); + cling.animate() + .alpha(1f) + .setInterpolator(new AccelerateInterpolator()) + .setDuration(SHOW_CLING_DURATION) + .setStartDelay(delay) + .start(); + } else { + cling.setAlpha(1f); + } + cling.setFocusableInTouchMode(true); + cling.post(new Runnable() { + public void run() { + cling.setFocusable(true); + cling.requestFocus(); + } + }); + mHideFromAccessibilityHelper.setImportantForAccessibilityToNo( + mDragLayer, clingId == R.id.all_apps_cling); + } + return cling; + } + + private void dismissCling(final Cling cling, final String flag, int duration) { + // To catch cases where siblings of top-level views are made invisible, just check whether + // the cling is directly set to GONE before dismissing it. + if (cling != null && cling.getVisibility() != View.GONE) { + ObjectAnimator anim = LauncherAnimUtils.ofFloat(cling, "alpha", 0f); + anim.setDuration(duration); + anim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + cling.setVisibility(View.GONE); + cling.cleanup(); + // We should update the shared preferences on a background thread + new Thread("dismissClingThread") { + public void run() { + SharedPreferences.Editor editor = mSharedPrefs.edit(); + editor.putBoolean(flag, true); + editor.commit(); + } + }.start(); + }; + }); + anim.start(); + mHideFromAccessibilityHelper.restoreImportantForAccessibility(mDragLayer); + } + } + + private void removeCling(int id) { + final View cling = findViewById(id); + if (cling != null) { + final ViewGroup parent = (ViewGroup) cling.getParent(); + parent.post(new Runnable() { + @Override + public void run() { + parent.removeView(cling); + } + }); + mHideFromAccessibilityHelper.restoreImportantForAccessibility(mDragLayer); + } + } + + private boolean skipCustomClingIfNoAccounts() { + Cling cling = (Cling) findViewById(R.id.workspace_cling); + boolean customCling = cling.getDrawIdentifier().equals("workspace_custom"); + if (customCling) { + AccountManager am = AccountManager.get(this); + if (am == null) return false; + Account[] accounts = am.getAccountsByType("com.google"); + return accounts.length == 0; + } + return false; + } + + public void showFirstRunWorkspaceCling() { + // Enable the clings only if they have not been dismissed before + if (isClingsEnabled() && + !mSharedPrefs.getBoolean(Cling.WORKSPACE_CLING_DISMISSED_KEY, false) && + !skipCustomClingIfNoAccounts() ) { + // If we're not using the default workspace layout, replace workspace cling + // with a custom workspace cling (usually specified in an overlay) + // For now, only do this on tablets + if (mSharedPrefs.getInt(LauncherProvider.DEFAULT_WORKSPACE_RESOURCE_ID, 0) != 0 && + getResources().getBoolean(R.bool.config_useCustomClings)) { + // Use a custom cling + View cling = findViewById(R.id.workspace_cling); + ViewGroup clingParent = (ViewGroup) cling.getParent(); + int clingIndex = clingParent.indexOfChild(cling); + clingParent.removeViewAt(clingIndex); + View customCling = mInflater.inflate(R.layout.custom_workspace_cling, clingParent, false); + clingParent.addView(customCling, clingIndex); + customCling.setId(R.id.workspace_cling); + } + initCling(R.id.workspace_cling, null, false, 0); + } else { + removeCling(R.id.workspace_cling); + } + } + public void showFirstRunAllAppsCling(int[] position) { + // Enable the clings only if they have not been dismissed before + if (isClingsEnabled() && + !mSharedPrefs.getBoolean(Cling.ALLAPPS_CLING_DISMISSED_KEY, false)) { + initCling(R.id.all_apps_cling, position, true, 0); + } else { + removeCling(R.id.all_apps_cling); + } + } + public Cling showFirstRunFoldersCling() { + // Enable the clings only if they have not been dismissed before + if (isClingsEnabled() && + !mSharedPrefs.getBoolean(Cling.FOLDER_CLING_DISMISSED_KEY, false)) { + return initCling(R.id.folder_cling, null, true, 0); + } else { + removeCling(R.id.folder_cling); + return null; + } + } + public boolean isFolderClingVisible() { + Cling cling = (Cling) findViewById(R.id.folder_cling); + if (cling != null) { + return cling.getVisibility() == View.VISIBLE; + } + return false; + } + public void dismissWorkspaceCling(View v) { + Cling cling = (Cling) findViewById(R.id.workspace_cling); + dismissCling(cling, Cling.WORKSPACE_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION); + } + public void dismissAllAppsCling(View v) { + Cling cling = (Cling) findViewById(R.id.all_apps_cling); + dismissCling(cling, Cling.ALLAPPS_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION); + } + public void dismissFolderCling(View v) { + Cling cling = (Cling) findViewById(R.id.folder_cling); + dismissCling(cling, Cling.FOLDER_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION); + } + + /** + * Prints out out state for debugging. + */ + public void dumpState() { + Log.d(TAG, "BEGIN launcher3 dump state for launcher " + this); + Log.d(TAG, "mSavedState=" + mSavedState); + Log.d(TAG, "mWorkspaceLoading=" + mWorkspaceLoading); + Log.d(TAG, "mRestoring=" + mRestoring); + Log.d(TAG, "mWaitingForResult=" + mWaitingForResult); + Log.d(TAG, "mSavedInstanceState=" + mSavedInstanceState); + Log.d(TAG, "sFolders.size=" + sFolders.size()); + mModel.dumpState(); + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.dumpState(); + } + Log.d(TAG, "END launcher3 dump state"); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + writer.println(" "); + writer.println("Debug logs: "); + for (int i = 0; i < sDumpLogs.size(); i++) { + writer.println(" " + sDumpLogs.get(i)); + } + } + + public static void dumpDebugLogsToConsole() { + Log.d(TAG, ""); + Log.d(TAG, "*********************"); + Log.d(TAG, "Launcher debug logs: "); + for (int i = 0; i < sDumpLogs.size(); i++) { + Log.d(TAG, " " + sDumpLogs.get(i)); + } + Log.d(TAG, "*********************"); + Log.d(TAG, ""); + } +} + +interface LauncherTransitionable { + View getContent(); + void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace); + void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace); + void onLauncherTransitionStep(Launcher l, float t); + void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace); +} diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java new file mode 100644 index 000000000..01f72a7ce --- /dev/null +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2012 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.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.view.View; +import android.view.ViewTreeObserver; + +import java.util.HashSet; + +public class LauncherAnimUtils { + static HashSet<Animator> sAnimators = new HashSet<Animator>(); + static Animator.AnimatorListener sEndAnimListener = new Animator.AnimatorListener() { + public void onAnimationStart(Animator animation) { + } + + public void onAnimationRepeat(Animator animation) { + } + + public void onAnimationEnd(Animator animation) { + sAnimators.remove(animation); + } + + public void onAnimationCancel(Animator animation) { + sAnimators.remove(animation); + } + }; + + public static void cancelOnDestroyActivity(Animator a) { + sAnimators.add(a); + a.addListener(sEndAnimListener); + } + + // Helper method. Assumes a draw is pending, and that if the animation's duration is 0 + // it should be cancelled + public static void startAnimationAfterNextDraw(final Animator animator, final View view) { + view.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() { + private boolean mStarted = false; + public void onDraw() { + if (mStarted) return; + mStarted = true; + // Use this as a signal that the animation was cancelled + if (animator.getDuration() == 0) { + return; + } + animator.start(); + + final ViewTreeObserver.OnDrawListener listener = this; + view.post(new Runnable() { + public void run() { + view.getViewTreeObserver().removeOnDrawListener(listener); + } + }); + } + }); + } + + public static void onDestroyActivity() { + HashSet<Animator> animators = new HashSet<Animator>(sAnimators); + for (Animator a : animators) { + if (a.isRunning()) { + a.cancel(); + } else { + sAnimators.remove(a); + } + } + } + + public static AnimatorSet createAnimatorSet() { + AnimatorSet anim = new AnimatorSet(); + cancelOnDestroyActivity(anim); + return anim; + } + + public static ValueAnimator ofFloat(View target, float... values) { + ValueAnimator anim = new ValueAnimator(); + anim.setFloatValues(values); + cancelOnDestroyActivity(anim); + return anim; + } + + public static ObjectAnimator ofFloat(View target, String propertyName, float... values) { + ObjectAnimator anim = new ObjectAnimator(); + anim.setTarget(target); + anim.setPropertyName(propertyName); + anim.setFloatValues(values); + cancelOnDestroyActivity(anim); + new FirstFrameAnimatorHelper(anim, target); + return anim; + } + + public static ObjectAnimator ofPropertyValuesHolder(View target, + PropertyValuesHolder... values) { + ObjectAnimator anim = new ObjectAnimator(); + anim.setTarget(target); + anim.setValues(values); + cancelOnDestroyActivity(anim); + new FirstFrameAnimatorHelper(anim, target); + return anim; + } + + public static ObjectAnimator ofPropertyValuesHolder(Object target, + View view, PropertyValuesHolder... values) { + ObjectAnimator anim = new ObjectAnimator(); + anim.setTarget(target); + anim.setValues(values); + cancelOnDestroyActivity(anim); + new FirstFrameAnimatorHelper(anim, view); + return anim; + } +} diff --git a/src/com/android/launcher3/LauncherAnimatorUpdateListener.java b/src/com/android/launcher3/LauncherAnimatorUpdateListener.java new file mode 100644 index 000000000..ec9fd4d16 --- /dev/null +++ b/src/com/android/launcher3/LauncherAnimatorUpdateListener.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 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.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; + +abstract class LauncherAnimatorUpdateListener implements AnimatorUpdateListener { + public void onAnimationUpdate(ValueAnimator animation) { + final float b = (Float) animation.getAnimatedValue(); + final float a = 1f - b; + onAnimationUpdate(a, b); + } + + abstract void onAnimationUpdate(float a, float b); +}
\ No newline at end of file diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java new file mode 100644 index 000000000..7b08d4403 --- /dev/null +++ b/src/com/android/launcher3/LauncherAppWidgetHost.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2009 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.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; + +/** + * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView} + * which correctly captures all long-press events. This ensures that users can + * always pick up and move widgets. + */ +public class LauncherAppWidgetHost extends AppWidgetHost { + + Launcher mLauncher; + + public LauncherAppWidgetHost(Launcher launcher, int hostId) { + super(launcher, hostId); + mLauncher = launcher; + } + + @Override + protected AppWidgetHostView onCreateView(Context context, int appWidgetId, + AppWidgetProviderInfo appWidget) { + return new LauncherAppWidgetHostView(context); + } + + @Override + public void stopListening() { + super.stopListening(); + clearViews(); + } + + protected void onProvidersChanged() { + // Once we get the message that widget packages are updated, we need to rebind items + // in AppsCustomize accordingly. + mLauncher.bindPackagesUpdated(LauncherModel.getSortedWidgetsAndShortcuts(mLauncher)); + } +} diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java new file mode 100644 index 000000000..6157a8721 --- /dev/null +++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2009 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.appwidget.AppWidgetHostView; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RemoteViews; + +import com.android.launcher3.R; + +/** + * {@inheritDoc} + */ +public class LauncherAppWidgetHostView extends AppWidgetHostView { + private CheckLongPressHelper mLongPressHelper; + private LayoutInflater mInflater; + private Context mContext; + private int mPreviousOrientation; + + public LauncherAppWidgetHostView(Context context) { + super(context); + mContext = context; + mLongPressHelper = new CheckLongPressHelper(this); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + protected View getErrorView() { + return mInflater.inflate(R.layout.appwidget_error, this, false); + } + + @Override + public void updateAppWidget(RemoteViews remoteViews) { + // Store the orientation in which the widget was inflated + mPreviousOrientation = mContext.getResources().getConfiguration().orientation; + super.updateAppWidget(remoteViews); + } + + public boolean orientationChangedSincedInflation() { + int orientation = mContext.getResources().getConfiguration().orientation; + if (mPreviousOrientation != orientation) { + return true; + } + return false; + } + + public boolean onInterceptTouchEvent(MotionEvent ev) { + // Consume any touch events for ourselves after longpress is triggered + if (mLongPressHelper.hasPerformedLongPress()) { + mLongPressHelper.cancelLongPress(); + return true; + } + + // Watch for longpress events at this level to make sure + // users can always pick up this widget + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: { + mLongPressHelper.postCheckForLongPress(); + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mLongPressHelper.cancelLongPress(); + break; + } + + // Otherwise continue letting touch events fall through to children + return false; + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + mLongPressHelper.cancelLongPress(); + } + + @Override + public int getDescendantFocusability() { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } +} diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java new file mode 100644 index 000000000..3fc67cb5f --- /dev/null +++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2009 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.appwidget.AppWidgetHostView; +import android.content.ComponentName; +import android.content.ContentValues; + +/** + * Represents a widget (either instantiated or about to be) in the Launcher. + */ +class LauncherAppWidgetInfo extends ItemInfo { + + /** + * Indicates that the widget hasn't been instantiated yet. + */ + static final int NO_ID = -1; + + /** + * Identifier for this widget when talking with + * {@link android.appwidget.AppWidgetManager} for updates. + */ + int appWidgetId = NO_ID; + + ComponentName providerName; + + // TODO: Are these necessary here? + int minWidth = -1; + int minHeight = -1; + + private boolean mHasNotifiedInitialWidgetSizeChanged; + + /** + * View that holds this widget after it's been created. This view isn't created + * until Launcher knows it's needed. + */ + AppWidgetHostView hostView = null; + + LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + this.appWidgetId = appWidgetId; + this.providerName = providerName; + + // Since the widget isn't instantiated yet, we don't know these values. Set them to -1 + // to indicate that they should be calculated based on the layout and minWidth/minHeight + spanX = -1; + spanY = -1; + } + + @Override + void onAddToDatabase(ContentValues values) { + super.onAddToDatabase(values); + values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId); + } + + /** + * When we bind the widget, we should notify the widget that the size has changed if we have not + * done so already (only really for default workspace widgets). + */ + void onBindAppWidget(Launcher launcher) { + if (!mHasNotifiedInitialWidgetSizeChanged) { + notifyWidgetSizeChanged(launcher); + } + } + + /** + * Trigger an update callback to the widget to notify it that its size has changed. + */ + void notifyWidgetSizeChanged(Launcher launcher) { + AppWidgetResizeFrame.updateWidgetSizeRanges(hostView, launcher, spanX, spanY); + mHasNotifiedInitialWidgetSizeChanged = true; + } + + @Override + public String toString() { + return "AppWidget(id=" + Integer.toString(appWidgetId) + ")"; + } + + @Override + void unbind() { + super.unbind(); + hostView = null; + } +} diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java new file mode 100644 index 000000000..45e24255e --- /dev/null +++ b/src/com/android/launcher3/LauncherApplication.java @@ -0,0 +1,151 @@ +/* + * 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.app.Application; +import android.app.SearchManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.database.ContentObserver; +import android.os.Handler; + +import com.android.launcher3.R; + +import java.lang.ref.WeakReference; + +public class LauncherApplication extends Application { + private LauncherModel mModel; + private IconCache mIconCache; + private WidgetPreviewLoader.CacheDb mWidgetPreviewCacheDb; + private static boolean sIsScreenLarge; + private static float sScreenDensity; + private static int sLongPressTimeout = 300; + private static final String sSharedPreferencesKey = "com.android.launcher3.prefs"; + WeakReference<LauncherProvider> mLauncherProvider; + + @Override + public void onCreate() { + super.onCreate(); + + // set sIsScreenXLarge and sScreenDensity *before* creating icon cache + sIsScreenLarge = getResources().getBoolean(R.bool.is_large_screen); + sScreenDensity = getResources().getDisplayMetrics().density; + + mWidgetPreviewCacheDb = new WidgetPreviewLoader.CacheDb(this); + mIconCache = new IconCache(this); + mModel = new LauncherModel(this, mIconCache); + + // Register intent receivers + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + registerReceiver(mModel, filter); + filter = new IntentFilter(); + filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + filter.addAction(Intent.ACTION_LOCALE_CHANGED); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + registerReceiver(mModel, filter); + filter = new IntentFilter(); + filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); + registerReceiver(mModel, filter); + filter = new IntentFilter(); + filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED); + registerReceiver(mModel, filter); + + // Register for changes to the favorites + ContentResolver resolver = getContentResolver(); + resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true, + mFavoritesObserver); + } + + /** + * There's no guarantee that this function is ever called. + */ + @Override + public void onTerminate() { + super.onTerminate(); + + unregisterReceiver(mModel); + + ContentResolver resolver = getContentResolver(); + resolver.unregisterContentObserver(mFavoritesObserver); + } + + /** + * Receives notifications whenever the user favorites have changed. + */ + private final ContentObserver mFavoritesObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + // If the database has ever changed, then we really need to force a reload of the + // workspace on the next load + mModel.resetLoadedState(false, true); + mModel.startLoaderFromBackground(); + } + }; + + LauncherModel setLauncher(Launcher launcher) { + mModel.initialize(launcher); + return mModel; + } + + IconCache getIconCache() { + return mIconCache; + } + + LauncherModel getModel() { + return mModel; + } + + WidgetPreviewLoader.CacheDb getWidgetPreviewCacheDb() { + return mWidgetPreviewCacheDb; + } + + void setLauncherProvider(LauncherProvider provider) { + mLauncherProvider = new WeakReference<LauncherProvider>(provider); + } + + LauncherProvider getLauncherProvider() { + return mLauncherProvider.get(); + } + + public static String getSharedPreferencesKey() { + return sSharedPreferencesKey; + } + + public static boolean isScreenLarge() { + return sIsScreenLarge; + } + + public static boolean isScreenLandscape(Context context) { + return context.getResources().getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE; + } + + public static float getScreenDensity() { + return sScreenDensity; + } + + public static int getLongPressTimeout() { + return sLongPressTimeout; + } +} diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java new file mode 100644 index 000000000..5459af21b --- /dev/null +++ b/src/com/android/launcher3/LauncherModel.java @@ -0,0 +1,2620 @@ +/* + * 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.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.Intent.ShortcutIconResource; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; + +import com.android.launcher3.R; +import com.android.launcher3.InstallWidgetReceiver.WidgetMimeTypeHandlerData; + +import java.lang.ref.WeakReference; +import java.net.URISyntaxException; +import java.text.Collator; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * Maintains in-memory state of the Launcher. It is expected that there should be only one + * LauncherModel object held in a static. Also provide APIs for updating the database state + * for the Launcher. + */ +public class LauncherModel extends BroadcastReceiver { + static final boolean DEBUG_LOADERS = false; + static final String TAG = "Launcher.Model"; + + private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons + private final boolean mAppsCanBeOnExternalStorage; + private int mBatchSize; // 0 is all apps at once + private int mAllAppsLoadDelay; // milliseconds between batches + + private final LauncherApplication mApp; + private final Object mLock = new Object(); + private DeferredHandler mHandler = new DeferredHandler(); + private LoaderTask mLoaderTask; + private boolean mIsLoaderTaskRunning; + private volatile boolean mFlushingWorkerThread; + + // Specific runnable types that are run on the main thread deferred handler, this allows us to + // clear all queued binding runnables when the Launcher activity is destroyed. + private static final int MAIN_THREAD_NORMAL_RUNNABLE = 0; + private static final int MAIN_THREAD_BINDING_RUNNABLE = 1; + + + private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); + static { + sWorkerThread.start(); + } + private static final Handler sWorker = new Handler(sWorkerThread.getLooper()); + + // We start off with everything not loaded. After that, we assume that + // our monitoring of the package manager provides all updates and we never + // need to do a requery. These are only ever touched from the loader thread. + private boolean mWorkspaceLoaded; + private boolean mAllAppsLoaded; + + // When we are loading pages synchronously, we can't just post the binding of items on the side + // pages as this delays the rotation process. Instead, we wait for a callback from the first + // draw (in Workspace) to initiate the binding of the remaining side pages. Any time we start + // a normal load, we also clear this set of Runnables. + static final ArrayList<Runnable> mDeferredBindRunnables = new ArrayList<Runnable>(); + + private WeakReference<Callbacks> mCallbacks; + + // < only access in worker thread > + private AllAppsList mBgAllAppsList; + + // The lock that must be acquired before referencing any static bg data structures. Unlike + // other locks, this one can generally be held long-term because we never expect any of these + // static data structures to be referenced outside of the worker thread except on the first + // load after configuration change. + static final Object sBgLock = new Object(); + + // sBgItemsIdMap maps *all* the ItemInfos (shortcuts, folders, and widgets) created by + // LauncherModel to their ids + static final HashMap<Long, ItemInfo> sBgItemsIdMap = new HashMap<Long, ItemInfo>(); + + // sBgWorkspaceItems is passed to bindItems, which expects a list of all folders and shortcuts + // created by LauncherModel that are directly on the home screen (however, no widgets or + // shortcuts within folders). + static final ArrayList<ItemInfo> sBgWorkspaceItems = new ArrayList<ItemInfo>(); + + // sBgAppWidgets is all LauncherAppWidgetInfo created by LauncherModel. Passed to bindAppWidget() + static final ArrayList<LauncherAppWidgetInfo> sBgAppWidgets = + new ArrayList<LauncherAppWidgetInfo>(); + + // sBgFolders is all FolderInfos created by LauncherModel. Passed to bindFolders() + static final HashMap<Long, FolderInfo> sBgFolders = new HashMap<Long, FolderInfo>(); + + // sBgDbIconCache is the set of ItemInfos that need to have their icons updated in the database + static final HashMap<Object, byte[]> sBgDbIconCache = new HashMap<Object, byte[]>(); + // </ only access in worker thread > + + private IconCache mIconCache; + private Bitmap mDefaultIcon; + + private static int mCellCountX; + private static int mCellCountY; + + protected int mPreviousConfigMcc; + + public interface Callbacks { + public boolean setLoadOnResume(); + public int getCurrentWorkspaceScreen(); + public void startBinding(); + public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end); + public void bindFolders(HashMap<Long,FolderInfo> folders); + public void finishBindingItems(); + public void bindAppWidget(LauncherAppWidgetInfo info); + public void bindAllApplications(ArrayList<ApplicationInfo> apps); + public void bindAppsAdded(ArrayList<ApplicationInfo> apps); + public void bindAppsUpdated(ArrayList<ApplicationInfo> apps); + public void bindComponentsRemoved(ArrayList<String> packageNames, + ArrayList<ApplicationInfo> appInfos, + boolean matchPackageNamesOnly); + public void bindPackagesUpdated(ArrayList<Object> widgetsAndShortcuts); + public boolean isAllAppsVisible(); + public boolean isAllAppsButtonRank(int rank); + public void bindSearchablesChanged(); + public void onPageBoundSynchronously(int page); + } + + LauncherModel(LauncherApplication app, IconCache iconCache) { + mAppsCanBeOnExternalStorage = !Environment.isExternalStorageEmulated(); + mApp = app; + mBgAllAppsList = new AllAppsList(iconCache); + mIconCache = iconCache; + + mDefaultIcon = Utilities.createIconBitmap( + mIconCache.getFullResDefaultActivityIcon(), app); + + final Resources res = app.getResources(); + mAllAppsLoadDelay = res.getInteger(R.integer.config_allAppsBatchLoadDelay); + mBatchSize = res.getInteger(R.integer.config_allAppsBatchSize); + Configuration config = res.getConfiguration(); + mPreviousConfigMcc = config.mcc; + } + + /** Runs the specified runnable immediately if called from the main thread, otherwise it is + * posted on the main thread handler. */ + private void runOnMainThread(Runnable r) { + runOnMainThread(r, 0); + } + private void runOnMainThread(Runnable r, int type) { + if (sWorkerThread.getThreadId() == Process.myTid()) { + // If we are on the worker thread, post onto the main handler + mHandler.post(r); + } else { + r.run(); + } + } + + /** Runs the specified runnable immediately if called from the worker thread, otherwise it is + * posted on the worker thread handler. */ + private static void runOnWorkerThread(Runnable r) { + if (sWorkerThread.getThreadId() == Process.myTid()) { + r.run(); + } else { + // If we are not on the worker thread, then post to the worker handler + sWorker.post(r); + } + } + + public Bitmap getFallbackIcon() { + return Bitmap.createBitmap(mDefaultIcon); + } + + public void unbindItemInfosAndClearQueuedBindRunnables() { + if (sWorkerThread.getThreadId() == Process.myTid()) { + throw new RuntimeException("Expected unbindLauncherItemInfos() to be called from the " + + "main thread"); + } + + // Clear any deferred bind runnables + mDeferredBindRunnables.clear(); + // Remove any queued bind runnables + mHandler.cancelAllRunnablesOfType(MAIN_THREAD_BINDING_RUNNABLE); + // Unbind all the workspace items + unbindWorkspaceItemsOnMainThread(); + } + + /** Unbinds all the sBgWorkspaceItems and sBgAppWidgets on the main thread */ + void unbindWorkspaceItemsOnMainThread() { + // Ensure that we don't use the same workspace items data structure on the main thread + // by making a copy of workspace items first. + final ArrayList<ItemInfo> tmpWorkspaceItems = new ArrayList<ItemInfo>(); + final ArrayList<ItemInfo> tmpAppWidgets = new ArrayList<ItemInfo>(); + synchronized (sBgLock) { + tmpWorkspaceItems.addAll(sBgWorkspaceItems); + tmpAppWidgets.addAll(sBgAppWidgets); + } + Runnable r = new Runnable() { + @Override + public void run() { + for (ItemInfo item : tmpWorkspaceItems) { + item.unbind(); + } + for (ItemInfo item : tmpAppWidgets) { + item.unbind(); + } + } + }; + runOnMainThread(r); + } + + /** + * Adds an item to the DB if it was not created previously, or move it to a new + * <container, screen, cellX, cellY> + */ + static void addOrMoveItemInDatabase(Context context, ItemInfo item, long container, + int screen, int cellX, int cellY) { + if (item.container == ItemInfo.NO_ID) { + // From all apps + addItemToDatabase(context, item, container, screen, cellX, cellY, false); + } else { + // From somewhere else + moveItemInDatabase(context, item, container, screen, cellX, cellY); + } + } + + static void checkItemInfoLocked( + final long itemId, final ItemInfo item, StackTraceElement[] stackTrace) { + ItemInfo modelItem = sBgItemsIdMap.get(itemId); + if (modelItem != null && item != modelItem) { + // check all the data is consistent + if (modelItem instanceof ShortcutInfo && item instanceof ShortcutInfo) { + ShortcutInfo modelShortcut = (ShortcutInfo) modelItem; + ShortcutInfo shortcut = (ShortcutInfo) item; + if (modelShortcut.title.toString().equals(shortcut.title.toString()) && + modelShortcut.intent.filterEquals(shortcut.intent) && + modelShortcut.id == shortcut.id && + modelShortcut.itemType == shortcut.itemType && + modelShortcut.container == shortcut.container && + modelShortcut.screen == shortcut.screen && + modelShortcut.cellX == shortcut.cellX && + modelShortcut.cellY == shortcut.cellY && + modelShortcut.spanX == shortcut.spanX && + modelShortcut.spanY == shortcut.spanY && + ((modelShortcut.dropPos == null && shortcut.dropPos == null) || + (modelShortcut.dropPos != null && + shortcut.dropPos != null && + modelShortcut.dropPos[0] == shortcut.dropPos[0] && + modelShortcut.dropPos[1] == shortcut.dropPos[1]))) { + // For all intents and purposes, this is the same object + return; + } + } + + // the modelItem needs to match up perfectly with item if our model is + // to be consistent with the database-- for now, just require + // modelItem == item or the equality check above + String msg = "item: " + ((item != null) ? item.toString() : "null") + + "modelItem: " + + ((modelItem != null) ? modelItem.toString() : "null") + + "Error: ItemInfo passed to checkItemInfo doesn't match original"; + RuntimeException e = new RuntimeException(msg); + if (stackTrace != null) { + e.setStackTrace(stackTrace); + } + throw e; + } + } + + static void checkItemInfo(final ItemInfo item) { + final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); + final long itemId = item.id; + Runnable r = new Runnable() { + public void run() { + synchronized (sBgLock) { + checkItemInfoLocked(itemId, item, stackTrace); + } + } + }; + runOnWorkerThread(r); + } + + static void updateItemInDatabaseHelper(Context context, final ContentValues values, + final ItemInfo item, final String callingFunction) { + final long itemId = item.id; + final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false); + final ContentResolver cr = context.getContentResolver(); + + final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); + Runnable r = new Runnable() { + public void run() { + cr.update(uri, values, null, null); + + // Lock on mBgLock *after* the db operation + synchronized (sBgLock) { + checkItemInfoLocked(itemId, item, stackTrace); + + if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP && + item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + // Item is in a folder, make sure this folder exists + if (!sBgFolders.containsKey(item.container)) { + // An items container is being set to a that of an item which is not in + // the list of Folders. + String msg = "item: " + item + " container being set to: " + + item.container + ", not in the list of folders"; + Log.e(TAG, msg); + Launcher.dumpDebugLogsToConsole(); + } + } + + // Items are added/removed from the corresponding FolderInfo elsewhere, such + // as in Workspace.onDrop. Here, we just add/remove them from the list of items + // that are on the desktop, as appropriate + ItemInfo modelItem = sBgItemsIdMap.get(itemId); + if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP || + modelItem.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + switch (modelItem.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + if (!sBgWorkspaceItems.contains(modelItem)) { + sBgWorkspaceItems.add(modelItem); + } + break; + default: + break; + } + } else { + sBgWorkspaceItems.remove(modelItem); + } + } + } + }; + runOnWorkerThread(r); + } + + public void flushWorkerThread() { + mFlushingWorkerThread = true; + Runnable waiter = new Runnable() { + public void run() { + synchronized (this) { + notifyAll(); + mFlushingWorkerThread = false; + } + } + }; + + synchronized(waiter) { + runOnWorkerThread(waiter); + if (mLoaderTask != null) { + synchronized(mLoaderTask) { + mLoaderTask.notify(); + } + } + boolean success = false; + while (!success) { + try { + waiter.wait(); + success = true; + } catch (InterruptedException e) { + } + } + } + } + + /** + * Move an item in the DB to a new <container, screen, cellX, cellY> + */ + static void moveItemInDatabase(Context context, final ItemInfo item, final long container, + final int screen, final int cellX, final int cellY) { + String transaction = "DbDebug Modify item (" + item.title + ") in db, id: " + item.id + + " (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY + + ") --> " + "(" + container + ", " + screen + ", " + cellX + ", " + cellY + ")"; + Launcher.sDumpLogs.add(transaction); + Log.d(TAG, transaction); + item.container = container; + item.cellX = cellX; + item.cellY = cellY; + + // We store hotseat items in canonical form which is this orientation invariant position + // in the hotseat + if (context instanceof Launcher && screen < 0 && + container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY); + } else { + item.screen = screen; + } + + final ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.CONTAINER, item.container); + values.put(LauncherSettings.Favorites.CELLX, item.cellX); + values.put(LauncherSettings.Favorites.CELLY, item.cellY); + values.put(LauncherSettings.Favorites.SCREEN, item.screen); + + updateItemInDatabaseHelper(context, values, item, "moveItemInDatabase"); + } + + /** + * Move and/or resize item in the DB to a new <container, screen, cellX, cellY, spanX, spanY> + */ + static void modifyItemInDatabase(Context context, final ItemInfo item, final long container, + final int screen, final int cellX, final int cellY, final int spanX, final int spanY) { + String transaction = "DbDebug Modify item (" + item.title + ") in db, id: " + item.id + + " (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY + + ") --> " + "(" + container + ", " + screen + ", " + cellX + ", " + cellY + ")"; + Launcher.sDumpLogs.add(transaction); + Log.d(TAG, transaction); + item.cellX = cellX; + item.cellY = cellY; + item.spanX = spanX; + item.spanY = spanY; + + // We store hotseat items in canonical form which is this orientation invariant position + // in the hotseat + if (context instanceof Launcher && screen < 0 && + container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY); + } else { + item.screen = screen; + } + + final ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.CONTAINER, item.container); + values.put(LauncherSettings.Favorites.CELLX, item.cellX); + values.put(LauncherSettings.Favorites.CELLY, item.cellY); + values.put(LauncherSettings.Favorites.SPANX, item.spanX); + values.put(LauncherSettings.Favorites.SPANY, item.spanY); + values.put(LauncherSettings.Favorites.SCREEN, item.screen); + + updateItemInDatabaseHelper(context, values, item, "modifyItemInDatabase"); + } + + /** + * Update an item to the database in a specified container. + */ + static void updateItemInDatabase(Context context, final ItemInfo item) { + final ContentValues values = new ContentValues(); + item.onAddToDatabase(values); + item.updateValuesWithCoordinates(values, item.cellX, item.cellY); + updateItemInDatabaseHelper(context, values, item, "updateItemInDatabase"); + } + + /** + * Returns true if the shortcuts already exists in the database. + * we identify a shortcut by its title and intent. + */ + static boolean shortcutExists(Context context, String title, Intent intent) { + final ContentResolver cr = context.getContentResolver(); + Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, + new String[] { "title", "intent" }, "title=? and intent=?", + new String[] { title, intent.toUri(0) }, null); + boolean result = false; + try { + result = c.moveToFirst(); + } finally { + c.close(); + } + return result; + } + + /** + * Returns an ItemInfo array containing all the items in the LauncherModel. + * The ItemInfo.id is not set through this function. + */ + static ArrayList<ItemInfo> getItemsInLocalCoordinates(Context context) { + ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); + final ContentResolver cr = context.getContentResolver(); + Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, new String[] { + LauncherSettings.Favorites.ITEM_TYPE, LauncherSettings.Favorites.CONTAINER, + LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY, + LauncherSettings.Favorites.SPANX, LauncherSettings.Favorites.SPANY }, null, null, null); + + final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); + final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); + final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); + final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); + final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); + final int spanXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX); + final int spanYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY); + + try { + while (c.moveToNext()) { + ItemInfo item = new ItemInfo(); + item.cellX = c.getInt(cellXIndex); + item.cellY = c.getInt(cellYIndex); + item.spanX = c.getInt(spanXIndex); + item.spanY = c.getInt(spanYIndex); + item.container = c.getInt(containerIndex); + item.itemType = c.getInt(itemTypeIndex); + item.screen = c.getInt(screenIndex); + + items.add(item); + } + } catch (Exception e) { + items.clear(); + } finally { + c.close(); + } + + return items; + } + + /** + * Find a folder in the db, creating the FolderInfo if necessary, and adding it to folderList. + */ + FolderInfo getFolderById(Context context, HashMap<Long,FolderInfo> folderList, long id) { + final ContentResolver cr = context.getContentResolver(); + Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, null, + "_id=? and (itemType=? or itemType=?)", + new String[] { String.valueOf(id), + String.valueOf(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}, null); + + try { + if (c.moveToFirst()) { + final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); + final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); + final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); + final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); + final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); + final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); + + FolderInfo folderInfo = null; + switch (c.getInt(itemTypeIndex)) { + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + folderInfo = findOrMakeFolder(folderList, id); + break; + } + + folderInfo.title = c.getString(titleIndex); + folderInfo.id = id; + folderInfo.container = c.getInt(containerIndex); + folderInfo.screen = c.getInt(screenIndex); + folderInfo.cellX = c.getInt(cellXIndex); + folderInfo.cellY = c.getInt(cellYIndex); + + return folderInfo; + } + } finally { + c.close(); + } + + return null; + } + + /** + * Add an item to the database in a specified container. Sets the container, screen, cellX and + * cellY fields of the item. Also assigns an ID to the item. + */ + static void addItemToDatabase(Context context, final ItemInfo item, final long container, + final int screen, final int cellX, final int cellY, final boolean notify) { + item.container = container; + item.cellX = cellX; + item.cellY = cellY; + // We store hotseat items in canonical form which is this orientation invariant position + // in the hotseat + if (context instanceof Launcher && screen < 0 && + container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY); + } else { + item.screen = screen; + } + + final ContentValues values = new ContentValues(); + final ContentResolver cr = context.getContentResolver(); + item.onAddToDatabase(values); + + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + item.id = app.getLauncherProvider().generateNewId(); + values.put(LauncherSettings.Favorites._ID, item.id); + item.updateValuesWithCoordinates(values, item.cellX, item.cellY); + + Runnable r = new Runnable() { + public void run() { + String transaction = "DbDebug Add item (" + item.title + ") to db, id: " + + item.id + " (" + container + ", " + screen + ", " + cellX + ", " + + cellY + ")"; + Launcher.sDumpLogs.add(transaction); + Log.d(TAG, transaction); + + cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI : + LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values); + + // Lock on mBgLock *after* the db operation + synchronized (sBgLock) { + checkItemInfoLocked(item.id, item, null); + sBgItemsIdMap.put(item.id, item); + switch (item.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + sBgFolders.put(item.id, (FolderInfo) item); + // Fall through + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP || + item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + sBgWorkspaceItems.add(item); + } else { + if (!sBgFolders.containsKey(item.container)) { + // Adding an item to a folder that doesn't exist. + String msg = "adding item: " + item + " to a folder that " + + " doesn't exist"; + Log.e(TAG, msg); + Launcher.dumpDebugLogsToConsole(); + } + } + break; + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + sBgAppWidgets.add((LauncherAppWidgetInfo) item); + break; + } + } + } + }; + runOnWorkerThread(r); + } + + /** + * Creates a new unique child id, for a given cell span across all layouts. + */ + static int getCellLayoutChildId( + long container, int screen, int localCellX, int localCellY, int spanX, int spanY) { + return (((int) container & 0xFF) << 24) + | (screen & 0xFF) << 16 | (localCellX & 0xFF) << 8 | (localCellY & 0xFF); + } + + static int getCellCountX() { + return mCellCountX; + } + + static int getCellCountY() { + return mCellCountY; + } + + /** + * Updates the model orientation helper to take into account the current layout dimensions + * when performing local/canonical coordinate transformations. + */ + static void updateWorkspaceLayoutCells(int shortAxisCellCount, int longAxisCellCount) { + mCellCountX = shortAxisCellCount; + mCellCountY = longAxisCellCount; + } + + /** + * Removes the specified item from the database + * @param context + * @param item + */ + static void deleteItemFromDatabase(Context context, final ItemInfo item) { + final ContentResolver cr = context.getContentResolver(); + final Uri uriToDelete = LauncherSettings.Favorites.getContentUri(item.id, false); + + Runnable r = new Runnable() { + public void run() { + String transaction = "DbDebug Delete item (" + item.title + ") from db, id: " + + item.id + " (" + item.container + ", " + item.screen + ", " + item.cellX + + ", " + item.cellY + ")"; + Launcher.sDumpLogs.add(transaction); + Log.d(TAG, transaction); + + cr.delete(uriToDelete, null, null); + + // Lock on mBgLock *after* the db operation + synchronized (sBgLock) { + switch (item.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + sBgFolders.remove(item.id); + for (ItemInfo info: sBgItemsIdMap.values()) { + if (info.container == item.id) { + // We are deleting a folder which still contains items that + // think they are contained by that folder. + String msg = "deleting a folder (" + item + ") which still " + + "contains items (" + info + ")"; + Log.e(TAG, msg); + Launcher.dumpDebugLogsToConsole(); + } + } + sBgWorkspaceItems.remove(item); + break; + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + sBgWorkspaceItems.remove(item); + break; + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + sBgAppWidgets.remove((LauncherAppWidgetInfo) item); + break; + } + sBgItemsIdMap.remove(item.id); + sBgDbIconCache.remove(item); + } + } + }; + runOnWorkerThread(r); + } + + /** + * Remove the contents of the specified folder from the database + */ + static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) { + final ContentResolver cr = context.getContentResolver(); + + Runnable r = new Runnable() { + public void run() { + cr.delete(LauncherSettings.Favorites.getContentUri(info.id, false), null, null); + // Lock on mBgLock *after* the db operation + synchronized (sBgLock) { + sBgItemsIdMap.remove(info.id); + sBgFolders.remove(info.id); + sBgDbIconCache.remove(info); + sBgWorkspaceItems.remove(info); + } + + cr.delete(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, + LauncherSettings.Favorites.CONTAINER + "=" + info.id, null); + // Lock on mBgLock *after* the db operation + synchronized (sBgLock) { + for (ItemInfo childInfo : info.contents) { + sBgItemsIdMap.remove(childInfo.id); + sBgDbIconCache.remove(childInfo); + } + } + } + }; + runOnWorkerThread(r); + } + + /** + * Set this as the current Launcher activity object for the loader. + */ + public void initialize(Callbacks callbacks) { + synchronized (mLock) { + mCallbacks = new WeakReference<Callbacks>(callbacks); + } + } + + /** + * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and + * ACTION_PACKAGE_CHANGED. + */ + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG_LOADERS) Log.d(TAG, "onReceive intent=" + intent); + + final String action = intent.getAction(); + + if (Intent.ACTION_PACKAGE_CHANGED.equals(action) + || Intent.ACTION_PACKAGE_REMOVED.equals(action) + || Intent.ACTION_PACKAGE_ADDED.equals(action)) { + final String packageName = intent.getData().getSchemeSpecificPart(); + final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); + + int op = PackageUpdatedTask.OP_NONE; + + if (packageName == null || packageName.length() == 0) { + // they sent us a bad intent + return; + } + + if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + op = PackageUpdatedTask.OP_UPDATE; + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + if (!replacing) { + op = PackageUpdatedTask.OP_REMOVE; + } + // else, we are replacing the package, so a PACKAGE_ADDED will be sent + // later, we will update the package at this time + } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + if (!replacing) { + op = PackageUpdatedTask.OP_ADD; + } else { + op = PackageUpdatedTask.OP_UPDATE; + } + } + + if (op != PackageUpdatedTask.OP_NONE) { + enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName })); + } + + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { + // First, schedule to add these apps back in. + String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packages)); + // Then, rebind everything. + startLoaderFromBackground(); + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { + String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + enqueuePackageUpdated(new PackageUpdatedTask( + PackageUpdatedTask.OP_UNAVAILABLE, packages)); + } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { + // If we have changed locale we need to clear out the labels in all apps/workspace. + forceReload(); + } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) { + // Check if configuration change was an mcc/mnc change which would affect app resources + // and we would need to clear out the labels in all apps/workspace. Same handling as + // above for ACTION_LOCALE_CHANGED + Configuration currentConfig = context.getResources().getConfiguration(); + if (mPreviousConfigMcc != currentConfig.mcc) { + Log.d(TAG, "Reload apps on config change. curr_mcc:" + + currentConfig.mcc + " prevmcc:" + mPreviousConfigMcc); + forceReload(); + } + // Update previousConfig + mPreviousConfigMcc = currentConfig.mcc; + } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action) || + SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)) { + if (mCallbacks != null) { + Callbacks callbacks = mCallbacks.get(); + if (callbacks != null) { + callbacks.bindSearchablesChanged(); + } + } + } + } + + private void forceReload() { + resetLoadedState(true, true); + + // Do this here because if the launcher activity is running it will be restarted. + // If it's not running startLoaderFromBackground will merely tell it that it needs + // to reload. + startLoaderFromBackground(); + } + + public void resetLoadedState(boolean resetAllAppsLoaded, boolean resetWorkspaceLoaded) { + synchronized (mLock) { + // Stop any existing loaders first, so they don't set mAllAppsLoaded or + // mWorkspaceLoaded to true later + stopLoaderLocked(); + if (resetAllAppsLoaded) mAllAppsLoaded = false; + if (resetWorkspaceLoaded) mWorkspaceLoaded = false; + } + } + + /** + * When the launcher is in the background, it's possible for it to miss paired + * configuration changes. So whenever we trigger the loader from the background + * tell the launcher that it needs to re-run the loader when it comes back instead + * of doing it now. + */ + public void startLoaderFromBackground() { + boolean runLoader = false; + if (mCallbacks != null) { + Callbacks callbacks = mCallbacks.get(); + if (callbacks != null) { + // Only actually run the loader if they're not paused. + if (!callbacks.setLoadOnResume()) { + runLoader = true; + } + } + } + if (runLoader) { + startLoader(false, -1); + } + } + + // If there is already a loader task running, tell it to stop. + // returns true if isLaunching() was true on the old task + private boolean stopLoaderLocked() { + boolean isLaunching = false; + LoaderTask oldTask = mLoaderTask; + if (oldTask != null) { + if (oldTask.isLaunching()) { + isLaunching = true; + } + oldTask.stopLocked(); + } + return isLaunching; + } + + public void startLoader(boolean isLaunching, int synchronousBindPage) { + synchronized (mLock) { + if (DEBUG_LOADERS) { + Log.d(TAG, "startLoader isLaunching=" + isLaunching); + } + + // Clear any deferred bind-runnables from the synchronized load process + // We must do this before any loading/binding is scheduled below. + mDeferredBindRunnables.clear(); + + // Don't bother to start the thread if we know it's not going to do anything + if (mCallbacks != null && mCallbacks.get() != null) { + // If there is already one running, tell it to stop. + // also, don't downgrade isLaunching if we're already running + isLaunching = isLaunching || stopLoaderLocked(); + mLoaderTask = new LoaderTask(mApp, isLaunching); + if (synchronousBindPage > -1 && mAllAppsLoaded && mWorkspaceLoaded) { + mLoaderTask.runBindSynchronousPage(synchronousBindPage); + } else { + sWorkerThread.setPriority(Thread.NORM_PRIORITY); + sWorker.post(mLoaderTask); + } + } + } + } + + void bindRemainingSynchronousPages() { + // Post the remaining side pages to be loaded + if (!mDeferredBindRunnables.isEmpty()) { + for (final Runnable r : mDeferredBindRunnables) { + mHandler.post(r, MAIN_THREAD_BINDING_RUNNABLE); + } + mDeferredBindRunnables.clear(); + } + } + + public void stopLoader() { + synchronized (mLock) { + if (mLoaderTask != null) { + mLoaderTask.stopLocked(); + } + } + } + + public boolean isAllAppsLoaded() { + return mAllAppsLoaded; + } + + boolean isLoadingWorkspace() { + synchronized (mLock) { + if (mLoaderTask != null) { + return mLoaderTask.isLoadingWorkspace(); + } + } + return false; + } + + /** + * Runnable for the thread that loads the contents of the launcher: + * - workspace icons + * - widgets + * - all apps icons + */ + private class LoaderTask implements Runnable { + private Context mContext; + private boolean mIsLaunching; + private boolean mIsLoadingAndBindingWorkspace; + private boolean mStopped; + private boolean mLoadAndBindStepFinished; + + private HashMap<Object, CharSequence> mLabelCache; + + LoaderTask(Context context, boolean isLaunching) { + mContext = context; + mIsLaunching = isLaunching; + mLabelCache = new HashMap<Object, CharSequence>(); + } + + boolean isLaunching() { + return mIsLaunching; + } + + boolean isLoadingWorkspace() { + return mIsLoadingAndBindingWorkspace; + } + + private void loadAndBindWorkspace() { + mIsLoadingAndBindingWorkspace = true; + + // Load the workspace + if (DEBUG_LOADERS) { + Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded); + } + + if (!mWorkspaceLoaded) { + loadWorkspace(); + synchronized (LoaderTask.this) { + if (mStopped) { + return; + } + mWorkspaceLoaded = true; + } + } + + // Bind the workspace + bindWorkspace(-1); + } + + private void waitForIdle() { + // Wait until the either we're stopped or the other threads are done. + // This way we don't start loading all apps until the workspace has settled + // down. + synchronized (LoaderTask.this) { + final long workspaceWaitTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + + mHandler.postIdle(new Runnable() { + public void run() { + synchronized (LoaderTask.this) { + mLoadAndBindStepFinished = true; + if (DEBUG_LOADERS) { + Log.d(TAG, "done with previous binding step"); + } + LoaderTask.this.notify(); + } + } + }); + + while (!mStopped && !mLoadAndBindStepFinished && !mFlushingWorkerThread) { + try { + // Just in case mFlushingWorkerThread changes but we aren't woken up, + // wait no longer than 1sec at a time + this.wait(1000); + } catch (InterruptedException ex) { + // Ignore + } + } + if (DEBUG_LOADERS) { + Log.d(TAG, "waited " + + (SystemClock.uptimeMillis()-workspaceWaitTime) + + "ms for previous step to finish binding"); + } + } + } + + void runBindSynchronousPage(int synchronousBindPage) { + if (synchronousBindPage < 0) { + // Ensure that we have a valid page index to load synchronously + throw new RuntimeException("Should not call runBindSynchronousPage() without " + + "valid page index"); + } + if (!mAllAppsLoaded || !mWorkspaceLoaded) { + // Ensure that we don't try and bind a specified page when the pages have not been + // loaded already (we should load everything asynchronously in that case) + throw new RuntimeException("Expecting AllApps and Workspace to be loaded"); + } + synchronized (mLock) { + if (mIsLoaderTaskRunning) { + // Ensure that we are never running the background loading at this point since + // we also touch the background collections + throw new RuntimeException("Error! Background loading is already running"); + } + } + + // XXX: Throw an exception if we are already loading (since we touch the worker thread + // data structures, we can't allow any other thread to touch that data, but because + // this call is synchronous, we can get away with not locking). + + // The LauncherModel is static in the LauncherApplication and mHandler may have queued + // operations from the previous activity. We need to ensure that all queued operations + // are executed before any synchronous binding work is done. + mHandler.flush(); + + // Divide the set of loaded items into those that we are binding synchronously, and + // everything else that is to be bound normally (asynchronously). + bindWorkspace(synchronousBindPage); + // XXX: For now, continue posting the binding of AllApps as there are other issues that + // arise from that. + onlyBindAllApps(); + } + + public void run() { + synchronized (mLock) { + mIsLoaderTaskRunning = true; + } + // Optimize for end-user experience: if the Launcher is up and // running with the + // All Apps interface in the foreground, load All Apps first. Otherwise, load the + // workspace first (default). + final Callbacks cbk = mCallbacks.get(); + final boolean loadWorkspaceFirst = cbk != null ? (!cbk.isAllAppsVisible()) : true; + + keep_running: { + // Elevate priority when Home launches for the first time to avoid + // starving at boot time. Staring at a blank home is not cool. + synchronized (mLock) { + if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to " + + (mIsLaunching ? "DEFAULT" : "BACKGROUND")); + android.os.Process.setThreadPriority(mIsLaunching + ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND); + } + if (loadWorkspaceFirst) { + if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace"); + loadAndBindWorkspace(); + } else { + if (DEBUG_LOADERS) Log.d(TAG, "step 1: special: loading all apps"); + loadAndBindAllApps(); + } + + if (mStopped) { + break keep_running; + } + + // Whew! Hard work done. Slow us down, and wait until the UI thread has + // settled down. + synchronized (mLock) { + if (mIsLaunching) { + if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to BACKGROUND"); + android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + } + } + waitForIdle(); + + // second step + if (loadWorkspaceFirst) { + if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps"); + loadAndBindAllApps(); + } else { + if (DEBUG_LOADERS) Log.d(TAG, "step 2: special: loading workspace"); + loadAndBindWorkspace(); + } + + // Restore the default thread priority after we are done loading items + synchronized (mLock) { + android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); + } + } + + + // Update the saved icons if necessary + if (DEBUG_LOADERS) Log.d(TAG, "Comparing loaded icons to database icons"); + synchronized (sBgLock) { + for (Object key : sBgDbIconCache.keySet()) { + updateSavedIcon(mContext, (ShortcutInfo) key, sBgDbIconCache.get(key)); + } + sBgDbIconCache.clear(); + } + + // Clear out this reference, otherwise we end up holding it until all of the + // callback runnables are done. + mContext = null; + + synchronized (mLock) { + // If we are still the last one to be scheduled, remove ourselves. + if (mLoaderTask == this) { + mLoaderTask = null; + } + mIsLoaderTaskRunning = false; + } + } + + public void stopLocked() { + synchronized (LoaderTask.this) { + mStopped = true; + this.notify(); + } + } + + /** + * Gets the callbacks object. If we've been stopped, or if the launcher object + * has somehow been garbage collected, return null instead. Pass in the Callbacks + * object that was around when the deferred message was scheduled, and if there's + * a new Callbacks object around then also return null. This will save us from + * calling onto it with data that will be ignored. + */ + Callbacks tryGetCallbacks(Callbacks oldCallbacks) { + synchronized (mLock) { + if (mStopped) { + return null; + } + + if (mCallbacks == null) { + return null; + } + + final Callbacks callbacks = mCallbacks.get(); + if (callbacks != oldCallbacks) { + return null; + } + if (callbacks == null) { + Log.w(TAG, "no mCallbacks"); + return null; + } + + return callbacks; + } + } + + // check & update map of what's occupied; used to discard overlapping/invalid items + private boolean checkItemPlacement(ItemInfo occupied[][][], ItemInfo item) { + int containerIndex = item.screen; + if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + // Return early if we detect that an item is under the hotseat button + if (mCallbacks == null || mCallbacks.get().isAllAppsButtonRank(item.screen)) { + return false; + } + + // We use the last index to refer to the hotseat and the screen as the rank, so + // test and update the occupied state accordingly + if (occupied[Launcher.SCREEN_COUNT][item.screen][0] != null) { + Log.e(TAG, "Error loading shortcut into hotseat " + item + + " into position (" + item.screen + ":" + item.cellX + "," + item.cellY + + ") occupied by " + occupied[Launcher.SCREEN_COUNT][item.screen][0]); + return false; + } else { + occupied[Launcher.SCREEN_COUNT][item.screen][0] = item; + return true; + } + } else if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP) { + // Skip further checking if it is not the hotseat or workspace container + return true; + } + + // Check if any workspace icons overlap with each other + for (int x = item.cellX; x < (item.cellX+item.spanX); x++) { + for (int y = item.cellY; y < (item.cellY+item.spanY); y++) { + if (occupied[containerIndex][x][y] != null) { + Log.e(TAG, "Error loading shortcut " + item + + " into cell (" + containerIndex + "-" + item.screen + ":" + + x + "," + y + + ") occupied by " + + occupied[containerIndex][x][y]); + return false; + } + } + } + for (int x = item.cellX; x < (item.cellX+item.spanX); x++) { + for (int y = item.cellY; y < (item.cellY+item.spanY); y++) { + occupied[containerIndex][x][y] = item; + } + } + + return true; + } + + private void loadWorkspace() { + final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + + final Context context = mContext; + final ContentResolver contentResolver = context.getContentResolver(); + final PackageManager manager = context.getPackageManager(); + final AppWidgetManager widgets = AppWidgetManager.getInstance(context); + final boolean isSafeMode = manager.isSafeMode(); + + // Make sure the default workspace is loaded, if needed + mApp.getLauncherProvider().loadDefaultFavoritesIfNecessary(0); + + synchronized (sBgLock) { + sBgWorkspaceItems.clear(); + sBgAppWidgets.clear(); + sBgFolders.clear(); + sBgItemsIdMap.clear(); + sBgDbIconCache.clear(); + + final ArrayList<Long> itemsToRemove = new ArrayList<Long>(); + + final Cursor c = contentResolver.query( + LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); + + // +1 for the hotseat (it can be larger than the workspace) + // Load workspace in reverse order to ensure that latest items are loaded first (and + // before any earlier duplicates) + final ItemInfo occupied[][][] = + new ItemInfo[Launcher.SCREEN_COUNT + 1][mCellCountX + 1][mCellCountY + 1]; + + try { + final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); + final int intentIndex = c.getColumnIndexOrThrow + (LauncherSettings.Favorites.INTENT); + final int titleIndex = c.getColumnIndexOrThrow + (LauncherSettings.Favorites.TITLE); + final int iconTypeIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.ICON_TYPE); + final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); + final int iconPackageIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.ICON_PACKAGE); + final int iconResourceIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.ICON_RESOURCE); + final int containerIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.CONTAINER); + final int itemTypeIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.ITEM_TYPE); + final int appWidgetIdIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.APPWIDGET_ID); + final int screenIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.SCREEN); + final int cellXIndex = c.getColumnIndexOrThrow + (LauncherSettings.Favorites.CELLX); + final int cellYIndex = c.getColumnIndexOrThrow + (LauncherSettings.Favorites.CELLY); + final int spanXIndex = c.getColumnIndexOrThrow + (LauncherSettings.Favorites.SPANX); + final int spanYIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.SPANY); + //final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); + //final int displayModeIndex = c.getColumnIndexOrThrow( + // LauncherSettings.Favorites.DISPLAY_MODE); + + ShortcutInfo info; + String intentDescription; + LauncherAppWidgetInfo appWidgetInfo; + int container; + long id; + Intent intent; + + while (!mStopped && c.moveToNext()) { + try { + int itemType = c.getInt(itemTypeIndex); + + switch (itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + intentDescription = c.getString(intentIndex); + try { + intent = Intent.parseUri(intentDescription, 0); + } catch (URISyntaxException e) { + continue; + } + + if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { + info = getShortcutInfo(manager, intent, context, c, iconIndex, + titleIndex, mLabelCache); + } else { + info = getShortcutInfo(c, context, iconTypeIndex, + iconPackageIndex, iconResourceIndex, iconIndex, + titleIndex); + + // App shortcuts that used to be automatically added to Launcher + // didn't always have the correct intent flags set, so do that + // here + if (intent.getAction() != null && + intent.getCategories() != null && + intent.getAction().equals(Intent.ACTION_MAIN) && + intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + } + } + + if (info != null) { + info.intent = intent; + info.id = c.getLong(idIndex); + container = c.getInt(containerIndex); + info.container = container; + info.screen = c.getInt(screenIndex); + info.cellX = c.getInt(cellXIndex); + info.cellY = c.getInt(cellYIndex); + + // check & update map of what's occupied + if (!checkItemPlacement(occupied, info)) { + break; + } + + switch (container) { + case LauncherSettings.Favorites.CONTAINER_DESKTOP: + case LauncherSettings.Favorites.CONTAINER_HOTSEAT: + sBgWorkspaceItems.add(info); + break; + default: + // Item is in a user folder + FolderInfo folderInfo = + findOrMakeFolder(sBgFolders, container); + folderInfo.add(info); + break; + } + sBgItemsIdMap.put(info.id, info); + + // now that we've loaded everthing re-save it with the + // icon in case it disappears somehow. + queueIconToBeChecked(sBgDbIconCache, info, c, iconIndex); + } else { + // Failed to load the shortcut, probably because the + // activity manager couldn't resolve it (maybe the app + // was uninstalled), or the db row was somehow screwed up. + // Delete it. + id = c.getLong(idIndex); + Log.e(TAG, "Error loading shortcut " + id + ", removing it"); + contentResolver.delete(LauncherSettings.Favorites.getContentUri( + id, false), null, null); + } + break; + + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + id = c.getLong(idIndex); + FolderInfo folderInfo = findOrMakeFolder(sBgFolders, id); + + folderInfo.title = c.getString(titleIndex); + folderInfo.id = id; + container = c.getInt(containerIndex); + folderInfo.container = container; + folderInfo.screen = c.getInt(screenIndex); + folderInfo.cellX = c.getInt(cellXIndex); + folderInfo.cellY = c.getInt(cellYIndex); + + // check & update map of what's occupied + if (!checkItemPlacement(occupied, folderInfo)) { + break; + } + switch (container) { + case LauncherSettings.Favorites.CONTAINER_DESKTOP: + case LauncherSettings.Favorites.CONTAINER_HOTSEAT: + sBgWorkspaceItems.add(folderInfo); + break; + } + + sBgItemsIdMap.put(folderInfo.id, folderInfo); + sBgFolders.put(folderInfo.id, folderInfo); + break; + + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + // Read all Launcher-specific widget details + int appWidgetId = c.getInt(appWidgetIdIndex); + id = c.getLong(idIndex); + + final AppWidgetProviderInfo provider = + widgets.getAppWidgetInfo(appWidgetId); + + if (!isSafeMode && (provider == null || provider.provider == null || + provider.provider.getPackageName() == null)) { + String log = "Deleting widget that isn't installed anymore: id=" + + id + " appWidgetId=" + appWidgetId; + Log.e(TAG, log); + Launcher.sDumpLogs.add(log); + itemsToRemove.add(id); + } else { + appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, + provider.provider); + appWidgetInfo.id = id; + appWidgetInfo.screen = c.getInt(screenIndex); + appWidgetInfo.cellX = c.getInt(cellXIndex); + appWidgetInfo.cellY = c.getInt(cellYIndex); + appWidgetInfo.spanX = c.getInt(spanXIndex); + appWidgetInfo.spanY = c.getInt(spanYIndex); + int[] minSpan = Launcher.getMinSpanForWidget(context, provider); + appWidgetInfo.minSpanX = minSpan[0]; + appWidgetInfo.minSpanY = minSpan[1]; + + container = c.getInt(containerIndex); + if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP && + container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + Log.e(TAG, "Widget found where container != " + + "CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!"); + continue; + } + appWidgetInfo.container = c.getInt(containerIndex); + + // check & update map of what's occupied + if (!checkItemPlacement(occupied, appWidgetInfo)) { + break; + } + sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); + sBgAppWidgets.add(appWidgetInfo); + } + break; + } + } catch (Exception e) { + Log.w(TAG, "Desktop items loading interrupted:", e); + } + } + } finally { + c.close(); + } + + if (itemsToRemove.size() > 0) { + ContentProviderClient client = contentResolver.acquireContentProviderClient( + LauncherSettings.Favorites.CONTENT_URI); + // Remove dead items + for (long id : itemsToRemove) { + if (DEBUG_LOADERS) { + Log.d(TAG, "Removed id = " + id); + } + // Don't notify content observers + try { + client.delete(LauncherSettings.Favorites.getContentUri(id, false), + null, null); + } catch (RemoteException e) { + Log.w(TAG, "Could not remove id = " + id); + } + } + } + + if (DEBUG_LOADERS) { + Log.d(TAG, "loaded workspace in " + (SystemClock.uptimeMillis()-t) + "ms"); + Log.d(TAG, "workspace layout: "); + for (int y = 0; y < mCellCountY; y++) { + String line = ""; + for (int s = 0; s < Launcher.SCREEN_COUNT; s++) { + if (s > 0) { + line += " | "; + } + for (int x = 0; x < mCellCountX; x++) { + line += ((occupied[s][x][y] != null) ? "#" : "."); + } + } + Log.d(TAG, "[ " + line + " ]"); + } + } + } + } + + /** Filters the set of items who are directly or indirectly (via another container) on the + * specified screen. */ + private void filterCurrentWorkspaceItems(int currentScreen, + ArrayList<ItemInfo> allWorkspaceItems, + ArrayList<ItemInfo> currentScreenItems, + ArrayList<ItemInfo> otherScreenItems) { + // Purge any null ItemInfos + Iterator<ItemInfo> iter = allWorkspaceItems.iterator(); + while (iter.hasNext()) { + ItemInfo i = iter.next(); + if (i == null) { + iter.remove(); + } + } + + // If we aren't filtering on a screen, then the set of items to load is the full set of + // items given. + if (currentScreen < 0) { + currentScreenItems.addAll(allWorkspaceItems); + } + + // Order the set of items by their containers first, this allows use to walk through the + // list sequentially, build up a list of containers that are in the specified screen, + // as well as all items in those containers. + Set<Long> itemsOnScreen = new HashSet<Long>(); + Collections.sort(allWorkspaceItems, new Comparator<ItemInfo>() { + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return (int) (lhs.container - rhs.container); + } + }); + for (ItemInfo info : allWorkspaceItems) { + if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + if (info.screen == currentScreen) { + currentScreenItems.add(info); + itemsOnScreen.add(info.id); + } else { + otherScreenItems.add(info); + } + } else if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + currentScreenItems.add(info); + itemsOnScreen.add(info.id); + } else { + if (itemsOnScreen.contains(info.container)) { + currentScreenItems.add(info); + itemsOnScreen.add(info.id); + } else { + otherScreenItems.add(info); + } + } + } + } + + /** Filters the set of widgets which are on the specified screen. */ + private void filterCurrentAppWidgets(int currentScreen, + ArrayList<LauncherAppWidgetInfo> appWidgets, + ArrayList<LauncherAppWidgetInfo> currentScreenWidgets, + ArrayList<LauncherAppWidgetInfo> otherScreenWidgets) { + // If we aren't filtering on a screen, then the set of items to load is the full set of + // widgets given. + if (currentScreen < 0) { + currentScreenWidgets.addAll(appWidgets); + } + + for (LauncherAppWidgetInfo widget : appWidgets) { + if (widget == null) continue; + if (widget.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && + widget.screen == currentScreen) { + currentScreenWidgets.add(widget); + } else { + otherScreenWidgets.add(widget); + } + } + } + + /** Filters the set of folders which are on the specified screen. */ + private void filterCurrentFolders(int currentScreen, + HashMap<Long, ItemInfo> itemsIdMap, + HashMap<Long, FolderInfo> folders, + HashMap<Long, FolderInfo> currentScreenFolders, + HashMap<Long, FolderInfo> otherScreenFolders) { + // If we aren't filtering on a screen, then the set of items to load is the full set of + // widgets given. + if (currentScreen < 0) { + currentScreenFolders.putAll(folders); + } + + for (long id : folders.keySet()) { + ItemInfo info = itemsIdMap.get(id); + FolderInfo folder = folders.get(id); + if (info == null || folder == null) continue; + if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && + info.screen == currentScreen) { + currentScreenFolders.put(id, folder); + } else { + otherScreenFolders.put(id, folder); + } + } + } + + /** Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to + * right) */ + private void sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems) { + // XXX: review this + Collections.sort(workspaceItems, new Comparator<ItemInfo>() { + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + int cellCountX = LauncherModel.getCellCountX(); + int cellCountY = LauncherModel.getCellCountY(); + int screenOffset = cellCountX * cellCountY; + int containerOffset = screenOffset * (Launcher.SCREEN_COUNT + 1); // +1 hotseat + long lr = (lhs.container * containerOffset + lhs.screen * screenOffset + + lhs.cellY * cellCountX + lhs.cellX); + long rr = (rhs.container * containerOffset + rhs.screen * screenOffset + + rhs.cellY * cellCountX + rhs.cellX); + return (int) (lr - rr); + } + }); + } + + private void bindWorkspaceItems(final Callbacks oldCallbacks, + final ArrayList<ItemInfo> workspaceItems, + final ArrayList<LauncherAppWidgetInfo> appWidgets, + final HashMap<Long, FolderInfo> folders, + ArrayList<Runnable> deferredBindRunnables) { + + final boolean postOnMainThread = (deferredBindRunnables != null); + + // Bind the workspace items + int N = workspaceItems.size(); + for (int i = 0; i < N; i += ITEMS_CHUNK) { + final int start = i; + final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i); + final Runnable r = new Runnable() { + @Override + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.bindItems(workspaceItems, start, start+chunkSize); + } + } + }; + if (postOnMainThread) { + deferredBindRunnables.add(r); + } else { + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + } + + // Bind the folders + if (!folders.isEmpty()) { + final Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.bindFolders(folders); + } + } + }; + if (postOnMainThread) { + deferredBindRunnables.add(r); + } else { + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + } + + // Bind the widgets, one at a time + N = appWidgets.size(); + for (int i = 0; i < N; i++) { + final LauncherAppWidgetInfo widget = appWidgets.get(i); + final Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.bindAppWidget(widget); + } + } + }; + if (postOnMainThread) { + deferredBindRunnables.add(r); + } else { + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + } + } + + /** + * Binds all loaded data to actual views on the main thread. + */ + private void bindWorkspace(int synchronizeBindPage) { + final long t = SystemClock.uptimeMillis(); + Runnable r; + + // Don't use these two variables in any of the callback runnables. + // Otherwise we hold a reference to them. + final Callbacks oldCallbacks = mCallbacks.get(); + if (oldCallbacks == null) { + // This launcher has exited and nobody bothered to tell us. Just bail. + Log.w(TAG, "LoaderTask running with no launcher"); + return; + } + + final boolean isLoadingSynchronously = (synchronizeBindPage > -1); + final int currentScreen = isLoadingSynchronously ? synchronizeBindPage : + oldCallbacks.getCurrentWorkspaceScreen(); + + // Load all the items that are on the current page first (and in the process, unbind + // all the existing workspace items before we call startBinding() below. + unbindWorkspaceItemsOnMainThread(); + ArrayList<ItemInfo> workspaceItems = new ArrayList<ItemInfo>(); + ArrayList<LauncherAppWidgetInfo> appWidgets = + new ArrayList<LauncherAppWidgetInfo>(); + HashMap<Long, FolderInfo> folders = new HashMap<Long, FolderInfo>(); + HashMap<Long, ItemInfo> itemsIdMap = new HashMap<Long, ItemInfo>(); + synchronized (sBgLock) { + workspaceItems.addAll(sBgWorkspaceItems); + appWidgets.addAll(sBgAppWidgets); + folders.putAll(sBgFolders); + itemsIdMap.putAll(sBgItemsIdMap); + } + + ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<ItemInfo>(); + ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<ItemInfo>(); + ArrayList<LauncherAppWidgetInfo> currentAppWidgets = + new ArrayList<LauncherAppWidgetInfo>(); + ArrayList<LauncherAppWidgetInfo> otherAppWidgets = + new ArrayList<LauncherAppWidgetInfo>(); + HashMap<Long, FolderInfo> currentFolders = new HashMap<Long, FolderInfo>(); + HashMap<Long, FolderInfo> otherFolders = new HashMap<Long, FolderInfo>(); + + // Separate the items that are on the current screen, and all the other remaining items + filterCurrentWorkspaceItems(currentScreen, workspaceItems, currentWorkspaceItems, + otherWorkspaceItems); + filterCurrentAppWidgets(currentScreen, appWidgets, currentAppWidgets, + otherAppWidgets); + filterCurrentFolders(currentScreen, itemsIdMap, folders, currentFolders, + otherFolders); + sortWorkspaceItemsSpatially(currentWorkspaceItems); + sortWorkspaceItemsSpatially(otherWorkspaceItems); + + // Tell the workspace that we're about to start binding items + r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.startBinding(); + } + } + }; + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + + // Load items on the current page + bindWorkspaceItems(oldCallbacks, currentWorkspaceItems, currentAppWidgets, + currentFolders, null); + if (isLoadingSynchronously) { + r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.onPageBoundSynchronously(currentScreen); + } + } + }; + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + + // Load all the remaining pages (if we are loading synchronously, we want to defer this + // work until after the first render) + mDeferredBindRunnables.clear(); + bindWorkspaceItems(oldCallbacks, otherWorkspaceItems, otherAppWidgets, otherFolders, + (isLoadingSynchronously ? mDeferredBindRunnables : null)); + + // Tell the workspace that we're done binding items + r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.finishBindingItems(); + } + + // If we're profiling, ensure this is the last thing in the queue. + if (DEBUG_LOADERS) { + Log.d(TAG, "bound workspace in " + + (SystemClock.uptimeMillis()-t) + "ms"); + } + + mIsLoadingAndBindingWorkspace = false; + } + }; + if (isLoadingSynchronously) { + mDeferredBindRunnables.add(r); + } else { + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + } + + private void loadAndBindAllApps() { + if (DEBUG_LOADERS) { + Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded); + } + if (!mAllAppsLoaded) { + loadAllAppsByBatch(); + synchronized (LoaderTask.this) { + if (mStopped) { + return; + } + mAllAppsLoaded = true; + } + } else { + onlyBindAllApps(); + } + } + + private void onlyBindAllApps() { + final Callbacks oldCallbacks = mCallbacks.get(); + if (oldCallbacks == null) { + // This launcher has exited and nobody bothered to tell us. Just bail. + Log.w(TAG, "LoaderTask running with no launcher (onlyBindAllApps)"); + return; + } + + // shallow copy + @SuppressWarnings("unchecked") + final ArrayList<ApplicationInfo> list + = (ArrayList<ApplicationInfo>) mBgAllAppsList.data.clone(); + Runnable r = new Runnable() { + public void run() { + final long t = SystemClock.uptimeMillis(); + final Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.bindAllApplications(list); + } + if (DEBUG_LOADERS) { + Log.d(TAG, "bound all " + list.size() + " apps from cache in " + + (SystemClock.uptimeMillis()-t) + "ms"); + } + } + }; + boolean isRunningOnMainThread = !(sWorkerThread.getThreadId() == Process.myTid()); + if (oldCallbacks.isAllAppsVisible() && isRunningOnMainThread) { + r.run(); + } else { + mHandler.post(r); + } + } + + private void loadAllAppsByBatch() { + final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + + // Don't use these two variables in any of the callback runnables. + // Otherwise we hold a reference to them. + final Callbacks oldCallbacks = mCallbacks.get(); + if (oldCallbacks == null) { + // This launcher has exited and nobody bothered to tell us. Just bail. + Log.w(TAG, "LoaderTask running with no launcher (loadAllAppsByBatch)"); + return; + } + + final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + + final PackageManager packageManager = mContext.getPackageManager(); + List<ResolveInfo> apps = null; + + int N = Integer.MAX_VALUE; + + int startIndex; + int i=0; + int batchSize = -1; + while (i < N && !mStopped) { + if (i == 0) { + mBgAllAppsList.clear(); + final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + apps = packageManager.queryIntentActivities(mainIntent, 0); + if (DEBUG_LOADERS) { + Log.d(TAG, "queryIntentActivities took " + + (SystemClock.uptimeMillis()-qiaTime) + "ms"); + } + if (apps == null) { + return; + } + N = apps.size(); + if (DEBUG_LOADERS) { + Log.d(TAG, "queryIntentActivities got " + N + " apps"); + } + if (N == 0) { + // There are no apps?!? + return; + } + if (mBatchSize == 0) { + batchSize = N; + } else { + batchSize = mBatchSize; + } + + final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + Collections.sort(apps, + new LauncherModel.ShortcutNameComparator(packageManager, mLabelCache)); + if (DEBUG_LOADERS) { + Log.d(TAG, "sort took " + + (SystemClock.uptimeMillis()-sortTime) + "ms"); + } + } + + final long t2 = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + + startIndex = i; + for (int j=0; i<N && j<batchSize; j++) { + // This builds the icon bitmaps. + mBgAllAppsList.add(new ApplicationInfo(packageManager, apps.get(i), + mIconCache, mLabelCache)); + i++; + } + + final boolean first = i <= batchSize; + final Callbacks callbacks = tryGetCallbacks(oldCallbacks); + final ArrayList<ApplicationInfo> added = mBgAllAppsList.added; + mBgAllAppsList.added = new ArrayList<ApplicationInfo>(); + + mHandler.post(new Runnable() { + public void run() { + final long t = SystemClock.uptimeMillis(); + if (callbacks != null) { + if (first) { + callbacks.bindAllApplications(added); + } else { + callbacks.bindAppsAdded(added); + } + if (DEBUG_LOADERS) { + Log.d(TAG, "bound " + added.size() + " apps in " + + (SystemClock.uptimeMillis() - t) + "ms"); + } + } else { + Log.i(TAG, "not binding apps: no Launcher activity"); + } + } + }); + + if (DEBUG_LOADERS) { + Log.d(TAG, "batch of " + (i-startIndex) + " icons processed in " + + (SystemClock.uptimeMillis()-t2) + "ms"); + } + + if (mAllAppsLoadDelay > 0 && i < N) { + try { + if (DEBUG_LOADERS) { + Log.d(TAG, "sleeping for " + mAllAppsLoadDelay + "ms"); + } + Thread.sleep(mAllAppsLoadDelay); + } catch (InterruptedException exc) { } + } + } + + if (DEBUG_LOADERS) { + Log.d(TAG, "cached all " + N + " apps in " + + (SystemClock.uptimeMillis()-t) + "ms" + + (mAllAppsLoadDelay > 0 ? " (including delay)" : "")); + } + } + + public void dumpState() { + synchronized (sBgLock) { + Log.d(TAG, "mLoaderTask.mContext=" + mContext); + Log.d(TAG, "mLoaderTask.mIsLaunching=" + mIsLaunching); + Log.d(TAG, "mLoaderTask.mStopped=" + mStopped); + Log.d(TAG, "mLoaderTask.mLoadAndBindStepFinished=" + mLoadAndBindStepFinished); + Log.d(TAG, "mItems size=" + sBgWorkspaceItems.size()); + } + } + } + + void enqueuePackageUpdated(PackageUpdatedTask task) { + sWorker.post(task); + } + + private class PackageUpdatedTask implements Runnable { + int mOp; + String[] mPackages; + + public static final int OP_NONE = 0; + public static final int OP_ADD = 1; + public static final int OP_UPDATE = 2; + public static final int OP_REMOVE = 3; // uninstlled + public static final int OP_UNAVAILABLE = 4; // external media unmounted + + + public PackageUpdatedTask(int op, String[] packages) { + mOp = op; + mPackages = packages; + } + + public void run() { + final Context context = mApp; + + final String[] packages = mPackages; + final int N = packages.length; + switch (mOp) { + case OP_ADD: + for (int i=0; i<N; i++) { + if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]); + mBgAllAppsList.addPackage(context, packages[i]); + } + break; + case OP_UPDATE: + for (int i=0; i<N; i++) { + if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]); + mBgAllAppsList.updatePackage(context, packages[i]); + LauncherApplication app = + (LauncherApplication) context.getApplicationContext(); + WidgetPreviewLoader.removeFromDb( + app.getWidgetPreviewCacheDb(), packages[i]); + } + break; + case OP_REMOVE: + case OP_UNAVAILABLE: + for (int i=0; i<N; i++) { + if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); + mBgAllAppsList.removePackage(packages[i]); + LauncherApplication app = + (LauncherApplication) context.getApplicationContext(); + WidgetPreviewLoader.removeFromDb( + app.getWidgetPreviewCacheDb(), packages[i]); + } + break; + } + + ArrayList<ApplicationInfo> added = null; + ArrayList<ApplicationInfo> modified = null; + final ArrayList<ApplicationInfo> removedApps = new ArrayList<ApplicationInfo>(); + + if (mBgAllAppsList.added.size() > 0) { + added = new ArrayList<ApplicationInfo>(mBgAllAppsList.added); + mBgAllAppsList.added.clear(); + } + if (mBgAllAppsList.modified.size() > 0) { + modified = new ArrayList<ApplicationInfo>(mBgAllAppsList.modified); + mBgAllAppsList.modified.clear(); + } + if (mBgAllAppsList.removed.size() > 0) { + removedApps.addAll(mBgAllAppsList.removed); + mBgAllAppsList.removed.clear(); + } + + final Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == null) { + Log.w(TAG, "Nobody to tell about the new app. Launcher is probably loading."); + return; + } + + if (added != null) { + final ArrayList<ApplicationInfo> addedFinal = added; + mHandler.post(new Runnable() { + public void run() { + Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == cb && cb != null) { + callbacks.bindAppsAdded(addedFinal); + } + } + }); + } + if (modified != null) { + final ArrayList<ApplicationInfo> modifiedFinal = modified; + mHandler.post(new Runnable() { + public void run() { + Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == cb && cb != null) { + callbacks.bindAppsUpdated(modifiedFinal); + } + } + }); + } + // If a package has been removed, or an app has been removed as a result of + // an update (for example), make the removed callback. + if (mOp == OP_REMOVE || !removedApps.isEmpty()) { + final boolean permanent = (mOp == OP_REMOVE); + final ArrayList<String> removedPackageNames = + new ArrayList<String>(Arrays.asList(packages)); + + mHandler.post(new Runnable() { + public void run() { + Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == cb && cb != null) { + callbacks.bindComponentsRemoved(removedPackageNames, + removedApps, permanent); + } + } + }); + } + + final ArrayList<Object> widgetsAndShortcuts = + getSortedWidgetsAndShortcuts(context); + mHandler.post(new Runnable() { + @Override + public void run() { + Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == cb && cb != null) { + callbacks.bindPackagesUpdated(widgetsAndShortcuts); + } + } + }); + } + } + + // Returns a list of ResolveInfos/AppWindowInfos in sorted order + public static ArrayList<Object> getSortedWidgetsAndShortcuts(Context context) { + PackageManager packageManager = context.getPackageManager(); + final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>(); + widgetsAndShortcuts.addAll(AppWidgetManager.getInstance(context).getInstalledProviders()); + Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); + widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0)); + Collections.sort(widgetsAndShortcuts, + new LauncherModel.WidgetAndShortcutNameComparator(packageManager)); + return widgetsAndShortcuts; + } + + /** + * This is called from the code that adds shortcuts from the intent receiver. This + * doesn't have a Cursor, but + */ + public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, Context context) { + return getShortcutInfo(manager, intent, context, null, -1, -1, null); + } + + /** + * Make an ShortcutInfo object for a shortcut that is an application. + * + * If c is not null, then it will be used to fill in missing data like the title and icon. + */ + public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, Context context, + Cursor c, int iconIndex, int titleIndex, HashMap<Object, CharSequence> labelCache) { + Bitmap icon = null; + final ShortcutInfo info = new ShortcutInfo(); + + ComponentName componentName = intent.getComponent(); + if (componentName == null) { + return null; + } + + try { + PackageInfo pi = manager.getPackageInfo(componentName.getPackageName(), 0); + if (!pi.applicationInfo.enabled) { + // If we return null here, the corresponding item will be removed from the launcher + // db and will not appear in the workspace. + return null; + } + } catch (NameNotFoundException e) { + Log.d(TAG, "getPackInfo failed for package " + componentName.getPackageName()); + } + + // TODO: See if the PackageManager knows about this case. If it doesn't + // then return null & delete this. + + // the resource -- This may implicitly give us back the fallback icon, + // but don't worry about that. All we're doing with usingFallbackIcon is + // to avoid saving lots of copies of that in the database, and most apps + // have icons anyway. + + // Attempt to use queryIntentActivities to get the ResolveInfo (with IntentFilter info) and + // if that fails, or is ambiguious, fallback to the standard way of getting the resolve info + // via resolveActivity(). + ResolveInfo resolveInfo = null; + ComponentName oldComponent = intent.getComponent(); + Intent newIntent = new Intent(intent.getAction(), null); + newIntent.addCategory(Intent.CATEGORY_LAUNCHER); + newIntent.setPackage(oldComponent.getPackageName()); + List<ResolveInfo> infos = manager.queryIntentActivities(newIntent, 0); + for (ResolveInfo i : infos) { + ComponentName cn = new ComponentName(i.activityInfo.packageName, + i.activityInfo.name); + if (cn.equals(oldComponent)) { + resolveInfo = i; + } + } + if (resolveInfo == null) { + resolveInfo = manager.resolveActivity(intent, 0); + } + if (resolveInfo != null) { + icon = mIconCache.getIcon(componentName, resolveInfo, labelCache); + } + // the db + if (icon == null) { + if (c != null) { + icon = getIconFromCursor(c, iconIndex, context); + } + } + // the fallback icon + if (icon == null) { + icon = getFallbackIcon(); + info.usingFallbackIcon = true; + } + info.setIcon(icon); + + // from the resource + if (resolveInfo != null) { + ComponentName key = LauncherModel.getComponentNameFromResolveInfo(resolveInfo); + if (labelCache != null && labelCache.containsKey(key)) { + info.title = labelCache.get(key); + } else { + info.title = resolveInfo.activityInfo.loadLabel(manager); + if (labelCache != null) { + labelCache.put(key, info.title); + } + } + } + // from the db + if (info.title == null) { + if (c != null) { + info.title = c.getString(titleIndex); + } + } + // fall back to the class name of the activity + if (info.title == null) { + info.title = componentName.getClassName(); + } + info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; + return info; + } + + /** + * Returns the set of workspace ShortcutInfos with the specified intent. + */ + static ArrayList<ItemInfo> getWorkspaceShortcutItemInfosWithIntent(Intent intent) { + ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); + synchronized (sBgLock) { + for (ItemInfo info : sBgWorkspaceItems) { + if (info instanceof ShortcutInfo) { + ShortcutInfo shortcut = (ShortcutInfo) info; + if (shortcut.intent.toUri(0).equals(intent.toUri(0))) { + items.add(shortcut); + } + } + } + } + return items; + } + + /** + * Make an ShortcutInfo object for a shortcut that isn't an application. + */ + private ShortcutInfo getShortcutInfo(Cursor c, Context context, + int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, int iconIndex, + int titleIndex) { + + Bitmap icon = null; + final ShortcutInfo info = new ShortcutInfo(); + info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + + // TODO: If there's an explicit component and we can't install that, delete it. + + info.title = c.getString(titleIndex); + + int iconType = c.getInt(iconTypeIndex); + switch (iconType) { + case LauncherSettings.Favorites.ICON_TYPE_RESOURCE: + String packageName = c.getString(iconPackageIndex); + String resourceName = c.getString(iconResourceIndex); + PackageManager packageManager = context.getPackageManager(); + info.customIcon = false; + // the resource + try { + Resources resources = packageManager.getResourcesForApplication(packageName); + if (resources != null) { + final int id = resources.getIdentifier(resourceName, null, null); + icon = Utilities.createIconBitmap( + mIconCache.getFullResIcon(resources, id), context); + } + } catch (Exception e) { + // drop this. we have other places to look for icons + } + // the db + if (icon == null) { + icon = getIconFromCursor(c, iconIndex, context); + } + // the fallback icon + if (icon == null) { + icon = getFallbackIcon(); + info.usingFallbackIcon = true; + } + break; + case LauncherSettings.Favorites.ICON_TYPE_BITMAP: + icon = getIconFromCursor(c, iconIndex, context); + if (icon == null) { + icon = getFallbackIcon(); + info.customIcon = false; + info.usingFallbackIcon = true; + } else { + info.customIcon = true; + } + break; + default: + icon = getFallbackIcon(); + info.usingFallbackIcon = true; + info.customIcon = false; + break; + } + info.setIcon(icon); + return info; + } + + Bitmap getIconFromCursor(Cursor c, int iconIndex, Context context) { + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + Log.d(TAG, "getIconFromCursor app=" + + c.getString(c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE))); + } + byte[] data = c.getBlob(iconIndex); + try { + return Utilities.createIconBitmap( + BitmapFactory.decodeByteArray(data, 0, data.length), context); + } catch (Exception e) { + return null; + } + } + + ShortcutInfo addShortcut(Context context, Intent data, long container, int screen, + int cellX, int cellY, boolean notify) { + final ShortcutInfo info = infoFromShortcutIntent(context, data, null); + if (info == null) { + return null; + } + addItemToDatabase(context, info, container, screen, cellX, cellY, notify); + + return info; + } + + /** + * Attempts to find an AppWidgetProviderInfo that matches the given component. + */ + AppWidgetProviderInfo findAppWidgetProviderInfoWithComponent(Context context, + ComponentName component) { + List<AppWidgetProviderInfo> widgets = + AppWidgetManager.getInstance(context).getInstalledProviders(); + for (AppWidgetProviderInfo info : widgets) { + if (info.provider.equals(component)) { + return info; + } + } + return null; + } + + /** + * Returns a list of all the widgets that can handle configuration with a particular mimeType. + */ + List<WidgetMimeTypeHandlerData> resolveWidgetsForMimeType(Context context, String mimeType) { + final PackageManager packageManager = context.getPackageManager(); + final List<WidgetMimeTypeHandlerData> supportedConfigurationActivities = + new ArrayList<WidgetMimeTypeHandlerData>(); + + final Intent supportsIntent = + new Intent(InstallWidgetReceiver.ACTION_SUPPORTS_CLIPDATA_MIMETYPE); + supportsIntent.setType(mimeType); + + // Create a set of widget configuration components that we can test against + final List<AppWidgetProviderInfo> widgets = + AppWidgetManager.getInstance(context).getInstalledProviders(); + final HashMap<ComponentName, AppWidgetProviderInfo> configurationComponentToWidget = + new HashMap<ComponentName, AppWidgetProviderInfo>(); + for (AppWidgetProviderInfo info : widgets) { + configurationComponentToWidget.put(info.configure, info); + } + + // Run through each of the intents that can handle this type of clip data, and cross + // reference them with the components that are actual configuration components + final List<ResolveInfo> activities = packageManager.queryIntentActivities(supportsIntent, + PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo info : activities) { + final ActivityInfo activityInfo = info.activityInfo; + final ComponentName infoComponent = new ComponentName(activityInfo.packageName, + activityInfo.name); + if (configurationComponentToWidget.containsKey(infoComponent)) { + supportedConfigurationActivities.add( + new InstallWidgetReceiver.WidgetMimeTypeHandlerData(info, + configurationComponentToWidget.get(infoComponent))); + } + } + return supportedConfigurationActivities; + } + + ShortcutInfo infoFromShortcutIntent(Context context, Intent data, Bitmap fallbackIcon) { + Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); + String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); + Parcelable bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON); + + if (intent == null) { + // If the intent is null, we can't construct a valid ShortcutInfo, so we return null + Log.e(TAG, "Can't construct ShorcutInfo with null intent"); + return null; + } + + Bitmap icon = null; + boolean customIcon = false; + ShortcutIconResource iconResource = null; + + if (bitmap != null && bitmap instanceof Bitmap) { + icon = Utilities.createIconBitmap(new FastBitmapDrawable((Bitmap)bitmap), context); + customIcon = true; + } else { + Parcelable extra = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE); + if (extra != null && extra instanceof ShortcutIconResource) { + try { + iconResource = (ShortcutIconResource) extra; + final PackageManager packageManager = context.getPackageManager(); + Resources resources = packageManager.getResourcesForApplication( + iconResource.packageName); + final int id = resources.getIdentifier(iconResource.resourceName, null, null); + icon = Utilities.createIconBitmap( + mIconCache.getFullResIcon(resources, id), context); + } catch (Exception e) { + Log.w(TAG, "Could not load shortcut icon: " + extra); + } + } + } + + final ShortcutInfo info = new ShortcutInfo(); + + if (icon == null) { + if (fallbackIcon != null) { + icon = fallbackIcon; + } else { + icon = getFallbackIcon(); + info.usingFallbackIcon = true; + } + } + info.setIcon(icon); + + info.title = name; + info.intent = intent; + info.customIcon = customIcon; + info.iconResource = iconResource; + + return info; + } + + boolean queueIconToBeChecked(HashMap<Object, byte[]> cache, ShortcutInfo info, Cursor c, + int iconIndex) { + // If apps can't be on SD, don't even bother. + if (!mAppsCanBeOnExternalStorage) { + return false; + } + // If this icon doesn't have a custom icon, check to see + // what's stored in the DB, and if it doesn't match what + // we're going to show, store what we are going to show back + // into the DB. We do this so when we're loading, if the + // package manager can't find an icon (for example because + // the app is on SD) then we can use that instead. + if (!info.customIcon && !info.usingFallbackIcon) { + cache.put(info, c.getBlob(iconIndex)); + return true; + } + return false; + } + void updateSavedIcon(Context context, ShortcutInfo info, byte[] data) { + boolean needSave = false; + try { + if (data != null) { + Bitmap saved = BitmapFactory.decodeByteArray(data, 0, data.length); + Bitmap loaded = info.getIcon(mIconCache); + needSave = !saved.sameAs(loaded); + } else { + needSave = true; + } + } catch (Exception e) { + needSave = true; + } + if (needSave) { + Log.d(TAG, "going to save icon bitmap for info=" + info); + // This is slower than is ideal, but this only happens once + // or when the app is updated with a new icon. + updateItemInDatabase(context, info); + } + } + + /** + * Return an existing FolderInfo object if we have encountered this ID previously, + * or make a new one. + */ + private static FolderInfo findOrMakeFolder(HashMap<Long, FolderInfo> folders, long id) { + // See if a placeholder was created for us already + FolderInfo folderInfo = folders.get(id); + if (folderInfo == null) { + // No placeholder -- create a new instance + folderInfo = new FolderInfo(); + folders.put(id, folderInfo); + } + return folderInfo; + } + + public static final Comparator<ApplicationInfo> getAppNameComparator() { + final Collator collator = Collator.getInstance(); + return new Comparator<ApplicationInfo>() { + public final int compare(ApplicationInfo a, ApplicationInfo b) { + int result = collator.compare(a.title.toString(), b.title.toString()); + if (result == 0) { + result = a.componentName.compareTo(b.componentName); + } + return result; + } + }; + } + public static final Comparator<ApplicationInfo> APP_INSTALL_TIME_COMPARATOR + = new Comparator<ApplicationInfo>() { + public final int compare(ApplicationInfo a, ApplicationInfo b) { + if (a.firstInstallTime < b.firstInstallTime) return 1; + if (a.firstInstallTime > b.firstInstallTime) return -1; + return 0; + } + }; + public static final Comparator<AppWidgetProviderInfo> getWidgetNameComparator() { + final Collator collator = Collator.getInstance(); + return new Comparator<AppWidgetProviderInfo>() { + public final int compare(AppWidgetProviderInfo a, AppWidgetProviderInfo b) { + return collator.compare(a.label.toString(), b.label.toString()); + } + }; + } + static ComponentName getComponentNameFromResolveInfo(ResolveInfo info) { + if (info.activityInfo != null) { + return new ComponentName(info.activityInfo.packageName, info.activityInfo.name); + } else { + return new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name); + } + } + public static class ShortcutNameComparator implements Comparator<ResolveInfo> { + private Collator mCollator; + private PackageManager mPackageManager; + private HashMap<Object, CharSequence> mLabelCache; + ShortcutNameComparator(PackageManager pm) { + mPackageManager = pm; + mLabelCache = new HashMap<Object, CharSequence>(); + mCollator = Collator.getInstance(); + } + ShortcutNameComparator(PackageManager pm, HashMap<Object, CharSequence> labelCache) { + mPackageManager = pm; + mLabelCache = labelCache; + mCollator = Collator.getInstance(); + } + public final int compare(ResolveInfo a, ResolveInfo b) { + CharSequence labelA, labelB; + ComponentName keyA = LauncherModel.getComponentNameFromResolveInfo(a); + ComponentName keyB = LauncherModel.getComponentNameFromResolveInfo(b); + if (mLabelCache.containsKey(keyA)) { + labelA = mLabelCache.get(keyA); + } else { + labelA = a.loadLabel(mPackageManager).toString(); + + mLabelCache.put(keyA, labelA); + } + if (mLabelCache.containsKey(keyB)) { + labelB = mLabelCache.get(keyB); + } else { + labelB = b.loadLabel(mPackageManager).toString(); + + mLabelCache.put(keyB, labelB); + } + return mCollator.compare(labelA, labelB); + } + }; + public static class WidgetAndShortcutNameComparator implements Comparator<Object> { + private Collator mCollator; + private PackageManager mPackageManager; + private HashMap<Object, String> mLabelCache; + WidgetAndShortcutNameComparator(PackageManager pm) { + mPackageManager = pm; + mLabelCache = new HashMap<Object, String>(); + mCollator = Collator.getInstance(); + } + public final int compare(Object a, Object b) { + String labelA, labelB; + if (mLabelCache.containsKey(a)) { + labelA = mLabelCache.get(a); + } else { + labelA = (a instanceof AppWidgetProviderInfo) ? + ((AppWidgetProviderInfo) a).label : + ((ResolveInfo) a).loadLabel(mPackageManager).toString(); + mLabelCache.put(a, labelA); + } + if (mLabelCache.containsKey(b)) { + labelB = mLabelCache.get(b); + } else { + labelB = (b instanceof AppWidgetProviderInfo) ? + ((AppWidgetProviderInfo) b).label : + ((ResolveInfo) b).loadLabel(mPackageManager).toString(); + mLabelCache.put(b, labelB); + } + return mCollator.compare(labelA, labelB); + } + }; + + public void dumpState() { + Log.d(TAG, "mCallbacks=" + mCallbacks); + ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.data", mBgAllAppsList.data); + ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.added", mBgAllAppsList.added); + ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.removed", mBgAllAppsList.removed); + ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.modified", mBgAllAppsList.modified); + if (mLoaderTask != null) { + mLoaderTask.dumpState(); + } else { + Log.d(TAG, "mLoaderTask=null"); + } + } +} diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java new file mode 100644 index 000000000..fb12f7163 --- /dev/null +++ b/src/com/android/launcher3/LauncherProvider.java @@ -0,0 +1,1193 @@ +/* + * 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.app.SearchManager; +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.database.sqlite.SQLiteStatement; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; + +import com.android.launcher3.R; +import com.android.launcher3.LauncherSettings.Favorites; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +public class LauncherProvider extends ContentProvider { + private static final String TAG = "Launcher.LauncherProvider"; + private static final boolean LOGD = false; + + private static final String DATABASE_NAME = "launcher.db"; + + private static final int DATABASE_VERSION = 12; + + static final String AUTHORITY = "com.android.launcher3.settings"; + + static final String TABLE_FAVORITES = "favorites"; + static final String PARAMETER_NOTIFY = "notify"; + static final String DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED = + "DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED"; + static final String DEFAULT_WORKSPACE_RESOURCE_ID = + "DEFAULT_WORKSPACE_RESOURCE_ID"; + + private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE = + "com.android.launcher3.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE"; + + /** + * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when + * {@link AppWidgetHost#deleteHost()} is called during database creation. + * Use this to recall {@link AppWidgetHost#startListening()} if needed. + */ + static final Uri CONTENT_APPWIDGET_RESET_URI = + Uri.parse("content://" + AUTHORITY + "/appWidgetReset"); + + private DatabaseHelper mOpenHelper; + + @Override + public boolean onCreate() { + mOpenHelper = new DatabaseHelper(getContext()); + ((LauncherApplication) getContext()).setLauncherProvider(this); + return true; + } + + @Override + public String getType(Uri uri) { + SqlArguments args = new SqlArguments(uri, null, null); + if (TextUtils.isEmpty(args.where)) { + return "vnd.android.cursor.dir/" + args.table; + } else { + return "vnd.android.cursor.item/" + args.table; + } + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + + SqlArguments args = new SqlArguments(uri, selection, selectionArgs); + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(args.table); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder); + result.setNotificationUri(getContext().getContentResolver(), uri); + + return result; + } + + private static long dbInsertAndCheck(DatabaseHelper helper, + SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) { + if (!values.containsKey(LauncherSettings.Favorites._ID)) { + throw new RuntimeException("Error: attempting to add item without specifying an id"); + } + return db.insert(table, nullColumnHack, values); + } + + private static void deleteId(SQLiteDatabase db, long id) { + Uri uri = LauncherSettings.Favorites.getContentUri(id, false); + SqlArguments args = new SqlArguments(uri, null, null); + db.delete(args.table, args.where, args.args); + } + + @Override + public Uri insert(Uri uri, ContentValues initialValues) { + SqlArguments args = new SqlArguments(uri); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); + if (rowId <= 0) return null; + + uri = ContentUris.withAppendedId(uri, rowId); + sendNotify(uri); + + return uri; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + SqlArguments args = new SqlArguments(uri); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + int numValues = values.length; + for (int i = 0; i < numValues; i++) { + if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) { + return 0; + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + sendNotify(uri); + return values.length; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + SqlArguments args = new SqlArguments(uri, selection, selectionArgs); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int count = db.delete(args.table, args.where, args.args); + if (count > 0) sendNotify(uri); + + return count; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + SqlArguments args = new SqlArguments(uri, selection, selectionArgs); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int count = db.update(args.table, values, args.where, args.args); + if (count > 0) sendNotify(uri); + + return count; + } + + private void sendNotify(Uri uri) { + String notify = uri.getQueryParameter(PARAMETER_NOTIFY); + if (notify == null || "true".equals(notify)) { + getContext().getContentResolver().notifyChange(uri, null); + } + } + + public long generateNewId() { + return mOpenHelper.generateNewId(); + } + + /** + * @param workspaceResId that can be 0 to use default or non-zero for specific resource + */ + synchronized public void loadDefaultFavoritesIfNecessary(int origWorkspaceResId) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE); + if (sp.getBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, false)) { + int workspaceResId = origWorkspaceResId; + + // Use default workspace resource if none provided + if (workspaceResId == 0) { + workspaceResId = sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, R.xml.default_workspace); + } + + // Populate favorites table with initial favorites + SharedPreferences.Editor editor = sp.edit(); + editor.remove(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED); + if (origWorkspaceResId != 0) { + editor.putInt(DEFAULT_WORKSPACE_RESOURCE_ID, origWorkspaceResId); + } + mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), workspaceResId); + editor.commit(); + } + } + + private static class DatabaseHelper extends SQLiteOpenHelper { + private static final String TAG_FAVORITES = "favorites"; + private static final String TAG_FAVORITE = "favorite"; + private static final String TAG_CLOCK = "clock"; + private static final String TAG_SEARCH = "search"; + private static final String TAG_APPWIDGET = "appwidget"; + private static final String TAG_SHORTCUT = "shortcut"; + private static final String TAG_FOLDER = "folder"; + private static final String TAG_EXTRA = "extra"; + + private final Context mContext; + private final AppWidgetHost mAppWidgetHost; + private long mMaxId = -1; + + DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + mContext = context; + mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); + + // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from + // the DB here + if (mMaxId == -1) { + mMaxId = initializeMaxId(getWritableDatabase()); + } + } + + /** + * Send notification that we've deleted the {@link AppWidgetHost}, + * probably as part of the initial database creation. The receiver may + * want to re-call {@link AppWidgetHost#startListening()} to ensure + * callbacks are correctly set. + */ + private void sendAppWidgetResetNotify() { + final ContentResolver resolver = mContext.getContentResolver(); + resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null); + } + + @Override + public void onCreate(SQLiteDatabase db) { + if (LOGD) Log.d(TAG, "creating new launcher database"); + + mMaxId = 1; + + db.execSQL("CREATE TABLE favorites (" + + "_id INTEGER PRIMARY KEY," + + "title TEXT," + + "intent TEXT," + + "container INTEGER," + + "screen INTEGER," + + "cellX INTEGER," + + "cellY INTEGER," + + "spanX INTEGER," + + "spanY INTEGER," + + "itemType INTEGER," + + "appWidgetId INTEGER NOT NULL DEFAULT -1," + + "isShortcut INTEGER," + + "iconType INTEGER," + + "iconPackage TEXT," + + "iconResource TEXT," + + "icon BLOB," + + "uri TEXT," + + "displayMode INTEGER" + + ");"); + + // Database was just created, so wipe any previous widgets + if (mAppWidgetHost != null) { + mAppWidgetHost.deleteHost(); + sendAppWidgetResetNotify(); + } + + if (!convertDatabase(db)) { + // Set a shared pref so that we know we need to load the default workspace later + setFlagToLoadDefaultWorkspaceLater(); + } + } + + private void setFlagToLoadDefaultWorkspaceLater() { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, true); + editor.commit(); + } + + private boolean convertDatabase(SQLiteDatabase db) { + if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade"); + boolean converted = false; + + final Uri uri = Uri.parse("content://" + Settings.AUTHORITY + + "/old_favorites?notify=true"); + final ContentResolver resolver = mContext.getContentResolver(); + Cursor cursor = null; + + try { + cursor = resolver.query(uri, null, null, null, null); + } catch (Exception e) { + // Ignore + } + + // We already have a favorites database in the old provider + if (cursor != null && cursor.getCount() > 0) { + try { + converted = copyFromCursor(db, cursor) > 0; + } finally { + cursor.close(); + } + + if (converted) { + resolver.delete(uri, null, null); + } + } + + if (converted) { + // Convert widgets from this import into widgets + if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade"); + convertWidgets(db); + } + + return converted; + } + + private int copyFromCursor(SQLiteDatabase db, Cursor c) { + final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); + final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); + final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); + final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE); + final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); + final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); + final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); + final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); + final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); + final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); + final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); + final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); + final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); + final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE); + + ContentValues[] rows = new ContentValues[c.getCount()]; + int i = 0; + while (c.moveToNext()) { + ContentValues values = new ContentValues(c.getColumnCount()); + values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex)); + values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex)); + values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex)); + values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex)); + values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex)); + values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex)); + values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex)); + values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex)); + values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex)); + values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1); + values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex)); + values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex)); + values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex)); + values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex)); + values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex)); + rows[i++] = values; + } + + db.beginTransaction(); + int total = 0; + try { + int numValues = rows.length; + for (i = 0; i < numValues; i++) { + if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) { + return 0; + } else { + total++; + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + return total; + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (LOGD) Log.d(TAG, "onUpgrade triggered"); + + int version = oldVersion; + if (version < 3) { + // upgrade 1,2 -> 3 added appWidgetId column + db.beginTransaction(); + try { + // Insert new column for holding appWidgetIds + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;"); + db.setTransactionSuccessful(); + version = 3; + } catch (SQLException ex) { + // Old version remains, which means we wipe old data + Log.e(TAG, ex.getMessage(), ex); + } finally { + db.endTransaction(); + } + + // Convert existing widgets only if table upgrade was successful + if (version == 3) { + convertWidgets(db); + } + } + + if (version < 4) { + version = 4; + } + + // Where's version 5? + // - Donut and sholes on 2.0 shipped with version 4 of launcher1. + // - Passion shipped on 2.1 with version 6 of launcher3 + // - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1 + // but version 5 on there was the updateContactsShortcuts change + // which was version 6 in launcher 2 (first shipped on passion 2.1r1). + // The updateContactsShortcuts change is idempotent, so running it twice + // is okay so we'll do that when upgrading the devices that shipped with it. + if (version < 6) { + // We went from 3 to 5 screens. Move everything 1 to the right + db.beginTransaction(); + try { + db.execSQL("UPDATE favorites SET screen=(screen + 1);"); + db.setTransactionSuccessful(); + } catch (SQLException ex) { + // Old version remains, which means we wipe old data + Log.e(TAG, ex.getMessage(), ex); + } finally { + db.endTransaction(); + } + + // We added the fast track. + if (updateContactsShortcuts(db)) { + version = 6; + } + } + + if (version < 7) { + // Version 7 gets rid of the special search widget. + convertWidgets(db); + version = 7; + } + + if (version < 8) { + // Version 8 (froyo) has the icons all normalized. This should + // already be the case in practice, but we now rely on it and don't + // resample the images each time. + normalizeIcons(db); + version = 8; + } + + if (version < 9) { + // The max id is not yet set at this point (onUpgrade is triggered in the ctor + // before it gets a change to get set, so we need to read it here when we use it) + if (mMaxId == -1) { + mMaxId = initializeMaxId(db); + } + + // Add default hotseat icons + loadFavorites(db, R.xml.update_workspace); + version = 9; + } + + // We bumped the version three time during JB, once to update the launch flags, once to + // update the override for the default launch animation and once to set the mimetype + // to improve startup performance + if (version < 12) { + // Contact shortcuts need a different set of flags to be launched now + // The updateContactsShortcuts change is idempotent, so we can keep using it like + // back in the Donut days + updateContactsShortcuts(db); + version = 12; + } + + if (version != DATABASE_VERSION) { + Log.w(TAG, "Destroying all old data."); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES); + onCreate(db); + } + } + + private boolean updateContactsShortcuts(SQLiteDatabase db) { + final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, + new int[] { Favorites.ITEM_TYPE_SHORTCUT }); + + Cursor c = null; + final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT"; + db.beginTransaction(); + try { + // Select and iterate through each matching widget + c = db.query(TABLE_FAVORITES, + new String[] { Favorites._ID, Favorites.INTENT }, + selectWhere, null, null, null, null); + if (c == null) return false; + + if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount()); + + final int idIndex = c.getColumnIndex(Favorites._ID); + final int intentIndex = c.getColumnIndex(Favorites.INTENT); + + while (c.moveToNext()) { + long favoriteId = c.getLong(idIndex); + final String intentUri = c.getString(intentIndex); + if (intentUri != null) { + try { + final Intent intent = Intent.parseUri(intentUri, 0); + android.util.Log.d("Home", intent.toString()); + final Uri uri = intent.getData(); + if (uri != null) { + final String data = uri.toString(); + if ((Intent.ACTION_VIEW.equals(intent.getAction()) || + actionQuickContact.equals(intent.getAction())) && + (data.startsWith("content://contacts/people/") || + data.startsWith("content://com.android.contacts/" + + "contacts/lookup/"))) { + + final Intent newIntent = new Intent(actionQuickContact); + // When starting from the launcher, start in a new, cleared task + // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we + // clear the whole thing preemptively here since + // QuickContactActivity will finish itself when launching other + // detail activities. + newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_CLEAR_TASK); + newIntent.putExtra( + Launcher.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true); + newIntent.setData(uri); + // Determine the type and also put that in the shortcut + // (that can speed up launch a bit) + newIntent.setDataAndType(uri, newIntent.resolveType(mContext)); + + final ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.INTENT, + newIntent.toUri(0)); + + String updateWhere = Favorites._ID + "=" + favoriteId; + db.update(TABLE_FAVORITES, values, updateWhere, null); + } + } + } catch (RuntimeException ex) { + Log.e(TAG, "Problem upgrading shortcut", ex); + } catch (URISyntaxException e) { + Log.e(TAG, "Problem upgrading shortcut", e); + } + } + } + + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.w(TAG, "Problem while upgrading contacts", ex); + return false; + } finally { + db.endTransaction(); + if (c != null) { + c.close(); + } + } + + return true; + } + + private void normalizeIcons(SQLiteDatabase db) { + Log.d(TAG, "normalizing icons"); + + db.beginTransaction(); + Cursor c = null; + SQLiteStatement update = null; + try { + boolean logged = false; + update = db.compileStatement("UPDATE favorites " + + "SET icon=? WHERE _id=?"); + + c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" + + Favorites.ICON_TYPE_BITMAP, null); + + final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); + final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON); + + while (c.moveToNext()) { + long id = c.getLong(idIndex); + byte[] data = c.getBlob(iconIndex); + try { + Bitmap bitmap = Utilities.resampleIconBitmap( + BitmapFactory.decodeByteArray(data, 0, data.length), + mContext); + if (bitmap != null) { + update.bindLong(1, id); + data = ItemInfo.flattenBitmap(bitmap); + if (data != null) { + update.bindBlob(2, data); + update.execute(); + } + bitmap.recycle(); + } + } catch (Exception e) { + if (!logged) { + Log.e(TAG, "Failed normalizing icon " + id, e); + } else { + Log.e(TAG, "Also failed normalizing icon " + id); + } + logged = true; + } + } + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex); + } finally { + db.endTransaction(); + if (update != null) { + update.close(); + } + if (c != null) { + c.close(); + } + } + } + + // Generates a new ID to use for an object in your database. This method should be only + // called from the main UI thread. As an exception, we do call it when we call the + // constructor from the worker thread; however, this doesn't extend until after the + // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp + // after that point + public long generateNewId() { + if (mMaxId < 0) { + throw new RuntimeException("Error: max id was not initialized"); + } + mMaxId += 1; + return mMaxId; + } + + private long initializeMaxId(SQLiteDatabase db) { + Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null); + + // get the result + final int maxIdIndex = 0; + long id = -1; + if (c != null && c.moveToNext()) { + id = c.getLong(maxIdIndex); + } + if (c != null) { + c.close(); + } + + if (id == -1) { + throw new RuntimeException("Error: could not query max id"); + } + + return id; + } + + /** + * Upgrade existing clock and photo frame widgets into their new widget + * equivalents. + */ + private void convertWidgets(SQLiteDatabase db) { + final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); + final int[] bindSources = new int[] { + Favorites.ITEM_TYPE_WIDGET_CLOCK, + Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME, + Favorites.ITEM_TYPE_WIDGET_SEARCH, + }; + + final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources); + + Cursor c = null; + + db.beginTransaction(); + try { + // Select and iterate through each matching widget + c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE }, + selectWhere, null, null, null, null); + + if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount()); + + final ContentValues values = new ContentValues(); + while (c != null && c.moveToNext()) { + long favoriteId = c.getLong(0); + int favoriteType = c.getInt(1); + + // Allocate and update database with new appWidgetId + try { + int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); + + if (LOGD) { + Log.d(TAG, "allocated appWidgetId=" + appWidgetId + + " for favoriteId=" + favoriteId); + } + values.clear(); + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); + values.put(Favorites.APPWIDGET_ID, appWidgetId); + + // Original widgets might not have valid spans when upgrading + if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) { + values.put(LauncherSettings.Favorites.SPANX, 4); + values.put(LauncherSettings.Favorites.SPANY, 1); + } else { + values.put(LauncherSettings.Favorites.SPANX, 2); + values.put(LauncherSettings.Favorites.SPANY, 2); + } + + String updateWhere = Favorites._ID + "=" + favoriteId; + db.update(TABLE_FAVORITES, values, updateWhere, null); + + if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) { + // TODO: check return value + appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, + new ComponentName("com.android.alarmclock", + "com.android.alarmclock.AnalogAppWidgetProvider")); + } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) { + // TODO: check return value + appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, + new ComponentName("com.android.camera", + "com.android.camera.PhotoAppWidgetProvider")); + } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) { + // TODO: check return value + appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, + getSearchWidgetProvider()); + } + } catch (RuntimeException ex) { + Log.e(TAG, "Problem allocating appWidgetId", ex); + } + } + + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex); + } finally { + db.endTransaction(); + if (c != null) { + c.close(); + } + } + } + + private static final void beginDocument(XmlPullParser parser, String firstElementName) + throws XmlPullParserException, IOException { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + ; + } + + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + if (!parser.getName().equals(firstElementName)) { + throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + + ", expected " + firstElementName); + } + } + + /** + * Loads the default set of favorite packages from an xml file. + * + * @param db The database to write the values into + * @param filterContainerId The specific container id of items to load + */ + private int loadFavorites(SQLiteDatabase db, int workspaceResourceId) { + Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + ContentValues values = new ContentValues(); + + PackageManager packageManager = mContext.getPackageManager(); + int allAppsButtonRank = + mContext.getResources().getInteger(R.integer.hotseat_all_apps_index); + int i = 0; + try { + XmlResourceParser parser = mContext.getResources().getXml(workspaceResourceId); + AttributeSet attrs = Xml.asAttributeSet(parser); + beginDocument(parser, TAG_FAVORITES); + + final int depth = parser.getDepth(); + + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + boolean added = false; + final String name = parser.getName(); + + TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Favorite); + + long container = LauncherSettings.Favorites.CONTAINER_DESKTOP; + if (a.hasValue(R.styleable.Favorite_container)) { + container = Long.valueOf(a.getString(R.styleable.Favorite_container)); + } + + String screen = a.getString(R.styleable.Favorite_screen); + String x = a.getString(R.styleable.Favorite_x); + String y = a.getString(R.styleable.Favorite_y); + + // If we are adding to the hotseat, the screen is used as the position in the + // hotseat. This screen can't be at position 0 because AllApps is in the + // zeroth position. + if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT + && Integer.valueOf(screen) == allAppsButtonRank) { + throw new RuntimeException("Invalid screen position for hotseat item"); + } + + values.clear(); + values.put(LauncherSettings.Favorites.CONTAINER, container); + values.put(LauncherSettings.Favorites.SCREEN, screen); + values.put(LauncherSettings.Favorites.CELLX, x); + values.put(LauncherSettings.Favorites.CELLY, y); + + if (TAG_FAVORITE.equals(name)) { + long id = addAppShortcut(db, values, a, packageManager, intent); + added = id >= 0; + } else if (TAG_SEARCH.equals(name)) { + added = addSearchWidget(db, values); + } else if (TAG_CLOCK.equals(name)) { + added = addClockWidget(db, values); + } else if (TAG_APPWIDGET.equals(name)) { + added = addAppWidget(parser, attrs, type, db, values, a, packageManager); + } else if (TAG_SHORTCUT.equals(name)) { + long id = addUriShortcut(db, values, a); + added = id >= 0; + } else if (TAG_FOLDER.equals(name)) { + String title; + int titleResId = a.getResourceId(R.styleable.Favorite_title, -1); + if (titleResId != -1) { + title = mContext.getResources().getString(titleResId); + } else { + title = mContext.getResources().getString(R.string.folder_name); + } + values.put(LauncherSettings.Favorites.TITLE, title); + long folderId = addFolder(db, values); + added = folderId >= 0; + + ArrayList<Long> folderItems = new ArrayList<Long>(); + + int folderDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > folderDepth) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final String folder_item_name = parser.getName(); + + TypedArray ar = mContext.obtainStyledAttributes(attrs, + R.styleable.Favorite); + values.clear(); + values.put(LauncherSettings.Favorites.CONTAINER, folderId); + + if (TAG_FAVORITE.equals(folder_item_name) && folderId >= 0) { + long id = + addAppShortcut(db, values, ar, packageManager, intent); + if (id >= 0) { + folderItems.add(id); + } + } else if (TAG_SHORTCUT.equals(folder_item_name) && folderId >= 0) { + long id = addUriShortcut(db, values, ar); + if (id >= 0) { + folderItems.add(id); + } + } else { + throw new RuntimeException("Folders can " + + "contain only shortcuts"); + } + ar.recycle(); + } + // We can only have folders with >= 2 items, so we need to remove the + // folder and clean up if less than 2 items were included, or some + // failed to add, and less than 2 were actually added + if (folderItems.size() < 2 && folderId >= 0) { + // We just delete the folder and any items that made it + deleteId(db, folderId); + if (folderItems.size() > 0) { + deleteId(db, folderItems.get(0)); + } + added = false; + } + } + if (added) i++; + a.recycle(); + } + } catch (XmlPullParserException e) { + Log.w(TAG, "Got exception parsing favorites.", e); + } catch (IOException e) { + Log.w(TAG, "Got exception parsing favorites.", e); + } catch (RuntimeException e) { + Log.w(TAG, "Got exception parsing favorites.", e); + } + + return i; + } + + private long addAppShortcut(SQLiteDatabase db, ContentValues values, TypedArray a, + PackageManager packageManager, Intent intent) { + long id = -1; + ActivityInfo info; + String packageName = a.getString(R.styleable.Favorite_packageName); + String className = a.getString(R.styleable.Favorite_className); + try { + ComponentName cn; + try { + cn = new ComponentName(packageName, className); + info = packageManager.getActivityInfo(cn, 0); + } catch (PackageManager.NameNotFoundException nnfe) { + String[] packages = packageManager.currentToCanonicalPackageNames( + new String[] { packageName }); + cn = new ComponentName(packages[0], className); + info = packageManager.getActivityInfo(cn, 0); + } + id = generateNewId(); + intent.setComponent(cn); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + values.put(Favorites.INTENT, intent.toUri(0)); + values.put(Favorites.TITLE, info.loadLabel(packageManager).toString()); + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION); + values.put(Favorites.SPANX, 1); + values.put(Favorites.SPANY, 1); + values.put(Favorites._ID, generateNewId()); + if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) { + return -1; + } + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Unable to add favorite: " + packageName + + "/" + className, e); + } + return id; + } + + private long addFolder(SQLiteDatabase db, ContentValues values) { + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER); + values.put(Favorites.SPANX, 1); + values.put(Favorites.SPANY, 1); + long id = generateNewId(); + values.put(Favorites._ID, id); + if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) <= 0) { + return -1; + } else { + return id; + } + } + + private ComponentName getSearchWidgetProvider() { + SearchManager searchManager = + (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); + ComponentName searchComponent = searchManager.getGlobalSearchActivity(); + if (searchComponent == null) return null; + return getProviderInPackage(searchComponent.getPackageName()); + } + + /** + * Gets an appwidget provider from the given package. If the package contains more than + * one appwidget provider, an arbitrary one is returned. + */ + private ComponentName getProviderInPackage(String packageName) { + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); + List<AppWidgetProviderInfo> providers = appWidgetManager.getInstalledProviders(); + if (providers == null) return null; + final int providerCount = providers.size(); + for (int i = 0; i < providerCount; i++) { + ComponentName provider = providers.get(i).provider; + if (provider != null && provider.getPackageName().equals(packageName)) { + return provider; + } + } + return null; + } + + private boolean addSearchWidget(SQLiteDatabase db, ContentValues values) { + ComponentName cn = getSearchWidgetProvider(); + return addAppWidget(db, values, cn, 4, 1, null); + } + + private boolean addClockWidget(SQLiteDatabase db, ContentValues values) { + ComponentName cn = new ComponentName("com.android.alarmclock", + "com.android.alarmclock.AnalogAppWidgetProvider"); + return addAppWidget(db, values, cn, 2, 2, null); + } + + private boolean addAppWidget(XmlResourceParser parser, AttributeSet attrs, int type, + SQLiteDatabase db, ContentValues values, TypedArray a, + PackageManager packageManager) throws XmlPullParserException, IOException { + + String packageName = a.getString(R.styleable.Favorite_packageName); + String className = a.getString(R.styleable.Favorite_className); + + if (packageName == null || className == null) { + return false; + } + + boolean hasPackage = true; + ComponentName cn = new ComponentName(packageName, className); + try { + packageManager.getReceiverInfo(cn, 0); + } catch (Exception e) { + String[] packages = packageManager.currentToCanonicalPackageNames( + new String[] { packageName }); + cn = new ComponentName(packages[0], className); + try { + packageManager.getReceiverInfo(cn, 0); + } catch (Exception e1) { + hasPackage = false; + } + } + + if (hasPackage) { + int spanX = a.getInt(R.styleable.Favorite_spanX, 0); + int spanY = a.getInt(R.styleable.Favorite_spanY, 0); + + // Read the extras + Bundle extras = new Bundle(); + int widgetDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > widgetDepth) { + if (type != XmlPullParser.START_TAG) { + continue; + } + + TypedArray ar = mContext.obtainStyledAttributes(attrs, R.styleable.Extra); + if (TAG_EXTRA.equals(parser.getName())) { + String key = ar.getString(R.styleable.Extra_key); + String value = ar.getString(R.styleable.Extra_value); + if (key != null && value != null) { + extras.putString(key, value); + } else { + throw new RuntimeException("Widget extras must have a key and value"); + } + } else { + throw new RuntimeException("Widgets can contain only extras"); + } + ar.recycle(); + } + + return addAppWidget(db, values, cn, spanX, spanY, extras); + } + + return false; + } + + private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn, + int spanX, int spanY, Bundle extras) { + boolean allocatedAppWidgets = false; + final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); + + try { + int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); + + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); + values.put(Favorites.SPANX, spanX); + values.put(Favorites.SPANY, spanY); + values.put(Favorites.APPWIDGET_ID, appWidgetId); + values.put(Favorites._ID, generateNewId()); + dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values); + + allocatedAppWidgets = true; + + // TODO: need to check return value + appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn); + + // Send a broadcast to configure the widget + if (extras != null && !extras.isEmpty()) { + Intent intent = new Intent(ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE); + intent.setComponent(cn); + intent.putExtras(extras); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + mContext.sendBroadcast(intent); + } + } catch (RuntimeException ex) { + Log.e(TAG, "Problem allocating appWidgetId", ex); + } + + return allocatedAppWidgets; + } + + private long addUriShortcut(SQLiteDatabase db, ContentValues values, + TypedArray a) { + Resources r = mContext.getResources(); + + final int iconResId = a.getResourceId(R.styleable.Favorite_icon, 0); + final int titleResId = a.getResourceId(R.styleable.Favorite_title, 0); + + Intent intent; + String uri = null; + try { + uri = a.getString(R.styleable.Favorite_uri); + intent = Intent.parseUri(uri, 0); + } catch (URISyntaxException e) { + Log.w(TAG, "Shortcut has malformed uri: " + uri); + return -1; // Oh well + } + + if (iconResId == 0 || titleResId == 0) { + Log.w(TAG, "Shortcut is missing title or icon resource ID"); + return -1; + } + + long id = generateNewId(); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + values.put(Favorites.INTENT, intent.toUri(0)); + values.put(Favorites.TITLE, r.getString(titleResId)); + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_SHORTCUT); + values.put(Favorites.SPANX, 1); + values.put(Favorites.SPANY, 1); + values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE); + values.put(Favorites.ICON_PACKAGE, mContext.getPackageName()); + values.put(Favorites.ICON_RESOURCE, r.getResourceName(iconResId)); + values.put(Favorites._ID, id); + + if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) { + return -1; + } + return id; + } + } + + /** + * Build a query string that will match any row where the column matches + * anything in the values list. + */ + static String buildOrWhereString(String column, int[] values) { + StringBuilder selectWhere = new StringBuilder(); + for (int i = values.length - 1; i >= 0; i--) { + selectWhere.append(column).append("=").append(values[i]); + if (i > 0) { + selectWhere.append(" OR "); + } + } + return selectWhere.toString(); + } + + static class SqlArguments { + public final String table; + public final String where; + public final String[] args; + + SqlArguments(Uri url, String where, String[] args) { + if (url.getPathSegments().size() == 1) { + this.table = url.getPathSegments().get(0); + this.where = where; + this.args = args; + } else if (url.getPathSegments().size() != 2) { + throw new IllegalArgumentException("Invalid URI: " + url); + } else if (!TextUtils.isEmpty(where)) { + throw new UnsupportedOperationException("WHERE clause not supported: " + url); + } else { + this.table = url.getPathSegments().get(0); + this.where = "_id=" + ContentUris.parseId(url); + this.args = null; + } + } + + SqlArguments(Uri url) { + if (url.getPathSegments().size() == 1) { + table = url.getPathSegments().get(0); + where = null; + args = null; + } else { + throw new IllegalArgumentException("Invalid URI: " + url); + } + } + } +} diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java new file mode 100644 index 000000000..7d2b843d0 --- /dev/null +++ b/src/com/android/launcher3/LauncherSettings.java @@ -0,0 +1,236 @@ +/* + * 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.net.Uri; +import android.provider.BaseColumns; + +/** + * Settings related utilities. + */ +class LauncherSettings { + static interface BaseLauncherColumns extends BaseColumns { + /** + * Descriptive name of the gesture that can be displayed to the user. + * <P>Type: TEXT</P> + */ + static final String TITLE = "title"; + + /** + * The Intent URL of the gesture, describing what it points to. This + * value is given to {@link android.content.Intent#parseUri(String, int)} to create + * an Intent that can be launched. + * <P>Type: TEXT</P> + */ + static final String INTENT = "intent"; + + /** + * The type of the gesture + * + * <P>Type: INTEGER</P> + */ + static final String ITEM_TYPE = "itemType"; + + /** + * The gesture is an application + */ + static final int ITEM_TYPE_APPLICATION = 0; + + /** + * The gesture is an application created shortcut + */ + static final int ITEM_TYPE_SHORTCUT = 1; + + /** + * The icon type. + * <P>Type: INTEGER</P> + */ + static final String ICON_TYPE = "iconType"; + + /** + * The icon is a resource identified by a package name and an integer id. + */ + static final int ICON_TYPE_RESOURCE = 0; + + /** + * The icon is a bitmap. + */ + static final int ICON_TYPE_BITMAP = 1; + + /** + * The icon package name, if icon type is ICON_TYPE_RESOURCE. + * <P>Type: TEXT</P> + */ + static final String ICON_PACKAGE = "iconPackage"; + + /** + * The icon resource id, if icon type is ICON_TYPE_RESOURCE. + * <P>Type: TEXT</P> + */ + static final String ICON_RESOURCE = "iconResource"; + + /** + * The custom icon bitmap, if icon type is ICON_TYPE_BITMAP. + * <P>Type: BLOB</P> + */ + static final String ICON = "icon"; + } + + /** + * Favorites. + */ + static final class Favorites implements BaseLauncherColumns { + /** + * The content:// style URL for this table + */ + static final Uri CONTENT_URI = Uri.parse("content://" + + LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + + "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); + + /** + * The content:// style URL for this table. When this Uri is used, no notification is + * sent if the content changes. + */ + static final Uri CONTENT_URI_NO_NOTIFICATION = Uri.parse("content://" + + LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + + "?" + LauncherProvider.PARAMETER_NOTIFY + "=false"); + + /** + * The content:// style URL for a given row, identified by its id. + * + * @param id The row id. + * @param notify True to send a notification is the content changes. + * + * @return The unique content URL for the specified row. + */ + static Uri getContentUri(long id, boolean notify) { + return Uri.parse("content://" + LauncherProvider.AUTHORITY + + "/" + LauncherProvider.TABLE_FAVORITES + "/" + id + "?" + + LauncherProvider.PARAMETER_NOTIFY + "=" + notify); + } + + /** + * The container holding the favorite + * <P>Type: INTEGER</P> + */ + static final String CONTAINER = "container"; + + /** + * The icon is a resource identified by a package name and an integer id. + */ + static final int CONTAINER_DESKTOP = -100; + static final int CONTAINER_HOTSEAT = -101; + + /** + * The screen holding the favorite (if container is CONTAINER_DESKTOP) + * <P>Type: INTEGER</P> + */ + static final String SCREEN = "screen"; + + /** + * The X coordinate of the cell holding the favorite + * (if container is CONTAINER_HOTSEAT or CONTAINER_HOTSEAT) + * <P>Type: INTEGER</P> + */ + static final String CELLX = "cellX"; + + /** + * The Y coordinate of the cell holding the favorite + * (if container is CONTAINER_DESKTOP) + * <P>Type: INTEGER</P> + */ + static final String CELLY = "cellY"; + + /** + * The X span of the cell holding the favorite + * <P>Type: INTEGER</P> + */ + static final String SPANX = "spanX"; + + /** + * The Y span of the cell holding the favorite + * <P>Type: INTEGER</P> + */ + static final String SPANY = "spanY"; + + /** + * The favorite is a user created folder + */ + static final int ITEM_TYPE_FOLDER = 2; + + /** + * The favorite is a live folder + * + * Note: live folders can no longer be added to Launcher, and any live folders which + * exist within the launcher database will be ignored when loading. That said, these + * entries in the database may still exist, and are not automatically stripped. + */ + static final int ITEM_TYPE_LIVE_FOLDER = 3; + + /** + * The favorite is a widget + */ + static final int ITEM_TYPE_APPWIDGET = 4; + + /** + * The favorite is a clock + */ + static final int ITEM_TYPE_WIDGET_CLOCK = 1000; + + /** + * The favorite is a search widget + */ + static final int ITEM_TYPE_WIDGET_SEARCH = 1001; + + /** + * The favorite is a photo frame + */ + static final int ITEM_TYPE_WIDGET_PHOTO_FRAME = 1002; + + /** + * The appWidgetId of the widget + * + * <P>Type: INTEGER</P> + */ + static final String APPWIDGET_ID = "appWidgetId"; + + /** + * Indicates whether this favorite is an application-created shortcut or not. + * If the value is 0, the favorite is not an application-created shortcut, if the + * value is 1, it is an application-created shortcut. + * <P>Type: INTEGER</P> + */ + @Deprecated + static final String IS_SHORTCUT = "isShortcut"; + + /** + * The URI associated with the favorite. It is used, for instance, by + * live folders to find the content provider. + * <P>Type: TEXT</P> + */ + static final String URI = "uri"; + + /** + * The display mode if the item is a live folder. + * <P>Type: INTEGER</P> + * + * @see android.provider.LiveFolders#DISPLAY_MODE_GRID + * @see android.provider.LiveFolders#DISPLAY_MODE_LIST + */ + static final String DISPLAY_MODE = "displayMode"; + } +} diff --git a/src/com/android/launcher3/LauncherViewPropertyAnimator.java b/src/com/android/launcher3/LauncherViewPropertyAnimator.java new file mode 100644 index 000000000..8a9c35dda --- /dev/null +++ b/src/com/android/launcher3/LauncherViewPropertyAnimator.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2012 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.Animator.AnimatorListener; +import android.animation.TimeInterpolator; +import android.view.View; +import android.view.ViewPropertyAnimator; + +import java.util.ArrayList; +import java.util.EnumSet; + +public class LauncherViewPropertyAnimator extends Animator implements AnimatorListener { + enum Properties { + TRANSLATION_X, + TRANSLATION_Y, + SCALE_X, + SCALE_Y, + ROTATION_Y, + ALPHA, + START_DELAY, + DURATION, + INTERPOLATOR + } + EnumSet<Properties> mPropertiesToSet = EnumSet.noneOf(Properties.class); + ViewPropertyAnimator mViewPropertyAnimator; + View mTarget; + + float mTranslationX; + float mTranslationY; + float mScaleX; + float mScaleY; + float mRotationY; + float mAlpha; + long mStartDelay; + long mDuration; + TimeInterpolator mInterpolator; + ArrayList<Animator.AnimatorListener> mListeners; + boolean mRunning = false; + FirstFrameAnimatorHelper mFirstFrameHelper; + + public LauncherViewPropertyAnimator(View target) { + mTarget = target; + mListeners = new ArrayList<Animator.AnimatorListener>(); + } + + @Override + public void addListener(Animator.AnimatorListener listener) { + mListeners.add(listener); + } + + @Override + public void cancel() { + if (mViewPropertyAnimator != null) { + mViewPropertyAnimator.cancel(); + } + } + + @Override + public Animator clone() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void end() { + throw new RuntimeException("Not implemented"); + } + + @Override + public long getDuration() { + return mDuration; + } + + @Override + public ArrayList<Animator.AnimatorListener> getListeners() { + return mListeners; + } + + @Override + public long getStartDelay() { + return mStartDelay; + } + + @Override + public void onAnimationCancel(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationCancel(this); + } + mRunning = false; + } + + @Override + public void onAnimationEnd(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationEnd(this); + } + mRunning = false; + } + + @Override + public void onAnimationRepeat(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationRepeat(this); + } + } + + @Override + public void onAnimationStart(Animator animation) { + // This is the first time we get a handle to the internal ValueAnimator + // used by the ViewPropertyAnimator. + mFirstFrameHelper.onAnimationStart(animation); + + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationStart(this); + } + mRunning = true; + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public boolean isStarted() { + return mViewPropertyAnimator != null; + } + + @Override + public void removeAllListeners() { + mListeners.clear(); + } + + @Override + public void removeListener(Animator.AnimatorListener listener) { + mListeners.remove(listener); + } + + @Override + public Animator setDuration(long duration) { + mPropertiesToSet.add(Properties.DURATION); + mDuration = duration; + return this; + } + + @Override + public void setInterpolator(TimeInterpolator value) { + mPropertiesToSet.add(Properties.INTERPOLATOR); + mInterpolator = value; + } + + @Override + public void setStartDelay(long startDelay) { + mPropertiesToSet.add(Properties.START_DELAY); + mStartDelay = startDelay; + } + + @Override + public void setTarget(Object target) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setupEndValues() { + + } + + @Override + public void setupStartValues() { + } + + @Override + public void start() { + mViewPropertyAnimator = mTarget.animate(); + + // FirstFrameAnimatorHelper hooks itself up to the updates on the animator, + // and then adjusts the play time to keep the first two frames jank-free + mFirstFrameHelper = new FirstFrameAnimatorHelper(mViewPropertyAnimator, mTarget); + + if (mPropertiesToSet.contains(Properties.TRANSLATION_X)) { + mViewPropertyAnimator.translationX(mTranslationX); + } + if (mPropertiesToSet.contains(Properties.TRANSLATION_Y)) { + mViewPropertyAnimator.translationY(mTranslationY); + } + if (mPropertiesToSet.contains(Properties.SCALE_X)) { + mViewPropertyAnimator.scaleX(mScaleX); + } + if (mPropertiesToSet.contains(Properties.ROTATION_Y)) { + mViewPropertyAnimator.rotationY(mRotationY); + } + if (mPropertiesToSet.contains(Properties.SCALE_Y)) { + mViewPropertyAnimator.scaleY(mScaleY); + } + if (mPropertiesToSet.contains(Properties.ALPHA)) { + mViewPropertyAnimator.alpha(mAlpha); + } + if (mPropertiesToSet.contains(Properties.START_DELAY)) { + mViewPropertyAnimator.setStartDelay(mStartDelay); + } + if (mPropertiesToSet.contains(Properties.DURATION)) { + mViewPropertyAnimator.setDuration(mDuration); + } + if (mPropertiesToSet.contains(Properties.INTERPOLATOR)) { + mViewPropertyAnimator.setInterpolator(mInterpolator); + } + mViewPropertyAnimator.setListener(this); + mViewPropertyAnimator.start(); + LauncherAnimUtils.cancelOnDestroyActivity(this); + } + + public LauncherViewPropertyAnimator translationX(float value) { + mPropertiesToSet.add(Properties.TRANSLATION_X); + mTranslationX = value; + return this; + } + + public LauncherViewPropertyAnimator translationY(float value) { + mPropertiesToSet.add(Properties.TRANSLATION_Y); + mTranslationY = value; + return this; + } + + public LauncherViewPropertyAnimator scaleX(float value) { + mPropertiesToSet.add(Properties.SCALE_X); + mScaleX = value; + return this; + } + + public LauncherViewPropertyAnimator scaleY(float value) { + mPropertiesToSet.add(Properties.SCALE_Y); + mScaleY = value; + return this; + } + + public LauncherViewPropertyAnimator rotationY(float value) { + mPropertiesToSet.add(Properties.ROTATION_Y); + mRotationY = value; + return this; + } + + public LauncherViewPropertyAnimator alpha(float value) { + mPropertiesToSet.add(Properties.ALPHA); + mAlpha = value; + return this; + } +} diff --git a/src/com/android/launcher3/PackageChangedReceiver.java b/src/com/android/launcher3/PackageChangedReceiver.java new file mode 100644 index 000000000..ded01a6fc --- /dev/null +++ b/src/com/android/launcher3/PackageChangedReceiver.java @@ -0,0 +1,19 @@ +package com.android.launcher3; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class PackageChangedReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, Intent intent) { + final String packageName = intent.getData().getSchemeSpecificPart(); + + if (packageName == null || packageName.length() == 0) { + // they sent us a bad intent + return; + } + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + WidgetPreviewLoader.removeFromDb(app.getWidgetPreviewCacheDb(), packageName); + } +} diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java new file mode 100644 index 000000000..8716a33be --- /dev/null +++ b/src/com/android/launcher3/PagedView.java @@ -0,0 +1,1981 @@ +/* + * Copyright (C) 2010 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.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +import com.android.launcher3.R; + +import java.util.ArrayList; + +/** + * An abstraction of the original Workspace which supports browsing through a + * sequential list of "pages" + */ +public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener { + private static final String TAG = "PagedView"; + private static final boolean DEBUG = false; + protected static final int INVALID_PAGE = -1; + + // the min drag distance for a fling to register, to prevent random page shifts + private static final int MIN_LENGTH_FOR_FLING = 25; + + protected static final int PAGE_SNAP_ANIMATION_DURATION = 550; + protected static final int MAX_PAGE_SNAP_DURATION = 750; + protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; + protected static final float NANOTIME_DIV = 1000000000.0f; + + private static final float OVERSCROLL_ACCELERATE_FACTOR = 2; + private static final float OVERSCROLL_DAMP_FACTOR = 0.14f; + + private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; + // The page is moved more than halfway, automatically move to the next page on touch up. + private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; + + // The following constants need to be scaled based on density. The scaled versions will be + // assigned to the corresponding member variables below. + private static final int FLING_THRESHOLD_VELOCITY = 500; + private static final int MIN_SNAP_VELOCITY = 1500; + private static final int MIN_FLING_VELOCITY = 250; + + static final int AUTOMATIC_PAGE_SPACING = -1; + + protected int mFlingThresholdVelocity; + protected int mMinFlingVelocity; + protected int mMinSnapVelocity; + + protected float mDensity; + protected float mSmoothingTime; + protected float mTouchX; + + protected boolean mFirstLayout = true; + + protected int mCurrentPage; + protected int mNextPage = INVALID_PAGE; + protected int mMaxScrollX; + protected Scroller mScroller; + private VelocityTracker mVelocityTracker; + + private float mDownMotionX; + protected float mLastMotionX; + protected float mLastMotionXRemainder; + protected float mLastMotionY; + protected float mTotalMotionX; + private int mLastScreenCenter = -1; + private int[] mChildOffsets; + private int[] mChildRelativeOffsets; + private int[] mChildOffsetsWithLayoutScale; + + protected final static int TOUCH_STATE_REST = 0; + protected final static int TOUCH_STATE_SCROLLING = 1; + protected final static int TOUCH_STATE_PREV_PAGE = 2; + protected final static int TOUCH_STATE_NEXT_PAGE = 3; + protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f; + + protected int mTouchState = TOUCH_STATE_REST; + protected boolean mForceScreenScrolled = false; + + protected OnLongClickListener mLongClickListener; + + protected boolean mAllowLongPress = true; + + protected int mTouchSlop; + private int mPagingTouchSlop; + private int mMaximumVelocity; + private int mMinimumWidth; + protected int mPageSpacing; + protected int mPageLayoutPaddingTop; + protected int mPageLayoutPaddingBottom; + protected int mPageLayoutPaddingLeft; + protected int mPageLayoutPaddingRight; + protected int mPageLayoutWidthGap; + protected int mPageLayoutHeightGap; + protected int mCellCountX = 0; + protected int mCellCountY = 0; + protected boolean mCenterPagesVertically; + protected boolean mAllowOverScroll = true; + protected int mUnboundedScrollX; + protected int[] mTempVisiblePagesRange = new int[2]; + protected boolean mForceDrawAllChildrenNextFrame; + + // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise + // it is equal to the scaled overscroll position. We use a separate value so as to prevent + // the screens from continuing to translate beyond the normal bounds. + protected int mOverScrollX; + + // parameter that adjusts the layout to be optimized for pages with that scale factor + protected float mLayoutScale = 1.0f; + + protected static final int INVALID_POINTER = -1; + + protected int mActivePointerId = INVALID_POINTER; + + private PageSwitchListener mPageSwitchListener; + + protected ArrayList<Boolean> mDirtyPageContent; + + // If true, syncPages and syncPageItems will be called to refresh pages + protected boolean mContentIsRefreshable = true; + + // If true, modify alpha of neighboring pages as user scrolls left/right + protected boolean mFadeInAdjacentScreens = true; + + // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding + // to switch to a new page + protected boolean mUsePagingTouchSlop = true; + + // If true, the subclass should directly update scrollX itself in its computeScroll method + // (SmoothPagedView does this) + protected boolean mDeferScrollUpdate = false; + + protected boolean mIsPageMoving = false; + + // All syncs and layout passes are deferred until data is ready. + protected boolean mIsDataReady = false; + + // Scrolling indicator + private ValueAnimator mScrollIndicatorAnimator; + private View mScrollIndicator; + private int mScrollIndicatorPaddingLeft; + private int mScrollIndicatorPaddingRight; + private boolean mHasScrollIndicator = true; + private boolean mShouldShowScrollIndicator = false; + private boolean mShouldShowScrollIndicatorImmediately = false; + protected static final int sScrollIndicatorFadeInDuration = 150; + protected static final int sScrollIndicatorFadeOutDuration = 650; + protected static final int sScrollIndicatorFlashDuration = 650; + private boolean mScrollingPaused = false; + + // If set, will defer loading associated pages until the scrolling settles + private boolean mDeferLoadAssociatedPagesUntilScrollCompletes; + + public interface PageSwitchListener { + void onPageSwitch(View newPage, int newPageIndex); + } + + public PagedView(Context context) { + this(context, null); + } + + public PagedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.PagedView, defStyle, 0); + setPageSpacing(a.getDimensionPixelSize(R.styleable.PagedView_pageSpacing, 0)); + mPageLayoutPaddingTop = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingTop, 0); + mPageLayoutPaddingBottom = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingBottom, 0); + mPageLayoutPaddingLeft = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingLeft, 0); + mPageLayoutPaddingRight = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingRight, 0); + mPageLayoutWidthGap = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutWidthGap, 0); + mPageLayoutHeightGap = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutHeightGap, 0); + mScrollIndicatorPaddingLeft = + a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingLeft, 0); + mScrollIndicatorPaddingRight = + a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingRight, 0); + a.recycle(); + + setHapticFeedbackEnabled(false); + init(); + } + + /** + * Initializes various states for this workspace. + */ + protected void init() { + mDirtyPageContent = new ArrayList<Boolean>(); + mDirtyPageContent.ensureCapacity(32); + mScroller = new Scroller(getContext(), new ScrollInterpolator()); + mCurrentPage = 0; + mCenterPagesVertically = true; + + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mDensity = getResources().getDisplayMetrics().density; + + mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); + mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity); + mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity); + setOnHierarchyChangeListener(this); + } + + public void setPageSwitchListener(PageSwitchListener pageSwitchListener) { + mPageSwitchListener = pageSwitchListener; + if (mPageSwitchListener != null) { + mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); + } + } + + /** + * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. + */ + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + /** + * Called by subclasses to mark that data is ready, and that we can begin loading and laying + * out pages. + */ + protected void setDataIsReady() { + mIsDataReady = true; + } + protected boolean isDataReady() { + return mIsDataReady; + } + + /** + * Returns the index of the currently displayed page. + * + * @return The index of the currently displayed page. + */ + int getCurrentPage() { + return mCurrentPage; + } + int getNextPage() { + return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + } + + int getPageCount() { + return getChildCount(); + } + + View getPageAt(int index) { + return getChildAt(index); + } + + protected int indexToPage(int index) { + return index; + } + + /** + * Updates the scroll of the current page immediately to its final scroll position. We use this + * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of + * the previous tab page. + */ + protected void updateCurrentPageScroll() { + // If the current page is invalid, just reset the scroll position to zero + int newX = 0; + if (0 <= mCurrentPage && mCurrentPage < getPageCount()) { + int offset = getChildOffset(mCurrentPage); + int relOffset = getRelativeChildOffset(mCurrentPage); + newX = offset - relOffset; + } + scrollTo(newX, 0); + mScroller.setFinalX(newX); + mScroller.forceFinished(true); + } + + /** + * Called during AllApps/Home transitions to avoid unnecessary work. When that other animation + * ends, {@link #resumeScrolling()} should be called, along with + * {@link #updateCurrentPageScroll()} to correctly set the final state and re-enable scrolling. + */ + void pauseScrolling() { + mScroller.forceFinished(true); + cancelScrollingIndicatorAnimations(); + mScrollingPaused = true; + } + + /** + * Enables scrolling again. + * @see #pauseScrolling() + */ + void resumeScrolling() { + mScrollingPaused = false; + } + /** + * Sets the current page. + */ + void setCurrentPage(int currentPage) { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + // don't introduce any checks like mCurrentPage == currentPage here-- if we change the + // the default + if (getChildCount() == 0) { + return; + } + + + mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1)); + updateCurrentPageScroll(); + updateScrollingIndicator(); + notifyPageSwitchListener(); + invalidate(); + } + + protected void notifyPageSwitchListener() { + if (mPageSwitchListener != null) { + mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); + } + } + + protected void pageBeginMoving() { + if (!mIsPageMoving) { + mIsPageMoving = true; + onPageBeginMoving(); + } + } + + protected void pageEndMoving() { + if (mIsPageMoving) { + mIsPageMoving = false; + onPageEndMoving(); + } + } + + protected boolean isPageMoving() { + return mIsPageMoving; + } + + // a method that subclasses can override to add behavior + protected void onPageBeginMoving() { + } + + // a method that subclasses can override to add behavior + protected void onPageEndMoving() { + } + + /** + * Registers the specified listener on each page contained in this workspace. + * + * @param l The listener used to respond to long clicks. + */ + @Override + public void setOnLongClickListener(OnLongClickListener l) { + mLongClickListener = l; + final int count = getPageCount(); + for (int i = 0; i < count; i++) { + getPageAt(i).setOnLongClickListener(l); + } + } + + @Override + public void scrollBy(int x, int y) { + scrollTo(mUnboundedScrollX + x, getScrollY() + y); + } + + @Override + public void scrollTo(int x, int y) { + final boolean isRtl = isLayoutRtl(); + mUnboundedScrollX = x; + + boolean isXBeforeFirstPage = isRtl ? (x > mMaxScrollX) : (x < 0); + boolean isXAfterLastPage = isRtl ? (x < 0) : (x > mMaxScrollX); + if (isXBeforeFirstPage) { + super.scrollTo(0, y); + if (mAllowOverScroll) { + if (isRtl) { + overScroll(x - mMaxScrollX); + } else { + overScroll(x); + } + } + } else if (isXAfterLastPage) { + super.scrollTo(mMaxScrollX, y); + if (mAllowOverScroll) { + if (isRtl) { + overScroll(x); + } else { + overScroll(x - mMaxScrollX); + } + } + } else { + mOverScrollX = x; + super.scrollTo(x, y); + } + + mTouchX = x; + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + } + + // we moved this functionality to a helper function so SmoothPagedView can reuse it + protected boolean computeScrollHelper() { + if (mScroller.computeScrollOffset()) { + // Don't bother scrolling if the page does not need to be moved + if (getScrollX() != mScroller.getCurrX() + || getScrollY() != mScroller.getCurrY() + || mOverScrollX != mScroller.getCurrX()) { + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + } + invalidate(); + return true; + } else if (mNextPage != INVALID_PAGE) { + mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1)); + mNextPage = INVALID_PAGE; + notifyPageSwitchListener(); + + // Load the associated pages if necessary + if (mDeferLoadAssociatedPagesUntilScrollCompletes) { + loadAssociatedPages(mCurrentPage); + mDeferLoadAssociatedPagesUntilScrollCompletes = false; + } + + // We don't want to trigger a page end moving unless the page has settled + // and the user has stopped scrolling + if (mTouchState == TOUCH_STATE_REST) { + pageEndMoving(); + } + + // Notify the user when the page changes + AccessibilityManager accessibilityManager = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.isEnabled()) { + AccessibilityEvent ev = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); + ev.getText().add(getCurrentPageDescription()); + sendAccessibilityEventUnchecked(ev); + } + return true; + } + return false; + } + + @Override + public void computeScroll() { + computeScrollHelper(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (!mIsDataReady) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException("Workspace can only be used in EXACTLY mode."); + } + + // Return early if we aren't given a proper dimension + if (widthSize <= 0 || heightSize <= 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + /* Allow the height to be set as WRAP_CONTENT. This allows the particular case + * of the All apps view on XLarge displays to not take up more space then it needs. Width + * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect + * each page to have the same width. + */ + int maxChildHeight = 0; + + final int verticalPadding = getPaddingTop() + getPaddingBottom(); + final int horizontalPadding = getPaddingLeft() + getPaddingRight(); + + + // The children are given the same width and height as the workspace + // unless they were set to WRAP_CONTENT + if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + // disallowing padding in paged view (just pass 0) + final View child = getPageAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int childWidthMode; + if (lp.width == LayoutParams.WRAP_CONTENT) { + childWidthMode = MeasureSpec.AT_MOST; + } else { + childWidthMode = MeasureSpec.EXACTLY; + } + + int childHeightMode; + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightMode = MeasureSpec.AT_MOST; + } else { + childHeightMode = MeasureSpec.EXACTLY; + } + + final int childWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(widthSize - horizontalPadding, childWidthMode); + final int childHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(heightSize - verticalPadding, childHeightMode); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()); + if (DEBUG) Log.d(TAG, "\tmeasure-child" + i + ": " + child.getMeasuredWidth() + ", " + + child.getMeasuredHeight()); + } + + if (heightMode == MeasureSpec.AT_MOST) { + heightSize = maxChildHeight + verticalPadding; + } + + setMeasuredDimension(widthSize, heightSize); + + // We can't call getChildOffset/getRelativeChildOffset until we set the measured dimensions. + // We also wait until we set the measured dimensions before flushing the cache as well, to + // ensure that the cache is filled with good values. + invalidateCachedOffsets(); + + if (childCount > 0) { + if (DEBUG) Log.d(TAG, "getRelativeChildOffset(): " + getMeasuredWidth() + ", " + + getChildWidth(0)); + + // Calculate the variable page spacing if necessary + if (mPageSpacing == AUTOMATIC_PAGE_SPACING) { + // The gap between pages in the PagedView should be equal to the gap from the page + // to the edge of the screen (so it is not visible in the current screen). To + // account for unequal padding on each side of the paged view, we take the maximum + // of the left/right gap and use that as the gap between each page. + int offset = getRelativeChildOffset(0); + int spacing = Math.max(offset, widthSize - offset - + getChildAt(0).getMeasuredWidth()); + setPageSpacing(spacing); + } + } + + updateScrollingIndicatorPosition(); + + if (childCount > 0) { + final int index = isLayoutRtl() ? 0 : childCount - 1; + mMaxScrollX = getChildOffset(index) - getRelativeChildOffset(index); + } else { + mMaxScrollX = 0; + } + } + + protected void scrollToNewPageWithoutMovingPages(int newCurrentPage) { + int newX = getChildOffset(newCurrentPage) - getRelativeChildOffset(newCurrentPage); + int delta = newX - getScrollX(); + + final int pageCount = getChildCount(); + for (int i = 0; i < pageCount; i++) { + View page = (View) getPageAt(i); + page.setX(page.getX() + delta); + } + setCurrentPage(newCurrentPage); + } + + // A layout scale of 1.0f assumes that the pages, in their unshrunken state, have a + // scale of 1.0f. A layout scale of 0.8f assumes the pages have a scale of 0.8f, and + // tightens the layout accordingly + public void setLayoutScale(float childrenScale) { + mLayoutScale = childrenScale; + invalidateCachedOffsets(); + + // Now we need to do a re-layout, but preserving absolute X and Y coordinates + int childCount = getChildCount(); + float childrenX[] = new float[childCount]; + float childrenY[] = new float[childCount]; + for (int i = 0; i < childCount; i++) { + final View child = getPageAt(i); + childrenX[i] = child.getX(); + childrenY[i] = child.getY(); + } + // Trigger a full re-layout (never just call onLayout directly!) + int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY); + int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY); + requestLayout(); + measure(widthSpec, heightSpec); + layout(getLeft(), getTop(), getRight(), getBottom()); + for (int i = 0; i < childCount; i++) { + final View child = getPageAt(i); + child.setX(childrenX[i]); + child.setY(childrenY[i]); + } + + // Also, the page offset has changed (since the pages are now smaller); + // update the page offset, but again preserving absolute X and Y coordinates + scrollToNewPageWithoutMovingPages(mCurrentPage); + } + + public void setPageSpacing(int pageSpacing) { + mPageSpacing = pageSpacing; + invalidateCachedOffsets(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (!mIsDataReady) { + return; + } + + if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); + final int verticalPadding = getPaddingTop() + getPaddingBottom(); + final int childCount = getChildCount(); + final boolean isRtl = isLayoutRtl(); + + final int startIndex = isRtl ? childCount - 1 : 0; + final int endIndex = isRtl ? -1 : childCount; + final int delta = isRtl ? -1 : 1; + int childLeft = getRelativeChildOffset(startIndex); + for (int i = startIndex; i != endIndex; i += delta) { + final View child = getPageAt(i); + if (child.getVisibility() != View.GONE) { + final int childWidth = getScaledMeasuredWidth(child); + final int childHeight = child.getMeasuredHeight(); + int childTop = getPaddingTop(); + if (mCenterPagesVertically) { + childTop += ((getMeasuredHeight() - verticalPadding) - childHeight) / 2; + } + + if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop); + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), childTop + childHeight); + childLeft += childWidth + mPageSpacing; + } + } + + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + setHorizontalScrollBarEnabled(false); + updateCurrentPageScroll(); + setHorizontalScrollBarEnabled(true); + mFirstLayout = false; + } + } + + protected void screenScrolled(int screenCenter) { + if (isScrollingIndicatorEnabled()) { + updateScrollingIndicator(); + } + boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; + + if (mFadeInAdjacentScreens && !isInOverscroll) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child != null) { + float scrollProgress = getScrollProgress(screenCenter, child, i); + float alpha = 1 - Math.abs(scrollProgress); + child.setAlpha(alpha); + } + } + invalidate(); + } + } + + @Override + public void onChildViewAdded(View parent, View child) { + // This ensures that when children are added, they get the correct transforms / alphas + // in accordance with any scroll effects. + mForceScreenScrolled = true; + invalidate(); + invalidateCachedOffsets(); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + } + + protected void invalidateCachedOffsets() { + int count = getChildCount(); + if (count == 0) { + mChildOffsets = null; + mChildRelativeOffsets = null; + mChildOffsetsWithLayoutScale = null; + return; + } + + mChildOffsets = new int[count]; + mChildRelativeOffsets = new int[count]; + mChildOffsetsWithLayoutScale = new int[count]; + for (int i = 0; i < count; i++) { + mChildOffsets[i] = -1; + mChildRelativeOffsets[i] = -1; + mChildOffsetsWithLayoutScale[i] = -1; + } + } + + protected int getChildOffset(int index) { + final boolean isRtl = isLayoutRtl(); + int[] childOffsets = Float.compare(mLayoutScale, 1f) == 0 ? + mChildOffsets : mChildOffsetsWithLayoutScale; + + if (childOffsets != null && childOffsets[index] != -1) { + return childOffsets[index]; + } else { + if (getChildCount() == 0) + return 0; + + final int startIndex = isRtl ? getChildCount() - 1 : 0; + final int endIndex = isRtl ? index : index; + final int delta = isRtl ? -1 : 1; + int offset = getRelativeChildOffset(startIndex); + for (int i = startIndex; i != endIndex; i += delta) { + offset += getScaledMeasuredWidth(getPageAt(i)) + mPageSpacing; + } + if (childOffsets != null) { + childOffsets[index] = offset; + } + return offset; + } + } + + protected int getRelativeChildOffset(int index) { + if (mChildRelativeOffsets != null && mChildRelativeOffsets[index] != -1) { + return mChildRelativeOffsets[index]; + } else { + final int padding = getPaddingLeft() + getPaddingRight(); + final int offset = getPaddingLeft() + + (getMeasuredWidth() - padding - getChildWidth(index)) / 2; + if (mChildRelativeOffsets != null) { + mChildRelativeOffsets[index] = offset; + } + return offset; + } + } + + protected int getScaledMeasuredWidth(View child) { + // This functions are called enough times that it actually makes a difference in the + // profiler -- so just inline the max() here + final int measuredWidth = child.getMeasuredWidth(); + final int minWidth = mMinimumWidth; + final int maxWidth = (minWidth > measuredWidth) ? minWidth : measuredWidth; + return (int) (maxWidth * mLayoutScale + 0.5f); + } + + protected void getVisiblePages(int[] range) { + final boolean isRtl = isLayoutRtl(); + final int pageCount = getChildCount(); + + if (pageCount > 0) { + final int screenWidth = getMeasuredWidth(); + int leftScreen = isRtl ? pageCount - 1 : 0; + int rightScreen = 0; + int endIndex = isRtl ? 0 : pageCount - 1; + int delta = isRtl ? -1 : 1; + View currPage = getPageAt(leftScreen); + while (leftScreen != endIndex && + currPage.getX() + currPage.getWidth() - + currPage.getPaddingRight() < getScrollX()) { + leftScreen += delta; + currPage = getPageAt(leftScreen); + } + rightScreen = leftScreen; + currPage = getPageAt(rightScreen + delta); + while (rightScreen != endIndex && + currPage.getX() - currPage.getPaddingLeft() < getScrollX() + screenWidth) { + rightScreen += delta; + currPage = getPageAt(rightScreen + delta); + } + range[0] = Math.min(leftScreen, rightScreen); + range[1] = Math.max(leftScreen, rightScreen); + } else { + range[0] = -1; + range[1] = -1; + } + } + + protected boolean shouldDrawChild(View child) { + return child.getAlpha() > 0; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + int halfScreenSize = getMeasuredWidth() / 2; + // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. + // Otherwise it is equal to the scaled overscroll position. + int screenCenter = mOverScrollX + halfScreenSize; + + if (screenCenter != mLastScreenCenter || mForceScreenScrolled) { + // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can + // set it for the next frame + mForceScreenScrolled = false; + screenScrolled(screenCenter); + mLastScreenCenter = screenCenter; + } + + // Find out which screens are visible; as an optimization we only call draw on them + final int pageCount = getChildCount(); + if (pageCount > 0) { + getVisiblePages(mTempVisiblePagesRange); + final int leftScreen = mTempVisiblePagesRange[0]; + final int rightScreen = mTempVisiblePagesRange[1]; + if (leftScreen != -1 && rightScreen != -1) { + final long drawingTime = getDrawingTime(); + // Clip to the bounds + canvas.save(); + canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getRight() - getLeft(), + getScrollY() + getBottom() - getTop()); + + for (int i = getChildCount() - 1; i >= 0; i--) { + final View v = getPageAt(i); + if (mForceDrawAllChildrenNextFrame || + (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) { + drawChild(canvas, v, drawingTime); + } + } + mForceDrawAllChildrenNextFrame = false; + canvas.restore(); + } + } + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { + int page = indexToPage(indexOfChild(child)); + if (page != mCurrentPage || !mScroller.isFinished()) { + snapToPage(page); + return true; + } + return false; + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + int focusablePage; + if (mNextPage != INVALID_PAGE) { + focusablePage = mNextPage; + } else { + focusablePage = mCurrentPage; + } + View v = getPageAt(focusablePage); + if (v != null) { + return v.requestFocus(direction, previouslyFocusedRect); + } + return false; + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + // XXX-RTL: This will be fixed in a future CL + if (direction == View.FOCUS_LEFT) { + if (getCurrentPage() > 0) { + snapToPage(getCurrentPage() - 1); + return true; + } + } else if (direction == View.FOCUS_RIGHT) { + if (getCurrentPage() < getPageCount() - 1) { + snapToPage(getCurrentPage() + 1); + return true; + } + } + return super.dispatchUnhandledMove(focused, direction); + } + + @Override + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + // XXX-RTL: This will be fixed in a future CL + if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { + getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode); + } + if (direction == View.FOCUS_LEFT) { + if (mCurrentPage > 0) { + getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode); + } + } else if (direction == View.FOCUS_RIGHT){ + if (mCurrentPage < getPageCount() - 1) { + getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode); + } + } + } + + /** + * If one of our descendant views decides that it could be focused now, only + * pass that along if it's on the current page. + * + * This happens when live folders requery, and if they're off page, they + * end up calling requestFocus, which pulls it on page. + */ + @Override + public void focusableViewAvailable(View focused) { + View current = getPageAt(mCurrentPage); + View v = focused; + while (true) { + if (v == current) { + super.focusableViewAvailable(focused); + return; + } + if (v == this) { + return; + } + ViewParent parent = v.getParent(); + if (parent instanceof View) { + v = (View)v.getParent(); + } else { + return; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + // We need to make sure to cancel our long press if + // a scrollable widget takes over touch events + final View currentPage = getPageAt(mCurrentPage); + currentPage.cancelLongPress(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + /** + * Return true if a tap at (x, y) should trigger a flip to the previous page. + */ + protected boolean hitsPreviousPage(float x, float y) { + if (isLayoutRtl()) { + return (x > (getMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing)); + } else { + return (x < getRelativeChildOffset(mCurrentPage) - mPageSpacing); + } + } + + /** + * Return true if a tap at (x, y) should trigger a flip to the next page. + */ + protected boolean hitsNextPage(float x, float y) { + if (isLayoutRtl()) { + return (x < getRelativeChildOffset(mCurrentPage) - mPageSpacing); + } else { + return (x > (getMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing)); + } + + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onTouchEvent will be called and we do the actual + * scrolling there. + */ + acquireVelocityTrackerAndAddMovement(ev); + + // Skip touch handling if there are no pages to swipe + if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && + (mTouchState == TOUCH_STATE_SCROLLING)) { + return true; + } + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + if (mActivePointerId != INVALID_POINTER) { + determineScrollingStart(ev); + break; + } + // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN + // event. in that case, treat the first occurence of a move event as a ACTION_DOWN + // i.e. fall through to the next case (don't break) + // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events + // while it's small- this was causing a crash before we checked for INVALID_POINTER) + } + + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + // Remember location of down touch + mDownMotionX = x; + mLastMotionX = x; + mLastMotionY = y; + mLastMotionXRemainder = 0; + mTotalMotionX = 0; + mActivePointerId = ev.getPointerId(0); + mAllowLongPress = true; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); + final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop); + if (finishedScrolling) { + mTouchState = TOUCH_STATE_REST; + mScroller.abortAnimation(); + } else { + mTouchState = TOUCH_STATE_SCROLLING; + } + + // check if this can be the beginning of a tap on the side of the pages + // to scroll the current page + if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) { + if (getChildCount() > 0) { + if (hitsPreviousPage(x, y)) { + mTouchState = TOUCH_STATE_PREV_PAGE; + } else if (hitsNextPage(x, y)) { + mTouchState = TOUCH_STATE_NEXT_PAGE; + } + } + } + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mTouchState = TOUCH_STATE_REST; + mAllowLongPress = false; + mActivePointerId = INVALID_POINTER; + releaseVelocityTracker(); + break; + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + releaseVelocityTracker(); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mTouchState != TOUCH_STATE_REST; + } + + protected void determineScrollingStart(MotionEvent ev) { + determineScrollingStart(ev, 1.0f); + } + + /* + * Determines if we should change the touch state to start scrolling after the + * user moves their touch point too far. + */ + protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { + /* + * Locally do absolute value. mLastMotionX is set to the y value + * of the down event. + */ + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + return; + } + final float x = ev.getX(pointerIndex); + final float y = ev.getY(pointerIndex); + final int xDiff = (int) Math.abs(x - mLastMotionX); + final int yDiff = (int) Math.abs(y - mLastMotionY); + + final int touchSlop = Math.round(touchSlopScale * mTouchSlop); + boolean xPaged = xDiff > mPagingTouchSlop; + boolean xMoved = xDiff > touchSlop; + boolean yMoved = yDiff > touchSlop; + + if (xMoved || xPaged || yMoved) { + if (mUsePagingTouchSlop ? xPaged : xMoved) { + // Scroll if the user moved far enough along the X axis + mTouchState = TOUCH_STATE_SCROLLING; + mTotalMotionX += Math.abs(mLastMotionX - x); + mLastMotionX = x; + mLastMotionXRemainder = 0; + mTouchX = getScrollX(); + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + pageBeginMoving(); + } + // Either way, cancel any pending longpress + cancelCurrentPageLongPress(); + } + } + + protected void cancelCurrentPageLongPress() { + if (mAllowLongPress) { + mAllowLongPress = false; + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + final View currentPage = getPageAt(mCurrentPage); + if (currentPage != null) { + currentPage.cancelLongPress(); + } + } + } + + protected float getScrollProgress(int screenCenter, View v, int page) { + final int halfScreenSize = getMeasuredWidth() / 2; + + int totalDistance = getScaledMeasuredWidth(v) + mPageSpacing; + int delta = screenCenter - (getChildOffset(page) - + getRelativeChildOffset(page) + halfScreenSize); + + float scrollProgress = delta / (totalDistance * 1.0f); + scrollProgress = Math.min(scrollProgress, 1.0f); + scrollProgress = Math.max(scrollProgress, -1.0f); + return scrollProgress; + } + + // This curve determines how the effect of scrolling over the limits of the page dimishes + // as the user pulls further and further from the bounds + private float overScrollInfluenceCurve(float f) { + f -= 1.0f; + return f * f * f + 1.0f; + } + + protected void acceleratedOverScroll(float amount) { + int screenSize = getMeasuredWidth(); + + // We want to reach the max over scroll effect when the user has + // over scrolled half the size of the screen + float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize); + + if (f == 0) return; + + // Clamp this factor, f, to -1 < f < 1 + if (Math.abs(f) >= 1) { + f /= Math.abs(f); + } + + int overScrollAmount = (int) Math.round(f * screenSize); + if (amount < 0) { + mOverScrollX = overScrollAmount; + super.scrollTo(0, getScrollY()); + } else { + mOverScrollX = mMaxScrollX + overScrollAmount; + super.scrollTo(mMaxScrollX, getScrollY()); + } + invalidate(); + } + + protected void dampedOverScroll(float amount) { + int screenSize = getMeasuredWidth(); + + float f = (amount / screenSize); + + if (f == 0) return; + f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); + + // Clamp this factor, f, to -1 < f < 1 + if (Math.abs(f) >= 1) { + f /= Math.abs(f); + } + + int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize); + if (amount < 0) { + mOverScrollX = overScrollAmount; + super.scrollTo(0, getScrollY()); + } else { + mOverScrollX = mMaxScrollX + overScrollAmount; + super.scrollTo(mMaxScrollX, getScrollY()); + } + invalidate(); + } + + protected void overScroll(float amount) { + dampedOverScroll(amount); + } + + protected float maxOverScroll() { + // Using the formula in overScroll, assuming that f = 1.0 (which it should generally not + // exceed). Used to find out how much extra wallpaper we need for the over scroll effect + float f = 1.0f; + f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); + return OVERSCROLL_DAMP_FACTOR * f; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + // Skip touch handling if there are no pages to swipe + if (getChildCount() <= 0) return super.onTouchEvent(ev); + + acquireVelocityTrackerAndAddMovement(ev); + + final int action = ev.getAction(); + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mDownMotionX = mLastMotionX = ev.getX(); + mLastMotionXRemainder = 0; + mTotalMotionX = 0; + mActivePointerId = ev.getPointerId(0); + if (mTouchState == TOUCH_STATE_SCROLLING) { + pageBeginMoving(); + } + break; + + case MotionEvent.ACTION_MOVE: + if (mTouchState == TOUCH_STATE_SCROLLING) { + // Scroll to follow the motion event + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float deltaX = mLastMotionX + mLastMotionXRemainder - x; + + mTotalMotionX += Math.abs(deltaX); + + // Only scroll and update mLastMotionX if we have moved some discrete amount. We + // keep the remainder because we are actually testing if we've moved from the last + // scrolled position (which is discrete). + if (Math.abs(deltaX) >= 1.0f) { + mTouchX += deltaX; + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + if (!mDeferScrollUpdate) { + scrollBy((int) deltaX, 0); + if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX); + } else { + invalidate(); + } + mLastMotionX = x; + mLastMotionXRemainder = deltaX - (int) deltaX; + } else { + awakenScrollBars(); + } + } else { + determineScrollingStart(ev); + } + break; + + case MotionEvent.ACTION_UP: + if (mTouchState == TOUCH_STATE_SCROLLING) { + final int activePointerId = mActivePointerId; + final int pointerIndex = ev.findPointerIndex(activePointerId); + final float x = ev.getX(pointerIndex); + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int velocityX = (int) velocityTracker.getXVelocity(activePointerId); + final int deltaX = (int) (x - mDownMotionX); + final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage)); + boolean isSignificantMove = Math.abs(deltaX) > pageWidth * + SIGNIFICANT_MOVE_THRESHOLD; + + mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); + + boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING && + Math.abs(velocityX) > mFlingThresholdVelocity; + + // In the case that the page is moved far to one direction and then is flung + // in the opposite direction, we use a threshold to determine whether we should + // just return to the starting page, or if we should skip one further. + boolean returnToOriginalPage = false; + if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && + Math.signum(velocityX) != Math.signum(deltaX) && isFling) { + returnToOriginalPage = true; + } + + int finalPage; + // We give flings precedence over large moves, which is why we short-circuit our + // test for a large move if a fling has been registered. That is, a large + // move to the left and fling to the right will register as a fling to the right. + final boolean isRtl = isLayoutRtl(); + boolean isDeltaXLeft = isRtl ? deltaX > 0 : deltaX < 0; + boolean isVelocityXLeft = isRtl ? velocityX > 0 : velocityX < 0; + if (((isSignificantMove && !isDeltaXLeft && !isFling) || + (isFling && !isVelocityXLeft)) && mCurrentPage > 0) { + finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; + snapToPageWithVelocity(finalPage, velocityX); + } else if (((isSignificantMove && isDeltaXLeft && !isFling) || + (isFling && isVelocityXLeft)) && + mCurrentPage < getChildCount() - 1) { + finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; + snapToPageWithVelocity(finalPage, velocityX); + } else { + snapToDestination(); + } + } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + int nextPage = Math.max(0, mCurrentPage - 1); + if (nextPage != mCurrentPage) { + snapToPage(nextPage); + } else { + snapToDestination(); + } + } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); + if (nextPage != mCurrentPage) { + snapToPage(nextPage); + } else { + snapToDestination(); + } + } else { + onUnhandledTap(ev); + } + mTouchState = TOUCH_STATE_REST; + mActivePointerId = INVALID_POINTER; + releaseVelocityTracker(); + break; + + case MotionEvent.ACTION_CANCEL: + if (mTouchState == TOUCH_STATE_SCROLLING) { + snapToDestination(); + } + mTouchState = TOUCH_STATE_REST; + mActivePointerId = INVALID_POINTER; + releaseVelocityTracker(); + break; + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + return true; + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: { + // Handle mouse (or ext. device) by shifting the page depending on the scroll + final float vscroll; + final float hscroll; + if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { + vscroll = 0; + hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + } else { + vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); + hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + } + if (hscroll != 0 || vscroll != 0) { + boolean isForwardScroll = isLayoutRtl() ? (hscroll < 0 || vscroll < 0) + : (hscroll > 0 || vscroll > 0); + if (isForwardScroll) { + scrollRight(); + } else { + scrollLeft(); + } + return true; + } + } + } + } + return super.onGenericMotionEvent(event); + } + + private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + } + + private void releaseVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); + mLastMotionY = ev.getY(newPointerIndex); + mLastMotionXRemainder = 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + protected void onUnhandledTap(MotionEvent ev) {} + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + int page = indexToPage(indexOfChild(child)); + if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) { + snapToPage(page); + } + } + + protected int getChildIndexForRelativeOffset(int relativeOffset) { + final boolean isRtl = isLayoutRtl(); + final int childCount = getChildCount(); + int left; + int right; + final int startIndex = isRtl ? childCount - 1 : 0; + final int endIndex = isRtl ? -1 : childCount; + final int delta = isRtl ? -1 : 1; + for (int i = startIndex; i != endIndex; i += delta) { + left = getRelativeChildOffset(i); + right = (left + getScaledMeasuredWidth(getPageAt(i))); + if (left <= relativeOffset && relativeOffset <= right) { + return i; + } + } + return -1; + } + + protected int getChildWidth(int index) { + // This functions are called enough times that it actually makes a difference in the + // profiler -- so just inline the max() here + final int measuredWidth = getPageAt(index).getMeasuredWidth(); + final int minWidth = mMinimumWidth; + return (minWidth > measuredWidth) ? minWidth : measuredWidth; + } + + int getPageNearestToCenterOfScreen() { + int minDistanceFromScreenCenter = Integer.MAX_VALUE; + int minDistanceFromScreenCenterIndex = -1; + int screenCenter = getScrollX() + (getMeasuredWidth() / 2); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; ++i) { + View layout = (View) getPageAt(i); + int childWidth = getScaledMeasuredWidth(layout); + int halfChildWidth = (childWidth / 2); + int childCenter = getChildOffset(i) + halfChildWidth; + int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); + if (distanceFromScreenCenter < minDistanceFromScreenCenter) { + minDistanceFromScreenCenter = distanceFromScreenCenter; + minDistanceFromScreenCenterIndex = i; + } + } + return minDistanceFromScreenCenterIndex; + } + + protected void snapToDestination() { + snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); + } + + private static class ScrollInterpolator implements Interpolator { + public ScrollInterpolator() { + } + + public float getInterpolation(float t) { + t -= 1.0f; + return t*t*t*t*t + 1; + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + protected void snapToPageWithVelocity(int whichPage, int velocity) { + whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); + int halfScreenSize = getMeasuredWidth() / 2; + + if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); + if (DEBUG) Log.d(TAG, "snapToPageWithVelocity.getRelativeChildOffset(): " + + getMeasuredWidth() + ", " + getChildWidth(whichPage)); + final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + int delta = newX - mUnboundedScrollX; + int duration = 0; + + if (Math.abs(velocity) < mMinFlingVelocity) { + // If the velocity is low enough, then treat this more as an automatic page advance + // as opposed to an apparent physical response to flinging + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); + return; + } + + // Here we compute a "distance" that will be used in the computation of the overall + // snap duration. This is a function of the actual distance that needs to be traveled; + // we keep this value close to half screen size in order to reduce the variance in snap + // duration as a function of the distance the page needs to travel. + float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); + float distance = halfScreenSize + halfScreenSize * + distanceInfluenceForSnapDuration(distanceRatio); + + velocity = Math.abs(velocity); + velocity = Math.max(mMinSnapVelocity, velocity); + + // we want the page's snap velocity to approximately match the velocity at which the + // user flings, so we scale the duration by a value near to the derivative of the scroll + // interpolator at zero, ie. 5. We use 4 to make it a little slower. + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + duration = Math.min(duration, MAX_PAGE_SNAP_DURATION); + + snapToPage(whichPage, delta, duration); + } + + protected void snapToPage(int whichPage) { + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); + } + + protected void snapToPage(int whichPage, int duration) { + whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1)); + + if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); + if (DEBUG) Log.d(TAG, "snapToPage.getRelativeChildOffset(): " + getMeasuredWidth() + ", " + + getChildWidth(whichPage)); + int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + final int sX = mUnboundedScrollX; + final int delta = newX - sX; + snapToPage(whichPage, delta, duration); + } + + protected void snapToPage(int whichPage, int delta, int duration) { + mNextPage = whichPage; + + View focusedChild = getFocusedChild(); + if (focusedChild != null && whichPage != mCurrentPage && + focusedChild == getPageAt(mCurrentPage)) { + focusedChild.clearFocus(); + } + + pageBeginMoving(); + awakenScrollBars(duration); + if (duration == 0) { + duration = Math.abs(delta); + } + + if (!mScroller.isFinished()) mScroller.abortAnimation(); + mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); + + // Load associated pages immediately if someone else is handling the scroll, otherwise defer + // loading associated pages until the scroll settles + if (mDeferScrollUpdate) { + loadAssociatedPages(mNextPage); + } else { + mDeferLoadAssociatedPagesUntilScrollCompletes = true; + } + notifyPageSwitchListener(); + invalidate(); + } + + public void scrollLeft() { + if (mScroller.isFinished()) { + if (mCurrentPage > 0) snapToPage(mCurrentPage - 1); + } else { + if (mNextPage > 0) snapToPage(mNextPage - 1); + } + } + + public void scrollRight() { + if (mScroller.isFinished()) { + if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1); + } else { + if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1); + } + } + + public int getPageForView(View v) { + int result = -1; + if (v != null) { + ViewParent vp = v.getParent(); + int count = getChildCount(); + for (int i = 0; i < count; i++) { + if (vp == getPageAt(i)) { + return i; + } + } + } + return result; + } + + /** + * @return True is long presses are still allowed for the current touch + */ + public boolean allowLongPress() { + return mAllowLongPress; + } + + /** + * Set true to allow long-press events to be triggered, usually checked by + * {@link Launcher} to accept or block dpad-initiated long-presses. + */ + public void setAllowLongPress(boolean allowLongPress) { + mAllowLongPress = allowLongPress; + } + + public static class SavedState extends BaseSavedState { + int currentPage = -1; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPage = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(currentPage); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + protected void loadAssociatedPages(int page) { + loadAssociatedPages(page, false); + } + protected void loadAssociatedPages(int page, boolean immediateAndOnly) { + if (mContentIsRefreshable) { + final int count = getChildCount(); + if (page < count) { + int lowerPageBound = getAssociatedLowerPageBound(page); + int upperPageBound = getAssociatedUpperPageBound(page); + if (DEBUG) Log.d(TAG, "loadAssociatedPages: " + lowerPageBound + "/" + + upperPageBound); + // First, clear any pages that should no longer be loaded + for (int i = 0; i < count; ++i) { + Page layout = (Page) getPageAt(i); + if ((i < lowerPageBound) || (i > upperPageBound)) { + if (layout.getPageChildCount() > 0) { + layout.removeAllViewsOnPage(); + } + mDirtyPageContent.set(i, true); + } + } + // Next, load any new pages + for (int i = 0; i < count; ++i) { + if ((i != page) && immediateAndOnly) { + continue; + } + if (lowerPageBound <= i && i <= upperPageBound) { + if (mDirtyPageContent.get(i)) { + syncPageItems(i, (i == page) && immediateAndOnly); + mDirtyPageContent.set(i, false); + } + } + } + } + } + } + + protected int getAssociatedLowerPageBound(int page) { + return Math.max(0, page - 1); + } + protected int getAssociatedUpperPageBound(int page) { + final int count = getChildCount(); + return Math.min(page + 1, count - 1); + } + + /** + * This method is called ONLY to synchronize the number of pages that the paged view has. + * To actually fill the pages with information, implement syncPageItems() below. It is + * guaranteed that syncPageItems() will be called for a particular page before it is shown, + * and therefore, individual page items do not need to be updated in this method. + */ + public abstract void syncPages(); + + /** + * This method is called to synchronize the items that are on a particular page. If views on + * the page can be reused, then they should be updated within this method. + */ + public abstract void syncPageItems(int page, boolean immediate); + + protected void invalidatePageData() { + invalidatePageData(-1, false); + } + protected void invalidatePageData(int currentPage) { + invalidatePageData(currentPage, false); + } + protected void invalidatePageData(int currentPage, boolean immediateAndOnly) { + if (!mIsDataReady) { + return; + } + + if (mContentIsRefreshable) { + // Force all scrolling-related behavior to end + mScroller.forceFinished(true); + mNextPage = INVALID_PAGE; + + // Update all the pages + syncPages(); + + // We must force a measure after we've loaded the pages to update the content width and + // to determine the full scroll width + measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); + + // Set a new page as the current page if necessary + if (currentPage > -1) { + setCurrentPage(Math.min(getPageCount() - 1, currentPage)); + } + + // Mark each of the pages as dirty + final int count = getChildCount(); + mDirtyPageContent.clear(); + for (int i = 0; i < count; ++i) { + mDirtyPageContent.add(true); + } + + // Load any pages that are necessary for the current window of views + loadAssociatedPages(mCurrentPage, immediateAndOnly); + requestLayout(); + } + } + + protected View getScrollingIndicator() { + // We use mHasScrollIndicator to prevent future lookups if there is no sibling indicator + // found + if (mHasScrollIndicator && mScrollIndicator == null) { + ViewGroup parent = (ViewGroup) getParent(); + if (parent != null) { + mScrollIndicator = (View) (parent.findViewById(R.id.paged_view_indicator)); + mHasScrollIndicator = mScrollIndicator != null; + if (mHasScrollIndicator) { + mScrollIndicator.setVisibility(View.VISIBLE); + } + } + } + return mScrollIndicator; + } + + protected boolean isScrollingIndicatorEnabled() { + return true; + } + + Runnable hideScrollingIndicatorRunnable = new Runnable() { + @Override + public void run() { + hideScrollingIndicator(false); + } + }; + protected void flashScrollingIndicator(boolean animated) { + removeCallbacks(hideScrollingIndicatorRunnable); + showScrollingIndicator(!animated); + postDelayed(hideScrollingIndicatorRunnable, sScrollIndicatorFlashDuration); + } + + protected void showScrollingIndicator(boolean immediately) { + mShouldShowScrollIndicator = true; + mShouldShowScrollIndicatorImmediately = true; + if (getChildCount() <= 1) return; + if (!isScrollingIndicatorEnabled()) return; + + mShouldShowScrollIndicator = false; + getScrollingIndicator(); + if (mScrollIndicator != null) { + // Fade the indicator in + updateScrollingIndicatorPosition(); + mScrollIndicator.setVisibility(View.VISIBLE); + cancelScrollingIndicatorAnimations(); + if (immediately || mScrollingPaused) { + mScrollIndicator.setAlpha(1f); + } else { + mScrollIndicatorAnimator = LauncherAnimUtils.ofFloat(mScrollIndicator, "alpha", 1f); + mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeInDuration); + mScrollIndicatorAnimator.start(); + } + } + } + + protected void cancelScrollingIndicatorAnimations() { + if (mScrollIndicatorAnimator != null) { + mScrollIndicatorAnimator.cancel(); + } + } + + protected void hideScrollingIndicator(boolean immediately) { + if (getChildCount() <= 1) return; + if (!isScrollingIndicatorEnabled()) return; + + getScrollingIndicator(); + if (mScrollIndicator != null) { + // Fade the indicator out + updateScrollingIndicatorPosition(); + cancelScrollingIndicatorAnimations(); + if (immediately || mScrollingPaused) { + mScrollIndicator.setVisibility(View.INVISIBLE); + mScrollIndicator.setAlpha(0f); + } else { + mScrollIndicatorAnimator = LauncherAnimUtils.ofFloat(mScrollIndicator, "alpha", 0f); + mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeOutDuration); + mScrollIndicatorAnimator.addListener(new AnimatorListenerAdapter() { + private boolean cancelled = false; + @Override + public void onAnimationCancel(android.animation.Animator animation) { + cancelled = true; + } + @Override + public void onAnimationEnd(Animator animation) { + if (!cancelled) { + mScrollIndicator.setVisibility(View.INVISIBLE); + } + } + }); + mScrollIndicatorAnimator.start(); + } + } + } + + /** + * To be overridden by subclasses to determine whether the scroll indicator should stretch to + * fill its space on the track or not. + */ + protected boolean hasElasticScrollIndicator() { + return true; + } + + private void updateScrollingIndicator() { + if (getChildCount() <= 1) return; + if (!isScrollingIndicatorEnabled()) return; + + getScrollingIndicator(); + if (mScrollIndicator != null) { + updateScrollingIndicatorPosition(); + } + if (mShouldShowScrollIndicator) { + showScrollingIndicator(mShouldShowScrollIndicatorImmediately); + } + } + + private void updateScrollingIndicatorPosition() { + final boolean isRtl = isLayoutRtl(); + if (!isScrollingIndicatorEnabled()) return; + if (mScrollIndicator == null) return; + int numPages = getChildCount(); + int pageWidth = getMeasuredWidth(); + int trackWidth = pageWidth - mScrollIndicatorPaddingLeft - mScrollIndicatorPaddingRight; + int indicatorWidth = mScrollIndicator.getMeasuredWidth() - + mScrollIndicator.getPaddingLeft() - mScrollIndicator.getPaddingRight(); + + float scrollPos = isRtl ? mMaxScrollX - getScrollX() : getScrollX(); + float offset = Math.max(0f, Math.min(1f, (float) scrollPos / mMaxScrollX)); + if (isRtl) { + offset = 1f - offset; + } + int indicatorSpace = trackWidth / numPages; + int indicatorPos = (int) (offset * (trackWidth - indicatorSpace)) + mScrollIndicatorPaddingLeft; + if (hasElasticScrollIndicator()) { + if (mScrollIndicator.getMeasuredWidth() != indicatorSpace) { + mScrollIndicator.getLayoutParams().width = indicatorSpace; + mScrollIndicator.requestLayout(); + } + } else { + int indicatorCenterOffset = indicatorSpace / 2 - indicatorWidth / 2; + indicatorPos += indicatorCenterOffset; + } + mScrollIndicator.setTranslationX(indicatorPos); + } + + public void showScrollIndicatorTrack() { + } + + public void hideScrollIndicatorTrack() { + } + + /* Accessibility */ + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setScrollable(getPageCount() > 1); + if (getCurrentPage() < getPageCount() - 1) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + } + if (getCurrentPage() > 0) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setScrollable(true); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + event.setFromIndex(mCurrentPage); + event.setToIndex(mCurrentPage); + event.setItemCount(getChildCount()); + } + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (super.performAccessibilityAction(action, arguments)) { + return true; + } + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { + if (getCurrentPage() < getPageCount() - 1) { + scrollRight(); + return true; + } + } break; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { + if (getCurrentPage() > 0) { + scrollLeft(); + return true; + } + } break; + } + return false; + } + + protected String getCurrentPageDescription() { + return String.format(getContext().getString(R.string.default_scroll_format), + getNextPage() + 1, getChildCount()); + } + + @Override + public boolean onHoverEvent(android.view.MotionEvent event) { + return true; + } +} diff --git a/src/com/android/launcher3/PagedViewCellLayout.java b/src/com/android/launcher3/PagedViewCellLayout.java new file mode 100644 index 000000000..177425aca --- /dev/null +++ b/src/com/android/launcher3/PagedViewCellLayout.java @@ -0,0 +1,505 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; + +import com.android.launcher3.R; + +/** + * An abstraction of the original CellLayout which supports laying out items + * which span multiple cells into a grid-like layout. Also supports dimming + * to give a preview of its contents. + */ +public class PagedViewCellLayout extends ViewGroup implements Page { + static final String TAG = "PagedViewCellLayout"; + + private int mCellCountX; + private int mCellCountY; + private int mOriginalCellWidth; + private int mOriginalCellHeight; + private int mCellWidth; + private int mCellHeight; + private int mOriginalWidthGap; + private int mOriginalHeightGap; + private int mWidthGap; + private int mHeightGap; + private int mMaxGap; + protected PagedViewCellLayoutChildren mChildren; + + public PagedViewCellLayout(Context context) { + this(context, null); + } + + public PagedViewCellLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedViewCellLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setAlwaysDrawnWithCacheEnabled(false); + + // setup default cell parameters + Resources resources = context.getResources(); + mOriginalCellWidth = mCellWidth = + resources.getDimensionPixelSize(R.dimen.apps_customize_cell_width); + mOriginalCellHeight = mCellHeight = + resources.getDimensionPixelSize(R.dimen.apps_customize_cell_height); + mCellCountX = LauncherModel.getCellCountX(); + mCellCountY = LauncherModel.getCellCountY(); + mOriginalWidthGap = mOriginalHeightGap = mWidthGap = mHeightGap = -1; + mMaxGap = resources.getDimensionPixelSize(R.dimen.apps_customize_max_gap); + + mChildren = new PagedViewCellLayoutChildren(context); + mChildren.setCellDimensions(mCellWidth, mCellHeight); + mChildren.setGap(mWidthGap, mHeightGap); + + addView(mChildren); + } + + public int getCellWidth() { + return mCellWidth; + } + + public int getCellHeight() { + return mCellHeight; + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + // Cancel long press for all children + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + child.cancelLongPress(); + } + } + + public boolean addViewToCellLayout(View child, int index, int childId, + PagedViewCellLayout.LayoutParams params) { + final PagedViewCellLayout.LayoutParams lp = params; + + // Generate an id for each view, this assumes we have at most 256x256 cells + // per workspace screen + if (lp.cellX >= 0 && lp.cellX <= (mCellCountX - 1) && + lp.cellY >= 0 && (lp.cellY <= mCellCountY - 1)) { + // If the horizontal or vertical span is set to -1, it is taken to + // mean that it spans the extent of the CellLayout + if (lp.cellHSpan < 0) lp.cellHSpan = mCellCountX; + if (lp.cellVSpan < 0) lp.cellVSpan = mCellCountY; + + child.setId(childId); + mChildren.addView(child, index, lp); + + return true; + } + return false; + } + + @Override + public void removeAllViewsOnPage() { + mChildren.removeAllViews(); + setLayerType(LAYER_TYPE_NONE, null); + } + + @Override + public void removeViewOnPageAt(int index) { + mChildren.removeViewAt(index); + } + + /** + * Clears all the key listeners for the individual icons. + */ + public void resetChildrenOnKeyListeners() { + int childCount = mChildren.getChildCount(); + for (int j = 0; j < childCount; ++j) { + mChildren.getChildAt(j).setOnKeyListener(null); + } + } + + @Override + public int getPageChildCount() { + return mChildren.getChildCount(); + } + + public PagedViewCellLayoutChildren getChildrenLayout() { + return mChildren; + } + + @Override + public View getChildOnPageAt(int i) { + return mChildren.getChildAt(i); + } + + @Override + public int indexOfChildOnPage(View v) { + return mChildren.indexOfChild(v); + } + + public int getCellCountX() { + return mCellCountX; + } + + public int getCellCountY() { + return mCellCountY; + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { + throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); + } + + int numWidthGaps = mCellCountX - 1; + int numHeightGaps = mCellCountY - 1; + + if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) { + int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight(); + int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom(); + int hFreeSpace = hSpace - (mCellCountX * mOriginalCellWidth); + int vFreeSpace = vSpace - (mCellCountY * mOriginalCellHeight); + mWidthGap = Math.min(mMaxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0); + mHeightGap = Math.min(mMaxGap,numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0); + + mChildren.setGap(mWidthGap, mHeightGap); + } else { + mWidthGap = mOriginalWidthGap; + mHeightGap = mOriginalHeightGap; + } + + // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY + int newWidth = widthSpecSize; + int newHeight = heightSpecSize; + if (widthSpecMode == MeasureSpec.AT_MOST) { + newWidth = getPaddingLeft() + getPaddingRight() + (mCellCountX * mCellWidth) + + ((mCellCountX - 1) * mWidthGap); + newHeight = getPaddingTop() + getPaddingBottom() + (mCellCountY * mCellHeight) + + ((mCellCountY - 1) * mHeightGap); + setMeasuredDimension(newWidth, newHeight); + } + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + int childWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(newWidth - getPaddingLeft() - + getPaddingRight(), MeasureSpec.EXACTLY); + int childheightMeasureSpec = + MeasureSpec.makeMeasureSpec(newHeight - getPaddingTop() - + getPaddingBottom(), MeasureSpec.EXACTLY); + child.measure(childWidthMeasureSpec, childheightMeasureSpec); + } + + setMeasuredDimension(newWidth, newHeight); + } + + int getContentWidth() { + return getWidthBeforeFirstLayout() + getPaddingLeft() + getPaddingRight(); + } + + int getContentHeight() { + if (mCellCountY > 0) { + return mCellCountY * mCellHeight + (mCellCountY - 1) * Math.max(0, mHeightGap); + } + return 0; + } + + int getWidthBeforeFirstLayout() { + if (mCellCountX > 0) { + return mCellCountX * mCellWidth + (mCellCountX - 1) * Math.max(0, mWidthGap); + } + return 0; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + child.layout(getPaddingLeft(), getPaddingTop(), + r - l - getPaddingRight(), b - t - getPaddingBottom()); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean result = super.onTouchEvent(event); + int count = getPageChildCount(); + if (count > 0) { + // We only intercept the touch if we are tapping in empty space after the final row + View child = getChildOnPageAt(count - 1); + int bottom = child.getBottom(); + int numRows = (int) Math.ceil((float) getPageChildCount() / getCellCountX()); + if (numRows < getCellCountY()) { + // Add a little bit of buffer if there is room for another row + bottom += mCellHeight / 2; + } + result = result || (event.getY() < bottom); + } + return result; + } + + public void enableCenteredContent(boolean enabled) { + mChildren.enableCenteredContent(enabled); + } + + @Override + protected void setChildrenDrawingCacheEnabled(boolean enabled) { + mChildren.setChildrenDrawingCacheEnabled(enabled); + } + + public void setCellCount(int xCount, int yCount) { + mCellCountX = xCount; + mCellCountY = yCount; + requestLayout(); + } + + public void setGap(int widthGap, int heightGap) { + mOriginalWidthGap = mWidthGap = widthGap; + mOriginalHeightGap = mHeightGap = heightGap; + mChildren.setGap(widthGap, heightGap); + } + + public int[] getCellCountForDimensions(int width, int height) { + // Always assume we're working with the smallest span to make sure we + // reserve enough space in both orientations + int smallerSize = Math.min(mCellWidth, mCellHeight); + + // Always round up to next largest cell + int spanX = (width + smallerSize) / smallerSize; + int spanY = (height + smallerSize) / smallerSize; + + return new int[] { spanX, spanY }; + } + + /** + * Start dragging the specified child + * + * @param child The child that is being dragged + */ + void onDragChild(View child) { + PagedViewCellLayout.LayoutParams lp = (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); + lp.isDragging = true; + } + + /** + * Estimates the number of cells that the specified width would take up. + */ + public int estimateCellHSpan(int width) { + // We don't show the next/previous pages any more, so we use the full width, minus the + // padding + int availWidth = width - (getPaddingLeft() + getPaddingRight()); + + // We know that we have to fit N cells with N-1 width gaps, so we just juggle to solve for N + int n = Math.max(1, (availWidth + mWidthGap) / (mCellWidth + mWidthGap)); + + // We don't do anything fancy to determine if we squeeze another row in. + return n; + } + + /** + * Estimates the number of cells that the specified height would take up. + */ + public int estimateCellVSpan(int height) { + // The space for a page is the height - top padding (current page) - bottom padding (current + // page) + int availHeight = height - (getPaddingTop() + getPaddingBottom()); + + // We know that we have to fit N cells with N-1 height gaps, so we juggle to solve for N + int n = Math.max(1, (availHeight + mHeightGap) / (mCellHeight + mHeightGap)); + + // We don't do anything fancy to determine if we squeeze another row in. + return n; + } + + /** Returns an estimated center position of the cell at the specified index */ + public int[] estimateCellPosition(int x, int y) { + return new int[] { + getPaddingLeft() + (x * mCellWidth) + (x * mWidthGap) + (mCellWidth / 2), + getPaddingTop() + (y * mCellHeight) + (y * mHeightGap) + (mCellHeight / 2) + }; + } + + public void calculateCellCount(int width, int height, int maxCellCountX, int maxCellCountY) { + mCellCountX = Math.min(maxCellCountX, estimateCellHSpan(width)); + mCellCountY = Math.min(maxCellCountY, estimateCellVSpan(height)); + requestLayout(); + } + + /** + * Estimates the width that the number of hSpan cells will take up. + */ + public int estimateCellWidth(int hSpan) { + // TODO: we need to take widthGap into effect + return hSpan * mCellWidth; + } + + /** + * Estimates the height that the number of vSpan cells will take up. + */ + public int estimateCellHeight(int vSpan) { + // TODO: we need to take heightGap into effect + return vSpan * mCellHeight; + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new PagedViewCellLayout.LayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof PagedViewCellLayout.LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new PagedViewCellLayout.LayoutParams(p); + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + /** + * Horizontal location of the item in the grid. + */ + @ViewDebug.ExportedProperty + public int cellX; + + /** + * Vertical location of the item in the grid. + */ + @ViewDebug.ExportedProperty + public int cellY; + + /** + * Number of cells spanned horizontally by the item. + */ + @ViewDebug.ExportedProperty + public int cellHSpan; + + /** + * Number of cells spanned vertically by the item. + */ + @ViewDebug.ExportedProperty + public int cellVSpan; + + /** + * Is this item currently being dragged + */ + public boolean isDragging; + + // a data object that you can bind to this layout params + private Object mTag; + + // X coordinate of the view in the layout. + @ViewDebug.ExportedProperty + int x; + // Y coordinate of the view in the layout. + @ViewDebug.ExportedProperty + int y; + + public LayoutParams() { + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + cellHSpan = 1; + cellVSpan = 1; + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + cellHSpan = 1; + cellVSpan = 1; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + cellHSpan = 1; + cellVSpan = 1; + } + + public LayoutParams(LayoutParams source) { + super(source); + this.cellX = source.cellX; + this.cellY = source.cellY; + this.cellHSpan = source.cellHSpan; + this.cellVSpan = source.cellVSpan; + } + + public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) { + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + this.cellX = cellX; + this.cellY = cellY; + this.cellHSpan = cellHSpan; + this.cellVSpan = cellVSpan; + } + + public void setup(int cellWidth, int cellHeight, int widthGap, int heightGap, + int hStartPadding, int vStartPadding) { + + final int myCellHSpan = cellHSpan; + final int myCellVSpan = cellVSpan; + final int myCellX = cellX; + final int myCellY = cellY; + + width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - + leftMargin - rightMargin; + height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - + topMargin - bottomMargin; + + if (LauncherApplication.isScreenLarge()) { + x = hStartPadding + myCellX * (cellWidth + widthGap) + leftMargin; + y = vStartPadding + myCellY * (cellHeight + heightGap) + topMargin; + } else { + x = myCellX * (cellWidth + widthGap) + leftMargin; + y = myCellY * (cellHeight + heightGap) + topMargin; + } + } + + public Object getTag() { + return mTag; + } + + public void setTag(Object tag) { + mTag = tag; + } + + public String toString() { + return "(" + this.cellX + ", " + this.cellY + ", " + + this.cellHSpan + ", " + this.cellVSpan + ")"; + } + } +} + +interface Page { + public int getPageChildCount(); + public View getChildOnPageAt(int i); + public void removeAllViewsOnPage(); + public void removeViewOnPageAt(int i); + public int indexOfChildOnPage(View v); +} diff --git a/src/com/android/launcher3/PagedViewCellLayoutChildren.java b/src/com/android/launcher3/PagedViewCellLayoutChildren.java new file mode 100644 index 000000000..c9e108d98 --- /dev/null +++ b/src/com/android/launcher3/PagedViewCellLayoutChildren.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +/** + * An abstraction of the original CellLayout which supports laying out items + * which span multiple cells into a grid-like layout. Also supports dimming + * to give a preview of its contents. + */ +public class PagedViewCellLayoutChildren extends ViewGroup { + static final String TAG = "PagedViewCellLayout"; + + private boolean mCenterContent; + + private int mCellWidth; + private int mCellHeight; + private int mWidthGap; + private int mHeightGap; + + public PagedViewCellLayoutChildren(Context context) { + super(context); + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + // Cancel long press for all children + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + child.cancelLongPress(); + } + } + + public void setGap(int widthGap, int heightGap) { + mWidthGap = widthGap; + mHeightGap = heightGap; + requestLayout(); + } + + public void setCellDimensions(int width, int height) { + mCellWidth = width; + mCellHeight = height; + requestLayout(); + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + if (child != null) { + Rect r = new Rect(); + child.getDrawingRect(r); + requestRectangleOnScreen(r); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { + throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); + } + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + PagedViewCellLayout.LayoutParams lp = + (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); + lp.setup(mCellWidth, mCellHeight, mWidthGap, mHeightGap, + getPaddingLeft(), + getPaddingTop()); + + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, + MeasureSpec.EXACTLY); + int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, + MeasureSpec.EXACTLY); + + child.measure(childWidthMeasureSpec, childheightMeasureSpec); + } + + setMeasuredDimension(widthSpecSize, heightSpecSize); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int count = getChildCount(); + + int offsetX = 0; + if (mCenterContent && count > 0) { + // determine the max width of all the rows and center accordingly + int maxRowX = 0; + int minRowX = Integer.MAX_VALUE; + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + PagedViewCellLayout.LayoutParams lp = + (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); + minRowX = Math.min(minRowX, lp.x); + maxRowX = Math.max(maxRowX, lp.x + lp.width); + } + } + int maxRowWidth = maxRowX - minRowX; + offsetX = (getMeasuredWidth() - maxRowWidth) / 2; + } + + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + PagedViewCellLayout.LayoutParams lp = + (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); + + int childLeft = offsetX + lp.x; + int childTop = lp.y; + child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height); + } + } + } + + public void enableCenteredContent(boolean enabled) { + mCenterContent = enabled; + } + + @Override + protected void setChildrenDrawingCacheEnabled(boolean enabled) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View view = getChildAt(i); + view.setDrawingCacheEnabled(enabled); + // Update the drawing caches + if (!view.isHardwareAccelerated()) { + view.buildDrawingCache(true); + } + } + } +} diff --git a/src/com/android/launcher3/PagedViewGridLayout.java b/src/com/android/launcher3/PagedViewGridLayout.java new file mode 100644 index 000000000..b28686113 --- /dev/null +++ b/src/com/android/launcher3/PagedViewGridLayout.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.GridLayout; + +/** + * The grid based layout used strictly for the widget/wallpaper tab of the AppsCustomize pane + */ +public class PagedViewGridLayout extends GridLayout implements Page { + static final String TAG = "PagedViewGridLayout"; + + private int mCellCountX; + private int mCellCountY; + private Runnable mOnLayoutListener; + + public PagedViewGridLayout(Context context, int cellCountX, int cellCountY) { + super(context, null, 0); + mCellCountX = cellCountX; + mCellCountY = cellCountY; + } + + int getCellCountX() { + return mCellCountX; + } + + int getCellCountY() { + return mCellCountY; + } + + /** + * Clears all the key listeners for the individual widgets. + */ + public void resetChildrenOnKeyListeners() { + int childCount = getChildCount(); + for (int j = 0; j < childCount; ++j) { + getChildAt(j).setOnKeyListener(null); + } + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // PagedView currently has issues with different-sized pages since it calculates the + // offset of each page to scroll to before it updates the actual size of each page + // (which can change depending on the content if the contents aren't a fixed size). + // We work around this by having a minimum size on each widget page). + int widthSpecSize = Math.min(getSuggestedMinimumWidth(), + MeasureSpec.getSize(widthMeasureSpec)); + int widthSpecMode = MeasureSpec.EXACTLY; + super.onMeasure(MeasureSpec.makeMeasureSpec(widthSpecSize, widthSpecMode), + heightMeasureSpec); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mOnLayoutListener = null; + } + + public void setOnLayoutListener(Runnable r) { + mOnLayoutListener = r; + } + + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mOnLayoutListener != null) { + mOnLayoutListener.run(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean result = super.onTouchEvent(event); + int count = getPageChildCount(); + if (count > 0) { + // We only intercept the touch if we are tapping in empty space after the final row + View child = getChildOnPageAt(count - 1); + int bottom = child.getBottom(); + result = result || (event.getY() < bottom); + } + return result; + } + + @Override + public void removeAllViewsOnPage() { + removeAllViews(); + mOnLayoutListener = null; + setLayerType(LAYER_TYPE_NONE, null); + } + + @Override + public void removeViewOnPageAt(int index) { + removeViewAt(index); + } + + @Override + public int getPageChildCount() { + return getChildCount(); + } + + @Override + public View getChildOnPageAt(int i) { + return getChildAt(i); + } + + @Override + public int indexOfChildOnPage(View v) { + return indexOfChild(v); + } + + public static class LayoutParams extends FrameLayout.LayoutParams { + public LayoutParams(int width, int height) { + super(width, height); + } + } +} diff --git a/src/com/android/launcher3/PagedViewIcon.java b/src/com/android/launcher3/PagedViewIcon.java new file mode 100644 index 000000000..73f62d60e --- /dev/null +++ b/src/com/android/launcher3/PagedViewIcon.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.graphics.Bitmap; +import android.util.AttributeSet; +import android.widget.TextView; + +/** + * An icon on a PagedView, specifically for items in the launcher's paged view (with compound + * drawables on the top). + */ +public class PagedViewIcon extends TextView { + /** A simple callback interface to allow a PagedViewIcon to notify when it has been pressed */ + public static interface PressedCallback { + void iconPressed(PagedViewIcon icon); + } + + @SuppressWarnings("unused") + private static final String TAG = "PagedViewIcon"; + private static final float PRESS_ALPHA = 0.4f; + + private PagedViewIcon.PressedCallback mPressedCallback; + private boolean mLockDrawableState = false; + + private Bitmap mIcon; + + public PagedViewIcon(Context context) { + this(context, null); + } + + public PagedViewIcon(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedViewIcon(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void applyFromApplicationInfo(ApplicationInfo info, boolean scaleUp, + PagedViewIcon.PressedCallback cb) { + mIcon = info.iconBitmap; + mPressedCallback = cb; + setCompoundDrawablesWithIntrinsicBounds(null, new FastBitmapDrawable(mIcon), null, null); + setText(info.title); + setTag(info); + } + + public void lockDrawableState() { + mLockDrawableState = true; + } + + public void resetDrawableState() { + mLockDrawableState = false; + post(new Runnable() { + @Override + public void run() { + refreshDrawableState(); + } + }); + } + + protected void drawableStateChanged() { + super.drawableStateChanged(); + + // We keep in the pressed state until resetDrawableState() is called to reset the press + // feedback + if (isPressed()) { + setAlpha(PRESS_ALPHA); + if (mPressedCallback != null) { + mPressedCallback.iconPressed(this); + } + } else if (!mLockDrawableState) { + setAlpha(1f); + } + } +} diff --git a/src/com/android/launcher3/PagedViewIconCache.java b/src/com/android/launcher3/PagedViewIconCache.java new file mode 100644 index 000000000..0d03b5a52 --- /dev/null +++ b/src/com/android/launcher3/PagedViewIconCache.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2011 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.pm.ComponentInfo; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; + +/** + * Simple cache mechanism for PagedView outlines. + */ +public class PagedViewIconCache { + public static class Key { + public enum Type { + ApplicationInfoKey, + AppWidgetProviderInfoKey, + ResolveInfoKey + } + private final ComponentName mComponentName; + private final Type mType; + + public Key(ApplicationInfo info) { + mComponentName = info.componentName; + mType = Type.ApplicationInfoKey; + } + public Key(ResolveInfo info) { + final ComponentInfo ci = info.activityInfo != null ? info.activityInfo : + info.serviceInfo; + mComponentName = new ComponentName(ci.packageName, ci.name); + mType = Type.ResolveInfoKey; + } + public Key(AppWidgetProviderInfo info) { + mComponentName = info.provider; + mType = Type.AppWidgetProviderInfoKey; + } + + private ComponentName getComponentName() { + return mComponentName; + } + public boolean isKeyType(Type t) { + return (mType == t); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Key) { + Key k = (Key) o; + return mComponentName.equals(k.mComponentName); + } + return super.equals(o); + } + @Override + public int hashCode() { + return getComponentName().hashCode(); + } + } + + private final HashMap<Key, Bitmap> mIconOutlineCache = new HashMap<Key, Bitmap>(); + + public void clear() { + for (Key key : mIconOutlineCache.keySet()) { + mIconOutlineCache.get(key).recycle(); + } + mIconOutlineCache.clear(); + } + private void retainAll(HashSet<Key> keysToKeep, Key.Type t) { + HashSet<Key> keysToRemove = new HashSet<Key>(mIconOutlineCache.keySet()); + keysToRemove.removeAll(keysToKeep); + for (Key key : keysToRemove) { + if (key.isKeyType(t)) { + mIconOutlineCache.get(key).recycle(); + mIconOutlineCache.remove(key); + } + } + } + /** Removes all the keys to applications that aren't in the passed in collection */ + public void retainAllApps(ArrayList<ApplicationInfo> keys) { + HashSet<Key> keysSet = new HashSet<Key>(); + for (ApplicationInfo info : keys) { + keysSet.add(new Key(info)); + } + retainAll(keysSet, Key.Type.ApplicationInfoKey); + } + /** Removes all the keys to shortcuts that aren't in the passed in collection */ + public void retainAllShortcuts(List<ResolveInfo> keys) { + HashSet<Key> keysSet = new HashSet<Key>(); + for (ResolveInfo info : keys) { + keysSet.add(new Key(info)); + } + retainAll(keysSet, Key.Type.ResolveInfoKey); + } + /** Removes all the keys to widgets that aren't in the passed in collection */ + public void retainAllAppWidgets(List<AppWidgetProviderInfo> keys) { + HashSet<Key> keysSet = new HashSet<Key>(); + for (AppWidgetProviderInfo info : keys) { + keysSet.add(new Key(info)); + } + retainAll(keysSet, Key.Type.AppWidgetProviderInfoKey); + } + public void addOutline(Key key, Bitmap b) { + mIconOutlineCache.put(key, b); + } + public void removeOutline(Key key) { + if (mIconOutlineCache.containsKey(key)) { + mIconOutlineCache.get(key).recycle(); + mIconOutlineCache.remove(key); + } + } + public Bitmap getOutline(Key key) { + return mIconOutlineCache.get(key); + } +} diff --git a/src/com/android/launcher3/PagedViewWidget.java b/src/com/android/launcher3/PagedViewWidget.java new file mode 100644 index 000000000..bd40c5cf5 --- /dev/null +++ b/src/com/android/launcher3/PagedViewWidget.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2010 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.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.R; + +/** + * The linear layout used strictly for the widget/wallpaper tab of the customization tray + */ +public class PagedViewWidget extends LinearLayout { + static final String TAG = "PagedViewWidgetLayout"; + + private static boolean sDeletePreviewsWhenDetachedFromWindow = true; + private static boolean sRecyclePreviewsWhenDetachedFromWindow = true; + + private String mDimensionsFormatString; + CheckForShortPress mPendingCheckForShortPress = null; + ShortPressListener mShortPressListener = null; + boolean mShortPressTriggered = false; + static PagedViewWidget sShortpressTarget = null; + boolean mIsAppWidget; + private final Rect mOriginalImagePadding = new Rect(); + private Object mInfo; + private WidgetPreviewLoader mWidgetPreviewLoader; + + public PagedViewWidget(Context context) { + this(context, null); + } + + public PagedViewWidget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedViewWidget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final Resources r = context.getResources(); + mDimensionsFormatString = r.getString(R.string.widget_dims_format); + + setWillNotDraw(false); + setClipToPadding(false); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + final ImageView image = (ImageView) findViewById(R.id.widget_preview); + mOriginalImagePadding.left = image.getPaddingLeft(); + mOriginalImagePadding.top = image.getPaddingTop(); + mOriginalImagePadding.right = image.getPaddingRight(); + mOriginalImagePadding.bottom = image.getPaddingBottom(); + } + + public static void setDeletePreviewsWhenDetachedFromWindow(boolean value) { + sDeletePreviewsWhenDetachedFromWindow = value; + } + + public static void setRecyclePreviewsWhenDetachedFromWindow(boolean value) { + sRecyclePreviewsWhenDetachedFromWindow = value; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (sDeletePreviewsWhenDetachedFromWindow) { + final ImageView image = (ImageView) findViewById(R.id.widget_preview); + if (image != null) { + FastBitmapDrawable preview = (FastBitmapDrawable) image.getDrawable(); + if (sRecyclePreviewsWhenDetachedFromWindow && + mInfo != null && preview != null && preview.getBitmap() != null) { + mWidgetPreviewLoader.recycleBitmap(mInfo, preview.getBitmap()); + } + image.setImageDrawable(null); + } + } + } + + public void applyFromAppWidgetProviderInfo(AppWidgetProviderInfo info, + int maxWidth, int[] cellSpan, WidgetPreviewLoader loader) { + mIsAppWidget = true; + mInfo = info; + final ImageView image = (ImageView) findViewById(R.id.widget_preview); + if (maxWidth > -1) { + image.setMaxWidth(maxWidth); + } + final TextView name = (TextView) findViewById(R.id.widget_name); + name.setText(info.label); + final TextView dims = (TextView) findViewById(R.id.widget_dims); + if (dims != null) { + int hSpan = Math.min(cellSpan[0], LauncherModel.getCellCountX()); + int vSpan = Math.min(cellSpan[1], LauncherModel.getCellCountY()); + dims.setText(String.format(mDimensionsFormatString, hSpan, vSpan)); + } + mWidgetPreviewLoader = loader; + } + + public void applyFromResolveInfo( + PackageManager pm, ResolveInfo info, WidgetPreviewLoader loader) { + mIsAppWidget = false; + mInfo = info; + CharSequence label = info.loadLabel(pm); + final TextView name = (TextView) findViewById(R.id.widget_name); + name.setText(label); + final TextView dims = (TextView) findViewById(R.id.widget_dims); + if (dims != null) { + dims.setText(String.format(mDimensionsFormatString, 1, 1)); + } + mWidgetPreviewLoader = loader; + } + + public int[] getPreviewSize() { + final ImageView i = (ImageView) findViewById(R.id.widget_preview); + int[] maxSize = new int[2]; + maxSize[0] = i.getWidth() - mOriginalImagePadding.left - mOriginalImagePadding.right; + maxSize[1] = i.getHeight() - mOriginalImagePadding.top; + return maxSize; + } + + void applyPreview(FastBitmapDrawable preview, int index) { + final PagedViewWidgetImageView image = + (PagedViewWidgetImageView) findViewById(R.id.widget_preview); + if (preview != null) { + image.mAllowRequestLayout = false; + image.setImageDrawable(preview); + if (mIsAppWidget) { + // center horizontally + int[] imageSize = getPreviewSize(); + int centerAmount = (imageSize[0] - preview.getIntrinsicWidth()) / 2; + image.setPadding(mOriginalImagePadding.left + centerAmount, + mOriginalImagePadding.top, + mOriginalImagePadding.right, + mOriginalImagePadding.bottom); + } + image.setAlpha(1f); + image.mAllowRequestLayout = true; + } + } + + void setShortPressListener(ShortPressListener listener) { + mShortPressListener = listener; + } + + interface ShortPressListener { + void onShortPress(View v); + void cleanUpShortPress(View v); + } + + class CheckForShortPress implements Runnable { + public void run() { + if (sShortpressTarget != null) return; + if (mShortPressListener != null) { + mShortPressListener.onShortPress(PagedViewWidget.this); + sShortpressTarget = PagedViewWidget.this; + } + mShortPressTriggered = true; + } + } + + private void checkForShortPress() { + if (sShortpressTarget != null) return; + if (mPendingCheckForShortPress == null) { + mPendingCheckForShortPress = new CheckForShortPress(); + } + postDelayed(mPendingCheckForShortPress, 120); + } + + /** + * Remove the longpress detection timer. + */ + private void removeShortPressCallback() { + if (mPendingCheckForShortPress != null) { + removeCallbacks(mPendingCheckForShortPress); + } + } + + private void cleanUpShortPress() { + removeShortPressCallback(); + if (mShortPressTriggered) { + if (mShortPressListener != null) { + mShortPressListener.cleanUpShortPress(PagedViewWidget.this); + } + mShortPressTriggered = false; + } + } + + static void resetShortPressTarget() { + sShortpressTarget = null; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + cleanUpShortPress(); + break; + case MotionEvent.ACTION_DOWN: + checkForShortPress(); + break; + case MotionEvent.ACTION_CANCEL: + cleanUpShortPress(); + break; + case MotionEvent.ACTION_MOVE: + break; + } + + // We eat up the touch events here, since the PagedView (which uses the same swiping + // touch code as Workspace previously) uses onInterceptTouchEvent() to determine when + // the user is scrolling between pages. This means that if the pages themselves don't + // handle touch events, it gets forwarded up to PagedView itself, and it's own + // onTouchEvent() handling will prevent further intercept touch events from being called + // (it's the same view in that case). This is not ideal, but to prevent more changes, + // we just always mark the touch event as handled. + return true; + } +} diff --git a/src/com/android/launcher3/PagedViewWidgetImageView.java b/src/com/android/launcher3/PagedViewWidgetImageView.java new file mode 100644 index 000000000..71f5eead3 --- /dev/null +++ b/src/com/android/launcher3/PagedViewWidgetImageView.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.ImageView; + +class PagedViewWidgetImageView extends ImageView { + public boolean mAllowRequestLayout = true; + + public PagedViewWidgetImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void requestLayout() { + if (mAllowRequestLayout) { + super.requestLayout(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.save(); + canvas.clipRect(getScrollX() + getPaddingLeft(), + getScrollY() + getPaddingTop(), + getScrollX() + getRight() - getLeft() - getPaddingRight(), + getScrollY() + getBottom() - getTop() - getPaddingBottom()); + + super.onDraw(canvas); + canvas.restore(); + + } +} diff --git a/src/com/android/launcher3/PagedViewWithDraggableItems.java b/src/com/android/launcher3/PagedViewWithDraggableItems.java new file mode 100644 index 000000000..8f10ecfa5 --- /dev/null +++ b/src/com/android/launcher3/PagedViewWithDraggableItems.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2010 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.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + + +/* Class that does most of the work of enabling dragging items out of a PagedView by performing a + * vertical drag. Used by both CustomizePagedView and AllAppsPagedView. + * Subclasses must do the following: + * * call setDragSlopeThreshold after making an instance of the PagedViewWithDraggableItems + * * call child.setOnLongClickListener(this) and child.setOnTouchListener(this) on all children + * (good place to do it is in syncPageItems) + * * override beginDragging(View) (but be careful to call super.beginDragging(View) + * + */ +public abstract class PagedViewWithDraggableItems extends PagedView + implements View.OnLongClickListener, View.OnTouchListener { + private View mLastTouchedItem; + private boolean mIsDragging; + private boolean mIsDragEnabled; + private float mDragSlopeThreshold; + private Launcher mLauncher; + + public PagedViewWithDraggableItems(Context context) { + this(context, null); + } + + public PagedViewWithDraggableItems(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedViewWithDraggableItems(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mLauncher = (Launcher) context; + } + + protected boolean beginDragging(View v) { + boolean wasDragging = mIsDragging; + mIsDragging = true; + return !wasDragging; + } + + protected void cancelDragging() { + mIsDragging = false; + mLastTouchedItem = null; + mIsDragEnabled = false; + } + + private void handleTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + cancelDragging(); + mIsDragEnabled = true; + break; + case MotionEvent.ACTION_MOVE: + if (mTouchState != TOUCH_STATE_SCROLLING && !mIsDragging && mIsDragEnabled) { + determineDraggingStart(ev); + } + break; + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + handleTouchEvent(ev); + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + handleTouchEvent(ev); + return super.onTouchEvent(ev); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + mLastTouchedItem = v; + mIsDragEnabled = true; + return false; + } + + @Override + public boolean onLongClick(View v) { + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return false; + // Return early if we are still animating the pages + if (mNextPage != INVALID_PAGE) return false; + // When we have exited all apps or are in transition, disregard long clicks + if (!mLauncher.isAllAppsVisible() || + mLauncher.getWorkspace().isSwitchingState()) return false; + // Return if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return false; + + return beginDragging(v); + } + + /* + * Determines if we should change the touch state to start scrolling after the + * user moves their touch point too far. + */ + protected void determineScrollingStart(MotionEvent ev) { + if (!mIsDragging) super.determineScrollingStart(ev); + } + + /* + * Determines if we should change the touch state to start dragging after the + * user moves their touch point far enough. + */ + protected void determineDraggingStart(MotionEvent ev) { + /* + * Locally do absolute value. mLastMotionX is set to the y value + * of the down event. + */ + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float y = ev.getY(pointerIndex); + final int xDiff = (int) Math.abs(x - mLastMotionX); + final int yDiff = (int) Math.abs(y - mLastMotionY); + + final int touchSlop = mTouchSlop; + boolean yMoved = yDiff > touchSlop; + boolean isUpwardMotion = (yDiff / (float) xDiff) > mDragSlopeThreshold; + + if (isUpwardMotion && yMoved && mLastTouchedItem != null) { + // Drag if the user moved far enough along the Y axis + beginDragging(mLastTouchedItem); + + // Cancel any pending long press + if (mAllowLongPress) { + mAllowLongPress = false; + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + final View currentPage = getPageAt(mCurrentPage); + if (currentPage != null) { + currentPage.cancelLongPress(); + } + } + } + } + + public void setDragSlopeThreshold(float dragSlopeThreshold) { + mDragSlopeThreshold = dragSlopeThreshold; + } + + @Override + protected void onDetachedFromWindow() { + cancelDragging(); + super.onDetachedFromWindow(); + } + + /** Show the scrolling indicators when we move the page */ + protected void onPageBeginMoving() { + showScrollingIndicator(false); + } + protected void onPageEndMoving() { + hideScrollingIndicator(false); + } +} diff --git a/src/com/android/launcher3/PendingAddItemInfo.java b/src/com/android/launcher3/PendingAddItemInfo.java new file mode 100644 index 000000000..967cc928e --- /dev/null +++ b/src/com/android/launcher3/PendingAddItemInfo.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010 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.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.os.Parcelable; + +/** + * We pass this object with a drag from the customization tray + */ +class PendingAddItemInfo extends ItemInfo { + /** + * The component that will be created. + */ + ComponentName componentName; +} + +class PendingAddShortcutInfo extends PendingAddItemInfo { + + ActivityInfo shortcutActivityInfo; + + public PendingAddShortcutInfo(ActivityInfo activityInfo) { + shortcutActivityInfo = activityInfo; + } + + @Override + public String toString() { + return "Shortcut: " + shortcutActivityInfo.packageName; + } +} + +class PendingAddWidgetInfo extends PendingAddItemInfo { + int minWidth; + int minHeight; + int minResizeWidth; + int minResizeHeight; + int previewImage; + int icon; + AppWidgetProviderInfo info; + AppWidgetHostView boundWidget; + Bundle bindOptions = null; + + // Any configuration data that we want to pass to a configuration activity when + // starting up a widget + String mimeType; + Parcelable configurationData; + + public PendingAddWidgetInfo(AppWidgetProviderInfo i, String dataMimeType, Parcelable data) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + this.info = i; + componentName = i.provider; + minWidth = i.minWidth; + minHeight = i.minHeight; + minResizeWidth = i.minResizeWidth; + minResizeHeight = i.minResizeHeight; + previewImage = i.previewImage; + icon = i.icon; + if (dataMimeType != null && data != null) { + mimeType = dataMimeType; + configurationData = data; + } + } + + // Copy constructor + public PendingAddWidgetInfo(PendingAddWidgetInfo copy) { + minWidth = copy.minWidth; + minHeight = copy.minHeight; + minResizeWidth = copy.minResizeWidth; + minResizeHeight = copy.minResizeHeight; + previewImage = copy.previewImage; + icon = copy.icon; + info = copy.info; + boundWidget = copy.boundWidget; + mimeType = copy.mimeType; + configurationData = copy.configurationData; + componentName = copy.componentName; + itemType = copy.itemType; + spanX = copy.spanX; + spanY = copy.spanY; + minSpanX = copy.minSpanX; + minSpanY = copy.minSpanY; + bindOptions = copy.bindOptions == null ? null : (Bundle) copy.bindOptions.clone(); + } + + @Override + public String toString() { + return "Widget: " + componentName.toShortString(); + } +} diff --git a/src/com/android/launcher3/PreloadReceiver.java b/src/com/android/launcher3/PreloadReceiver.java new file mode 100644 index 000000000..ee3434822 --- /dev/null +++ b/src/com/android/launcher3/PreloadReceiver.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2012 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; + +public class PreloadReceiver extends BroadcastReceiver { + private static final String TAG = "Launcher.PreloadReceiver"; + private static final boolean LOGD = false; + + public static final String EXTRA_WORKSPACE_NAME = + "com.android.launcher3.action.EXTRA_WORKSPACE_NAME"; + + @Override + public void onReceive(Context context, Intent intent) { + final LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + final LauncherProvider provider = app.getLauncherProvider(); + if (provider != null) { + String name = intent.getStringExtra(EXTRA_WORKSPACE_NAME); + final int workspaceResId = !TextUtils.isEmpty(name) + ? context.getResources().getIdentifier(name, "xml", "com.android.launcher3") : 0; + if (LOGD) { + Log.d(TAG, "workspace name: " + name + " id: " + workspaceResId); + } + new Thread(new Runnable() { + @Override + public void run() { + provider.loadDefaultFavoritesIfNecessary(workspaceResId); + } + }).start(); + } + } +} diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java new file mode 100644 index 000000000..32d094b05 --- /dev/null +++ b/src/com/android/launcher3/SearchDropTargetBar.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2011 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.ObjectAnimator; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.widget.FrameLayout; + +import com.android.launcher3.R; + +/* + * Ths bar will manage the transition between the QSB search bar and the delete drop + * targets so that each of the individual IconDropTargets don't have to. + */ +public class SearchDropTargetBar extends FrameLayout implements DragController.DragListener { + + private static final int sTransitionInDuration = 200; + private static final int sTransitionOutDuration = 175; + + private ObjectAnimator mDropTargetBarAnim; + private ObjectAnimator mQSBSearchBarAnim; + private static final AccelerateInterpolator sAccelerateInterpolator = + new AccelerateInterpolator(); + + private boolean mIsSearchBarHidden; + private View mQSBSearchBar; + private View mDropTargetBar; + private ButtonDropTarget mInfoDropTarget; + private ButtonDropTarget mDeleteDropTarget; + private int mBarHeight; + private boolean mDeferOnDragEnd = false; + + private Drawable mPreviousBackground; + private boolean mEnableDropDownDropTargets; + + public SearchDropTargetBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SearchDropTargetBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setup(Launcher launcher, DragController dragController) { + dragController.addDragListener(this); + dragController.addDragListener(mInfoDropTarget); + dragController.addDragListener(mDeleteDropTarget); + dragController.addDropTarget(mInfoDropTarget); + dragController.addDropTarget(mDeleteDropTarget); + dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); + mInfoDropTarget.setLauncher(launcher); + mDeleteDropTarget.setLauncher(launcher); + } + + private void prepareStartAnimation(View v) { + // Enable the hw layers before the animation starts (will be disabled in the onAnimationEnd + // callback below) + v.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + private void setupAnimation(ObjectAnimator anim, final View v) { + anim.setInterpolator(sAccelerateInterpolator); + anim.setDuration(sTransitionInDuration); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + v.setLayerType(View.LAYER_TYPE_NONE, null); + } + }); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + // Get the individual components + mQSBSearchBar = findViewById(R.id.qsb_search_bar); + mDropTargetBar = findViewById(R.id.drag_target_bar); + mInfoDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.info_target_text); + mDeleteDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.delete_target_text); + mBarHeight = getResources().getDimensionPixelSize(R.dimen.qsb_bar_height); + + mInfoDropTarget.setSearchDropTargetBar(this); + mDeleteDropTarget.setSearchDropTargetBar(this); + + mEnableDropDownDropTargets = + getResources().getBoolean(R.bool.config_useDropTargetDownTransition); + + // Create the various fade animations + if (mEnableDropDownDropTargets) { + mDropTargetBar.setTranslationY(-mBarHeight); + mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "translationY", + -mBarHeight, 0f); + mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "translationY", 0, + -mBarHeight); + } else { + mDropTargetBar.setAlpha(0f); + mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "alpha", 0f, 1f); + mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f); + } + setupAnimation(mDropTargetBarAnim, mDropTargetBar); + setupAnimation(mQSBSearchBarAnim, mQSBSearchBar); + } + + public void finishAnimations() { + prepareStartAnimation(mDropTargetBar); + mDropTargetBarAnim.reverse(); + prepareStartAnimation(mQSBSearchBar); + mQSBSearchBarAnim.reverse(); + } + + /* + * Shows and hides the search bar. + */ + public void showSearchBar(boolean animated) { + if (!mIsSearchBarHidden) return; + if (animated) { + prepareStartAnimation(mQSBSearchBar); + mQSBSearchBarAnim.reverse(); + } else { + mQSBSearchBarAnim.cancel(); + if (mEnableDropDownDropTargets) { + mQSBSearchBar.setTranslationY(0); + } else { + mQSBSearchBar.setAlpha(1f); + } + } + mIsSearchBarHidden = false; + } + public void hideSearchBar(boolean animated) { + if (mIsSearchBarHidden) return; + if (animated) { + prepareStartAnimation(mQSBSearchBar); + mQSBSearchBarAnim.start(); + } else { + mQSBSearchBarAnim.cancel(); + if (mEnableDropDownDropTargets) { + mQSBSearchBar.setTranslationY(-mBarHeight); + } else { + mQSBSearchBar.setAlpha(0f); + } + } + mIsSearchBarHidden = true; + } + + /* + * Gets various transition durations. + */ + public int getTransitionInDuration() { + return sTransitionInDuration; + } + public int getTransitionOutDuration() { + return sTransitionOutDuration; + } + + /* + * DragController.DragListener implementation + */ + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { + // Animate out the QSB search bar, and animate in the drop target bar + prepareStartAnimation(mDropTargetBar); + mDropTargetBarAnim.start(); + if (!mIsSearchBarHidden) { + prepareStartAnimation(mQSBSearchBar); + mQSBSearchBarAnim.start(); + } + } + + public void deferOnDragEnd() { + mDeferOnDragEnd = true; + } + + @Override + public void onDragEnd() { + if (!mDeferOnDragEnd) { + // Restore the QSB search bar, and animate out the drop target bar + prepareStartAnimation(mDropTargetBar); + mDropTargetBarAnim.reverse(); + if (!mIsSearchBarHidden) { + prepareStartAnimation(mQSBSearchBar); + mQSBSearchBarAnim.reverse(); + } + } else { + mDeferOnDragEnd = false; + } + } + + public void onSearchPackagesChanged(boolean searchVisible, boolean voiceVisible) { + if (mQSBSearchBar != null) { + Drawable bg = mQSBSearchBar.getBackground(); + if (bg != null && (!searchVisible && !voiceVisible)) { + // Save the background and disable it + mPreviousBackground = bg; + mQSBSearchBar.setBackgroundResource(0); + } else if (mPreviousBackground != null && (searchVisible || voiceVisible)) { + // Restore the background + mQSBSearchBar.setBackground(mPreviousBackground); + } + } + } + + public Rect getSearchBarBounds() { + if (mQSBSearchBar != null) { + final int[] pos = new int[2]; + mQSBSearchBar.getLocationOnScreen(pos); + + final Rect rect = new Rect(); + rect.left = pos[0]; + rect.top = pos[1]; + rect.right = pos[0] + mQSBSearchBar.getWidth(); + rect.bottom = pos[1] + mQSBSearchBar.getHeight(); + return rect; + } else { + return null; + } + } +} diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java new file mode 100644 index 000000000..18b9399d1 --- /dev/null +++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java @@ -0,0 +1,204 @@ +/* + * 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.app.WallpaperManager; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +public class ShortcutAndWidgetContainer extends ViewGroup { + static final String TAG = "CellLayoutChildren"; + + // These are temporary variables to prevent having to allocate a new object just to + // return an (x, y) value from helper functions. Do NOT use them to maintain other state. + private final int[] mTmpCellXY = new int[2]; + + private final WallpaperManager mWallpaperManager; + + private int mCellWidth; + private int mCellHeight; + + private int mWidthGap; + private int mHeightGap; + + private int mCountX; + + private boolean mInvertIfRtl = false; + + public ShortcutAndWidgetContainer(Context context) { + super(context); + mWallpaperManager = WallpaperManager.getInstance(context); + } + + public void setCellDimensions(int cellWidth, int cellHeight, int widthGap, int heightGap, + int countX) { + mCellWidth = cellWidth; + mCellHeight = cellHeight; + mWidthGap = widthGap; + mHeightGap = heightGap; + mCountX = countX; + } + + public View getChildAt(int x, int y) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + if ((lp.cellX <= x) && (x < lp.cellX + lp.cellHSpan) && + (lp.cellY <= y) && (y < lp.cellY + lp.cellVSpan)) { + return child; + } + } + return null; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + // Debug drawing for hit space + Paint p = new Paint(); + p.setColor(0x6600FF00); + for (int i = getChildCount() - 1; i >= 0; i--) { + final View child = getChildAt(i); + final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + canvas.drawRect(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height, p); + } + } + super.dispatchDraw(canvas); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + measureChild(child); + } + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(widthSpecSize, heightSpecSize); + } + + public void setupLp(CellLayout.LayoutParams lp) { + lp.setup(mCellWidth, mCellHeight, mWidthGap, mHeightGap, invertLayoutHorizontally(), + mCountX); + } + + // Set whether or not to invert the layout horizontally if the layout is in RTL mode. + public void setInvertIfRtl(boolean invert) { + mInvertIfRtl = invert; + } + + public void measureChild(View child) { + final int cellWidth = mCellWidth; + final int cellHeight = mCellHeight; + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + lp.setup(cellWidth, cellHeight, mWidthGap, mHeightGap, invertLayoutHorizontally(), mCountX); + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); + int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, + MeasureSpec.EXACTLY); + child.measure(childWidthMeasureSpec, childheightMeasureSpec); + } + + private boolean invertLayoutHorizontally() { + return mInvertIfRtl && isLayoutRtl(); + } + + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + int childLeft = lp.x; + int childTop = lp.y; + child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height); + + if (lp.dropped) { + lp.dropped = false; + + final int[] cellXY = mTmpCellXY; + getLocationOnScreen(cellXY); + mWallpaperManager.sendWallpaperCommand(getWindowToken(), + WallpaperManager.COMMAND_DROP, + cellXY[0] + childLeft + lp.width / 2, + cellXY[1] + childTop + lp.height / 2, 0, null); + } + } + } + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + if (child != null) { + Rect r = new Rect(); + child.getDrawingRect(r); + requestRectangleOnScreen(r); + } + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + // Cancel long press for all children + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + child.cancelLongPress(); + } + } + + @Override + protected void setChildrenDrawingCacheEnabled(boolean enabled) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View view = getChildAt(i); + view.setDrawingCacheEnabled(enabled); + // Update the drawing caches + if (!view.isHardwareAccelerated() && enabled) { + view.buildDrawingCache(true); + } + } + } + + @Override + protected void setChildrenDrawnWithCacheEnabled(boolean enabled) { + super.setChildrenDrawnWithCacheEnabled(enabled); + } +} diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java new file mode 100644 index 000000000..5249fec02 --- /dev/null +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -0,0 +1,162 @@ +/* + * 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 java.util.ArrayList; + +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Intent; +import android.graphics.Bitmap; +import android.util.Log; + +/** + * Represents a launchable icon on the workspaces and in folders. + */ +class ShortcutInfo extends ItemInfo { + + /** + * The intent used to start the application. + */ + Intent intent; + + /** + * Indicates whether the icon comes from an application's resource (if false) + * or from a custom Bitmap (if true.) + */ + boolean customIcon; + + /** + * Indicates whether we're using the default fallback icon instead of something from the + * app. + */ + boolean usingFallbackIcon; + + /** + * If isShortcut=true and customIcon=false, this contains a reference to the + * shortcut icon as an application's resource. + */ + Intent.ShortcutIconResource iconResource; + + /** + * The application icon. + */ + private Bitmap mIcon; + + ShortcutInfo() { + itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; + } + + public ShortcutInfo(ShortcutInfo info) { + super(info); + title = info.title.toString(); + intent = new Intent(info.intent); + if (info.iconResource != null) { + iconResource = new Intent.ShortcutIconResource(); + iconResource.packageName = info.iconResource.packageName; + iconResource.resourceName = info.iconResource.resourceName; + } + mIcon = info.mIcon; // TODO: should make a copy here. maybe we don't need this ctor at all + customIcon = info.customIcon; + } + + /** TODO: Remove this. It's only called by ApplicationInfo.makeShortcut. */ + public ShortcutInfo(ApplicationInfo info) { + super(info); + title = info.title.toString(); + intent = new Intent(info.intent); + customIcon = false; + } + + public void setIcon(Bitmap b) { + mIcon = b; + } + + public Bitmap getIcon(IconCache iconCache) { + if (mIcon == null) { + updateIcon(iconCache); + } + return mIcon; + } + + public void updateIcon(IconCache iconCache) { + mIcon = iconCache.getIcon(intent); + usingFallbackIcon = iconCache.isDefaultIcon(mIcon); + } + + /** + * Creates the application intent based on a component name and various launch flags. + * Sets {@link #itemType} to {@link LauncherSettings.BaseLauncherColumns#ITEM_TYPE_APPLICATION}. + * + * @param className the class name of the component representing the intent + * @param launchFlags the launch flags + */ + final void setActivity(ComponentName className, int launchFlags) { + intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(className); + intent.setFlags(launchFlags); + itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION; + } + + @Override + void onAddToDatabase(ContentValues values) { + super.onAddToDatabase(values); + + String titleStr = title != null ? title.toString() : null; + values.put(LauncherSettings.BaseLauncherColumns.TITLE, titleStr); + + String uri = intent != null ? intent.toUri(0) : null; + values.put(LauncherSettings.BaseLauncherColumns.INTENT, uri); + + if (customIcon) { + values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, + LauncherSettings.BaseLauncherColumns.ICON_TYPE_BITMAP); + writeBitmap(values, mIcon); + } else { + if (!usingFallbackIcon) { + writeBitmap(values, mIcon); + } + values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, + LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE); + if (iconResource != null) { + values.put(LauncherSettings.BaseLauncherColumns.ICON_PACKAGE, + iconResource.packageName); + values.put(LauncherSettings.BaseLauncherColumns.ICON_RESOURCE, + iconResource.resourceName); + } + } + } + + @Override + public String toString() { + return "ShortcutInfo(title=" + title.toString() + "intent=" + intent + "id=" + this.id + + " type=" + this.itemType + " container=" + this.container + " screen=" + screen + + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY + + " dropPos=" + dropPos + ")"; + } + + public static void dumpShortcutInfoList(String tag, String label, + ArrayList<ShortcutInfo> list) { + Log.d(tag, label + " size=" + list.size()); + for (ShortcutInfo info: list) { + Log.d(tag, " title=\"" + info.title + " icon=" + info.mIcon + + " customIcon=" + info.customIcon); + } + } +} + diff --git a/src/com/android/launcher3/SmoothPagedView.java b/src/com/android/launcher3/SmoothPagedView.java new file mode 100644 index 000000000..1beffacb5 --- /dev/null +++ b/src/com/android/launcher3/SmoothPagedView.java @@ -0,0 +1,188 @@ +/* + * 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.content.Context; +import android.util.AttributeSet; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +public abstract class SmoothPagedView extends PagedView { + private static final float SMOOTHING_SPEED = 0.75f; + private static final float SMOOTHING_CONSTANT = (float) (0.016 / Math.log(SMOOTHING_SPEED)); + + private float mBaseLineFlingVelocity; + private float mFlingVelocityInfluence; + + static final int DEFAULT_MODE = 0; + static final int X_LARGE_MODE = 1; + + int mScrollMode; + + private Interpolator mScrollInterpolator; + + public static class OvershootInterpolator implements Interpolator { + private static final float DEFAULT_TENSION = 1.3f; + private float mTension; + + public OvershootInterpolator() { + mTension = DEFAULT_TENSION; + } + + public void setDistance(int distance) { + mTension = distance > 0 ? DEFAULT_TENSION / distance : DEFAULT_TENSION; + } + + public void disableSettle() { + mTension = 0.f; + } + + public float getInterpolation(float t) { + // _o(t) = t * t * ((tension + 1) * t + tension) + // o(t) = _o(t - 1) + 1 + t -= 1.0f; + return t * t * ((mTension + 1) * t + mTension) + 1.0f; + } + } + + /** + * 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 SmoothPagedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + /** + * Used to inflate the Workspace from XML. + * + * @param context The application's context. + * @param attrs The attributes set containing the Workspace's customization values. + * @param defStyle Unused. + */ + public SmoothPagedView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mUsePagingTouchSlop = false; + + // This means that we'll take care of updating the scroll parameter ourselves (we do it + // in computeScroll), we only do this in the OVERSHOOT_MODE, ie. on phones + mDeferScrollUpdate = mScrollMode != X_LARGE_MODE; + } + + protected int getScrollMode() { + return X_LARGE_MODE; + } + + /** + * Initializes various states for this workspace. + */ + @Override + protected void init() { + super.init(); + + mScrollMode = getScrollMode(); + if (mScrollMode == DEFAULT_MODE) { + mBaseLineFlingVelocity = 2500.0f; + mFlingVelocityInfluence = 0.4f; + mScrollInterpolator = new OvershootInterpolator(); + mScroller = new Scroller(getContext(), mScrollInterpolator); + } + } + + @Override + protected void snapToDestination() { + if (mScrollMode == X_LARGE_MODE) { + super.snapToDestination(); + } else { + snapToPageWithVelocity(getPageNearestToCenterOfScreen(), 0); + } + } + + @Override + protected void snapToPageWithVelocity(int whichPage, int velocity) { + if (mScrollMode == X_LARGE_MODE) { + super.snapToPageWithVelocity(whichPage, velocity); + } else { + snapToPageWithVelocity(whichPage, 0, true); + } + } + + private void snapToPageWithVelocity(int whichPage, int velocity, boolean settle) { + // if (!mScroller.isFinished()) return; + + whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); + + final int screenDelta = Math.max(1, Math.abs(whichPage - mCurrentPage)); + final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + final int delta = newX - mUnboundedScrollX; + int duration = (screenDelta + 1) * 100; + + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + if (settle) { + ((OvershootInterpolator) mScrollInterpolator).setDistance(screenDelta); + } else { + ((OvershootInterpolator) mScrollInterpolator).disableSettle(); + } + + velocity = Math.abs(velocity); + if (velocity > 0) { + duration += (duration / (velocity / mBaseLineFlingVelocity)) * mFlingVelocityInfluence; + } else { + duration += 100; + } + + snapToPage(whichPage, delta, duration); + } + + @Override + protected void snapToPage(int whichPage) { + if (mScrollMode == X_LARGE_MODE) { + super.snapToPage(whichPage); + } else { + snapToPageWithVelocity(whichPage, 0, false); + } + } + + @Override + public void computeScroll() { + if (mScrollMode == X_LARGE_MODE) { + super.computeScroll(); + } else { + boolean scrollComputed = computeScrollHelper(); + + if (!scrollComputed && mTouchState == TOUCH_STATE_SCROLLING) { + final float now = System.nanoTime() / NANOTIME_DIV; + final float e = (float) Math.exp((now - mSmoothingTime) / SMOOTHING_CONSTANT); + + final float dx = mTouchX - mUnboundedScrollX; + scrollTo(Math.round(mUnboundedScrollX + dx * e), getScrollY()); + mSmoothingTime = now; + + // Keep generating points as long as we're more than 1px away from the target + if (dx > 1.f || dx < -1.f) { + invalidate(); + } + } + } + } +} diff --git a/src/com/android/launcher3/SpringLoadedDragController.java b/src/com/android/launcher3/SpringLoadedDragController.java new file mode 100644 index 000000000..45edaef86 --- /dev/null +++ b/src/com/android/launcher3/SpringLoadedDragController.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 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; + +public class SpringLoadedDragController implements OnAlarmListener { + // how long the user must hover over a mini-screen before it unshrinks + final long ENTER_SPRING_LOAD_HOVER_TIME = 500; + final long ENTER_SPRING_LOAD_CANCEL_HOVER_TIME = 950; + final long EXIT_SPRING_LOAD_HOVER_TIME = 200; + + Alarm mAlarm; + + // the screen the user is currently hovering over, if any + private CellLayout mScreen; + private Launcher mLauncher; + + public SpringLoadedDragController(Launcher launcher) { + mLauncher = launcher; + mAlarm = new Alarm(); + mAlarm.setOnAlarmListener(this); + } + + public void cancel() { + mAlarm.cancelAlarm(); + } + + // Set a new alarm to expire for the screen that we are hovering over now + public void setAlarm(CellLayout cl) { + mAlarm.cancelAlarm(); + mAlarm.setAlarm((cl == null) ? ENTER_SPRING_LOAD_CANCEL_HOVER_TIME : + ENTER_SPRING_LOAD_HOVER_TIME); + mScreen = cl; + } + + // this is called when our timer runs out + public void onAlarm(Alarm alarm) { + if (mScreen != null) { + // Snap to the screen that we are hovering over now + Workspace w = mLauncher.getWorkspace(); + int page = w.indexOfChild(mScreen); + if (page != w.getCurrentPage()) { + w.snapToPage(page); + } + } else { + mLauncher.getDragController().cancelDrag(); + } + } +} diff --git a/src/com/android/launcher3/UninstallShortcutReceiver.java b/src/com/android/launcher3/UninstallShortcutReceiver.java new file mode 100644 index 000000000..6bc289a5e --- /dev/null +++ b/src/com/android/launcher3/UninstallShortcutReceiver.java @@ -0,0 +1,166 @@ +/* + * 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.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.widget.Toast; + +import com.android.launcher3.R; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +public class UninstallShortcutReceiver extends BroadcastReceiver { + private static final String ACTION_UNINSTALL_SHORTCUT = + "com.android.launcher3.action.UNINSTALL_SHORTCUT"; + + // The set of shortcuts that are pending uninstall + private static ArrayList<PendingUninstallShortcutInfo> mUninstallQueue = + new ArrayList<PendingUninstallShortcutInfo>(); + + // Determines whether to defer uninstalling shortcuts immediately until + // disableAndFlushUninstallQueue() is called. + private static boolean mUseUninstallQueue = false; + + private static class PendingUninstallShortcutInfo { + Intent data; + + public PendingUninstallShortcutInfo(Intent rawData) { + data = rawData; + } + } + + public void onReceive(Context context, Intent data) { + if (!ACTION_UNINSTALL_SHORTCUT.equals(data.getAction())) { + return; + } + + PendingUninstallShortcutInfo info = new PendingUninstallShortcutInfo(data); + if (mUseUninstallQueue) { + mUninstallQueue.add(info); + } else { + processUninstallShortcut(context, info); + } + } + + static void enableUninstallQueue() { + mUseUninstallQueue = true; + } + + static void disableAndFlushUninstallQueue(Context context) { + mUseUninstallQueue = false; + Iterator<PendingUninstallShortcutInfo> iter = mUninstallQueue.iterator(); + while (iter.hasNext()) { + processUninstallShortcut(context, iter.next()); + iter.remove(); + } + } + + private static void processUninstallShortcut(Context context, + PendingUninstallShortcutInfo pendingInfo) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sharedPrefs = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + + final Intent data = pendingInfo.data; + + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + synchronized (app) { + removeShortcut(context, data, sharedPrefs); + } + } + + private static void removeShortcut(Context context, Intent data, + final SharedPreferences sharedPrefs) { + Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); + String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); + boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); + + if (intent != null && name != null) { + final ContentResolver cr = context.getContentResolver(); + Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, + new String[] { LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT }, + LauncherSettings.Favorites.TITLE + "=?", new String[] { name }, null); + + final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); + final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); + + boolean changed = false; + + try { + while (c.moveToNext()) { + try { + if (intent.filterEquals(Intent.parseUri(c.getString(intentIndex), 0))) { + final long id = c.getLong(idIndex); + final Uri uri = LauncherSettings.Favorites.getContentUri(id, false); + cr.delete(uri, null, null); + changed = true; + if (!duplicate) { + break; + } + } + } catch (URISyntaxException e) { + // Ignore + } + } + } finally { + c.close(); + } + + if (changed) { + cr.notifyChange(LauncherSettings.Favorites.CONTENT_URI, null); + Toast.makeText(context, context.getString(R.string.shortcut_uninstalled, name), + Toast.LENGTH_SHORT).show(); + } + + // Remove any items due to be animated + boolean appRemoved; + Set<String> newApps = new HashSet<String>(); + newApps = sharedPrefs.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, newApps); + synchronized (newApps) { + do { + appRemoved = newApps.remove(intent.toUri(0).toString()); + } while (appRemoved); + } + if (appRemoved) { + final Set<String> savedNewApps = newApps; + new Thread("setNewAppsThread-remove") { + public void run() { + synchronized (savedNewApps) { + SharedPreferences.Editor editor = sharedPrefs.edit(); + editor.putStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, + savedNewApps); + if (savedNewApps.isEmpty()) { + // Reset the page index if there are no more items + editor.putInt(InstallShortcutReceiver.NEW_APPS_PAGE_KEY, -1); + } + editor.commit(); + } + } + }.start(); + } + } + } +} diff --git a/src/com/android/launcher3/UserInitializeReceiver.java b/src/com/android/launcher3/UserInitializeReceiver.java new file mode 100644 index 000000000..5cd518190 --- /dev/null +++ b/src/com/android/launcher3/UserInitializeReceiver.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012 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 java.io.IOException; +import java.util.ArrayList; + +import com.android.launcher3.R; + +import android.app.WallpaperManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; + +/** + * Takes care of setting initial wallpaper for a user, by selecting the + * first wallpaper that is not in use by another user. + */ +public class UserInitializeReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final Resources resources = context.getResources(); + // Context.getPackageName() may return the "original" package name, + // com.android.launcher3; Resources needs the real package name, + // com.android.launcher3. So we ask Resources for what it thinks the + // package name should be. + final String packageName = resources.getResourcePackageName(R.array.wallpapers); + ArrayList<Integer> list = new ArrayList<Integer>(); + addWallpapers(resources, packageName, R.array.wallpapers, list); + addWallpapers(resources, packageName, R.array.extra_wallpapers, list); + WallpaperManager wpm = (WallpaperManager) context.getSystemService( + Context.WALLPAPER_SERVICE); + for (int i=1; i<list.size(); i++) { + int resid = list.get(i); + if (!wpm.hasResourceWallpaper(resid)) { + try { + wpm.setResource(resid); + } catch (IOException e) { + } + return; + } + } + } + + private void addWallpapers(Resources resources, String packageName, int resid, + ArrayList<Integer> outList) { + final String[] extras = resources.getStringArray(resid); + for (String extra : extras) { + int res = resources.getIdentifier(extra, "drawable", packageName); + if (res != 0) { + outList.add(res); + } + } + } +} diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java new file mode 100644 index 000000000..0cc29faa3 --- /dev/null +++ b/src/com/android/launcher3/Utilities.java @@ -0,0 +1,275 @@ +/* + * 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 java.util.Random; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.PaintDrawable; +import android.util.DisplayMetrics; + +import com.android.launcher3.R; + +/** + * Various utilities shared amongst the Launcher's classes. + */ +final class Utilities { + @SuppressWarnings("unused") + private static final String TAG = "Launcher.Utilities"; + + private static int sIconWidth = -1; + private static int sIconHeight = -1; + private static int sIconTextureWidth = -1; + private static int sIconTextureHeight = -1; + + private static final Paint sBlurPaint = new Paint(); + private static final Paint sGlowColorPressedPaint = new Paint(); + private static final Paint sGlowColorFocusedPaint = new Paint(); + private static final Paint sDisabledPaint = new Paint(); + private static final Rect sOldBounds = new Rect(); + private static final Canvas sCanvas = new Canvas(); + + static { + sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, + Paint.FILTER_BITMAP_FLAG)); + } + static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff }; + static int sColorIndex = 0; + + /** + * Returns a bitmap suitable for the all apps view. Used to convert pre-ICS + * icon bitmaps that are stored in the database (which were 74x74 pixels at hdpi size) + * to the proper size (48dp) + */ + static Bitmap createIconBitmap(Bitmap icon, Context context) { + int textureWidth = sIconTextureWidth; + int textureHeight = sIconTextureHeight; + int sourceWidth = icon.getWidth(); + int sourceHeight = icon.getHeight(); + if (sourceWidth > textureWidth && sourceHeight > textureHeight) { + // Icon is bigger than it should be; clip it (solves the GB->ICS migration case) + return Bitmap.createBitmap(icon, + (sourceWidth - textureWidth) / 2, + (sourceHeight - textureHeight) / 2, + textureWidth, textureHeight); + } else if (sourceWidth == textureWidth && sourceHeight == textureHeight) { + // Icon is the right size, no need to change it + return icon; + } else { + // Icon is too small, render to a larger bitmap + final Resources resources = context.getResources(); + return createIconBitmap(new BitmapDrawable(resources, icon), context); + } + } + + /** + * Returns a bitmap suitable for the all apps view. + */ + static Bitmap createIconBitmap(Drawable icon, Context context) { + synchronized (sCanvas) { // we share the statics :-( + if (sIconWidth == -1) { + initStatics(context); + } + + int width = sIconWidth; + int height = sIconHeight; + + if (icon instanceof PaintDrawable) { + PaintDrawable painter = (PaintDrawable) icon; + painter.setIntrinsicWidth(width); + painter.setIntrinsicHeight(height); + } else if (icon instanceof BitmapDrawable) { + // Ensure the bitmap has a density. + BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; + Bitmap bitmap = bitmapDrawable.getBitmap(); + if (bitmap.getDensity() == Bitmap.DENSITY_NONE) { + bitmapDrawable.setTargetDensity(context.getResources().getDisplayMetrics()); + } + } + int sourceWidth = icon.getIntrinsicWidth(); + int sourceHeight = icon.getIntrinsicHeight(); + if (sourceWidth > 0 && sourceHeight > 0) { + // There are intrinsic sizes. + if (width < sourceWidth || height < sourceHeight) { + // It's too big, scale it down. + final float ratio = (float) sourceWidth / sourceHeight; + if (sourceWidth > sourceHeight) { + height = (int) (width / ratio); + } else if (sourceHeight > sourceWidth) { + width = (int) (height * ratio); + } + } else if (sourceWidth < width && sourceHeight < height) { + // Don't scale up the icon + width = sourceWidth; + height = sourceHeight; + } + } + + // no intrinsic size --> use default size + int textureWidth = sIconTextureWidth; + int textureHeight = sIconTextureHeight; + + final Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, + Bitmap.Config.ARGB_8888); + final Canvas canvas = sCanvas; + canvas.setBitmap(bitmap); + + final int left = (textureWidth-width) / 2; + final int top = (textureHeight-height) / 2; + + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + // draw a big box for the icon for debugging + canvas.drawColor(sColors[sColorIndex]); + if (++sColorIndex >= sColors.length) sColorIndex = 0; + Paint debugPaint = new Paint(); + debugPaint.setColor(0xffcccc00); + canvas.drawRect(left, top, left+width, top+height, debugPaint); + } + + sOldBounds.set(icon.getBounds()); + icon.setBounds(left, top, left+width, top+height); + icon.draw(canvas); + icon.setBounds(sOldBounds); + canvas.setBitmap(null); + + return bitmap; + } + } + + static void drawSelectedAllAppsBitmap(Canvas dest, int destWidth, int destHeight, + boolean pressed, Bitmap src) { + synchronized (sCanvas) { // we share the statics :-( + if (sIconWidth == -1) { + // We can't have gotten to here without src being initialized, which + // comes from this file already. So just assert. + //initStatics(context); + throw new RuntimeException("Assertion failed: Utilities not initialized"); + } + + dest.drawColor(0, PorterDuff.Mode.CLEAR); + + int[] xy = new int[2]; + Bitmap mask = src.extractAlpha(sBlurPaint, xy); + + float px = (destWidth - src.getWidth()) / 2; + float py = (destHeight - src.getHeight()) / 2; + dest.drawBitmap(mask, px + xy[0], py + xy[1], + pressed ? sGlowColorPressedPaint : sGlowColorFocusedPaint); + + mask.recycle(); + } + } + + /** + * Returns a Bitmap representing the thumbnail of the specified Bitmap. + * The size of the thumbnail is defined by the dimension + * android.R.dimen.launcher_application_icon_size. + * + * @param bitmap The bitmap to get a thumbnail of. + * @param context The application's context. + * + * @return A thumbnail for the specified bitmap or the bitmap itself if the + * thumbnail could not be created. + */ + static Bitmap resampleIconBitmap(Bitmap bitmap, Context context) { + synchronized (sCanvas) { // we share the statics :-( + if (sIconWidth == -1) { + initStatics(context); + } + + if (bitmap.getWidth() == sIconWidth && bitmap.getHeight() == sIconHeight) { + return bitmap; + } else { + final Resources resources = context.getResources(); + return createIconBitmap(new BitmapDrawable(resources, bitmap), context); + } + } + } + + static Bitmap drawDisabledBitmap(Bitmap bitmap, Context context) { + synchronized (sCanvas) { // we share the statics :-( + if (sIconWidth == -1) { + initStatics(context); + } + final Bitmap disabled = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), + Bitmap.Config.ARGB_8888); + final Canvas canvas = sCanvas; + canvas.setBitmap(disabled); + + canvas.drawBitmap(bitmap, 0.0f, 0.0f, sDisabledPaint); + + canvas.setBitmap(null); + + return disabled; + } + } + + private static void initStatics(Context context) { + final Resources resources = context.getResources(); + final DisplayMetrics metrics = resources.getDisplayMetrics(); + final float density = metrics.density; + + sIconWidth = sIconHeight = (int) resources.getDimension(R.dimen.app_icon_size); + sIconTextureWidth = sIconTextureHeight = sIconWidth; + + sBlurPaint.setMaskFilter(new BlurMaskFilter(5 * density, BlurMaskFilter.Blur.NORMAL)); + sGlowColorPressedPaint.setColor(0xffffc300); + sGlowColorFocusedPaint.setColor(0xffff8e00); + + ColorMatrix cm = new ColorMatrix(); + cm.setSaturation(0.2f); + sDisabledPaint.setColorFilter(new ColorMatrixColorFilter(cm)); + sDisabledPaint.setAlpha(0x88); + } + + /** Only works for positive numbers. */ + static int roundToPow2(int n) { + int orig = n; + n >>= 1; + int mask = 0x8000000; + while (mask != 0 && (n & mask) == 0) { + mask >>= 1; + } + while (mask != 0) { + n |= mask; + mask >>= 1; + } + n += 1; + if (n != orig) { + n <<= 1; + } + return n; + } + + static int generateRandomId() { + return new Random(System.currentTimeMillis()).nextInt(1 << 24); + } +} diff --git a/src/com/android/launcher3/WallpaperChooser.java b/src/com/android/launcher3/WallpaperChooser.java new file mode 100644 index 000000000..fe81ccbb5 --- /dev/null +++ b/src/com/android/launcher3/WallpaperChooser.java @@ -0,0 +1,47 @@ +/* + * 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 com.android.launcher3.R; + +import android.app.Activity; +import android.app.DialogFragment; +import android.app.Fragment; +import android.os.Bundle; + +public class WallpaperChooser extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "Launcher.WallpaperChooser"; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.wallpaper_chooser_base); + + Fragment fragmentView = + getFragmentManager().findFragmentById(R.id.wallpaper_chooser_fragment); + // TODO: The following code is currently not exercised. Leaving it here in case it + // needs to be revived again. + if (fragmentView == null) { + /* When the screen is XLarge, the fragment is not included in the layout, so show it + * as a dialog + */ + DialogFragment fragment = WallpaperChooserDialogFragment.newInstance(); + fragment.show(getFragmentManager(), "dialog"); + } + } +} diff --git a/src/com/android/launcher3/WallpaperChooserDialogFragment.java b/src/com/android/launcher3/WallpaperChooserDialogFragment.java new file mode 100644 index 000000000..99de815f9 --- /dev/null +++ b/src/com/android/launcher3/WallpaperChooserDialogFragment.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2010 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.app.Activity; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.WallpaperManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.Gallery; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; + +import com.android.launcher3.R; + +import java.io.IOException; +import java.util.ArrayList; + +public class WallpaperChooserDialogFragment extends DialogFragment implements + AdapterView.OnItemSelectedListener, AdapterView.OnItemClickListener { + + private static final String TAG = "Launcher.WallpaperChooserDialogFragment"; + private static final String EMBEDDED_KEY = "com.android.launcher3." + + "WallpaperChooserDialogFragment.EMBEDDED_KEY"; + + private boolean mEmbedded; + private Bitmap mBitmap = null; + + private ArrayList<Integer> mThumbs; + private ArrayList<Integer> mImages; + private WallpaperLoader mLoader; + private WallpaperDrawable mWallpaperDrawable = new WallpaperDrawable(); + + public static WallpaperChooserDialogFragment newInstance() { + WallpaperChooserDialogFragment fragment = new WallpaperChooserDialogFragment(); + fragment.setCancelable(true); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null && savedInstanceState.containsKey(EMBEDDED_KEY)) { + mEmbedded = savedInstanceState.getBoolean(EMBEDDED_KEY); + } else { + mEmbedded = isInLayout(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putBoolean(EMBEDDED_KEY, mEmbedded); + } + + private void cancelLoader() { + if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) { + mLoader.cancel(true); + mLoader = null; + } + } + + @Override + public void onDetach() { + super.onDetach(); + + cancelLoader(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + cancelLoader(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + /* On orientation changes, the dialog is effectively "dismissed" so this is called + * when the activity is no longer associated with this dying dialog fragment. We + * should just safely ignore this case by checking if getActivity() returns null + */ + Activity activity = getActivity(); + if (activity != null) { + activity.finish(); + } + } + + /* This will only be called when in XLarge mode, since this Fragment is invoked like + * a dialog in that mode + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + findWallpapers(); + + return null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + findWallpapers(); + + /* If this fragment is embedded in the layout of this activity, then we should + * generate a view to display. Otherwise, a dialog will be created in + * onCreateDialog() + */ + if (mEmbedded) { + View view = inflater.inflate(R.layout.wallpaper_chooser, container, false); + view.setBackground(mWallpaperDrawable); + + final Gallery gallery = (Gallery) view.findViewById(R.id.gallery); + gallery.setCallbackDuringFling(false); + gallery.setOnItemSelectedListener(this); + gallery.setAdapter(new ImageAdapter(getActivity())); + + View setButton = view.findViewById(R.id.set); + setButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + selectWallpaper(gallery.getSelectedItemPosition()); + } + }); + return view; + } + return null; + } + + private void selectWallpaper(int position) { + try { + WallpaperManager wpm = (WallpaperManager) getActivity().getSystemService( + Context.WALLPAPER_SERVICE); + wpm.setResource(mImages.get(position)); + Activity activity = getActivity(); + activity.setResult(Activity.RESULT_OK); + activity.finish(); + } catch (IOException e) { + Log.e(TAG, "Failed to set wallpaper: " + e); + } + } + + // Click handler for the Dialog's GridView + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + selectWallpaper(position); + } + + // Selection handler for the embedded Gallery view + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) { + mLoader.cancel(); + } + mLoader = (WallpaperLoader) new WallpaperLoader().execute(position); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + + private void findWallpapers() { + mThumbs = new ArrayList<Integer>(24); + mImages = new ArrayList<Integer>(24); + + final Resources resources = getResources(); + // Context.getPackageName() may return the "original" package name, + // com.android.launcher3; Resources needs the real package name, + // com.android.launcher3. So we ask Resources for what it thinks the + // package name should be. + final String packageName = resources.getResourcePackageName(R.array.wallpapers); + + addWallpapers(resources, packageName, R.array.wallpapers); + addWallpapers(resources, packageName, R.array.extra_wallpapers); + } + + private void addWallpapers(Resources resources, String packageName, int list) { + final String[] extras = resources.getStringArray(list); + for (String extra : extras) { + int res = resources.getIdentifier(extra, "drawable", packageName); + if (res != 0) { + final int thumbRes = resources.getIdentifier(extra + "_small", + "drawable", packageName); + + if (thumbRes != 0) { + mThumbs.add(thumbRes); + mImages.add(res); + // Log.d(TAG, "add: [" + packageName + "]: " + extra + " (" + res + ")"); + } + } + } + } + + private class ImageAdapter extends BaseAdapter implements ListAdapter, SpinnerAdapter { + private LayoutInflater mLayoutInflater; + + ImageAdapter(Activity activity) { + mLayoutInflater = activity.getLayoutInflater(); + } + + public int getCount() { + return mThumbs.size(); + } + + public Object getItem(int position) { + return position; + } + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + View view; + + if (convertView == null) { + view = mLayoutInflater.inflate(R.layout.wallpaper_item, parent, false); + } else { + view = convertView; + } + + ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); + + int thumbRes = mThumbs.get(position); + image.setImageResource(thumbRes); + Drawable thumbDrawable = image.getDrawable(); + if (thumbDrawable != null) { + thumbDrawable.setDither(true); + } else { + Log.e(TAG, "Error decoding thumbnail resId=" + thumbRes + " for wallpaper #" + + position); + } + + return view; + } + } + + class WallpaperLoader extends AsyncTask<Integer, Void, Bitmap> { + BitmapFactory.Options mOptions; + + WallpaperLoader() { + mOptions = new BitmapFactory.Options(); + mOptions.inDither = false; + mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; + } + + @Override + protected Bitmap doInBackground(Integer... params) { + if (isCancelled()) return null; + try { + return BitmapFactory.decodeResource(getResources(), + mImages.get(params[0]), mOptions); + } catch (OutOfMemoryError e) { + return null; + } + } + + @Override + protected void onPostExecute(Bitmap b) { + if (b == null) return; + + if (!isCancelled() && !mOptions.mCancel) { + // Help the GC + if (mBitmap != null) { + mBitmap.recycle(); + } + + View v = getView(); + if (v != null) { + mBitmap = b; + mWallpaperDrawable.setBitmap(b); + v.postInvalidate(); + } else { + mBitmap = null; + mWallpaperDrawable.setBitmap(null); + } + mLoader = null; + } else { + b.recycle(); + } + } + + void cancel() { + mOptions.requestCancelDecode(); + super.cancel(true); + } + } + + /** + * Custom drawable that centers the bitmap fed to it. + */ + static class WallpaperDrawable extends Drawable { + + Bitmap mBitmap; + int mIntrinsicWidth; + int mIntrinsicHeight; + + /* package */void setBitmap(Bitmap bitmap) { + mBitmap = bitmap; + if (mBitmap == null) + return; + mIntrinsicWidth = mBitmap.getWidth(); + mIntrinsicHeight = mBitmap.getHeight(); + } + + @Override + public void draw(Canvas canvas) { + if (mBitmap == null) return; + int width = canvas.getWidth(); + int height = canvas.getHeight(); + int x = (width - mIntrinsicWidth) / 2; + int y = (height - mIntrinsicHeight) / 2; + canvas.drawBitmap(mBitmap, x, y, null); + } + + @Override + public int getOpacity() { + return android.graphics.PixelFormat.OPAQUE; + } + + @Override + public void setAlpha(int alpha) { + // Ignore + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Ignore + } + } +} diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java new file mode 100644 index 000000000..ddc478a20 --- /dev/null +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -0,0 +1,610 @@ +package com.android.launcher3; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.util.Log; + +import com.android.launcher3.R; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; + +abstract class SoftReferenceThreadLocal<T> { + private ThreadLocal<SoftReference<T>> mThreadLocal; + public SoftReferenceThreadLocal() { + mThreadLocal = new ThreadLocal<SoftReference<T>>(); + } + + abstract T initialValue(); + + public void set(T t) { + mThreadLocal.set(new SoftReference<T>(t)); + } + + public T get() { + SoftReference<T> reference = mThreadLocal.get(); + T obj; + if (reference == null) { + obj = initialValue(); + mThreadLocal.set(new SoftReference<T>(obj)); + return obj; + } else { + obj = reference.get(); + if (obj == null) { + obj = initialValue(); + mThreadLocal.set(new SoftReference<T>(obj)); + } + return obj; + } + } +} + +class CanvasCache extends SoftReferenceThreadLocal<Canvas> { + @Override + protected Canvas initialValue() { + return new Canvas(); + } +} + +class PaintCache extends SoftReferenceThreadLocal<Paint> { + @Override + protected Paint initialValue() { + return null; + } +} + +class BitmapCache extends SoftReferenceThreadLocal<Bitmap> { + @Override + protected Bitmap initialValue() { + return null; + } +} + +class RectCache extends SoftReferenceThreadLocal<Rect> { + @Override + protected Rect initialValue() { + return new Rect(); + } +} + +class BitmapFactoryOptionsCache extends SoftReferenceThreadLocal<BitmapFactory.Options> { + @Override + protected BitmapFactory.Options initialValue() { + return new BitmapFactory.Options(); + } +} + +public class WidgetPreviewLoader { + static final String TAG = "WidgetPreviewLoader"; + + private int mPreviewBitmapWidth; + private int mPreviewBitmapHeight; + private String mSize; + private Context mContext; + private Launcher mLauncher; + private PackageManager mPackageManager; + private PagedViewCellLayout mWidgetSpacingLayout; + + // Used for drawing shortcut previews + private BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); + private PaintCache mCachedShortcutPreviewPaint = new PaintCache(); + private CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); + + // Used for drawing widget previews + private CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); + private RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); + private RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); + private PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); + private String mCachedSelectQuery; + private BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache(); + + private int mAppIconSize; + private IconCache mIconCache; + + private final float sWidgetPreviewIconPaddingPercentage = 0.25f; + + private CacheDb mDb; + + private HashMap<String, WeakReference<Bitmap>> mLoadedPreviews; + private ArrayList<SoftReference<Bitmap>> mUnusedBitmaps; + private static HashSet<String> sInvalidPackages; + + static { + sInvalidPackages = new HashSet<String>(); + } + + public WidgetPreviewLoader(Launcher launcher) { + mContext = mLauncher = launcher; + mPackageManager = mContext.getPackageManager(); + mAppIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.app_icon_size); + LauncherApplication app = (LauncherApplication) launcher.getApplicationContext(); + mIconCache = app.getIconCache(); + mDb = app.getWidgetPreviewCacheDb(); + mLoadedPreviews = new HashMap<String, WeakReference<Bitmap>>(); + mUnusedBitmaps = new ArrayList<SoftReference<Bitmap>>(); + } + + public void setPreviewSize(int previewWidth, int previewHeight, + PagedViewCellLayout widgetSpacingLayout) { + mPreviewBitmapWidth = previewWidth; + mPreviewBitmapHeight = previewHeight; + mSize = previewWidth + "x" + previewHeight; + mWidgetSpacingLayout = widgetSpacingLayout; + } + + public Bitmap getPreview(final Object o) { + String name = getObjectName(o); + // check if the package is valid + boolean packageValid = true; + synchronized(sInvalidPackages) { + packageValid = !sInvalidPackages.contains(getObjectPackage(o)); + } + if (!packageValid) { + return null; + } + if (packageValid) { + synchronized(mLoadedPreviews) { + // check if it exists in our existing cache + if (mLoadedPreviews.containsKey(name) && mLoadedPreviews.get(name).get() != null) { + return mLoadedPreviews.get(name).get(); + } + } + } + + Bitmap unusedBitmap = null; + synchronized(mUnusedBitmaps) { + // not in cache; we need to load it from the db + while ((unusedBitmap == null || !unusedBitmap.isMutable() || + unusedBitmap.getWidth() != mPreviewBitmapWidth || + unusedBitmap.getHeight() != mPreviewBitmapHeight) + && mUnusedBitmaps.size() > 0) { + unusedBitmap = mUnusedBitmaps.remove(0).get(); + } + if (unusedBitmap != null) { + final Canvas c = mCachedAppWidgetPreviewCanvas.get(); + c.setBitmap(unusedBitmap); + c.drawColor(0, PorterDuff.Mode.CLEAR); + c.setBitmap(null); + } + } + + if (unusedBitmap == null) { + unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight, + Bitmap.Config.ARGB_8888); + } + + Bitmap preview = null; + + if (packageValid) { + preview = readFromDb(name, unusedBitmap); + } + + if (preview != null) { + synchronized(mLoadedPreviews) { + mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); + } + return preview; + } else { + // it's not in the db... we need to generate it + final Bitmap generatedPreview = generatePreview(o, unusedBitmap); + preview = generatedPreview; + if (preview != unusedBitmap) { + throw new RuntimeException("generatePreview is not recycling the bitmap " + o); + } + + synchronized(mLoadedPreviews) { + mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); + } + + // write to db on a thread pool... this can be done lazily and improves the performance + // of the first time widget previews are loaded + new AsyncTask<Void, Void, Void>() { + public Void doInBackground(Void ... args) { + writeToDb(o, generatedPreview); + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + + return preview; + } + } + + public void recycleBitmap(Object o, Bitmap bitmapToRecycle) { + String name = getObjectName(o); + synchronized (mLoadedPreviews) { + if (mLoadedPreviews.containsKey(name)) { + Bitmap b = mLoadedPreviews.get(name).get(); + if (b == bitmapToRecycle) { + mLoadedPreviews.remove(name); + if (bitmapToRecycle.isMutable()) { + synchronized (mUnusedBitmaps) { + mUnusedBitmaps.add(new SoftReference<Bitmap>(b)); + } + } + } else { + throw new RuntimeException("Bitmap passed in doesn't match up"); + } + } + } + } + + static class CacheDb extends SQLiteOpenHelper { + final static int DB_VERSION = 2; + final static String DB_NAME = "widgetpreviews.db"; + final static String TABLE_NAME = "shortcut_and_widget_previews"; + final static String COLUMN_NAME = "name"; + final static String COLUMN_SIZE = "size"; + final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; + Context mContext; + + public CacheDb(Context context) { + super(context, new File(context.getCacheDir(), DB_NAME).getPath(), null, DB_VERSION); + // Store the context for later use + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + + COLUMN_NAME + " TEXT NOT NULL, " + + COLUMN_SIZE + " TEXT NOT NULL, " + + COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " + + "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + // Delete all the records; they'll be repopulated as this is a cache + db.execSQL("DELETE FROM " + TABLE_NAME); + } + } + } + + private static final String WIDGET_PREFIX = "Widget:"; + private static final String SHORTCUT_PREFIX = "Shortcut:"; + + private static String getObjectName(Object o) { + // should cache the string builder + StringBuilder sb = new StringBuilder(); + String output; + if (o instanceof AppWidgetProviderInfo) { + sb.append(WIDGET_PREFIX); + sb.append(((AppWidgetProviderInfo) o).provider.flattenToString()); + output = sb.toString(); + sb.setLength(0); + } else { + sb.append(SHORTCUT_PREFIX); + + ResolveInfo info = (ResolveInfo) o; + sb.append(new ComponentName(info.activityInfo.packageName, + info.activityInfo.name).flattenToString()); + output = sb.toString(); + sb.setLength(0); + } + return output; + } + + private String getObjectPackage(Object o) { + if (o instanceof AppWidgetProviderInfo) { + return ((AppWidgetProviderInfo) o).provider.getPackageName(); + } else { + ResolveInfo info = (ResolveInfo) o; + return info.activityInfo.packageName; + } + } + + private void writeToDb(Object o, Bitmap preview) { + String name = getObjectName(o); + SQLiteDatabase db = mDb.getWritableDatabase(); + ContentValues values = new ContentValues(); + + values.put(CacheDb.COLUMN_NAME, name); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + preview.compress(Bitmap.CompressFormat.PNG, 100, stream); + values.put(CacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray()); + values.put(CacheDb.COLUMN_SIZE, mSize); + db.insert(CacheDb.TABLE_NAME, null, values); + } + + public static void removeFromDb(final CacheDb cacheDb, final String packageName) { + synchronized(sInvalidPackages) { + sInvalidPackages.add(packageName); + } + new AsyncTask<Void, Void, Void>() { + public Void doInBackground(Void ... args) { + SQLiteDatabase db = cacheDb.getWritableDatabase(); + db.delete(CacheDb.TABLE_NAME, + CacheDb.COLUMN_NAME + " LIKE ? OR " + + CacheDb.COLUMN_NAME + " LIKE ?", // SELECT query + new String[] { + WIDGET_PREFIX + packageName + "/%", + SHORTCUT_PREFIX + packageName + "/%"} // args to SELECT query + ); + synchronized(sInvalidPackages) { + sInvalidPackages.remove(packageName); + } + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + } + + private Bitmap readFromDb(String name, Bitmap b) { + if (mCachedSelectQuery == null) { + mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " + + CacheDb.COLUMN_SIZE + " = ?"; + } + SQLiteDatabase db = mDb.getReadableDatabase(); + Cursor result = db.query(CacheDb.TABLE_NAME, + new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return + mCachedSelectQuery, // select query + new String[] { name, mSize }, // args to select query + null, + null, + null, + null); + if (result.getCount() > 0) { + result.moveToFirst(); + byte[] blob = result.getBlob(0); + result.close(); + final BitmapFactory.Options opts = mCachedBitmapFactoryOptions.get(); + opts.inBitmap = b; + opts.inSampleSize = 1; + Bitmap out = BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); + return out; + } else { + result.close(); + return null; + } + } + + public Bitmap generatePreview(Object info, Bitmap preview) { + if (preview != null && + (preview.getWidth() != mPreviewBitmapWidth || + preview.getHeight() != mPreviewBitmapHeight)) { + throw new RuntimeException("Improperly sized bitmap passed as argument"); + } + if (info instanceof AppWidgetProviderInfo) { + return generateWidgetPreview((AppWidgetProviderInfo) info, preview); + } else { + return generateShortcutPreview( + (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview); + } + } + + public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, Bitmap preview) { + int[] cellSpans = Launcher.getSpanForWidget(mLauncher, info); + int maxWidth = maxWidthForWidgetPreview(cellSpans[0]); + int maxHeight = maxHeightForWidgetPreview(cellSpans[1]); + return generateWidgetPreview(info.provider, info.previewImage, info.icon, + cellSpans[0], cellSpans[1], maxWidth, maxHeight, preview, null); + } + + public int maxWidthForWidgetPreview(int spanX) { + return Math.min(mPreviewBitmapWidth, + mWidgetSpacingLayout.estimateCellWidth(spanX)); + } + + public int maxHeightForWidgetPreview(int spanY) { + return Math.min(mPreviewBitmapHeight, + mWidgetSpacingLayout.estimateCellHeight(spanY)); + } + + public Bitmap generateWidgetPreview(ComponentName provider, int previewImage, + int iconId, int cellHSpan, int cellVSpan, int maxPreviewWidth, int maxPreviewHeight, + Bitmap preview, int[] preScaledWidthOut) { + // Load the preview image if possible + String packageName = provider.getPackageName(); + if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; + if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE; + + Drawable drawable = null; + if (previewImage != 0) { + drawable = mPackageManager.getDrawable(packageName, previewImage, null); + if (drawable == null) { + Log.w(TAG, "Can't load widget preview drawable 0x" + + Integer.toHexString(previewImage) + " for provider: " + provider); + } + } + + int previewWidth; + int previewHeight; + Bitmap defaultPreview = null; + boolean widgetPreviewExists = (drawable != null); + if (widgetPreviewExists) { + previewWidth = drawable.getIntrinsicWidth(); + previewHeight = drawable.getIntrinsicHeight(); + } else { + // Generate a preview image if we couldn't load one + if (cellHSpan < 1) cellHSpan = 1; + if (cellVSpan < 1) cellVSpan = 1; + + BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources() + .getDrawable(R.drawable.widget_preview_tile); + final int previewDrawableWidth = previewDrawable + .getIntrinsicWidth(); + final int previewDrawableHeight = previewDrawable + .getIntrinsicHeight(); + previewWidth = previewDrawableWidth * cellHSpan; // subtract 2 dips + previewHeight = previewDrawableHeight * cellVSpan; + + defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, + Config.ARGB_8888); + final Canvas c = mCachedAppWidgetPreviewCanvas.get(); + c.setBitmap(defaultPreview); + previewDrawable.setBounds(0, 0, previewWidth, previewHeight); + previewDrawable.setTileModeXY(Shader.TileMode.REPEAT, + Shader.TileMode.REPEAT); + previewDrawable.draw(c); + c.setBitmap(null); + + // Draw the icon in the top left corner + int minOffset = (int) (mAppIconSize * sWidgetPreviewIconPaddingPercentage); + int smallestSide = Math.min(previewWidth, previewHeight); + float iconScale = Math.min((float) smallestSide + / (mAppIconSize + 2 * minOffset), 1f); + + try { + Drawable icon = null; + int hoffset = + (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2); + int yoffset = + (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2); + if (iconId > 0) + icon = mIconCache.getFullResIcon(packageName, iconId); + if (icon != null) { + renderDrawableToBitmap(icon, defaultPreview, hoffset, + yoffset, (int) (mAppIconSize * iconScale), + (int) (mAppIconSize * iconScale)); + } + } catch (Resources.NotFoundException e) { + } + } + + // Scale to fit width only - let the widget preview be clipped in the + // vertical dimension + float scale = 1f; + if (preScaledWidthOut != null) { + preScaledWidthOut[0] = previewWidth; + } + if (previewWidth > maxPreviewWidth) { + scale = maxPreviewWidth / (float) previewWidth; + } + if (scale != 1f) { + previewWidth = (int) (scale * previewWidth); + previewHeight = (int) (scale * previewHeight); + } + + // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size + if (preview == null) { + preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); + } + + // Draw the scaled preview into the final bitmap + int x = (preview.getWidth() - previewWidth) / 2; + if (widgetPreviewExists) { + renderDrawableToBitmap(drawable, preview, x, 0, previewWidth, + previewHeight); + } else { + final Canvas c = mCachedAppWidgetPreviewCanvas.get(); + final Rect src = mCachedAppWidgetPreviewSrcRect.get(); + final Rect dest = mCachedAppWidgetPreviewDestRect.get(); + c.setBitmap(preview); + src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight()); + dest.set(x, 0, x + previewWidth, previewHeight); + + Paint p = mCachedAppWidgetPreviewPaint.get(); + if (p == null) { + p = new Paint(); + p.setFilterBitmap(true); + mCachedAppWidgetPreviewPaint.set(p); + } + c.drawBitmap(defaultPreview, src, dest, p); + c.setBitmap(null); + } + return preview; + } + + private Bitmap generateShortcutPreview( + ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { + Bitmap tempBitmap = mCachedShortcutPreviewBitmap.get(); + final Canvas c = mCachedShortcutPreviewCanvas.get(); + if (tempBitmap == null || + tempBitmap.getWidth() != maxWidth || + tempBitmap.getHeight() != maxHeight) { + tempBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); + mCachedShortcutPreviewBitmap.set(tempBitmap); + } else { + c.setBitmap(tempBitmap); + c.drawColor(0, PorterDuff.Mode.CLEAR); + c.setBitmap(null); + } + // Render the icon + Drawable icon = mIconCache.getFullResIcon(info); + + int paddingTop = mContext. + getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); + int paddingLeft = mContext. + getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); + int paddingRight = mContext. + getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); + + int scaledIconWidth = (maxWidth - paddingLeft - paddingRight); + + renderDrawableToBitmap( + icon, tempBitmap, paddingLeft, paddingTop, scaledIconWidth, scaledIconWidth); + + if (preview != null && + (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight)) { + throw new RuntimeException("Improperly sized bitmap passed as argument"); + } else if (preview == null) { + preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); + } + + c.setBitmap(preview); + // Draw a desaturated/scaled version of the icon in the background as a watermark + Paint p = mCachedShortcutPreviewPaint.get(); + if (p == null) { + p = new Paint(); + ColorMatrix colorMatrix = new ColorMatrix(); + colorMatrix.setSaturation(0); + p.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); + p.setAlpha((int) (255 * 0.06f)); + mCachedShortcutPreviewPaint.set(p); + } + c.drawBitmap(tempBitmap, 0, 0, p); + c.setBitmap(null); + + renderDrawableToBitmap(icon, preview, 0, 0, mAppIconSize, mAppIconSize); + + return preview; + } + + + public static void renderDrawableToBitmap( + Drawable d, Bitmap bitmap, int x, int y, int w, int h) { + renderDrawableToBitmap(d, bitmap, x, y, w, h, 1f); + } + + private static void renderDrawableToBitmap( + Drawable d, Bitmap bitmap, int x, int y, int w, int h, + float scale) { + if (bitmap != null) { + Canvas c = new Canvas(bitmap); + c.scale(scale, scale); + Rect oldBounds = d.copyBounds(); + d.setBounds(x, y, x + w, y + h); + d.draw(c); + d.setBounds(oldBounds); // Restore the bounds + c.setBitmap(null); + } + } + +} diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java new file mode 100644 index 000000000..4f1fb0809 --- /dev/null +++ b/src/com/android/launcher3/Workspace.java @@ -0,0 +1,3894 @@ +/* + * 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.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.app.WallpaperManager; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.os.IBinder; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.Display; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.launcher3.R; +import com.android.launcher3.FolderIcon.FolderRingAnimator; +import com.android.launcher3.LauncherSettings.Favorites; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * The workspace is a wide area with a wallpaper and a finite number of pages. + * Each page contains a number of icons, folders or widgets the user can + * interact with. A workspace is meant to be used with a fixed width only. + */ +public class Workspace extends SmoothPagedView + implements DropTarget, DragSource, DragScroller, View.OnTouchListener, + DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener { + private static final String TAG = "Launcher.Workspace"; + + // Y rotation to apply to the workspace screens + private static final float WORKSPACE_OVERSCROLL_ROTATION = 24f; + + private static final int CHILDREN_OUTLINE_FADE_OUT_DELAY = 0; + private static final int CHILDREN_OUTLINE_FADE_OUT_DURATION = 375; + private static final int CHILDREN_OUTLINE_FADE_IN_DURATION = 100; + + private static final int BACKGROUND_FADE_OUT_DURATION = 350; + private static final int ADJACENT_SCREEN_DROP_DURATION = 300; + private static final int FLING_THRESHOLD_VELOCITY = 500; + + // These animators are used to fade the children's outlines + private ObjectAnimator mChildrenOutlineFadeInAnimation; + private ObjectAnimator mChildrenOutlineFadeOutAnimation; + private float mChildrenOutlineAlpha = 0; + + // These properties refer to the background protection gradient used for AllApps and Customize + private ValueAnimator mBackgroundFadeInAnimation; + private ValueAnimator mBackgroundFadeOutAnimation; + private Drawable mBackground; + boolean mDrawBackground = true; + private float mBackgroundAlpha = 0; + + private float mWallpaperScrollRatio = 1.0f; + private int mOriginalPageSpacing; + + private final WallpaperManager mWallpaperManager; + private IBinder mWindowToken; + private static final float WALLPAPER_SCREENS_SPAN = 2f; + + private int mDefaultPage; + + /** + * CellInfo for the cell that is currently being dragged + */ + private CellLayout.CellInfo mDragInfo; + + /** + * Target drop area calculated during last acceptDrop call. + */ + private int[] mTargetCell = new int[2]; + private int mDragOverX = -1; + private int mDragOverY = -1; + + static Rect mLandscapeCellLayoutMetrics = null; + static Rect mPortraitCellLayoutMetrics = null; + + /** + * The CellLayout that is currently being dragged over + */ + private CellLayout mDragTargetLayout = null; + /** + * The CellLayout that we will show as glowing + */ + private CellLayout mDragOverlappingLayout = null; + + /** + * The CellLayout which will be dropped to + */ + private CellLayout mDropToLayout = null; + + private Launcher mLauncher; + private IconCache mIconCache; + private DragController mDragController; + + // These are temporary variables to prevent having to allocate a new object just to + // return an (x, y) value from helper functions. Do NOT use them to maintain other state. + private int[] mTempCell = new int[2]; + private int[] mTempEstimate = new int[2]; + private float[] mDragViewVisualCenter = new float[2]; + private float[] mTempDragCoordinates = new float[2]; + private float[] mTempCellLayoutCenterCoordinates = new float[2]; + private float[] mTempDragBottomRightCoordinates = new float[2]; + private Matrix mTempInverseMatrix = new Matrix(); + + private SpringLoadedDragController mSpringLoadedDragController; + private float mSpringLoadedShrinkFactor; + + private static final int DEFAULT_CELL_COUNT_X = 4; + private static final int DEFAULT_CELL_COUNT_Y = 4; + + // State variable that indicates whether the pages are small (ie when you're + // in all apps or customize mode) + + enum State { NORMAL, SPRING_LOADED, SMALL }; + private State mState = State.NORMAL; + private boolean mIsSwitchingState = false; + + boolean mAnimatingViewIntoPlace = false; + boolean mIsDragOccuring = false; + boolean mChildrenLayersEnabled = true; + + /** Is the user is dragging an item near the edge of a page? */ + private boolean mInScrollArea = false; + + private final HolographicOutlineHelper mOutlineHelper = new HolographicOutlineHelper(); + private Bitmap mDragOutline = null; + private final Rect mTempRect = new Rect(); + private final int[] mTempXY = new int[2]; + private int[] mTempVisiblePagesRange = new int[2]; + private float mOverscrollFade = 0; + private boolean mOverscrollTransformsSet; + public static final int DRAG_BITMAP_PADDING = 2; + private boolean mWorkspaceFadeInAdjacentScreens; + + enum WallpaperVerticalOffset { TOP, MIDDLE, BOTTOM }; + int mWallpaperWidth; + int mWallpaperHeight; + WallpaperOffsetInterpolator mWallpaperOffset; + boolean mUpdateWallpaperOffsetImmediately = false; + private Runnable mDelayedResizeRunnable; + private Runnable mDelayedSnapToPageRunnable; + private Point mDisplaySize = new Point(); + private boolean mIsStaticWallpaper; + private int mWallpaperTravelWidth; + private int mSpringLoadedPageSpacing; + private int mCameraDistance; + + // Variables relating to the creation of user folders by hovering shortcuts over shortcuts + private static final int FOLDER_CREATION_TIMEOUT = 0; + private static final int REORDER_TIMEOUT = 250; + private final Alarm mFolderCreationAlarm = new Alarm(); + private final Alarm mReorderAlarm = new Alarm(); + private FolderRingAnimator mDragFolderRingAnimator = null; + private FolderIcon mDragOverFolderIcon = null; + private boolean mCreateUserFolderOnDrop = false; + private boolean mAddToExistingFolderOnDrop = false; + private DropTarget.DragEnforcer mDragEnforcer; + private float mMaxDistanceForFolderCreation; + + // Variables relating to touch disambiguation (scrolling workspace vs. scrolling a widget) + private float mXDown; + private float mYDown; + final static float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6; + final static float MAX_SWIPE_ANGLE = (float) Math.PI / 3; + final static float TOUCH_SLOP_DAMPING_FACTOR = 4; + + // Relating to the animation of items being dropped externally + public static final int ANIMATE_INTO_POSITION_AND_DISAPPEAR = 0; + public static final int ANIMATE_INTO_POSITION_AND_REMAIN = 1; + public static final int ANIMATE_INTO_POSITION_AND_RESIZE = 2; + public static final int COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION = 3; + public static final int CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION = 4; + + // Related to dragging, folder creation and reordering + private static final int DRAG_MODE_NONE = 0; + private static final int DRAG_MODE_CREATE_FOLDER = 1; + private static final int DRAG_MODE_ADD_TO_FOLDER = 2; + private static final int DRAG_MODE_REORDER = 3; + private int mDragMode = DRAG_MODE_NONE; + private int mLastReorderX = -1; + private int mLastReorderY = -1; + + private SparseArray<Parcelable> mSavedStates; + private final ArrayList<Integer> mRestoredPages = new ArrayList<Integer>(); + + // These variables are used for storing the initial and final values during workspace animations + private int mSavedScrollX; + private float mSavedRotationY; + private float mSavedTranslationX; + private float mCurrentScaleX; + private float mCurrentScaleY; + private float mCurrentRotationY; + private float mCurrentTranslationX; + private float mCurrentTranslationY; + private float[] mOldTranslationXs; + private float[] mOldTranslationYs; + private float[] mOldScaleXs; + private float[] mOldScaleYs; + private float[] mOldBackgroundAlphas; + private float[] mOldAlphas; + private float[] mNewTranslationXs; + private float[] mNewTranslationYs; + private float[] mNewScaleXs; + private float[] mNewScaleYs; + private float[] mNewBackgroundAlphas; + private float[] mNewAlphas; + private float[] mNewRotationYs; + private float mTransitionProgress; + + private final Runnable mBindPages = new Runnable() { + @Override + public void run() { + mLauncher.getModel().bindRemainingSynchronousPages(); + } + }; + + /** + * 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 Workspace(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + /** + * Used to inflate the Workspace from XML. + * + * @param context The application's context. + * @param attrs The attributes set containing the Workspace's customization values. + * @param defStyle Unused. + */ + public Workspace(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mContentIsRefreshable = false; + mOriginalPageSpacing = mPageSpacing; + + mDragEnforcer = new DropTarget.DragEnforcer(context); + // With workspace, data is available straight from the get-go + setDataIsReady(); + + mLauncher = (Launcher) context; + final Resources res = getResources(); + mWorkspaceFadeInAdjacentScreens = res.getBoolean(R.bool.config_workspaceFadeAdjacentScreens); + mFadeInAdjacentScreens = false; + mWallpaperManager = WallpaperManager.getInstance(context); + + int cellCountX = DEFAULT_CELL_COUNT_X; + int cellCountY = DEFAULT_CELL_COUNT_Y; + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.Workspace, defStyle, 0); + + if (LauncherApplication.isScreenLarge()) { + // Determine number of rows/columns dynamically + // TODO: This code currently fails on tablets with an aspect ratio < 1.3. + // Around that ratio we should make cells the same size in portrait and + // landscape + TypedArray actionBarSizeTypedArray = + context.obtainStyledAttributes(new int[] { android.R.attr.actionBarSize }); + final float actionBarHeight = actionBarSizeTypedArray.getDimension(0, 0f); + + Point minDims = new Point(); + Point maxDims = new Point(); + mLauncher.getWindowManager().getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); + + cellCountX = 1; + while (CellLayout.widthInPortrait(res, cellCountX + 1) <= minDims.x) { + cellCountX++; + } + + cellCountY = 1; + while (actionBarHeight + CellLayout.heightInLandscape(res, cellCountY + 1) + <= minDims.y) { + cellCountY++; + } + } + + mSpringLoadedShrinkFactor = + res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; + mSpringLoadedPageSpacing = + res.getDimensionPixelSize(R.dimen.workspace_spring_loaded_page_spacing); + mCameraDistance = res.getInteger(R.integer.config_cameraDistance); + + // if the value is manually specified, use that instead + cellCountX = a.getInt(R.styleable.Workspace_cellCountX, cellCountX); + cellCountY = a.getInt(R.styleable.Workspace_cellCountY, cellCountY); + mDefaultPage = a.getInt(R.styleable.Workspace_defaultScreen, 1); + a.recycle(); + + setOnHierarchyChangeListener(this); + + LauncherModel.updateWorkspaceLayoutCells(cellCountX, cellCountY); + setHapticFeedbackEnabled(false); + + initWorkspace(); + + // Disable multitouch across the workspace/all apps/customize tray + setMotionEventSplittingEnabled(true); + + // Unless otherwise specified this view is important for accessibility. + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + // estimate the size of a widget with spans hSpan, vSpan. return MAX_VALUE for each + // dimension if unsuccessful + public int[] estimateItemSize(int hSpan, int vSpan, + ItemInfo itemInfo, boolean springLoaded) { + int[] size = new int[2]; + if (getChildCount() > 0) { + CellLayout cl = (CellLayout) mLauncher.getWorkspace().getChildAt(0); + Rect r = estimateItemPosition(cl, itemInfo, 0, 0, hSpan, vSpan); + size[0] = r.width(); + size[1] = r.height(); + if (springLoaded) { + size[0] *= mSpringLoadedShrinkFactor; + size[1] *= mSpringLoadedShrinkFactor; + } + return size; + } else { + size[0] = Integer.MAX_VALUE; + size[1] = Integer.MAX_VALUE; + return size; + } + } + public Rect estimateItemPosition(CellLayout cl, ItemInfo pendingInfo, + int hCell, int vCell, int hSpan, int vSpan) { + Rect r = new Rect(); + cl.cellToRect(hCell, vCell, hSpan, vSpan, r); + return r; + } + + public void onDragStart(DragSource source, Object info, int dragAction) { + mIsDragOccuring = true; + updateChildrenLayersEnabled(false); + mLauncher.lockScreenOrientation(); + setChildrenBackgroundAlphaMultipliers(1f); + // Prevent any Un/InstallShortcutReceivers from updating the db while we are dragging + InstallShortcutReceiver.enableInstallQueue(); + UninstallShortcutReceiver.enableUninstallQueue(); + } + + public void onDragEnd() { + mIsDragOccuring = false; + updateChildrenLayersEnabled(false); + mLauncher.unlockScreenOrientation(false); + + // Re-enable any Un/InstallShortcutReceiver and now process any queued items + InstallShortcutReceiver.disableAndFlushInstallQueue(getContext()); + UninstallShortcutReceiver.disableAndFlushUninstallQueue(getContext()); + } + + /** + * Initializes various states for this workspace. + */ + protected void initWorkspace() { + Context context = getContext(); + mCurrentPage = mDefaultPage; + Launcher.setScreen(mCurrentPage); + LauncherApplication app = (LauncherApplication)context.getApplicationContext(); + mIconCache = app.getIconCache(); + setWillNotDraw(false); + setClipChildren(false); + setClipToPadding(false); + setChildrenDrawnWithCacheEnabled(true); + + final Resources res = getResources(); + try { + mBackground = res.getDrawable(R.drawable.apps_customize_bg); + } catch (Resources.NotFoundException e) { + // In this case, we will skip drawing background protection + } + + mWallpaperOffset = new WallpaperOffsetInterpolator(); + Display display = mLauncher.getWindowManager().getDefaultDisplay(); + display.getSize(mDisplaySize); + mWallpaperTravelWidth = (int) (mDisplaySize.x * + wallpaperTravelToScreenWidthRatio(mDisplaySize.x, mDisplaySize.y)); + + mMaxDistanceForFolderCreation = (0.55f * res.getDimensionPixelSize(R.dimen.app_icon_size)); + mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); + } + + @Override + protected int getScrollMode() { + return SmoothPagedView.X_LARGE_MODE; + } + + @Override + public void onChildViewAdded(View parent, View child) { + if (!(child instanceof CellLayout)) { + throw new IllegalArgumentException("A Workspace can only have CellLayout children."); + } + CellLayout cl = ((CellLayout) child); + cl.setOnInterceptTouchListener(this); + cl.setClickable(true); + cl.setContentDescription(getContext().getString( + R.string.workspace_description_format, getChildCount())); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + } + + protected boolean shouldDrawChild(View child) { + final CellLayout cl = (CellLayout) child; + return super.shouldDrawChild(child) && + (cl.getShortcutsAndWidgets().getAlpha() > 0 || + cl.getBackgroundAlpha() > 0); + } + + /** + * @return The open folder on the current screen, or null if there is none + */ + Folder getOpenFolder() { + DragLayer dragLayer = mLauncher.getDragLayer(); + int count = dragLayer.getChildCount(); + for (int i = 0; i < count; i++) { + View child = dragLayer.getChildAt(i); + if (child instanceof Folder) { + Folder folder = (Folder) child; + if (folder.getInfo().opened) + return folder; + } + } + return null; + } + + boolean isTouchActive() { + return mTouchState != TOUCH_STATE_REST; + } + + /** + * Adds the specified child in the specified screen. The position and dimension of + * the child are defined by x, y, spanX and spanY. + * + * @param child The child to add in one of the workspace's screens. + * @param screen The screen in which to add the child. + * @param x The X position of the child in the screen's grid. + * @param y The Y position of the child in the screen's grid. + * @param spanX The number of cells spanned horizontally by the child. + * @param spanY The number of cells spanned vertically by the child. + */ + void addInScreen(View child, long container, int screen, int x, int y, int spanX, int spanY) { + addInScreen(child, container, screen, x, y, spanX, spanY, false); + } + + /** + * Adds the specified child in the specified screen. The position and dimension of + * the child are defined by x, y, spanX and spanY. + * + * @param child The child to add in one of the workspace's screens. + * @param screen The screen in which to add the child. + * @param x The X position of the child in the screen's grid. + * @param y The Y position of the child in the screen's grid. + * @param spanX The number of cells spanned horizontally by the child. + * @param spanY The number of cells spanned vertically by the child. + * @param insert When true, the child is inserted at the beginning of the children list. + */ + void addInScreen(View child, long container, int screen, int x, int y, int spanX, int spanY, + boolean insert) { + if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + if (screen < 0 || screen >= getChildCount()) { + Log.e(TAG, "The screen must be >= 0 and < " + getChildCount() + + " (was " + screen + "); skipping child"); + return; + } + } + + final CellLayout layout; + if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + layout = mLauncher.getHotseat().getLayout(); + child.setOnKeyListener(null); + + // Hide folder title in the hotseat + if (child instanceof FolderIcon) { + ((FolderIcon) child).setTextVisible(false); + } + + if (screen < 0) { + screen = mLauncher.getHotseat().getOrderInHotseat(x, y); + } else { + // Note: We do this to ensure that the hotseat is always laid out in the orientation + // of the hotseat in order regardless of which orientation they were added + x = mLauncher.getHotseat().getCellXFromOrder(screen); + y = mLauncher.getHotseat().getCellYFromOrder(screen); + } + } else { + // Show folder title if not in the hotseat + if (child instanceof FolderIcon) { + ((FolderIcon) child).setTextVisible(true); + } + + layout = (CellLayout) getChildAt(screen); + child.setOnKeyListener(new IconKeyEventListener()); + } + + LayoutParams genericLp = child.getLayoutParams(); + CellLayout.LayoutParams lp; + if (genericLp == null || !(genericLp instanceof CellLayout.LayoutParams)) { + lp = new CellLayout.LayoutParams(x, y, spanX, spanY); + } else { + lp = (CellLayout.LayoutParams) genericLp; + lp.cellX = x; + lp.cellY = y; + lp.cellHSpan = spanX; + lp.cellVSpan = spanY; + } + + if (spanX < 0 && spanY < 0) { + lp.isLockedToGrid = false; + } + + // Get the canonical child id to uniquely represent this view in this screen + int childId = LauncherModel.getCellLayoutChildId(container, screen, x, y, spanX, spanY); + boolean markCellsAsOccupied = !(child instanceof Folder); + if (!layout.addViewToCellLayout(child, insert ? 0 : -1, childId, lp, markCellsAsOccupied)) { + // TODO: This branch occurs when the workspace is adding views + // outside of the defined grid + // maybe we should be deleting these items from the LauncherModel? + Log.w(TAG, "Failed to add to item at (" + lp.cellX + "," + lp.cellY + ") to CellLayout"); + } + + if (!(child instanceof Folder)) { + child.setHapticFeedbackEnabled(false); + child.setOnLongClickListener(mLongClickListener); + } + if (child instanceof DropTarget) { + mDragController.addDropTarget((DropTarget) child); + } + } + + /** + * Check if the point (x, y) hits a given page. + */ + private boolean hitsPage(int index, float x, float y) { + final View page = getChildAt(index); + if (page != null) { + float[] localXY = { x, y }; + mapPointFromSelfToChild(page, localXY); + return (localXY[0] >= 0 && localXY[0] < page.getWidth() + && localXY[1] >= 0 && localXY[1] < page.getHeight()); + } + return false; + } + + @Override + protected boolean hitsPreviousPage(float x, float y) { + // mNextPage is set to INVALID_PAGE whenever we are stationary. + // Calculating "next page" this way ensures that you scroll to whatever page you tap on + final int current = (mNextPage == INVALID_PAGE) ? mCurrentPage : mNextPage; + + // Only allow tap to next page on large devices, where there's significant margin outside + // the active workspace + return LauncherApplication.isScreenLarge() && hitsPage(current - 1, x, y); + } + + @Override + protected boolean hitsNextPage(float x, float y) { + // mNextPage is set to INVALID_PAGE whenever we are stationary. + // Calculating "next page" this way ensures that you scroll to whatever page you tap on + final int current = (mNextPage == INVALID_PAGE) ? mCurrentPage : mNextPage; + + // Only allow tap to next page on large devices, where there's significant margin outside + // the active workspace + return LauncherApplication.isScreenLarge() && hitsPage(current + 1, x, y); + } + + /** + * Called directly from a CellLayout (not by the framework), after we've been added as a + * listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout + * that it should intercept touch events, which is not something that is normally supported. + */ + @Override + public boolean onTouch(View v, MotionEvent event) { + return (isSmall() || !isFinishedSwitchingState()); + } + + public boolean isSwitchingState() { + return mIsSwitchingState; + } + + /** This differs from isSwitchingState in that we take into account how far the transition + * has completed. */ + public boolean isFinishedSwitchingState() { + return !mIsSwitchingState || (mTransitionProgress > 0.5f); + } + + protected void onWindowVisibilityChanged (int visibility) { + mLauncher.onWindowVisibilityChanged(visibility); + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + if (isSmall() || !isFinishedSwitchingState()) { + // when the home screens are shrunken, shouldn't allow side-scrolling + return false; + } + return super.dispatchUnhandledMove(focused, direction); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + switch (ev.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + mXDown = ev.getX(); + mYDown = ev.getY(); + break; + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: + if (mTouchState == TOUCH_STATE_REST) { + final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage); + if (!currentPage.lastDownOnOccupiedCell()) { + onWallpaperTap(ev); + } + } + } + return super.onInterceptTouchEvent(ev); + } + + protected void reinflateWidgetsIfNecessary() { + final int clCount = getChildCount(); + for (int i = 0; i < clCount; i++) { + CellLayout cl = (CellLayout) getChildAt(i); + ShortcutAndWidgetContainer swc = cl.getShortcutsAndWidgets(); + final int itemCount = swc.getChildCount(); + for (int j = 0; j < itemCount; j++) { + View v = swc.getChildAt(j); + + if (v.getTag() instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag(); + LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) info.hostView; + if (lahv != null && lahv.orientationChangedSincedInflation()) { + mLauncher.removeAppWidget(info); + // Remove the current widget which is inflated with the wrong orientation + cl.removeView(lahv); + mLauncher.bindAppWidget(info); + } + } + } + } + } + + @Override + protected void determineScrollingStart(MotionEvent ev) { + if (isSmall()) return; + if (!isFinishedSwitchingState()) return; + + float deltaX = Math.abs(ev.getX() - mXDown); + float deltaY = Math.abs(ev.getY() - mYDown); + + if (Float.compare(deltaX, 0f) == 0) return; + + float slope = deltaY / deltaX; + float theta = (float) Math.atan(slope); + + if (deltaX > mTouchSlop || deltaY > mTouchSlop) { + cancelCurrentPageLongPress(); + } + + if (theta > MAX_SWIPE_ANGLE) { + // Above MAX_SWIPE_ANGLE, we don't want to ever start scrolling the workspace + return; + } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) { + // Above START_DAMPING_TOUCH_SLOP_ANGLE and below MAX_SWIPE_ANGLE, we want to + // increase the touch slop to make it harder to begin scrolling the workspace. This + // results in vertically scrolling widgets to more easily. The higher the angle, the + // more we increase touch slop. + theta -= START_DAMPING_TOUCH_SLOP_ANGLE; + float extraRatio = (float) + Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE))); + super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio); + } else { + // Below START_DAMPING_TOUCH_SLOP_ANGLE, we don't do anything special + super.determineScrollingStart(ev); + } + } + + protected void onPageBeginMoving() { + super.onPageBeginMoving(); + + if (isHardwareAccelerated()) { + updateChildrenLayersEnabled(false); + } else { + if (mNextPage != INVALID_PAGE) { + // we're snapping to a particular screen + enableChildrenCache(mCurrentPage, mNextPage); + } else { + // this is when user is actively dragging a particular screen, they might + // swipe it either left or right (but we won't advance by more than one screen) + enableChildrenCache(mCurrentPage - 1, mCurrentPage + 1); + } + } + + // Only show page outlines as we pan if we are on large screen + if (LauncherApplication.isScreenLarge()) { + showOutlines(); + mIsStaticWallpaper = mWallpaperManager.getWallpaperInfo() == null; + } + + // If we are not fading in adjacent screens, we still need to restore the alpha in case the + // user scrolls while we are transitioning (should not affect dispatchDraw optimizations) + if (!mWorkspaceFadeInAdjacentScreens) { + for (int i = 0; i < getChildCount(); ++i) { + ((CellLayout) getPageAt(i)).setShortcutAndWidgetAlpha(1f); + } + } + + // Show the scroll indicator as you pan the page + showScrollingIndicator(false); + } + + protected void onPageEndMoving() { + super.onPageEndMoving(); + + if (isHardwareAccelerated()) { + updateChildrenLayersEnabled(false); + } else { + clearChildrenCache(); + } + + + if (mDragController.isDragging()) { + if (isSmall()) { + // If we are in springloaded mode, then force an event to check if the current touch + // is under a new page (to scroll to) + mDragController.forceTouchMove(); + } + } else { + // If we are not mid-dragging, hide the page outlines if we are on a large screen + if (LauncherApplication.isScreenLarge()) { + hideOutlines(); + } + + // Hide the scroll indicator as you pan the page + if (!mDragController.isDragging()) { + hideScrollingIndicator(false); + } + } + + if (mDelayedResizeRunnable != null) { + mDelayedResizeRunnable.run(); + mDelayedResizeRunnable = null; + } + + if (mDelayedSnapToPageRunnable != null) { + mDelayedSnapToPageRunnable.run(); + mDelayedSnapToPageRunnable = null; + } + } + + @Override + protected void notifyPageSwitchListener() { + super.notifyPageSwitchListener(); + Launcher.setScreen(mCurrentPage); + }; + + // As a ratio of screen height, the total distance we want the parallax effect to span + // horizontally + private float wallpaperTravelToScreenWidthRatio(int width, int height) { + float aspectRatio = width / (float) height; + + // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width + // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width + // We will use these two data points to extrapolate how much the wallpaper parallax effect + // to span (ie travel) at any aspect ratio: + + final float ASPECT_RATIO_LANDSCAPE = 16/10f; + final float ASPECT_RATIO_PORTRAIT = 10/16f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; + + // To find out the desired width at different aspect ratios, we use the following two + // formulas, where the coefficient on x is the aspect ratio (width/height): + // (16/10)x + y = 1.5 + // (10/16)x + y = 1.2 + // We solve for x and y and end up with a final formula: + final float x = + (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / + (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); + final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; + return x * aspectRatio + y; + } + + // The range of scroll values for Workspace + private int getScrollRange() { + return getChildOffset(getChildCount() - 1) - getChildOffset(0); + } + + protected void setWallpaperDimension() { + Point minDims = new Point(); + Point maxDims = new Point(); + mLauncher.getWindowManager().getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); + + final int maxDim = Math.max(maxDims.x, maxDims.y); + final int minDim = Math.min(minDims.x, minDims.y); + + // We need to ensure that there is enough extra space in the wallpaper for the intended + // parallax effects + if (LauncherApplication.isScreenLarge()) { + mWallpaperWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); + mWallpaperHeight = maxDim; + } else { + mWallpaperWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); + mWallpaperHeight = maxDim; + } + new Thread("setWallpaperDimension") { + public void run() { + mWallpaperManager.suggestDesiredDimensions(mWallpaperWidth, mWallpaperHeight); + } + }.start(); + } + + private float wallpaperOffsetForCurrentScroll() { + // Set wallpaper offset steps (1 / (number of screens - 1)) + mWallpaperManager.setWallpaperOffsetSteps(1.0f / (getChildCount() - 1), 1.0f); + + // For the purposes of computing the scrollRange and overScrollOffset, we assume + // that mLayoutScale is 1. This means that when we're in spring-loaded mode, + // there's no discrepancy between the wallpaper offset for a given page. + float layoutScale = mLayoutScale; + mLayoutScale = 1f; + int scrollRange = getScrollRange(); + + // Again, we adjust the wallpaper offset to be consistent between values of mLayoutScale + float adjustedScrollX = Math.max(0, Math.min(getScrollX(), mMaxScrollX)); + adjustedScrollX *= mWallpaperScrollRatio; + mLayoutScale = layoutScale; + + float scrollProgress = + adjustedScrollX / (float) scrollRange; + + if (LauncherApplication.isScreenLarge() && mIsStaticWallpaper) { + // The wallpaper travel width is how far, from left to right, the wallpaper will move + // at this orientation. On tablets in portrait mode we don't move all the way to the + // edges of the wallpaper, or otherwise the parallax effect would be too strong. + int wallpaperTravelWidth = Math.min(mWallpaperTravelWidth, mWallpaperWidth); + + float offsetInDips = wallpaperTravelWidth * scrollProgress + + (mWallpaperWidth - wallpaperTravelWidth) / 2; // center it + float offset = offsetInDips / (float) mWallpaperWidth; + return offset; + } else { + return scrollProgress; + } + } + + private void syncWallpaperOffsetWithScroll() { + final boolean enableWallpaperEffects = isHardwareAccelerated(); + if (enableWallpaperEffects) { + mWallpaperOffset.setFinalX(wallpaperOffsetForCurrentScroll()); + } + } + + public void updateWallpaperOffsetImmediately() { + mUpdateWallpaperOffsetImmediately = true; + } + + private void updateWallpaperOffsets() { + boolean updateNow = false; + boolean keepUpdating = true; + if (mUpdateWallpaperOffsetImmediately) { + updateNow = true; + keepUpdating = false; + mWallpaperOffset.jumpToFinal(); + mUpdateWallpaperOffsetImmediately = false; + } else { + updateNow = keepUpdating = mWallpaperOffset.computeScrollOffset(); + } + if (updateNow) { + if (mWindowToken != null) { + mWallpaperManager.setWallpaperOffsets(mWindowToken, + mWallpaperOffset.getCurrX(), mWallpaperOffset.getCurrY()); + } + } + if (keepUpdating) { + invalidate(); + } + } + + @Override + protected void updateCurrentPageScroll() { + super.updateCurrentPageScroll(); + computeWallpaperScrollRatio(mCurrentPage); + } + + @Override + protected void snapToPage(int whichPage) { + super.snapToPage(whichPage); + computeWallpaperScrollRatio(whichPage); + } + + @Override + protected void snapToPage(int whichPage, int duration) { + super.snapToPage(whichPage, duration); + computeWallpaperScrollRatio(whichPage); + } + + protected void snapToPage(int whichPage, Runnable r) { + if (mDelayedSnapToPageRunnable != null) { + mDelayedSnapToPageRunnable.run(); + } + mDelayedSnapToPageRunnable = r; + snapToPage(whichPage, SLOW_PAGE_SNAP_ANIMATION_DURATION); + } + + private void computeWallpaperScrollRatio(int page) { + // Here, we determine what the desired scroll would be with and without a layout scale, + // and compute a ratio between the two. This allows us to adjust the wallpaper offset + // as though there is no layout scale. + float layoutScale = mLayoutScale; + int scaled = getChildOffset(page) - getRelativeChildOffset(page); + mLayoutScale = 1.0f; + float unscaled = getChildOffset(page) - getRelativeChildOffset(page); + mLayoutScale = layoutScale; + if (scaled > 0) { + mWallpaperScrollRatio = (1.0f * unscaled) / scaled; + } else { + mWallpaperScrollRatio = 1f; + } + } + + class WallpaperOffsetInterpolator { + float mFinalHorizontalWallpaperOffset = 0.0f; + float mFinalVerticalWallpaperOffset = 0.5f; + float mHorizontalWallpaperOffset = 0.0f; + float mVerticalWallpaperOffset = 0.5f; + long mLastWallpaperOffsetUpdateTime; + boolean mIsMovingFast; + boolean mOverrideHorizontalCatchupConstant; + float mHorizontalCatchupConstant = 0.35f; + float mVerticalCatchupConstant = 0.35f; + + public WallpaperOffsetInterpolator() { + } + + public void setOverrideHorizontalCatchupConstant(boolean override) { + mOverrideHorizontalCatchupConstant = override; + } + + public void setHorizontalCatchupConstant(float f) { + mHorizontalCatchupConstant = f; + } + + public void setVerticalCatchupConstant(float f) { + mVerticalCatchupConstant = f; + } + + public boolean computeScrollOffset() { + if (Float.compare(mHorizontalWallpaperOffset, mFinalHorizontalWallpaperOffset) == 0 && + Float.compare(mVerticalWallpaperOffset, mFinalVerticalWallpaperOffset) == 0) { + mIsMovingFast = false; + return false; + } + boolean isLandscape = mDisplaySize.x > mDisplaySize.y; + + long currentTime = System.currentTimeMillis(); + long timeSinceLastUpdate = currentTime - mLastWallpaperOffsetUpdateTime; + timeSinceLastUpdate = Math.min((long) (1000/30f), timeSinceLastUpdate); + timeSinceLastUpdate = Math.max(1L, timeSinceLastUpdate); + + float xdiff = Math.abs(mFinalHorizontalWallpaperOffset - mHorizontalWallpaperOffset); + if (!mIsMovingFast && xdiff > 0.07) { + mIsMovingFast = true; + } + + float fractionToCatchUpIn1MsHorizontal; + if (mOverrideHorizontalCatchupConstant) { + fractionToCatchUpIn1MsHorizontal = mHorizontalCatchupConstant; + } else if (mIsMovingFast) { + fractionToCatchUpIn1MsHorizontal = isLandscape ? 0.5f : 0.75f; + } else { + // slow + fractionToCatchUpIn1MsHorizontal = isLandscape ? 0.27f : 0.5f; + } + float fractionToCatchUpIn1MsVertical = mVerticalCatchupConstant; + + fractionToCatchUpIn1MsHorizontal /= 33f; + fractionToCatchUpIn1MsVertical /= 33f; + + final float UPDATE_THRESHOLD = 0.00001f; + float hOffsetDelta = mFinalHorizontalWallpaperOffset - mHorizontalWallpaperOffset; + float vOffsetDelta = mFinalVerticalWallpaperOffset - mVerticalWallpaperOffset; + boolean jumpToFinalValue = Math.abs(hOffsetDelta) < UPDATE_THRESHOLD && + Math.abs(vOffsetDelta) < UPDATE_THRESHOLD; + + // Don't have any lag between workspace and wallpaper on non-large devices + if (!LauncherApplication.isScreenLarge() || jumpToFinalValue) { + mHorizontalWallpaperOffset = mFinalHorizontalWallpaperOffset; + mVerticalWallpaperOffset = mFinalVerticalWallpaperOffset; + } else { + float percentToCatchUpVertical = + Math.min(1.0f, timeSinceLastUpdate * fractionToCatchUpIn1MsVertical); + float percentToCatchUpHorizontal = + Math.min(1.0f, timeSinceLastUpdate * fractionToCatchUpIn1MsHorizontal); + mHorizontalWallpaperOffset += percentToCatchUpHorizontal * hOffsetDelta; + mVerticalWallpaperOffset += percentToCatchUpVertical * vOffsetDelta; + } + + mLastWallpaperOffsetUpdateTime = System.currentTimeMillis(); + return true; + } + + public float getCurrX() { + return mHorizontalWallpaperOffset; + } + + public float getFinalX() { + return mFinalHorizontalWallpaperOffset; + } + + public float getCurrY() { + return mVerticalWallpaperOffset; + } + + public float getFinalY() { + return mFinalVerticalWallpaperOffset; + } + + public void setFinalX(float x) { + mFinalHorizontalWallpaperOffset = Math.max(0f, Math.min(x, 1.0f)); + } + + public void setFinalY(float y) { + mFinalVerticalWallpaperOffset = Math.max(0f, Math.min(y, 1.0f)); + } + + public void jumpToFinal() { + mHorizontalWallpaperOffset = mFinalHorizontalWallpaperOffset; + mVerticalWallpaperOffset = mFinalVerticalWallpaperOffset; + } + } + + @Override + public void computeScroll() { + super.computeScroll(); + syncWallpaperOffsetWithScroll(); + } + + void showOutlines() { + if (!isSmall() && !mIsSwitchingState) { + if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); + if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); + mChildrenOutlineFadeInAnimation = LauncherAnimUtils.ofFloat(this, "childrenOutlineAlpha", 1.0f); + mChildrenOutlineFadeInAnimation.setDuration(CHILDREN_OUTLINE_FADE_IN_DURATION); + mChildrenOutlineFadeInAnimation.start(); + } + } + + void hideOutlines() { + if (!isSmall() && !mIsSwitchingState) { + if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); + if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); + mChildrenOutlineFadeOutAnimation = LauncherAnimUtils.ofFloat(this, "childrenOutlineAlpha", 0.0f); + mChildrenOutlineFadeOutAnimation.setDuration(CHILDREN_OUTLINE_FADE_OUT_DURATION); + mChildrenOutlineFadeOutAnimation.setStartDelay(CHILDREN_OUTLINE_FADE_OUT_DELAY); + mChildrenOutlineFadeOutAnimation.start(); + } + } + + public void showOutlinesTemporarily() { + if (!mIsPageMoving && !isTouchActive()) { + snapToPage(mCurrentPage); + } + } + + public void setChildrenOutlineAlpha(float alpha) { + mChildrenOutlineAlpha = alpha; + for (int i = 0; i < getChildCount(); i++) { + CellLayout cl = (CellLayout) getChildAt(i); + cl.setBackgroundAlpha(alpha); + } + } + + public float getChildrenOutlineAlpha() { + return mChildrenOutlineAlpha; + } + + void disableBackground() { + mDrawBackground = false; + } + void enableBackground() { + mDrawBackground = true; + } + + private void animateBackgroundGradient(float finalAlpha, boolean animated) { + if (mBackground == null) return; + if (mBackgroundFadeInAnimation != null) { + mBackgroundFadeInAnimation.cancel(); + mBackgroundFadeInAnimation = null; + } + if (mBackgroundFadeOutAnimation != null) { + mBackgroundFadeOutAnimation.cancel(); + mBackgroundFadeOutAnimation = null; + } + float startAlpha = getBackgroundAlpha(); + if (finalAlpha != startAlpha) { + if (animated) { + mBackgroundFadeOutAnimation = + LauncherAnimUtils.ofFloat(this, startAlpha, finalAlpha); + mBackgroundFadeOutAnimation.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + setBackgroundAlpha(((Float) animation.getAnimatedValue()).floatValue()); + } + }); + mBackgroundFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); + mBackgroundFadeOutAnimation.setDuration(BACKGROUND_FADE_OUT_DURATION); + mBackgroundFadeOutAnimation.start(); + } else { + setBackgroundAlpha(finalAlpha); + } + } + } + + public void setBackgroundAlpha(float alpha) { + if (alpha != mBackgroundAlpha) { + mBackgroundAlpha = alpha; + invalidate(); + } + } + + public float getBackgroundAlpha() { + return mBackgroundAlpha; + } + + float backgroundAlphaInterpolator(float r) { + float pivotA = 0.1f; + float pivotB = 0.4f; + if (r < pivotA) { + return 0; + } else if (r > pivotB) { + return 1.0f; + } else { + return (r - pivotA)/(pivotB - pivotA); + } + } + + private void updatePageAlphaValues(int screenCenter) { + boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; + if (mWorkspaceFadeInAdjacentScreens && + mState == State.NORMAL && + !mIsSwitchingState && + !isInOverscroll) { + for (int i = 0; i < getChildCount(); i++) { + CellLayout child = (CellLayout) getChildAt(i); + if (child != null) { + float scrollProgress = getScrollProgress(screenCenter, child, i); + float alpha = 1 - Math.abs(scrollProgress); + child.getShortcutsAndWidgets().setAlpha(alpha); + if (!mIsDragOccuring) { + child.setBackgroundAlphaMultiplier( + backgroundAlphaInterpolator(Math.abs(scrollProgress))); + } else { + child.setBackgroundAlphaMultiplier(1f); + } + } + } + } + } + + private void setChildrenBackgroundAlphaMultipliers(float a) { + for (int i = 0; i < getChildCount(); i++) { + CellLayout child = (CellLayout) getChildAt(i); + child.setBackgroundAlphaMultiplier(a); + } + } + + @Override + protected void screenScrolled(int screenCenter) { + final boolean isRtl = isLayoutRtl(); + super.screenScrolled(screenCenter); + + updatePageAlphaValues(screenCenter); + enableHwLayersOnVisiblePages(); + + if (mOverScrollX < 0 || mOverScrollX > mMaxScrollX) { + int index = 0; + float pivotX = 0f; + final float leftBiasedPivot = 0.25f; + final float rightBiasedPivot = 0.75f; + final int lowerIndex = 0; + final int upperIndex = getChildCount() - 1; + if (isRtl) { + index = mOverScrollX < 0 ? upperIndex : lowerIndex; + pivotX = (index == 0 ? leftBiasedPivot : rightBiasedPivot); + } else { + index = mOverScrollX < 0 ? lowerIndex : upperIndex; + pivotX = (index == 0 ? rightBiasedPivot : leftBiasedPivot); + } + + CellLayout cl = (CellLayout) getChildAt(index); + float scrollProgress = getScrollProgress(screenCenter, cl, index); + final boolean isLeftPage = (isRtl ? index > 0 : index == 0); + cl.setOverScrollAmount(Math.abs(scrollProgress), isLeftPage); + float rotation = -WORKSPACE_OVERSCROLL_ROTATION * scrollProgress; + cl.setRotationY(rotation); + setFadeForOverScroll(Math.abs(scrollProgress)); + if (!mOverscrollTransformsSet) { + mOverscrollTransformsSet = true; + cl.setCameraDistance(mDensity * mCameraDistance); + cl.setPivotX(cl.getMeasuredWidth() * pivotX); + cl.setPivotY(cl.getMeasuredHeight() * 0.5f); + cl.setOverscrollTransformsDirty(true); + } + } else { + if (mOverscrollFade != 0) { + setFadeForOverScroll(0); + } + if (mOverscrollTransformsSet) { + mOverscrollTransformsSet = false; + ((CellLayout) getChildAt(0)).resetOverscrollTransforms(); + ((CellLayout) getChildAt(getChildCount() - 1)).resetOverscrollTransforms(); + } + } + } + + @Override + protected void overScroll(float amount) { + acceleratedOverScroll(amount); + } + + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mWindowToken = getWindowToken(); + computeScroll(); + mDragController.setWindowToken(mWindowToken); + } + + protected void onDetachedFromWindow() { + mWindowToken = null; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + mUpdateWallpaperOffsetImmediately = true; + } + super.onLayout(changed, left, top, right, bottom); + } + + @Override + protected void onDraw(Canvas canvas) { + updateWallpaperOffsets(); + + // Draw the background gradient if necessary + if (mBackground != null && mBackgroundAlpha > 0.0f && mDrawBackground) { + int alpha = (int) (mBackgroundAlpha * 255); + mBackground.setAlpha(alpha); + mBackground.setBounds(getScrollX(), 0, getScrollX() + getMeasuredWidth(), + getMeasuredHeight()); + mBackground.draw(canvas); + } + + super.onDraw(canvas); + + // Call back to LauncherModel to finish binding after the first draw + post(mBindPages); + } + + boolean isDrawingBackgroundGradient() { + return (mBackground != null && mBackgroundAlpha > 0.0f && mDrawBackground); + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + if (!mLauncher.isAllAppsVisible()) { + final Folder openFolder = getOpenFolder(); + if (openFolder != null) { + return openFolder.requestFocus(direction, previouslyFocusedRect); + } else { + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); + } + } + return false; + } + + @Override + public int getDescendantFocusability() { + if (isSmall()) { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } + return super.getDescendantFocusability(); + } + + @Override + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + if (!mLauncher.isAllAppsVisible()) { + final Folder openFolder = getOpenFolder(); + if (openFolder != null) { + openFolder.addFocusables(views, direction); + } else { + super.addFocusables(views, direction, focusableMode); + } + } + } + + public boolean isSmall() { + return mState == State.SMALL || mState == State.SPRING_LOADED; + } + + void enableChildrenCache(int fromPage, int toPage) { + if (fromPage > toPage) { + final int temp = fromPage; + fromPage = toPage; + toPage = temp; + } + + final int screenCount = getChildCount(); + + fromPage = Math.max(fromPage, 0); + toPage = Math.min(toPage, screenCount - 1); + + for (int i = fromPage; i <= toPage; i++) { + final CellLayout layout = (CellLayout) getChildAt(i); + layout.setChildrenDrawnWithCacheEnabled(true); + layout.setChildrenDrawingCacheEnabled(true); + } + } + + void clearChildrenCache() { + final int screenCount = getChildCount(); + for (int i = 0; i < screenCount; i++) { + final CellLayout layout = (CellLayout) getChildAt(i); + layout.setChildrenDrawnWithCacheEnabled(false); + // In software mode, we don't want the items to continue to be drawn into bitmaps + if (!isHardwareAccelerated()) { + layout.setChildrenDrawingCacheEnabled(false); + } + } + } + + + private void updateChildrenLayersEnabled(boolean force) { + boolean small = mState == State.SMALL || mIsSwitchingState; + boolean enableChildrenLayers = force || small || mAnimatingViewIntoPlace || isPageMoving(); + + if (enableChildrenLayers != mChildrenLayersEnabled) { + mChildrenLayersEnabled = enableChildrenLayers; + if (mChildrenLayersEnabled) { + enableHwLayersOnVisiblePages(); + } else { + for (int i = 0; i < getPageCount(); i++) { + final CellLayout cl = (CellLayout) getChildAt(i); + cl.disableHardwareLayers(); + } + } + } + } + + private void enableHwLayersOnVisiblePages() { + if (mChildrenLayersEnabled) { + final int screenCount = getChildCount(); + getVisiblePages(mTempVisiblePagesRange); + int leftScreen = mTempVisiblePagesRange[0]; + int rightScreen = mTempVisiblePagesRange[1]; + if (leftScreen == rightScreen) { + // make sure we're caching at least two pages always + if (rightScreen < screenCount - 1) { + rightScreen++; + } else if (leftScreen > 0) { + leftScreen--; + } + } + for (int i = 0; i < screenCount; i++) { + final CellLayout layout = (CellLayout) getPageAt(i); + if (!(leftScreen <= i && i <= rightScreen && shouldDrawChild(layout))) { + layout.disableHardwareLayers(); + } + } + for (int i = 0; i < screenCount; i++) { + final CellLayout layout = (CellLayout) getPageAt(i); + if (leftScreen <= i && i <= rightScreen && shouldDrawChild(layout)) { + layout.enableHardwareLayers(); + } + } + } + } + + public void buildPageHardwareLayers() { + // force layers to be enabled just for the call to buildLayer + updateChildrenLayersEnabled(true); + if (getWindowToken() != null) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + CellLayout cl = (CellLayout) getChildAt(i); + cl.buildHardwareLayer(); + } + } + updateChildrenLayersEnabled(false); + } + + protected void onWallpaperTap(MotionEvent ev) { + final int[] position = mTempCell; + getLocationOnScreen(position); + + int pointerIndex = ev.getActionIndex(); + position[0] += (int) ev.getX(pointerIndex); + position[1] += (int) ev.getY(pointerIndex); + + mWallpaperManager.sendWallpaperCommand(getWindowToken(), + ev.getAction() == MotionEvent.ACTION_UP + ? WallpaperManager.COMMAND_TAP : WallpaperManager.COMMAND_SECONDARY_TAP, + position[0], position[1], 0, null); + } + + /* + * This interpolator emulates the rate at which the perceived scale of an object changes + * as its distance from a camera increases. When this interpolator is applied to a scale + * animation on a view, it evokes the sense that the object is shrinking due to moving away + * from the camera. + */ + static class ZInterpolator implements TimeInterpolator { + private float focalLength; + + public ZInterpolator(float foc) { + focalLength = foc; + } + + public float getInterpolation(float input) { + return (1.0f - focalLength / (focalLength + input)) / + (1.0f - focalLength / (focalLength + 1.0f)); + } + } + + /* + * The exact reverse of ZInterpolator. + */ + static class InverseZInterpolator implements TimeInterpolator { + private ZInterpolator zInterpolator; + public InverseZInterpolator(float foc) { + zInterpolator = new ZInterpolator(foc); + } + public float getInterpolation(float input) { + return 1 - zInterpolator.getInterpolation(1 - input); + } + } + + /* + * ZInterpolator compounded with an ease-out. + */ + static class ZoomOutInterpolator implements TimeInterpolator { + private final DecelerateInterpolator decelerate = new DecelerateInterpolator(0.75f); + private final ZInterpolator zInterpolator = new ZInterpolator(0.13f); + + public float getInterpolation(float input) { + return decelerate.getInterpolation(zInterpolator.getInterpolation(input)); + } + } + + /* + * InvereZInterpolator compounded with an ease-out. + */ + static class ZoomInInterpolator implements TimeInterpolator { + private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f); + private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f); + + public float getInterpolation(float input) { + return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input)); + } + } + + private final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator(); + + /* + * + * We call these methods (onDragStartedWithItemSpans/onDragStartedWithSize) whenever we + * start a drag in Launcher, regardless of whether the drag has ever entered the Workspace + * + * These methods mark the appropriate pages as accepting drops (which alters their visual + * appearance). + * + */ + public void onDragStartedWithItem(View v) { + final Canvas canvas = new Canvas(); + + // The outline is used to visualize where the item will land if dropped + mDragOutline = createDragOutline(v, canvas, DRAG_BITMAP_PADDING); + } + + public void onDragStartedWithItem(PendingAddItemInfo info, Bitmap b, boolean clipAlpha) { + final Canvas canvas = new Canvas(); + + int[] size = estimateItemSize(info.spanX, info.spanY, info, false); + + // The outline is used to visualize where the item will land if dropped + mDragOutline = createDragOutline(b, canvas, DRAG_BITMAP_PADDING, size[0], + size[1], clipAlpha); + } + + public void exitWidgetResizeMode() { + DragLayer dragLayer = mLauncher.getDragLayer(); + dragLayer.clearAllResizeFrames(); + } + + private void initAnimationArrays() { + final int childCount = getChildCount(); + if (mOldTranslationXs != null) return; + mOldTranslationXs = new float[childCount]; + mOldTranslationYs = new float[childCount]; + mOldScaleXs = new float[childCount]; + mOldScaleYs = new float[childCount]; + mOldBackgroundAlphas = new float[childCount]; + mOldAlphas = new float[childCount]; + mNewTranslationXs = new float[childCount]; + mNewTranslationYs = new float[childCount]; + mNewScaleXs = new float[childCount]; + mNewScaleYs = new float[childCount]; + mNewBackgroundAlphas = new float[childCount]; + mNewAlphas = new float[childCount]; + mNewRotationYs = new float[childCount]; + } + + Animator getChangeStateAnimation(final State state, boolean animated) { + return getChangeStateAnimation(state, animated, 0); + } + + Animator getChangeStateAnimation(final State state, boolean animated, int delay) { + if (mState == state) { + return null; + } + + // Initialize animation arrays for the first time if necessary + initAnimationArrays(); + + AnimatorSet anim = animated ? LauncherAnimUtils.createAnimatorSet() : null; + + // Stop any scrolling, move to the current page right away + setCurrentPage(getNextPage()); + + final State oldState = mState; + final boolean oldStateIsNormal = (oldState == State.NORMAL); + final boolean oldStateIsSpringLoaded = (oldState == State.SPRING_LOADED); + final boolean oldStateIsSmall = (oldState == State.SMALL); + mState = state; + final boolean stateIsNormal = (state == State.NORMAL); + final boolean stateIsSpringLoaded = (state == State.SPRING_LOADED); + final boolean stateIsSmall = (state == State.SMALL); + float finalScaleFactor = 1.0f; + float finalBackgroundAlpha = stateIsSpringLoaded ? 1.0f : 0f; + float translationX = 0; + float translationY = 0; + boolean zoomIn = true; + + if (state != State.NORMAL) { + finalScaleFactor = mSpringLoadedShrinkFactor - (stateIsSmall ? 0.1f : 0); + setPageSpacing(mSpringLoadedPageSpacing); + if (oldStateIsNormal && stateIsSmall) { + zoomIn = false; + setLayoutScale(finalScaleFactor); + updateChildrenLayersEnabled(false); + } else { + finalBackgroundAlpha = 1.0f; + setLayoutScale(finalScaleFactor); + } + } else { + setPageSpacing(mOriginalPageSpacing); + setLayoutScale(1.0f); + } + + final int duration = zoomIn ? + getResources().getInteger(R.integer.config_workspaceUnshrinkTime) : + getResources().getInteger(R.integer.config_appsCustomizeWorkspaceShrinkTime); + for (int i = 0; i < getChildCount(); i++) { + final CellLayout cl = (CellLayout) getChildAt(i); + float finalAlpha = (!mWorkspaceFadeInAdjacentScreens || stateIsSpringLoaded || + (i == mCurrentPage)) ? 1f : 0f; + float currentAlpha = cl.getShortcutsAndWidgets().getAlpha(); + float initialAlpha = currentAlpha; + + // Determine the pages alpha during the state transition + if ((oldStateIsSmall && stateIsNormal) || + (oldStateIsNormal && stateIsSmall)) { + // To/from workspace - only show the current page unless the transition is not + // animated and the animation end callback below doesn't run; + // or, if we're in spring-loaded mode + if (i == mCurrentPage || !animated || oldStateIsSpringLoaded) { + finalAlpha = 1f; + } else { + initialAlpha = 0f; + finalAlpha = 0f; + } + } + + mOldAlphas[i] = initialAlpha; + mNewAlphas[i] = finalAlpha; + if (animated) { + mOldTranslationXs[i] = cl.getTranslationX(); + mOldTranslationYs[i] = cl.getTranslationY(); + mOldScaleXs[i] = cl.getScaleX(); + mOldScaleYs[i] = cl.getScaleY(); + mOldBackgroundAlphas[i] = cl.getBackgroundAlpha(); + + mNewTranslationXs[i] = translationX; + mNewTranslationYs[i] = translationY; + mNewScaleXs[i] = finalScaleFactor; + mNewScaleYs[i] = finalScaleFactor; + mNewBackgroundAlphas[i] = finalBackgroundAlpha; + } else { + cl.setTranslationX(translationX); + cl.setTranslationY(translationY); + cl.setScaleX(finalScaleFactor); + cl.setScaleY(finalScaleFactor); + cl.setBackgroundAlpha(finalBackgroundAlpha); + cl.setShortcutAndWidgetAlpha(finalAlpha); + } + } + + if (animated) { + for (int index = 0; index < getChildCount(); index++) { + final int i = index; + final CellLayout cl = (CellLayout) getChildAt(i); + float currentAlpha = cl.getShortcutsAndWidgets().getAlpha(); + if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) { + cl.setTranslationX(mNewTranslationXs[i]); + cl.setTranslationY(mNewTranslationYs[i]); + cl.setScaleX(mNewScaleXs[i]); + cl.setScaleY(mNewScaleYs[i]); + cl.setBackgroundAlpha(mNewBackgroundAlphas[i]); + cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); + cl.setRotationY(mNewRotationYs[i]); + } else { + LauncherViewPropertyAnimator a = new LauncherViewPropertyAnimator(cl); + a.translationX(mNewTranslationXs[i]) + .translationY(mNewTranslationYs[i]) + .scaleX(mNewScaleXs[i]) + .scaleY(mNewScaleYs[i]) + .setDuration(duration) + .setInterpolator(mZoomInInterpolator); + anim.play(a); + + if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) { + LauncherViewPropertyAnimator alphaAnim = + new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets()); + alphaAnim.alpha(mNewAlphas[i]) + .setDuration(duration) + .setInterpolator(mZoomInInterpolator); + anim.play(alphaAnim); + } + if (mOldBackgroundAlphas[i] != 0 || + mNewBackgroundAlphas[i] != 0) { + ValueAnimator bgAnim = + LauncherAnimUtils.ofFloat(cl, 0f, 1f).setDuration(duration); + bgAnim.setInterpolator(mZoomInInterpolator); + bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { + public void onAnimationUpdate(float a, float b) { + cl.setBackgroundAlpha( + a * mOldBackgroundAlphas[i] + + b * mNewBackgroundAlphas[i]); + } + }); + anim.play(bgAnim); + } + } + } + anim.setStartDelay(delay); + } + + if (stateIsSpringLoaded) { + // Right now we're covered by Apps Customize + // Show the background gradient immediately, so the gradient will + // be showing once AppsCustomize disappears + animateBackgroundGradient(getResources().getInteger( + R.integer.config_appsCustomizeSpringLoadedBgAlpha) / 100f, false); + } else { + // Fade the background gradient away + animateBackgroundGradient(0f, true); + } + return anim; + } + + @Override + public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { + mIsSwitchingState = true; + updateChildrenLayersEnabled(false); + cancelScrollingIndicatorAnimations(); + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + mTransitionProgress = t; + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + mIsSwitchingState = false; + mWallpaperOffset.setOverrideHorizontalCatchupConstant(false); + updateChildrenLayersEnabled(false); + // The code in getChangeStateAnimation to determine initialAlpha and finalAlpha will ensure + // ensure that only the current page is visible during (and subsequently, after) the + // transition animation. If fade adjacent pages is disabled, then re-enable the page + // visibility after the transition animation. + if (!mWorkspaceFadeInAdjacentScreens) { + for (int i = 0; i < getChildCount(); i++) { + final CellLayout cl = (CellLayout) getChildAt(i); + cl.setShortcutAndWidgetAlpha(1f); + } + } + } + + @Override + public View getContent() { + return this; + } + + /** + * Draw the View v into the given Canvas. + * + * @param v the view to draw + * @param destCanvas the canvas to draw on + * @param padding the horizontal and vertical padding to use when drawing + */ + private void drawDragView(View v, Canvas destCanvas, int padding, boolean pruneToDrawable) { + final Rect clipRect = mTempRect; + v.getDrawingRect(clipRect); + + boolean textVisible = false; + + destCanvas.save(); + if (v instanceof TextView && pruneToDrawable) { + Drawable d = ((TextView) v).getCompoundDrawables()[1]; + clipRect.set(0, 0, d.getIntrinsicWidth() + padding, d.getIntrinsicHeight() + padding); + destCanvas.translate(padding / 2, padding / 2); + d.draw(destCanvas); + } else { + if (v instanceof FolderIcon) { + // For FolderIcons the text can bleed into the icon area, and so we need to + // hide the text completely (which can't be achieved by clipping). + if (((FolderIcon) v).getTextVisible()) { + ((FolderIcon) v).setTextVisible(false); + textVisible = true; + } + } else if (v instanceof BubbleTextView) { + final BubbleTextView tv = (BubbleTextView) v; + clipRect.bottom = tv.getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V + + tv.getLayout().getLineTop(0); + } else if (v instanceof TextView) { + final TextView tv = (TextView) v; + clipRect.bottom = tv.getExtendedPaddingTop() - tv.getCompoundDrawablePadding() + + tv.getLayout().getLineTop(0); + } + destCanvas.translate(-v.getScrollX() + padding / 2, -v.getScrollY() + padding / 2); + destCanvas.clipRect(clipRect, Op.REPLACE); + v.draw(destCanvas); + + // Restore text visibility of FolderIcon if necessary + if (textVisible) { + ((FolderIcon) v).setTextVisible(true); + } + } + destCanvas.restore(); + } + + /** + * Returns a new bitmap to show when the given View is being dragged around. + * Responsibility for the bitmap is transferred to the caller. + */ + public Bitmap createDragBitmap(View v, Canvas canvas, int padding) { + Bitmap b; + + if (v instanceof TextView) { + Drawable d = ((TextView) v).getCompoundDrawables()[1]; + b = Bitmap.createBitmap(d.getIntrinsicWidth() + padding, + d.getIntrinsicHeight() + padding, Bitmap.Config.ARGB_8888); + } else { + b = Bitmap.createBitmap( + v.getWidth() + padding, v.getHeight() + padding, Bitmap.Config.ARGB_8888); + } + + canvas.setBitmap(b); + drawDragView(v, canvas, padding, true); + canvas.setBitmap(null); + + return b; + } + + /** + * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createDragOutline(View v, Canvas canvas, int padding) { + final int outlineColor = getResources().getColor(android.R.color.holo_blue_light); + final Bitmap b = Bitmap.createBitmap( + v.getWidth() + padding, v.getHeight() + padding, Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + drawDragView(v, canvas, padding, true); + mOutlineHelper.applyMediumExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor); + canvas.setBitmap(null); + return b; + } + + /** + * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createDragOutline(Bitmap orig, Canvas canvas, int padding, int w, int h, + boolean clipAlpha) { + final int outlineColor = getResources().getColor(android.R.color.holo_blue_light); + final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + canvas.setBitmap(b); + + Rect src = new Rect(0, 0, orig.getWidth(), orig.getHeight()); + float scaleFactor = Math.min((w - padding) / (float) orig.getWidth(), + (h - padding) / (float) orig.getHeight()); + int scaledWidth = (int) (scaleFactor * orig.getWidth()); + int scaledHeight = (int) (scaleFactor * orig.getHeight()); + Rect dst = new Rect(0, 0, scaledWidth, scaledHeight); + + // center the image + dst.offset((w - scaledWidth) / 2, (h - scaledHeight) / 2); + + canvas.drawBitmap(orig, src, dst, null); + mOutlineHelper.applyMediumExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor, + clipAlpha); + canvas.setBitmap(null); + + return b; + } + + void startDrag(CellLayout.CellInfo cellInfo) { + View child = cellInfo.cell; + + // Make sure the drag was started by a long press as opposed to a long click. + if (!child.isInTouchMode()) { + return; + } + + mDragInfo = cellInfo; + child.setVisibility(INVISIBLE); + CellLayout layout = (CellLayout) child.getParent().getParent(); + layout.prepareChildForDrag(child); + + child.clearFocus(); + child.setPressed(false); + + final Canvas canvas = new Canvas(); + + // The outline is used to visualize where the item will land if dropped + mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING); + beginDragShared(child, this); + } + + public void beginDragShared(View child, DragSource source) { + Resources r = getResources(); + + // The drag bitmap follows the touch point around on the screen + final Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING); + + final int bmpWidth = b.getWidth(); + final int bmpHeight = b.getHeight(); + + float scale = mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY); + int dragLayerX = + Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2); + int dragLayerY = + Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2 + - DRAG_BITMAP_PADDING / 2); + + Point dragVisualizeOffset = null; + Rect dragRect = null; + if (child instanceof BubbleTextView || child instanceof PagedViewIcon) { + int iconSize = r.getDimensionPixelSize(R.dimen.app_icon_size); + int iconPaddingTop = r.getDimensionPixelSize(R.dimen.app_icon_padding_top); + int top = child.getPaddingTop(); + int left = (bmpWidth - iconSize) / 2; + int right = left + iconSize; + int bottom = top + iconSize; + dragLayerY += top; + // Note: The drag region is used to calculate drag layer offsets, but the + // dragVisualizeOffset in addition to the dragRect (the size) to position the outline. + dragVisualizeOffset = new Point(-DRAG_BITMAP_PADDING / 2, + iconPaddingTop - DRAG_BITMAP_PADDING / 2); + dragRect = new Rect(left, top, right, bottom); + } else if (child instanceof FolderIcon) { + int previewSize = r.getDimensionPixelSize(R.dimen.folder_preview_size); + dragRect = new Rect(0, 0, child.getWidth(), previewSize); + } + + // Clear the pressed state if necessary + if (child instanceof BubbleTextView) { + BubbleTextView icon = (BubbleTextView) child; + icon.clearPressedOrFocusedBackground(); + } + + mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); + b.recycle(); + + // Show the scrolling indicator when you pick up an item + showScrollingIndicator(false); + } + + void addApplicationShortcut(ShortcutInfo info, CellLayout target, long container, int screen, + int cellX, int cellY, boolean insertAtFirst, int intersectX, int intersectY) { + View view = mLauncher.createShortcut(R.layout.application, target, (ShortcutInfo) info); + + final int[] cellXY = new int[2]; + target.findCellForSpanThatIntersects(cellXY, 1, 1, intersectX, intersectY); + addInScreen(view, container, screen, cellXY[0], cellXY[1], 1, 1, insertAtFirst); + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screen, cellXY[0], + cellXY[1]); + } + + public boolean transitionStateShouldAllowDrop() { + return ((!isSwitchingState() || mTransitionProgress > 0.5f) && mState != State.SMALL); + } + + /** + * {@inheritDoc} + */ + public boolean acceptDrop(DragObject d) { + // If it's an external drop (e.g. from All Apps), check if it should be accepted + CellLayout dropTargetLayout = mDropToLayout; + if (d.dragSource != this) { + // Don't accept the drop if we're not over a screen at time of drop + if (dropTargetLayout == null) { + return false; + } + if (!transitionStateShouldAllowDrop()) return false; + + mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, + d.dragView, mDragViewVisualCenter); + + // We want the point to be mapped to the dragTarget. + if (mLauncher.isHotseatLayout(dropTargetLayout)) { + mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter); + } else { + mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null); + } + + int spanX = 1; + int spanY = 1; + if (mDragInfo != null) { + final CellLayout.CellInfo dragCellInfo = mDragInfo; + spanX = dragCellInfo.spanX; + spanY = dragCellInfo.spanY; + } else { + final ItemInfo dragInfo = (ItemInfo) d.dragInfo; + spanX = dragInfo.spanX; + spanY = dragInfo.spanY; + } + + int minSpanX = spanX; + int minSpanY = spanY; + if (d.dragInfo instanceof PendingAddWidgetInfo) { + minSpanX = ((PendingAddWidgetInfo) d.dragInfo).minSpanX; + minSpanY = ((PendingAddWidgetInfo) d.dragInfo).minSpanY; + } + + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, dropTargetLayout, + mTargetCell); + float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + if (willCreateUserFolder((ItemInfo) d.dragInfo, dropTargetLayout, + mTargetCell, distance, true)) { + return true; + } + if (willAddToExistingUserFolder((ItemInfo) d.dragInfo, dropTargetLayout, + mTargetCell, distance)) { + return true; + } + + int[] resultSpan = new int[2]; + mTargetCell = dropTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, + null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP); + boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0; + + // Don't accept the drop if there's no room for the item + if (!foundCell) { + // Don't show the message if we are dropping on the AllApps button and the hotseat + // is full + boolean isHotseat = mLauncher.isHotseatLayout(dropTargetLayout); + if (mTargetCell != null && isHotseat) { + Hotseat hotseat = mLauncher.getHotseat(); + if (hotseat.isAllAppsButtonRank( + hotseat.getOrderInHotseat(mTargetCell[0], mTargetCell[1]))) { + return false; + } + } + + mLauncher.showOutOfSpaceMessage(isHotseat); + return false; + } + } + return true; + } + + boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell, float + distance, boolean considerTimeout) { + if (distance > mMaxDistanceForFolderCreation) return false; + View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + + if (dropOverView != null) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams(); + if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) { + return false; + } + } + + boolean hasntMoved = false; + if (mDragInfo != null) { + hasntMoved = dropOverView == mDragInfo.cell; + } + + if (dropOverView == null || hasntMoved || (considerTimeout && !mCreateUserFolderOnDrop)) { + return false; + } + + boolean aboveShortcut = (dropOverView.getTag() instanceof ShortcutInfo); + boolean willBecomeShortcut = + (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || + info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT); + + return (aboveShortcut && willBecomeShortcut); + } + + boolean willAddToExistingUserFolder(Object dragInfo, CellLayout target, int[] targetCell, + float distance) { + if (distance > mMaxDistanceForFolderCreation) return false; + View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + + if (dropOverView != null) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams(); + if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) { + return false; + } + } + + if (dropOverView instanceof FolderIcon) { + FolderIcon fi = (FolderIcon) dropOverView; + if (fi.acceptDrop(dragInfo)) { + return true; + } + } + return false; + } + + boolean createUserFolderIfNecessary(View newView, long container, CellLayout target, + int[] targetCell, float distance, boolean external, DragView dragView, + Runnable postAnimationRunnable) { + if (distance > mMaxDistanceForFolderCreation) return false; + View v = target.getChildAt(targetCell[0], targetCell[1]); + + boolean hasntMoved = false; + if (mDragInfo != null) { + CellLayout cellParent = getParentCellLayoutForView(mDragInfo.cell); + hasntMoved = (mDragInfo.cellX == targetCell[0] && + mDragInfo.cellY == targetCell[1]) && (cellParent == target); + } + + if (v == null || hasntMoved || !mCreateUserFolderOnDrop) return false; + mCreateUserFolderOnDrop = false; + final int screen = (targetCell == null) ? mDragInfo.screen : indexOfChild(target); + + boolean aboveShortcut = (v.getTag() instanceof ShortcutInfo); + boolean willBecomeShortcut = (newView.getTag() instanceof ShortcutInfo); + + if (aboveShortcut && willBecomeShortcut) { + ShortcutInfo sourceInfo = (ShortcutInfo) newView.getTag(); + ShortcutInfo destInfo = (ShortcutInfo) v.getTag(); + // if the drag started here, we need to remove it from the workspace + if (!external) { + getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell); + } + + Rect folderLocation = new Rect(); + float scale = mLauncher.getDragLayer().getDescendantRectRelativeToSelf(v, folderLocation); + target.removeView(v); + + FolderIcon fi = + mLauncher.addFolder(target, container, screen, targetCell[0], targetCell[1]); + destInfo.cellX = -1; + destInfo.cellY = -1; + sourceInfo.cellX = -1; + sourceInfo.cellY = -1; + + // If the dragView is null, we can't animate + boolean animate = dragView != null; + if (animate) { + fi.performCreateAnimation(destInfo, v, sourceInfo, dragView, folderLocation, scale, + postAnimationRunnable); + } else { + fi.addItem(destInfo); + fi.addItem(sourceInfo); + } + return true; + } + return false; + } + + boolean addToExistingFolderIfNecessary(View newView, CellLayout target, int[] targetCell, + float distance, DragObject d, boolean external) { + if (distance > mMaxDistanceForFolderCreation) return false; + + View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + if (!mAddToExistingFolderOnDrop) return false; + mAddToExistingFolderOnDrop = false; + + if (dropOverView instanceof FolderIcon) { + FolderIcon fi = (FolderIcon) dropOverView; + if (fi.acceptDrop(d.dragInfo)) { + fi.onDrop(d); + + // if the drag started here, we need to remove it from the workspace + if (!external) { + getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell); + } + return true; + } + } + return false; + } + + public void onDrop(final DragObject d) { + mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, + mDragViewVisualCenter); + + CellLayout dropTargetLayout = mDropToLayout; + + // We want the point to be mapped to the dragTarget. + if (dropTargetLayout != null) { + if (mLauncher.isHotseatLayout(dropTargetLayout)) { + mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter); + } else { + mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null); + } + } + + int snapScreen = -1; + boolean resizeOnDrop = false; + if (d.dragSource != this) { + final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1] }; + onDropExternal(touchXY, d.dragInfo, dropTargetLayout, false, d); + } else if (mDragInfo != null) { + final View cell = mDragInfo.cell; + + Runnable resizeRunnable = null; + if (dropTargetLayout != null) { + // Move internally + boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout); + boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout); + long container = hasMovedIntoHotseat ? + LauncherSettings.Favorites.CONTAINER_HOTSEAT : + LauncherSettings.Favorites.CONTAINER_DESKTOP; + int screen = (mTargetCell[0] < 0) ? + mDragInfo.screen : indexOfChild(dropTargetLayout); + int spanX = mDragInfo != null ? mDragInfo.spanX : 1; + int spanY = mDragInfo != null ? mDragInfo.spanY : 1; + // First we find the cell nearest to point at which the item is + // dropped, without any consideration to whether there is an item there. + + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int) + mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell); + float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + + // If the item being dropped is a shortcut and the nearest drop + // cell also contains a shortcut, then create a folder with the two shortcuts. + if (!mInScrollArea && createUserFolderIfNecessary(cell, container, + dropTargetLayout, mTargetCell, distance, false, d.dragView, null)) { + return; + } + + if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell, + distance, d, false)) { + return; + } + + // Aside from the special case where we're dropping a shortcut onto a shortcut, + // we need to find the nearest cell location that is vacant + ItemInfo item = (ItemInfo) d.dragInfo; + int minSpanX = item.spanX; + int minSpanY = item.spanY; + if (item.minSpanX > 0 && item.minSpanY > 0) { + minSpanX = item.minSpanX; + minSpanY = item.minSpanY; + } + + int[] resultSpan = new int[2]; + mTargetCell = dropTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell, + mTargetCell, resultSpan, CellLayout.MODE_ON_DROP); + + boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0; + + // if the widget resizes on drop + if (foundCell && (cell instanceof AppWidgetHostView) && + (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) { + resizeOnDrop = true; + item.spanX = resultSpan[0]; + item.spanY = resultSpan[1]; + AppWidgetHostView awhv = (AppWidgetHostView) cell; + AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0], + resultSpan[1]); + } + + if (mCurrentPage != screen && !hasMovedIntoHotseat) { + snapScreen = screen; + snapToPage(screen); + } + + if (foundCell) { + final ItemInfo info = (ItemInfo) cell.getTag(); + if (hasMovedLayouts) { + // Reparent the view + getParentCellLayoutForView(cell).removeView(cell); + addInScreen(cell, container, screen, mTargetCell[0], mTargetCell[1], + info.spanX, info.spanY); + } + + // update the item's position after drop + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams(); + lp.cellX = lp.tmpCellX = mTargetCell[0]; + lp.cellY = lp.tmpCellY = mTargetCell[1]; + lp.cellHSpan = item.spanX; + lp.cellVSpan = item.spanY; + lp.isLockedToGrid = true; + cell.setId(LauncherModel.getCellLayoutChildId(container, mDragInfo.screen, + mTargetCell[0], mTargetCell[1], mDragInfo.spanX, mDragInfo.spanY)); + + if (container != LauncherSettings.Favorites.CONTAINER_HOTSEAT && + cell instanceof LauncherAppWidgetHostView) { + final CellLayout cellLayout = dropTargetLayout; + // We post this call so that the widget has a chance to be placed + // in its final location + + final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell; + AppWidgetProviderInfo pinfo = hostView.getAppWidgetInfo(); + if (pinfo != null && + pinfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) { + final Runnable addResizeFrame = new Runnable() { + public void run() { + DragLayer dragLayer = mLauncher.getDragLayer(); + dragLayer.addResizeFrame(info, hostView, cellLayout); + } + }; + resizeRunnable = (new Runnable() { + public void run() { + if (!isPageMoving()) { + addResizeFrame.run(); + } else { + mDelayedResizeRunnable = addResizeFrame; + } + } + }); + } + } + + LauncherModel.moveItemInDatabase(mLauncher, info, container, screen, lp.cellX, + lp.cellY); + } else { + // If we can't find a drop location, we return the item to its original position + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams(); + mTargetCell[0] = lp.cellX; + mTargetCell[1] = lp.cellY; + CellLayout layout = (CellLayout) cell.getParent().getParent(); + layout.markCellsAsOccupiedForView(cell); + } + } + + final CellLayout parent = (CellLayout) cell.getParent().getParent(); + final Runnable finalResizeRunnable = resizeRunnable; + // Prepare it to be animated into its new position + // This must be called after the view has been re-parented + final Runnable onCompleteRunnable = new Runnable() { + @Override + public void run() { + mAnimatingViewIntoPlace = false; + updateChildrenLayersEnabled(false); + if (finalResizeRunnable != null) { + finalResizeRunnable.run(); + } + } + }; + mAnimatingViewIntoPlace = true; + if (d.dragView.hasDrawn()) { + final ItemInfo info = (ItemInfo) cell.getTag(); + if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) { + int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE : + ANIMATE_INTO_POSITION_AND_DISAPPEAR; + animateWidgetDrop(info, parent, d.dragView, + onCompleteRunnable, animationType, cell, false); + } else { + int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION; + mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration, + onCompleteRunnable, this); + } + } else { + d.deferDragViewCleanupPostAnimation = false; + cell.setVisibility(VISIBLE); + } + parent.onDropChild(cell); + } + } + + public void setFinalScrollForPageChange(int screen) { + if (screen >= 0) { + mSavedScrollX = getScrollX(); + CellLayout cl = (CellLayout) getChildAt(screen); + mSavedTranslationX = cl.getTranslationX(); + mSavedRotationY = cl.getRotationY(); + final int newX = getChildOffset(screen) - getRelativeChildOffset(screen); + setScrollX(newX); + cl.setTranslationX(0f); + cl.setRotationY(0f); + } + } + + public void resetFinalScrollForPageChange(int screen) { + if (screen >= 0) { + CellLayout cl = (CellLayout) getChildAt(screen); + setScrollX(mSavedScrollX); + cl.setTranslationX(mSavedTranslationX); + cl.setRotationY(mSavedRotationY); + } + } + + public void getViewLocationRelativeToSelf(View v, int[] location) { + getLocationInWindow(location); + int x = location[0]; + int y = location[1]; + + v.getLocationInWindow(location); + int vX = location[0]; + int vY = location[1]; + + location[0] = vX - x; + location[1] = vY - y; + } + + public void onDragEnter(DragObject d) { + mDragEnforcer.onDragEnter(); + mCreateUserFolderOnDrop = false; + mAddToExistingFolderOnDrop = false; + + mDropToLayout = null; + CellLayout layout = getCurrentDropLayout(); + setCurrentDropLayout(layout); + setCurrentDragOverlappingLayout(layout); + + // Because we don't have space in the Phone UI (the CellLayouts run to the edge) we + // don't need to show the outlines + if (LauncherApplication.isScreenLarge()) { + showOutlines(); + } + } + + static Rect getCellLayoutMetrics(Launcher launcher, int orientation) { + Resources res = launcher.getResources(); + Display display = launcher.getWindowManager().getDefaultDisplay(); + Point smallestSize = new Point(); + Point largestSize = new Point(); + display.getCurrentSizeRange(smallestSize, largestSize); + if (orientation == CellLayout.LANDSCAPE) { + if (mLandscapeCellLayoutMetrics == null) { + int paddingLeft = res.getDimensionPixelSize(R.dimen.workspace_left_padding_land); + int paddingRight = res.getDimensionPixelSize(R.dimen.workspace_right_padding_land); + int paddingTop = res.getDimensionPixelSize(R.dimen.workspace_top_padding_land); + int paddingBottom = res.getDimensionPixelSize(R.dimen.workspace_bottom_padding_land); + int width = largestSize.x - paddingLeft - paddingRight; + int height = smallestSize.y - paddingTop - paddingBottom; + mLandscapeCellLayoutMetrics = new Rect(); + CellLayout.getMetrics(mLandscapeCellLayoutMetrics, res, + width, height, LauncherModel.getCellCountX(), LauncherModel.getCellCountY(), + orientation); + } + return mLandscapeCellLayoutMetrics; + } else if (orientation == CellLayout.PORTRAIT) { + if (mPortraitCellLayoutMetrics == null) { + int paddingLeft = res.getDimensionPixelSize(R.dimen.workspace_left_padding_land); + int paddingRight = res.getDimensionPixelSize(R.dimen.workspace_right_padding_land); + int paddingTop = res.getDimensionPixelSize(R.dimen.workspace_top_padding_land); + int paddingBottom = res.getDimensionPixelSize(R.dimen.workspace_bottom_padding_land); + int width = smallestSize.x - paddingLeft - paddingRight; + int height = largestSize.y - paddingTop - paddingBottom; + mPortraitCellLayoutMetrics = new Rect(); + CellLayout.getMetrics(mPortraitCellLayoutMetrics, res, + width, height, LauncherModel.getCellCountX(), LauncherModel.getCellCountY(), + orientation); + } + return mPortraitCellLayoutMetrics; + } + return null; + } + + public void onDragExit(DragObject d) { + mDragEnforcer.onDragExit(); + + // Here we store the final page that will be dropped to, if the workspace in fact + // receives the drop + if (mInScrollArea) { + if (isPageMoving()) { + // If the user drops while the page is scrolling, we should use that page as the + // destination instead of the page that is being hovered over. + mDropToLayout = (CellLayout) getPageAt(getNextPage()); + } else { + mDropToLayout = mDragOverlappingLayout; + } + } else { + mDropToLayout = mDragTargetLayout; + } + + if (mDragMode == DRAG_MODE_CREATE_FOLDER) { + mCreateUserFolderOnDrop = true; + } else if (mDragMode == DRAG_MODE_ADD_TO_FOLDER) { + mAddToExistingFolderOnDrop = true; + } + + // Reset the scroll area and previous drag target + onResetScrollArea(); + setCurrentDropLayout(null); + setCurrentDragOverlappingLayout(null); + + mSpringLoadedDragController.cancel(); + + if (!mIsPageMoving) { + hideOutlines(); + } + } + + void setCurrentDropLayout(CellLayout layout) { + if (mDragTargetLayout != null) { + mDragTargetLayout.revertTempState(); + mDragTargetLayout.onDragExit(); + } + mDragTargetLayout = layout; + if (mDragTargetLayout != null) { + mDragTargetLayout.onDragEnter(); + } + cleanupReorder(true); + cleanupFolderCreation(); + setCurrentDropOverCell(-1, -1); + } + + void setCurrentDragOverlappingLayout(CellLayout layout) { + if (mDragOverlappingLayout != null) { + mDragOverlappingLayout.setIsDragOverlapping(false); + } + mDragOverlappingLayout = layout; + if (mDragOverlappingLayout != null) { + mDragOverlappingLayout.setIsDragOverlapping(true); + } + invalidate(); + } + + void setCurrentDropOverCell(int x, int y) { + if (x != mDragOverX || y != mDragOverY) { + mDragOverX = x; + mDragOverY = y; + setDragMode(DRAG_MODE_NONE); + } + } + + void setDragMode(int dragMode) { + if (dragMode != mDragMode) { + if (dragMode == DRAG_MODE_NONE) { + cleanupAddToFolder(); + // We don't want to cancel the re-order alarm every time the target cell changes + // as this feels to slow / unresponsive. + cleanupReorder(false); + cleanupFolderCreation(); + } else if (dragMode == DRAG_MODE_ADD_TO_FOLDER) { + cleanupReorder(true); + cleanupFolderCreation(); + } else if (dragMode == DRAG_MODE_CREATE_FOLDER) { + cleanupAddToFolder(); + cleanupReorder(true); + } else if (dragMode == DRAG_MODE_REORDER) { + cleanupAddToFolder(); + cleanupFolderCreation(); + } + mDragMode = dragMode; + } + } + + private void cleanupFolderCreation() { + if (mDragFolderRingAnimator != null) { + mDragFolderRingAnimator.animateToNaturalState(); + } + mFolderCreationAlarm.cancelAlarm(); + } + + private void cleanupAddToFolder() { + if (mDragOverFolderIcon != null) { + mDragOverFolderIcon.onDragExit(null); + mDragOverFolderIcon = null; + } + } + + private void cleanupReorder(boolean cancelAlarm) { + // Any pending reorders are canceled + if (cancelAlarm) { + mReorderAlarm.cancelAlarm(); + } + mLastReorderX = -1; + mLastReorderY = -1; + } + + public DropTarget getDropTargetDelegate(DragObject d) { + return null; + } + + /* + * + * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's + * coordinate space. The argument xy is modified with the return result. + * + */ + void mapPointFromSelfToChild(View v, float[] xy) { + mapPointFromSelfToChild(v, xy, null); + } + + /* + * + * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's + * coordinate space. The argument xy is modified with the return result. + * + * if cachedInverseMatrix is not null, this method will just use that matrix instead of + * computing it itself; we use this to avoid redundant matrix inversions in + * findMatchingPageForDragOver + * + */ + void mapPointFromSelfToChild(View v, float[] xy, Matrix cachedInverseMatrix) { + if (cachedInverseMatrix == null) { + v.getMatrix().invert(mTempInverseMatrix); + cachedInverseMatrix = mTempInverseMatrix; + } + int scrollX = getScrollX(); + if (mNextPage != INVALID_PAGE) { + scrollX = mScroller.getFinalX(); + } + xy[0] = xy[0] + scrollX - v.getLeft(); + xy[1] = xy[1] + getScrollY() - v.getTop(); + cachedInverseMatrix.mapPoints(xy); + } + + + void mapPointFromSelfToHotseatLayout(Hotseat hotseat, float[] xy) { + hotseat.getLayout().getMatrix().invert(mTempInverseMatrix); + xy[0] = xy[0] - hotseat.getLeft() - hotseat.getLayout().getLeft(); + xy[1] = xy[1] - hotseat.getTop() - hotseat.getLayout().getTop(); + mTempInverseMatrix.mapPoints(xy); + } + + /* + * + * Convert the 2D coordinate xy from this CellLayout's coordinate space to + * the parent View's coordinate space. The argument xy is modified with the return result. + * + */ + void mapPointFromChildToSelf(View v, float[] xy) { + v.getMatrix().mapPoints(xy); + int scrollX = getScrollX(); + if (mNextPage != INVALID_PAGE) { + scrollX = mScroller.getFinalX(); + } + xy[0] -= (scrollX - v.getLeft()); + xy[1] -= (getScrollY() - v.getTop()); + } + + static private float squaredDistance(float[] point1, float[] point2) { + float distanceX = point1[0] - point2[0]; + float distanceY = point2[1] - point2[1]; + return distanceX * distanceX + distanceY * distanceY; + } + + /* + * + * Returns true if the passed CellLayout cl overlaps with dragView + * + */ + boolean overlaps(CellLayout cl, DragView dragView, + int dragViewX, int dragViewY, Matrix cachedInverseMatrix) { + // Transform the coordinates of the item being dragged to the CellLayout's coordinates + final float[] draggedItemTopLeft = mTempDragCoordinates; + draggedItemTopLeft[0] = dragViewX; + draggedItemTopLeft[1] = dragViewY; + final float[] draggedItemBottomRight = mTempDragBottomRightCoordinates; + draggedItemBottomRight[0] = draggedItemTopLeft[0] + dragView.getDragRegionWidth(); + draggedItemBottomRight[1] = draggedItemTopLeft[1] + dragView.getDragRegionHeight(); + + // Transform the dragged item's top left coordinates + // to the CellLayout's local coordinates + mapPointFromSelfToChild(cl, draggedItemTopLeft, cachedInverseMatrix); + float overlapRegionLeft = Math.max(0f, draggedItemTopLeft[0]); + float overlapRegionTop = Math.max(0f, draggedItemTopLeft[1]); + + if (overlapRegionLeft <= cl.getWidth() && overlapRegionTop >= 0) { + // Transform the dragged item's bottom right coordinates + // to the CellLayout's local coordinates + mapPointFromSelfToChild(cl, draggedItemBottomRight, cachedInverseMatrix); + float overlapRegionRight = Math.min(cl.getWidth(), draggedItemBottomRight[0]); + float overlapRegionBottom = Math.min(cl.getHeight(), draggedItemBottomRight[1]); + + if (overlapRegionRight >= 0 && overlapRegionBottom <= cl.getHeight()) { + float overlap = (overlapRegionRight - overlapRegionLeft) * + (overlapRegionBottom - overlapRegionTop); + if (overlap > 0) { + return true; + } + } + } + return false; + } + + /* + * + * This method returns the CellLayout that is currently being dragged to. In order to drag + * to a CellLayout, either the touch point must be directly over the CellLayout, or as a second + * strategy, we see if the dragView is overlapping any CellLayout and choose the closest one + * + * Return null if no CellLayout is currently being dragged over + * + */ + private CellLayout findMatchingPageForDragOver( + DragView dragView, float originX, float originY, boolean exact) { + // We loop through all the screens (ie CellLayouts) and see which ones overlap + // with the item being dragged and then choose the one that's closest to the touch point + final int screenCount = getChildCount(); + CellLayout bestMatchingScreen = null; + float smallestDistSoFar = Float.MAX_VALUE; + + for (int i = 0; i < screenCount; i++) { + CellLayout cl = (CellLayout) getChildAt(i); + + final float[] touchXy = {originX, originY}; + // Transform the touch coordinates to the CellLayout's local coordinates + // If the touch point is within the bounds of the cell layout, we can return immediately + cl.getMatrix().invert(mTempInverseMatrix); + mapPointFromSelfToChild(cl, touchXy, mTempInverseMatrix); + + if (touchXy[0] >= 0 && touchXy[0] <= cl.getWidth() && + touchXy[1] >= 0 && touchXy[1] <= cl.getHeight()) { + return cl; + } + + if (!exact) { + // Get the center of the cell layout in screen coordinates + final float[] cellLayoutCenter = mTempCellLayoutCenterCoordinates; + cellLayoutCenter[0] = cl.getWidth()/2; + cellLayoutCenter[1] = cl.getHeight()/2; + mapPointFromChildToSelf(cl, cellLayoutCenter); + + touchXy[0] = originX; + touchXy[1] = originY; + + // Calculate the distance between the center of the CellLayout + // and the touch point + float dist = squaredDistance(touchXy, cellLayoutCenter); + + if (dist < smallestDistSoFar) { + smallestDistSoFar = dist; + bestMatchingScreen = cl; + } + } + } + return bestMatchingScreen; + } + + // This is used to compute the visual center of the dragView. This point is then + // used to visualize drop locations and determine where to drop an item. The idea is that + // the visual center represents the user's interpretation of where the item is, and hence + // is the appropriate point to use when determining drop location. + private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, + DragView dragView, float[] recycle) { + float res[]; + if (recycle == null) { + res = new float[2]; + } else { + res = recycle; + } + + // First off, the drag view has been shifted in a way that is not represented in the + // x and y values or the x/yOffsets. Here we account for that shift. + x += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetX); + y += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY); + + // These represent the visual top and left of drag view if a dragRect was provided. + // If a dragRect was not provided, then they correspond to the actual view left and + // top, as the dragRect is in that case taken to be the entire dragView. + // R.dimen.dragViewOffsetY. + int left = x - xOffset; + int top = y - yOffset; + + // In order to find the visual center, we shift by half the dragRect + res[0] = left + dragView.getDragRegion().width() / 2; + res[1] = top + dragView.getDragRegion().height() / 2; + + return res; + } + + private boolean isDragWidget(DragObject d) { + return (d.dragInfo instanceof LauncherAppWidgetInfo || + d.dragInfo instanceof PendingAddWidgetInfo); + } + private boolean isExternalDragWidget(DragObject d) { + return d.dragSource != this && isDragWidget(d); + } + + public void onDragOver(DragObject d) { + // Skip drag over events while we are dragging over side pages + if (mInScrollArea || mIsSwitchingState || mState == State.SMALL) return; + + Rect r = new Rect(); + CellLayout layout = null; + ItemInfo item = (ItemInfo) d.dragInfo; + + // Ensure that we have proper spans for the item that we are dropping + if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found"); + mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, + d.dragView, mDragViewVisualCenter); + + final View child = (mDragInfo == null) ? null : mDragInfo.cell; + // Identify whether we have dragged over a side page + if (isSmall()) { + if (mLauncher.getHotseat() != null && !isExternalDragWidget(d)) { + mLauncher.getHotseat().getHitRect(r); + if (r.contains(d.x, d.y)) { + layout = mLauncher.getHotseat().getLayout(); + } + } + if (layout == null) { + layout = findMatchingPageForDragOver(d.dragView, d.x, d.y, false); + } + if (layout != mDragTargetLayout) { + + setCurrentDropLayout(layout); + setCurrentDragOverlappingLayout(layout); + + boolean isInSpringLoadedMode = (mState == State.SPRING_LOADED); + if (isInSpringLoadedMode) { + if (mLauncher.isHotseatLayout(layout)) { + mSpringLoadedDragController.cancel(); + } else { + mSpringLoadedDragController.setAlarm(mDragTargetLayout); + } + } + } + } else { + // Test to see if we are over the hotseat otherwise just use the current page + if (mLauncher.getHotseat() != null && !isDragWidget(d)) { + mLauncher.getHotseat().getHitRect(r); + if (r.contains(d.x, d.y)) { + layout = mLauncher.getHotseat().getLayout(); + } + } + if (layout == null) { + layout = getCurrentDropLayout(); + } + if (layout != mDragTargetLayout) { + setCurrentDropLayout(layout); + setCurrentDragOverlappingLayout(layout); + } + } + + // Handle the drag over + if (mDragTargetLayout != null) { + // We want the point to be mapped to the dragTarget. + if (mLauncher.isHotseatLayout(mDragTargetLayout)) { + mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter); + } else { + mapPointFromSelfToChild(mDragTargetLayout, mDragViewVisualCenter, null); + } + + ItemInfo info = (ItemInfo) d.dragInfo; + + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], item.spanX, item.spanY, + mDragTargetLayout, mTargetCell); + + setCurrentDropOverCell(mTargetCell[0], mTargetCell[1]); + + float targetCellDistance = mDragTargetLayout.getDistanceFromCell( + mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell); + + final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], + mTargetCell[1]); + + manageFolderFeedback(info, mDragTargetLayout, mTargetCell, + targetCellDistance, dragOverView); + + int minSpanX = item.spanX; + int minSpanY = item.spanY; + if (item.minSpanX > 0 && item.minSpanY > 0) { + minSpanX = item.minSpanX; + minSpanY = item.minSpanY; + } + + boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int) + mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX, + item.spanY, child, mTargetCell); + + if (!nearestDropOccupied) { + mDragTargetLayout.visualizeDropLocation(child, mDragOutline, + (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], + mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, + d.dragView.getDragVisualizeOffset(), d.dragView.getDragRegion()); + } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER) + && !mReorderAlarm.alarmPending() && (mLastReorderX != mTargetCell[0] || + mLastReorderY != mTargetCell[1])) { + + // Otherwise, if we aren't adding to or creating a folder and there's no pending + // reorder, then we schedule a reorder + ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter, + minSpanX, minSpanY, item.spanX, item.spanY, d.dragView, child); + mReorderAlarm.setOnAlarmListener(listener); + mReorderAlarm.setAlarm(REORDER_TIMEOUT); + } + + if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER || + !nearestDropOccupied) { + if (mDragTargetLayout != null) { + mDragTargetLayout.revertTempState(); + } + } + } + } + + private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout, + int[] targetCell, float distance, View dragOverView) { + boolean userFolderPending = willCreateUserFolder(info, targetLayout, targetCell, distance, + false); + + if (mDragMode == DRAG_MODE_NONE && userFolderPending && + !mFolderCreationAlarm.alarmPending()) { + mFolderCreationAlarm.setOnAlarmListener(new + FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1])); + mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); + return; + } + + boolean willAddToFolder = + willAddToExistingUserFolder(info, targetLayout, targetCell, distance); + + if (willAddToFolder && mDragMode == DRAG_MODE_NONE) { + mDragOverFolderIcon = ((FolderIcon) dragOverView); + mDragOverFolderIcon.onDragEnter(info); + if (targetLayout != null) { + targetLayout.clearDragOutlines(); + } + setDragMode(DRAG_MODE_ADD_TO_FOLDER); + return; + } + + if (mDragMode == DRAG_MODE_ADD_TO_FOLDER && !willAddToFolder) { + setDragMode(DRAG_MODE_NONE); + } + if (mDragMode == DRAG_MODE_CREATE_FOLDER && !userFolderPending) { + setDragMode(DRAG_MODE_NONE); + } + + return; + } + + class FolderCreationAlarmListener implements OnAlarmListener { + CellLayout layout; + int cellX; + int cellY; + + public FolderCreationAlarmListener(CellLayout layout, int cellX, int cellY) { + this.layout = layout; + this.cellX = cellX; + this.cellY = cellY; + } + + public void onAlarm(Alarm alarm) { + if (mDragFolderRingAnimator == null) { + mDragFolderRingAnimator = new FolderRingAnimator(mLauncher, null); + } + mDragFolderRingAnimator.setCell(cellX, cellY); + mDragFolderRingAnimator.setCellLayout(layout); + mDragFolderRingAnimator.animateToAcceptState(); + layout.showFolderAccept(mDragFolderRingAnimator); + layout.clearDragOutlines(); + setDragMode(DRAG_MODE_CREATE_FOLDER); + } + } + + class ReorderAlarmListener implements OnAlarmListener { + float[] dragViewCenter; + int minSpanX, minSpanY, spanX, spanY; + DragView dragView; + View child; + + public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX, + int spanY, DragView dragView, View child) { + this.dragViewCenter = dragViewCenter; + this.minSpanX = minSpanX; + this.minSpanY = minSpanY; + this.spanX = spanX; + this.spanY = spanY; + this.child = child; + this.dragView = dragView; + } + + public void onAlarm(Alarm alarm) { + int[] resultSpan = new int[2]; + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], spanX, spanY, mDragTargetLayout, mTargetCell); + mLastReorderX = mTargetCell[0]; + mLastReorderY = mTargetCell[1]; + + mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, + child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER); + + if (mTargetCell[0] < 0 || mTargetCell[1] < 0) { + mDragTargetLayout.revertTempState(); + } else { + setDragMode(DRAG_MODE_REORDER); + } + + boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY; + mDragTargetLayout.visualizeDropLocation(child, mDragOutline, + (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], + mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, + dragView.getDragVisualizeOffset(), dragView.getDragRegion()); + } + } + + @Override + public void getHitRect(Rect outRect) { + // We want the workspace to have the whole area of the display (it will find the correct + // cell layout to drop to in the existing drag/drop logic. + outRect.set(0, 0, mDisplaySize.x, mDisplaySize.y); + } + + /** + * Add the item specified by dragInfo to the given layout. + * @return true if successful + */ + public boolean addExternalItemToScreen(ItemInfo dragInfo, CellLayout layout) { + if (layout.findCellForSpan(mTempEstimate, dragInfo.spanX, dragInfo.spanY)) { + onDropExternal(dragInfo.dropPos, (ItemInfo) dragInfo, (CellLayout) layout, false); + return true; + } + mLauncher.showOutOfSpaceMessage(mLauncher.isHotseatLayout(layout)); + return false; + } + + private void onDropExternal(int[] touchXY, Object dragInfo, + CellLayout cellLayout, boolean insertAtFirst) { + onDropExternal(touchXY, dragInfo, cellLayout, insertAtFirst, null); + } + + /** + * Drop an item that didn't originate on one of the workspace screens. + * It may have come from Launcher (e.g. from all apps or customize), or it may have + * come from another app altogether. + * + * NOTE: This can also be called when we are outside of a drag event, when we want + * to add an item to one of the workspace screens. + */ + private void onDropExternal(final int[] touchXY, final Object dragInfo, + final CellLayout cellLayout, boolean insertAtFirst, DragObject d) { + final Runnable exitSpringLoadedRunnable = new Runnable() { + @Override + public void run() { + mLauncher.exitSpringLoadedDragModeDelayed(true, false, null); + } + }; + + ItemInfo info = (ItemInfo) dragInfo; + int spanX = info.spanX; + int spanY = info.spanY; + if (mDragInfo != null) { + spanX = mDragInfo.spanX; + spanY = mDragInfo.spanY; + } + + final long container = mLauncher.isHotseatLayout(cellLayout) ? + LauncherSettings.Favorites.CONTAINER_HOTSEAT : + LauncherSettings.Favorites.CONTAINER_DESKTOP; + final int screen = indexOfChild(cellLayout); + if (!mLauncher.isHotseatLayout(cellLayout) && screen != mCurrentPage + && mState != State.SPRING_LOADED) { + snapToPage(screen); + } + + if (info instanceof PendingAddItemInfo) { + final PendingAddItemInfo pendingInfo = (PendingAddItemInfo) dragInfo; + + boolean findNearestVacantCell = true; + if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { + mTargetCell = findNearestArea((int) touchXY[0], (int) touchXY[1], spanX, spanY, + cellLayout, mTargetCell); + float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + if (willCreateUserFolder((ItemInfo) d.dragInfo, cellLayout, mTargetCell, + distance, true) || willAddToExistingUserFolder((ItemInfo) d.dragInfo, + cellLayout, mTargetCell, distance)) { + findNearestVacantCell = false; + } + } + + final ItemInfo item = (ItemInfo) d.dragInfo; + boolean updateWidgetSize = false; + if (findNearestVacantCell) { + int minSpanX = item.spanX; + int minSpanY = item.spanY; + if (item.minSpanX > 0 && item.minSpanY > 0) { + minSpanX = item.minSpanX; + minSpanY = item.minSpanY; + } + int[] resultSpan = new int[2]; + mTargetCell = cellLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, info.spanX, info.spanY, + null, mTargetCell, resultSpan, CellLayout.MODE_ON_DROP_EXTERNAL); + + if (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY) { + updateWidgetSize = true; + } + item.spanX = resultSpan[0]; + item.spanY = resultSpan[1]; + } + + Runnable onAnimationCompleteRunnable = new Runnable() { + @Override + public void run() { + // When dragging and dropping from customization tray, we deal with creating + // widgets/shortcuts/folders in a slightly different way + switch (pendingInfo.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + int span[] = new int[2]; + span[0] = item.spanX; + span[1] = item.spanY; + mLauncher.addAppWidgetFromDrop((PendingAddWidgetInfo) pendingInfo, + container, screen, mTargetCell, span, null); + break; + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + mLauncher.processShortcutFromDrop(pendingInfo.componentName, + container, screen, mTargetCell, null); + break; + default: + throw new IllegalStateException("Unknown item type: " + + pendingInfo.itemType); + } + } + }; + View finalView = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET + ? ((PendingAddWidgetInfo) pendingInfo).boundWidget : null; + + if (finalView instanceof AppWidgetHostView && updateWidgetSize) { + AppWidgetHostView awhv = (AppWidgetHostView) finalView; + AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, item.spanX, + item.spanY); + } + + int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR; + if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && + ((PendingAddWidgetInfo) pendingInfo).info.configure != null) { + animationStyle = ANIMATE_INTO_POSITION_AND_REMAIN; + } + animateWidgetDrop(info, cellLayout, d.dragView, onAnimationCompleteRunnable, + animationStyle, finalView, true); + } else { + // This is for other drag/drop cases, like dragging from All Apps + View view = null; + + switch (info.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + if (info.container == NO_ID && info instanceof ApplicationInfo) { + // Came from all apps -- make a copy + info = new ShortcutInfo((ApplicationInfo) info); + } + view = mLauncher.createShortcut(R.layout.application, cellLayout, + (ShortcutInfo) info); + break; + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout, + (FolderInfo) info, mIconCache); + break; + default: + throw new IllegalStateException("Unknown item type: " + info.itemType); + } + + // First we find the cell nearest to point at which the item is + // dropped, without any consideration to whether there is an item there. + if (touchXY != null) { + mTargetCell = findNearestArea((int) touchXY[0], (int) touchXY[1], spanX, spanY, + cellLayout, mTargetCell); + float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + d.postAnimationRunnable = exitSpringLoadedRunnable; + if (createUserFolderIfNecessary(view, container, cellLayout, mTargetCell, distance, + true, d.dragView, d.postAnimationRunnable)) { + return; + } + if (addToExistingFolderIfNecessary(view, cellLayout, mTargetCell, distance, d, + true)) { + return; + } + } + + if (touchXY != null) { + // when dragging and dropping, just find the closest free spot + mTargetCell = cellLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], 1, 1, 1, 1, + null, mTargetCell, null, CellLayout.MODE_ON_DROP_EXTERNAL); + } else { + cellLayout.findCellForSpan(mTargetCell, 1, 1); + } + addInScreen(view, container, screen, mTargetCell[0], mTargetCell[1], info.spanX, + info.spanY, insertAtFirst); + cellLayout.onDropChild(view); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); + cellLayout.getShortcutsAndWidgets().measureChild(view); + + + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screen, + lp.cellX, lp.cellY); + + if (d.dragView != null) { + // We wrap the animation call in the temporary set and reset of the current + // cellLayout to its final transform -- this means we animate the drag view to + // the correct final location. + setFinalTransitionTransform(cellLayout); + mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, view, + exitSpringLoadedRunnable); + resetTransitionTransform(cellLayout); + } + } + } + + public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) { + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo.spanX, + widgetInfo.spanY, widgetInfo, false); + int visibility = layout.getVisibility(); + layout.setVisibility(VISIBLE); + + int width = MeasureSpec.makeMeasureSpec(unScaledSize[0], MeasureSpec.EXACTLY); + int height = MeasureSpec.makeMeasureSpec(unScaledSize[1], MeasureSpec.EXACTLY); + Bitmap b = Bitmap.createBitmap(unScaledSize[0], unScaledSize[1], + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + + layout.measure(width, height); + layout.layout(0, 0, unScaledSize[0], unScaledSize[1]); + layout.draw(c); + c.setBitmap(null); + layout.setVisibility(visibility); + return b; + } + + private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY, + DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, + boolean external, boolean scale) { + // Now we animate the dragView, (ie. the widget or shortcut preview) into its final + // location and size on the home screen. + int spanX = info.spanX; + int spanY = info.spanY; + + Rect r = estimateItemPosition(layout, info, targetCell[0], targetCell[1], spanX, spanY); + loc[0] = r.left; + loc[1] = r.top; + + setFinalTransitionTransform(layout); + float cellLayoutScale = + mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(layout, loc); + resetTransitionTransform(layout); + + float dragViewScaleX; + float dragViewScaleY; + if (scale) { + dragViewScaleX = (1.0f * r.width()) / dragView.getMeasuredWidth(); + dragViewScaleY = (1.0f * r.height()) / dragView.getMeasuredHeight(); + } else { + dragViewScaleX = 1f; + dragViewScaleY = 1f; + } + + // The animation will scale the dragView about its center, so we need to center about + // the final location. + loc[0] -= (dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2; + loc[1] -= (dragView.getMeasuredHeight() - cellLayoutScale * r.height()) / 2; + + scaleXY[0] = dragViewScaleX * cellLayoutScale; + scaleXY[1] = dragViewScaleY * cellLayoutScale; + } + + public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, DragView dragView, + final Runnable onCompleteRunnable, int animationType, final View finalView, + boolean external) { + Rect from = new Rect(); + mLauncher.getDragLayer().getViewRectRelativeToSelf(dragView, from); + + int[] finalPos = new int[2]; + float scaleXY[] = new float[2]; + boolean scalePreview = !(info instanceof PendingAddShortcutInfo); + getFinalPositionForDropAnimation(finalPos, scaleXY, dragView, cellLayout, info, mTargetCell, + external, scalePreview); + + Resources res = mLauncher.getResources(); + int duration = res.getInteger(R.integer.config_dropAnimMaxDuration) - 200; + + // In the case where we've prebound the widget, we remove it from the DragLayer + if (finalView instanceof AppWidgetHostView && external) { + Log.d(TAG, "6557954 Animate widget drop, final view is appWidgetHostView"); + mLauncher.getDragLayer().removeView(finalView); + } + if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external) && finalView != null) { + Bitmap crossFadeBitmap = createWidgetBitmap(info, finalView); + dragView.setCrossFadeBitmap(crossFadeBitmap); + dragView.crossFade((int) (duration * 0.8f)); + } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && external) { + scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]); + } + + DragLayer dragLayer = mLauncher.getDragLayer(); + if (animationType == CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION) { + mLauncher.getDragLayer().animateViewIntoPosition(dragView, finalPos, 0f, 0.1f, 0.1f, + DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration); + } else { + int endStyle; + if (animationType == ANIMATE_INTO_POSITION_AND_REMAIN) { + endStyle = DragLayer.ANIMATION_END_REMAIN_VISIBLE; + } else { + endStyle = DragLayer.ANIMATION_END_DISAPPEAR;; + } + + Runnable onComplete = new Runnable() { + @Override + public void run() { + if (finalView != null) { + finalView.setVisibility(VISIBLE); + } + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + }; + dragLayer.animateViewIntoPosition(dragView, from.left, from.top, finalPos[0], + finalPos[1], 1, 1, 1, scaleXY[0], scaleXY[1], onComplete, endStyle, + duration, this); + } + } + + public void setFinalTransitionTransform(CellLayout layout) { + if (isSwitchingState()) { + int index = indexOfChild(layout); + mCurrentScaleX = layout.getScaleX(); + mCurrentScaleY = layout.getScaleY(); + mCurrentTranslationX = layout.getTranslationX(); + mCurrentTranslationY = layout.getTranslationY(); + mCurrentRotationY = layout.getRotationY(); + layout.setScaleX(mNewScaleXs[index]); + layout.setScaleY(mNewScaleYs[index]); + layout.setTranslationX(mNewTranslationXs[index]); + layout.setTranslationY(mNewTranslationYs[index]); + layout.setRotationY(mNewRotationYs[index]); + } + } + public void resetTransitionTransform(CellLayout layout) { + if (isSwitchingState()) { + mCurrentScaleX = layout.getScaleX(); + mCurrentScaleY = layout.getScaleY(); + mCurrentTranslationX = layout.getTranslationX(); + mCurrentTranslationY = layout.getTranslationY(); + mCurrentRotationY = layout.getRotationY(); + layout.setScaleX(mCurrentScaleX); + layout.setScaleY(mCurrentScaleY); + layout.setTranslationX(mCurrentTranslationX); + layout.setTranslationY(mCurrentTranslationY); + layout.setRotationY(mCurrentRotationY); + } + } + + /** + * Return the current {@link CellLayout}, correctly picking the destination + * screen while a scroll is in progress. + */ + public CellLayout getCurrentDropLayout() { + return (CellLayout) getChildAt(getNextPage()); + } + + /** + * Return the current CellInfo describing our current drag; this method exists + * so that Launcher can sync this object with the correct info when the activity is created/ + * destroyed + * + */ + public CellLayout.CellInfo getDragInfo() { + return mDragInfo; + } + + /** + * Calculate the nearest cell where the given object would be dropped. + * + * pixelX and pixelY should be in the coordinate system of layout + */ + private int[] findNearestArea(int pixelX, int pixelY, + int spanX, int spanY, CellLayout layout, int[] recycle) { + return layout.findNearestArea( + pixelX, pixelY, spanX, spanY, recycle); + } + + void setup(DragController dragController) { + mSpringLoadedDragController = new SpringLoadedDragController(mLauncher); + mDragController = dragController; + + // hardware layers on children are enabled on startup, but should be disabled until + // needed + updateChildrenLayersEnabled(false); + setWallpaperDimension(); + } + + /** + * Called at the end of a drag which originated on the workspace. + */ + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + if (success) { + if (target != this) { + if (mDragInfo != null) { + getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell); + if (mDragInfo.cell instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) mDragInfo.cell); + } + } + } + } else if (mDragInfo != null) { + CellLayout cellLayout; + if (mLauncher.isHotseatLayout(target)) { + cellLayout = mLauncher.getHotseat().getLayout(); + } else { + cellLayout = (CellLayout) getChildAt(mDragInfo.screen); + } + cellLayout.onDropChild(mDragInfo.cell); + } + if (d.cancelled && mDragInfo.cell != null) { + mDragInfo.cell.setVisibility(VISIBLE); + } + mDragOutline = null; + mDragInfo = null; + + // Hide the scrolling indicator after you pick up an item + hideScrollingIndicator(false); + } + + void updateItemLocationsInDatabase(CellLayout cl) { + int count = cl.getShortcutsAndWidgets().getChildCount(); + + int screen = indexOfChild(cl); + int container = Favorites.CONTAINER_DESKTOP; + + if (mLauncher.isHotseatLayout(cl)) { + screen = -1; + container = Favorites.CONTAINER_HOTSEAT; + } + + for (int i = 0; i < count; i++) { + View v = cl.getShortcutsAndWidgets().getChildAt(i); + ItemInfo info = (ItemInfo) v.getTag(); + // Null check required as the AllApps button doesn't have an item info + if (info != null && info.requiresDbUpdate) { + info.requiresDbUpdate = false; + LauncherModel.modifyItemInDatabase(mLauncher, info, container, screen, info.cellX, + info.cellY, info.spanX, info.spanY); + } + } + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + @Override + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + + @Override + public void onFlingToDeleteCompleted() { + // Do nothing + } + + public boolean isDropEnabled() { + return true; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + super.onRestoreInstanceState(state); + Launcher.setScreen(mCurrentPage); + } + + @Override + protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { + // We don't dispatch restoreInstanceState to our children using this code path. + // Some pages will be restored immediately as their items are bound immediately, and + // others we will need to wait until after their items are bound. + mSavedStates = container; + } + + public void restoreInstanceStateForChild(int child) { + if (mSavedStates != null) { + mRestoredPages.add(child); + CellLayout cl = (CellLayout) getChildAt(child); + cl.restoreInstanceState(mSavedStates); + } + } + + public void restoreInstanceStateForRemainingPages() { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + if (!mRestoredPages.contains(i)) { + restoreInstanceStateForChild(i); + } + } + mRestoredPages.clear(); + } + + @Override + public void scrollLeft() { + if (!isSmall() && !mIsSwitchingState) { + super.scrollLeft(); + } + Folder openFolder = getOpenFolder(); + if (openFolder != null) { + openFolder.completeDragExit(); + } + } + + @Override + public void scrollRight() { + if (!isSmall() && !mIsSwitchingState) { + super.scrollRight(); + } + Folder openFolder = getOpenFolder(); + if (openFolder != null) { + openFolder.completeDragExit(); + } + } + + @Override + public boolean onEnterScrollArea(int x, int y, int direction) { + // Ignore the scroll area if we are dragging over the hot seat + boolean isPortrait = !LauncherApplication.isScreenLandscape(getContext()); + if (mLauncher.getHotseat() != null && isPortrait) { + Rect r = new Rect(); + mLauncher.getHotseat().getHitRect(r); + if (r.contains(x, y)) { + return false; + } + } + + boolean result = false; + if (!isSmall() && !mIsSwitchingState) { + mInScrollArea = true; + + final int page = getNextPage() + + (direction == DragController.SCROLL_LEFT ? -1 : 1); + + // We always want to exit the current layout to ensure parity of enter / exit + setCurrentDropLayout(null); + + if (0 <= page && page < getChildCount()) { + CellLayout layout = (CellLayout) getChildAt(page); + setCurrentDragOverlappingLayout(layout); + + // Workspace is responsible for drawing the edge glow on adjacent pages, + // so we need to redraw the workspace when this may have changed. + invalidate(); + result = true; + } + } + return result; + } + + @Override + public boolean onExitScrollArea() { + boolean result = false; + if (mInScrollArea) { + invalidate(); + CellLayout layout = getCurrentDropLayout(); + setCurrentDropLayout(layout); + setCurrentDragOverlappingLayout(layout); + + result = true; + mInScrollArea = false; + } + return result; + } + + private void onResetScrollArea() { + setCurrentDragOverlappingLayout(null); + mInScrollArea = false; + } + + /** + * Returns a specific CellLayout + */ + CellLayout getParentCellLayoutForView(View v) { + ArrayList<CellLayout> layouts = getWorkspaceAndHotseatCellLayouts(); + for (CellLayout layout : layouts) { + if (layout.getShortcutsAndWidgets().indexOfChild(v) > -1) { + return layout; + } + } + return null; + } + + /** + * Returns a list of all the CellLayouts in the workspace. + */ + ArrayList<CellLayout> getWorkspaceAndHotseatCellLayouts() { + ArrayList<CellLayout> layouts = new ArrayList<CellLayout>(); + int screenCount = getChildCount(); + for (int screen = 0; screen < screenCount; screen++) { + layouts.add(((CellLayout) getChildAt(screen))); + } + if (mLauncher.getHotseat() != null) { + layouts.add(mLauncher.getHotseat().getLayout()); + } + return layouts; + } + + /** + * We should only use this to search for specific children. Do not use this method to modify + * ShortcutsAndWidgetsContainer directly. Includes ShortcutAndWidgetContainers from + * the hotseat and workspace pages + */ + ArrayList<ShortcutAndWidgetContainer> getAllShortcutAndWidgetContainers() { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = + new ArrayList<ShortcutAndWidgetContainer>(); + int screenCount = getChildCount(); + for (int screen = 0; screen < screenCount; screen++) { + childrenLayouts.add(((CellLayout) getChildAt(screen)).getShortcutsAndWidgets()); + } + if (mLauncher.getHotseat() != null) { + childrenLayouts.add(mLauncher.getHotseat().getLayout().getShortcutsAndWidgets()); + } + return childrenLayouts; + } + + public Folder getFolderForTag(Object tag) { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = + getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { + int count = layout.getChildCount(); + for (int i = 0; i < count; i++) { + View child = layout.getChildAt(i); + if (child instanceof Folder) { + Folder f = (Folder) child; + if (f.getInfo() == tag && f.getInfo().opened) { + return f; + } + } + } + } + return null; + } + + public View getViewForTag(Object tag) { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = + getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { + int count = layout.getChildCount(); + for (int i = 0; i < count; i++) { + View child = layout.getChildAt(i); + if (child.getTag() == tag) { + return child; + } + } + } + return null; + } + + void clearDropTargets() { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = + getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { + int childCount = layout.getChildCount(); + for (int j = 0; j < childCount; j++) { + View v = layout.getChildAt(j); + if (v instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) v); + } + } + } + } + + // Removes ALL items that match a given package name, this is usually called when a package + // has been removed and we want to remove all components (widgets, shortcuts, apps) that + // belong to that package. + void removeItemsByPackageName(final ArrayList<String> packages) { + HashSet<String> packageNames = new HashSet<String>(); + packageNames.addAll(packages); + + // Just create a hash table of all the specific components that this will affect + HashSet<ComponentName> cns = new HashSet<ComponentName>(); + ArrayList<CellLayout> cellLayouts = getWorkspaceAndHotseatCellLayouts(); + for (CellLayout layoutParent : cellLayouts) { + ViewGroup layout = layoutParent.getShortcutsAndWidgets(); + int childCount = layout.getChildCount(); + for (int i = 0; i < childCount; ++i) { + View view = layout.getChildAt(i); + Object tag = view.getTag(); + + if (tag instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) tag; + ComponentName cn = info.intent.getComponent(); + if ((cn != null) && packageNames.contains(cn.getPackageName())) { + cns.add(cn); + } + } else if (tag instanceof FolderInfo) { + FolderInfo info = (FolderInfo) tag; + for (ShortcutInfo s : info.contents) { + ComponentName cn = s.intent.getComponent(); + if ((cn != null) && packageNames.contains(cn.getPackageName())) { + cns.add(cn); + } + } + } else if (tag instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) tag; + ComponentName cn = info.providerName; + if ((cn != null) && packageNames.contains(cn.getPackageName())) { + cns.add(cn); + } + } + } + } + + // Remove all the things + removeItemsByComponentName(cns); + } + + // Removes items that match the application info specified, when applications are removed + // as a part of an update, this is called to ensure that other widgets and application + // shortcuts are not removed. + void removeItemsByApplicationInfo(final ArrayList<ApplicationInfo> appInfos) { + // Just create a hash table of all the specific components that this will affect + HashSet<ComponentName> cns = new HashSet<ComponentName>(); + for (ApplicationInfo info : appInfos) { + cns.add(info.componentName); + } + + // Remove all the things + removeItemsByComponentName(cns); + } + + void removeItemsByComponentName(final HashSet<ComponentName> componentNames) { + ArrayList<CellLayout> cellLayouts = getWorkspaceAndHotseatCellLayouts(); + for (final CellLayout layoutParent: cellLayouts) { + final ViewGroup layout = layoutParent.getShortcutsAndWidgets(); + + // Avoid ANRs by treating each screen separately + post(new Runnable() { + public void run() { + final ArrayList<View> childrenToRemove = new ArrayList<View>(); + childrenToRemove.clear(); + + int childCount = layout.getChildCount(); + for (int j = 0; j < childCount; j++) { + final View view = layout.getChildAt(j); + Object tag = view.getTag(); + + if (tag instanceof ShortcutInfo) { + final ShortcutInfo info = (ShortcutInfo) tag; + final Intent intent = info.intent; + final ComponentName name = intent.getComponent(); + + if (name != null) { + if (componentNames.contains(name)) { + LauncherModel.deleteItemFromDatabase(mLauncher, info); + childrenToRemove.add(view); + } + } + } else if (tag instanceof FolderInfo) { + final FolderInfo info = (FolderInfo) tag; + final ArrayList<ShortcutInfo> contents = info.contents; + final int contentsCount = contents.size(); + final ArrayList<ShortcutInfo> appsToRemoveFromFolder = + new ArrayList<ShortcutInfo>(); + + for (int k = 0; k < contentsCount; k++) { + final ShortcutInfo appInfo = contents.get(k); + final Intent intent = appInfo.intent; + final ComponentName name = intent.getComponent(); + + if (name != null) { + if (componentNames.contains(name)) { + appsToRemoveFromFolder.add(appInfo); + } + } + } + for (ShortcutInfo item: appsToRemoveFromFolder) { + info.remove(item); + LauncherModel.deleteItemFromDatabase(mLauncher, item); + } + } else if (tag instanceof LauncherAppWidgetInfo) { + final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) tag; + final ComponentName provider = info.providerName; + if (provider != null) { + if (componentNames.contains(provider)) { + LauncherModel.deleteItemFromDatabase(mLauncher, info); + childrenToRemove.add(view); + } + } + } + } + + childCount = childrenToRemove.size(); + for (int j = 0; j < childCount; j++) { + View child = childrenToRemove.get(j); + // Note: We can not remove the view directly from CellLayoutChildren as this + // does not re-mark the spaces as unoccupied. + layoutParent.removeViewInLayout(child); + if (child instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget)child); + } + } + + if (childCount > 0) { + layout.requestLayout(); + layout.invalidate(); + } + } + }); + } + + // Clean up new-apps animation list + final Context context = getContext(); + post(new Runnable() { + @Override + public void run() { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = context.getSharedPreferences(spKey, + Context.MODE_PRIVATE); + Set<String> newApps = sp.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, + null); + + // Remove all queued items that match the same package + if (newApps != null) { + synchronized (newApps) { + Iterator<String> iter = newApps.iterator(); + while (iter.hasNext()) { + try { + Intent intent = Intent.parseUri(iter.next(), 0); + if (componentNames.contains(intent.getComponent())) { + iter.remove(); + } + + // It is possible that we've queued an item to be loaded, yet it has + // not been added to the workspace, so remove those items as well. + ArrayList<ItemInfo> shortcuts; + shortcuts = LauncherModel.getWorkspaceShortcutItemInfosWithIntent( + intent); + for (ItemInfo info : shortcuts) { + LauncherModel.deleteItemFromDatabase(context, info); + } + } catch (URISyntaxException e) {} + } + } + } + } + }); + } + + void updateShortcuts(ArrayList<ApplicationInfo> apps) { + ArrayList<ShortcutAndWidgetContainer> childrenLayouts = getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { + int childCount = layout.getChildCount(); + for (int j = 0; j < childCount; j++) { + final View view = layout.getChildAt(j); + Object tag = view.getTag(); + if (tag instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) tag; + // We need to check for ACTION_MAIN otherwise getComponent() might + // return null for some shortcuts (for instance, for shortcuts to + // web pages.) + final Intent intent = info.intent; + final ComponentName name = intent.getComponent(); + if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && + Intent.ACTION_MAIN.equals(intent.getAction()) && name != null) { + final int appCount = apps.size(); + for (int k = 0; k < appCount; k++) { + ApplicationInfo app = apps.get(k); + if (app.componentName.equals(name)) { + BubbleTextView shortcut = (BubbleTextView) view; + info.updateIcon(mIconCache); + info.title = app.title.toString(); + shortcut.applyFromShortcutInfo(info, mIconCache); + } + } + } + } + } + } + } + + void moveToDefaultScreen(boolean animate) { + if (!isSmall()) { + if (animate) { + snapToPage(mDefaultPage); + } else { + setCurrentPage(mDefaultPage); + } + } + getChildAt(mDefaultPage).requestFocus(); + } + + @Override + public void syncPages() { + } + + @Override + public void syncPageItems(int page, boolean immediate) { + } + + @Override + protected String getCurrentPageDescription() { + int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + return String.format(getContext().getString(R.string.workspace_scroll_format), + page + 1, getChildCount()); + } + + public void getLocationInDragLayer(int[] loc) { + mLauncher.getDragLayer().getLocationInDragLayer(this, loc); + } + + void setFadeForOverScroll(float fade) { + if (!isScrollingIndicatorEnabled()) return; + + mOverscrollFade = fade; + float reducedFade = 0.5f + 0.5f * (1 - fade); + final ViewGroup parent = (ViewGroup) getParent(); + final ImageView qsbDivider = (ImageView) (parent.findViewById(R.id.qsb_divider)); + final ImageView dockDivider = (ImageView) (parent.findViewById(R.id.dock_divider)); + final View scrollIndicator = getScrollingIndicator(); + + cancelScrollingIndicatorAnimations(); + if (qsbDivider != null) qsbDivider.setAlpha(reducedFade); + if (dockDivider != null) dockDivider.setAlpha(reducedFade); + scrollIndicator.setAlpha(1 - fade); + } +} |