summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/launcher3')
-rw-r--r--src/com/android/launcher3/AccessibleTabView.java51
-rw-r--r--src/com/android/launcher3/AddAdapter.java103
-rw-r--r--src/com/android/launcher3/Alarm.java84
-rw-r--r--src/com/android/launcher3/AllAppsList.java221
-rw-r--r--src/com/android/launcher3/AppWidgetResizeFrame.java471
-rw-r--r--src/com/android/launcher3/ApplicationInfo.java133
-rw-r--r--src/com/android/launcher3/AppsCustomizePagedView.java1710
-rw-r--r--src/com/android/launcher3/AppsCustomizeTabHost.java482
-rw-r--r--src/com/android/launcher3/BubbleTextView.java342
-rw-r--r--src/com/android/launcher3/ButtonDropTarget.java166
-rw-r--r--src/com/android/launcher3/CellLayout.java3338
-rw-r--r--src/com/android/launcher3/CheckLongPressHelper.java62
-rw-r--r--src/com/android/launcher3/Cling.java271
-rw-r--r--src/com/android/launcher3/DeferredHandler.java146
-rw-r--r--src/com/android/launcher3/DeleteDropTarget.java437
-rw-r--r--src/com/android/launcher3/DragController.java821
-rw-r--r--src/com/android/launcher3/DragLayer.java804
-rw-r--r--src/com/android/launcher3/DragScroller.java40
-rw-r--r--src/com/android/launcher3/DragSource.java45
-rw-r--r--src/com/android/launcher3/DragView.java295
-rw-r--r--src/com/android/launcher3/DrawableStateProxyView.java69
-rw-r--r--src/com/android/launcher3/DropTarget.java184
-rw-r--r--src/com/android/launcher3/FastBitmapDrawable.java109
-rw-r--r--src/com/android/launcher3/FirstFrameAnimatorHelper.java141
-rw-r--r--src/com/android/launcher3/FocusHelper.java898
-rw-r--r--src/com/android/launcher3/FocusOnlyTabWidget.java86
-rw-r--r--src/com/android/launcher3/Folder.java1114
-rw-r--r--src/com/android/launcher3/FolderEditText.java36
-rw-r--r--src/com/android/launcher3/FolderIcon.java667
-rw-r--r--src/com/android/launcher3/FolderInfo.java111
-rw-r--r--src/com/android/launcher3/HandleView.java76
-rw-r--r--src/com/android/launcher3/HideFromAccessibilityHelper.java113
-rw-r--r--src/com/android/launcher3/HolographicImageView.java54
-rw-r--r--src/com/android/launcher3/HolographicLinearLayout.java85
-rw-r--r--src/com/android/launcher3/HolographicOutlineHelper.java221
-rw-r--r--src/com/android/launcher3/HolographicViewHelper.java104
-rw-r--r--src/com/android/launcher3/Hotseat.java147
-rw-r--r--src/com/android/launcher3/IconCache.java229
-rw-r--r--src/com/android/launcher3/InfoDropTarget.java129
-rw-r--r--src/com/android/launcher3/InstallShortcutReceiver.java363
-rw-r--r--src/com/android/launcher3/InstallWidgetReceiver.java195
-rw-r--r--src/com/android/launcher3/InterruptibleInOutAnimator.java131
-rw-r--r--src/com/android/launcher3/ItemInfo.java194
-rw-r--r--src/com/android/launcher3/Launcher.java4070
-rw-r--r--src/com/android/launcher3/LauncherAnimUtils.java129
-rw-r--r--src/com/android/launcher3/LauncherAnimatorUpdateListener.java30
-rw-r--r--src/com/android/launcher3/LauncherAppWidgetHost.java55
-rw-r--r--src/com/android/launcher3/LauncherAppWidgetHostView.java101
-rw-r--r--src/com/android/launcher3/LauncherAppWidgetInfo.java98
-rw-r--r--src/com/android/launcher3/LauncherApplication.java151
-rw-r--r--src/com/android/launcher3/LauncherModel.java2620
-rw-r--r--src/com/android/launcher3/LauncherProvider.java1193
-rw-r--r--src/com/android/launcher3/LauncherSettings.java236
-rw-r--r--src/com/android/launcher3/LauncherViewPropertyAnimator.java266
-rw-r--r--src/com/android/launcher3/PackageChangedReceiver.java19
-rw-r--r--src/com/android/launcher3/PagedView.java1981
-rw-r--r--src/com/android/launcher3/PagedViewCellLayout.java505
-rw-r--r--src/com/android/launcher3/PagedViewCellLayoutChildren.java160
-rw-r--r--src/com/android/launcher3/PagedViewGridLayout.java133
-rw-r--r--src/com/android/launcher3/PagedViewIcon.java92
-rw-r--r--src/com/android/launcher3/PagedViewIconCache.java133
-rw-r--r--src/com/android/launcher3/PagedViewWidget.java246
-rw-r--r--src/com/android/launcher3/PagedViewWidgetImageView.java49
-rw-r--r--src/com/android/launcher3/PagedViewWithDraggableItems.java178
-rw-r--r--src/com/android/launcher3/PendingAddItemInfo.java107
-rw-r--r--src/com/android/launcher3/PreloadReceiver.java51
-rw-r--r--src/com/android/launcher3/SearchDropTargetBar.java239
-rw-r--r--src/com/android/launcher3/ShortcutAndWidgetContainer.java204
-rw-r--r--src/com/android/launcher3/ShortcutInfo.java162
-rw-r--r--src/com/android/launcher3/SmoothPagedView.java188
-rw-r--r--src/com/android/launcher3/SpringLoadedDragController.java62
-rw-r--r--src/com/android/launcher3/UninstallShortcutReceiver.java166
-rw-r--r--src/com/android/launcher3/UserInitializeReceiver.java70
-rw-r--r--src/com/android/launcher3/Utilities.java275
-rw-r--r--src/com/android/launcher3/WallpaperChooser.java47
-rw-r--r--src/com/android/launcher3/WallpaperChooserDialogFragment.java360
-rw-r--r--src/com/android/launcher3/WidgetPreviewLoader.java610
-rw-r--r--src/com/android/launcher3/Workspace.java3894
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);
+ }
+}