diff options
Diffstat (limited to 'src/com')
81 files changed, 9488 insertions, 4811 deletions
diff --git a/src/com/android/launcher3/AddAdapter.java b/src/com/android/launcher3/AddAdapter.java deleted file mode 100644 index 5308a3de4..000000000 --- a/src/com/android/launcher3/AddAdapter.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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; - -/** - * 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/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java index 89b291f28..38d2fa541 100644 --- a/src/com/android/launcher3/AllAppsList.java +++ b/src/com/android/launcher3/AllAppsList.java @@ -23,6 +23,10 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import com.android.launcher3.compat.LauncherActivityInfoCompat; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.UserHandleCompat; + import java.util.ArrayList; import java.util.List; @@ -66,7 +70,7 @@ class AllAppsList { if (mAppFilter != null && !mAppFilter.shouldShowApp(info.componentName)) { return; } - if (findActivity(data, info.componentName)) { + if (findActivity(data, info.componentName, info.user)) { return; } data.add(info); @@ -92,12 +96,14 @@ class AllAppsList { /** * Add the icons for the supplied apk called packageName. */ - public void addPackage(Context context, String packageName) { - final List<ResolveInfo> matches = findActivitiesForPackage(context, packageName); + public void addPackage(Context context, String packageName, UserHandleCompat user) { + final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); + final List<LauncherActivityInfoCompat> matches = launcherApps.getActivityList(packageName, + user); if (matches.size() > 0) { - for (ResolveInfo info : matches) { - add(new AppInfo(context.getPackageManager(), info, mIconCache, null)); + for (LauncherActivityInfoCompat info : matches) { + add(new AppInfo(context, info, user, mIconCache, null)); } } } @@ -105,34 +111,37 @@ class AllAppsList { /** * Remove the apps for the given apk identified by packageName. */ - public void removePackage(String packageName) { + public void removePackage(String packageName, UserHandleCompat user) { final List<AppInfo> data = this.data; for (int i = data.size() - 1; i >= 0; i--) { AppInfo info = data.get(i); final ComponentName component = info.intent.getComponent(); - if (packageName.equals(component.getPackageName())) { + if (info.user.equals(user) && packageName.equals(component.getPackageName())) { removed.add(info); data.remove(i); } } - mIconCache.remove(packageName); + mIconCache.remove(packageName, user); } /** * 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); + public void updatePackage(Context context, String packageName, UserHandleCompat user) { + final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); + final List<LauncherActivityInfoCompat> matches = launcherApps.getActivityList(packageName, + user); 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 AppInfo applicationInfo = data.get(i); final ComponentName component = applicationInfo.intent.getComponent(); - if (packageName.equals(component.getPackageName())) { + if (user.equals(applicationInfo.user) + && packageName.equals(component.getPackageName())) { if (!findActivity(matches, component)) { removed.add(applicationInfo); - mIconCache.remove(component); + mIconCache.remove(component, user); data.remove(i); } } @@ -142,14 +151,14 @@ class AllAppsList { // 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); + final LauncherActivityInfoCompat info = matches.get(i); AppInfo applicationInfo = findApplicationInfoLocked( - info.activityInfo.applicationInfo.packageName, - info.activityInfo.name); + info.getComponentName().getPackageName(), user, + info.getComponentName().getClassName()); if (applicationInfo == null) { - add(new AppInfo(context.getPackageManager(), info, mIconCache, null)); + add(new AppInfo(context, info, user, mIconCache, null)); } else { - mIconCache.remove(applicationInfo.componentName); + mIconCache.remove(applicationInfo.componentName, user); mIconCache.getTitleAndIcon(applicationInfo, info, null); modified.add(applicationInfo); } @@ -159,37 +168,24 @@ class AllAppsList { for (int i = data.size() - 1; i >= 0; i--) { final AppInfo applicationInfo = data.get(i); final ComponentName component = applicationInfo.intent.getComponent(); - if (packageName.equals(component.getPackageName())) { + if (user.equals(applicationInfo.user) + && packageName.equals(component.getPackageName())) { removed.add(applicationInfo); - mIconCache.remove(component); + mIconCache.remove(component, user); data.remove(i); } } } } - /** - * Query the package manager for MAIN/LAUNCHER activities in the supplied package. - */ - 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)) { + private static boolean findActivity(List<LauncherActivityInfoCompat> apps, + ComponentName component) { + for (LauncherActivityInfoCompat info : apps) { + if (info.getComponentName().equals(component)) { return true; } } @@ -197,13 +193,24 @@ class AllAppsList { } /** + * Query the launcher apps service for whether the supplied package has + * MAIN/LAUNCHER activities in the supplied package. + */ + static boolean packageHasActivities(Context context, String packageName, + UserHandleCompat user) { + final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); + return launcherApps.getActivityList(packageName, user).size() > 0; + } + + /** * Returns whether <em>apps</em> contains <em>component</em>. */ - private static boolean findActivity(ArrayList<AppInfo> apps, ComponentName component) { + private static boolean findActivity(ArrayList<AppInfo> apps, ComponentName component, + UserHandleCompat user) { final int N = apps.size(); - for (int i=0; i<N; i++) { + for (int i = 0; i < N; i++) { final AppInfo info = apps.get(i); - if (info.componentName.equals(component)) { + if (info.user.equals(user) && info.componentName.equals(component)) { return true; } } @@ -213,10 +220,11 @@ class AllAppsList { /** * Find an ApplicationInfo object for the given packageName and className. */ - private AppInfo findApplicationInfoLocked(String packageName, String className) { + private AppInfo findApplicationInfoLocked(String packageName, UserHandleCompat user, + String className) { for (AppInfo info: data) { final ComponentName component = info.intent.getComponent(); - if (packageName.equals(component.getPackageName()) + if (user.equals(info.user) && packageName.equals(component.getPackageName()) && className.equals(component.getClassName())) { return info; } diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index da222f11f..bfcad84b3 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -17,21 +17,26 @@ package com.android.launcher3; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; -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 com.android.launcher3.compat.LauncherActivityInfoCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; + import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; /** * Represents an app in AllAppsView. */ -class AppInfo extends ItemInfo { +public class AppInfo extends ItemInfo { private static final String TAG = "Launcher3.AppInfo"; /** @@ -60,7 +65,7 @@ class AppInfo extends ItemInfo { itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; } - protected Intent getIntent() { + public Intent getIntent() { return intent; } @@ -71,28 +76,25 @@ class AppInfo extends ItemInfo { /** * Must not hold the Context. */ - public AppInfo(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); + public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user, + IconCache iconCache, HashMap<Object, CharSequence> labelCache) { + this.componentName = info.getComponentName(); this.container = ItemInfo.NO_ID; - this.setActivity(componentName, - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - - try { - PackageInfo pi = pm.getPackageInfo(packageName, 0); - flags = initFlags(pi); - firstInstallTime = initFirstInstallTime(pi); - } catch (NameNotFoundException e) { - Log.d(TAG, "PackageManager.getApplicationInfo failed for " + packageName); - } + flags = initFlags(info); + firstInstallTime = info.getFirstInstallTime(); iconCache.getTitleAndIcon(this, info, labelCache); + intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(info.getComponentName()); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + long serialNumber = UserManagerCompat.getInstance(context).getSerialNumberForUser(user); + intent.putExtra(EXTRA_PROFILE, serialNumber); + this.user = user; } - public static int initFlags(PackageInfo pi) { - int appFlags = pi.applicationInfo.flags; + private static int initFlags(LauncherActivityInfoCompat info) { + int appFlags = info.getApplicationInfo().flags; int flags = 0; if ((appFlags & android.content.pm.ApplicationInfo.FLAG_SYSTEM) == 0) { flags |= DOWNLOADED_FLAG; @@ -104,10 +106,6 @@ class AppInfo extends ItemInfo { return flags; } - public static long initFirstInstallTime(PackageInfo pi) { - return pi.firstInstallTime; - } - public AppInfo(AppInfo info) { super(info); componentName = info.componentName; @@ -115,21 +113,7 @@ class AppInfo extends ItemInfo { 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; + iconBitmap = info.iconBitmap; } @Override @@ -137,7 +121,8 @@ class AppInfo extends ItemInfo { return "ApplicationInfo(title=" + title.toString() + " id=" + this.id + " type=" + this.itemType + " container=" + this.container + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY - + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + dropPos + ")"; + + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + + " user=" + user + ")"; } public static void dumpApplicationInfoList(String tag, String label, ArrayList<AppInfo> list) { diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java new file mode 100644 index 000000000..880aaf1ec --- /dev/null +++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java @@ -0,0 +1,94 @@ +package com.android.launcher3; + +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.AsyncTask; +import android.util.Log; + +import com.android.launcher3.LauncherSettings.Favorites; + +import java.util.ArrayList; +import java.util.List; + +public class AppWidgetsRestoredReceiver extends BroadcastReceiver { + + private static final String TAG = "AppWidgetsRestoredReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + if (AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED.equals(intent.getAction())) { + int[] oldIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS); + int[] newIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); + if (oldIds.length == newIds.length) { + restoreAppWidgetIds(context, oldIds, newIds); + } else { + Log.e(TAG, "Invalid host restored received"); + } + } + } + + /** + * Updates the app widgets whose id has changed during the restore process. + */ + static void restoreAppWidgetIds(Context context, int[] oldWidgetIds, int[] newWidgetIds) { + final ContentResolver cr = context.getContentResolver(); + final List<Integer> idsToRemove = new ArrayList<Integer>(); + final AppWidgetManager widgets = AppWidgetManager.getInstance(context); + + for (int i = 0; i < oldWidgetIds.length; i++) { + Log.i(TAG, "Widget state restore id " + oldWidgetIds[i] + " => " + newWidgetIds[i]); + + final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(newWidgetIds[i]); + final int state; + if (LauncherModel.isValidProvider(provider)) { + state = LauncherAppWidgetInfo.RESTORE_COMPLETED; + } else { + state = LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; + } + + ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i]); + values.put(LauncherSettings.Favorites.RESTORED, state); + + String[] widgetIdParams = new String[] { Integer.toString(oldWidgetIds[i]) }; + + int result = cr.update(Favorites.CONTENT_URI, values, + "appWidgetId=? and (restored & 1) = 1", widgetIdParams); + if (result == 0) { + Cursor cursor = cr.query(Favorites.CONTENT_URI, + new String[] {Favorites.APPWIDGET_ID}, + "appWidgetId=?", widgetIdParams, null); + try { + if (!cursor.moveToFirst()) { + // The widget no long exists. + idsToRemove.add(newWidgetIds[i]); + } + } finally { + cursor.close(); + } + } + } + // Unregister the widget IDs which are not present on the workspace. This could happen + // when a widget place holder is removed from workspace, before this method is called. + if (!idsToRemove.isEmpty()) { + final AppWidgetHost appWidgetHost = + new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); + new AsyncTask<Void, Void, Void>() { + public Void doInBackground(Void ... args) { + for (Integer id : idsToRemove) { + appWidgetHost.deleteAppWidgetId(id); + Log.e(TAG, "Widget no longer present, appWidgetId=" + id); + } + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + } + } +} diff --git a/src/com/android/launcher3/AppsCustomizeCellLayout.java b/src/com/android/launcher3/AppsCustomizeCellLayout.java index 3c8bda9db..a50fb6821 100644 --- a/src/com/android/launcher3/AppsCustomizeCellLayout.java +++ b/src/com/android/launcher3/AppsCustomizeCellLayout.java @@ -20,8 +20,16 @@ import android.content.Context; import android.view.View; public class AppsCustomizeCellLayout extends CellLayout implements Page { + + final FocusIndicatorView mFocusHandlerView; + public AppsCustomizeCellLayout(Context context) { super(context); + + mFocusHandlerView = new FocusIndicatorView(context); + addView(mFocusHandlerView, 0); + mFocusHandlerView.getLayoutParams().width = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; + mFocusHandlerView.getLayoutParams().height = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; } @Override @@ -60,4 +68,4 @@ public class AppsCustomizeCellLayout extends CellLayout implements Page { children.getChildAt(j).setOnKeyListener(null); } } -}
\ No newline at end of file +} diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java index 251ae2108..1bd290777 100644 --- a/src/com/android/launcher3/AppsCustomizePagedView.java +++ b/src/com/android/launcher3/AppsCustomizePagedView.java @@ -28,7 +28,6 @@ import android.content.pm.ResolveInfo; 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; @@ -44,12 +43,12 @@ 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.DropTarget.DragObject; +import com.android.launcher3.compat.AppWidgetManagerCompat; import java.util.ArrayList; import java.util.Collections; @@ -144,10 +143,11 @@ class AppsCustomizeAsyncTask extends AsyncTask<AsyncTaskPageData, Void, AsyncTas */ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implements View.OnClickListener, View.OnKeyListener, DragSource, - PagedViewIcon.PressedCallback, PagedViewWidget.ShortPressListener, - LauncherTransitionable { + PagedViewWidget.ShortPressListener, LauncherTransitionable { static final String TAG = "AppsCustomizePagedView"; + private static Rect sTmpRect = new Rect(); + /** * The different content types that this paged view can show. */ @@ -165,40 +165,22 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // Save and Restore private int mSaveInstanceStateItemIndex = -1; - private PagedViewIcon mPressedIcon; // Content private ArrayList<AppInfo> 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, mContentHeight; private int mWidgetCountX, mWidgetCountY; - private int mWidgetWidthGap, mWidgetHeightGap; private PagedViewCellLayout mWidgetSpacingLayout; private int mNumAppsPages; private int mNumWidgetPages; private Rect mAllAppsPadding = new Rect(); - // 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; @@ -213,6 +195,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen int mWidgetLoadingId = -1; PendingAddWidgetInfo mCreateWidgetInfo = null; private boolean mDraggingWidget = false; + boolean mPageBackgroundsVisible = true; private Toast mWidgetInstructionToast; @@ -223,19 +206,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen 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; @@ -248,18 +218,12 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen mApps = new ArrayList<AppInfo>(); mWidgets = new ArrayList<Object>(); mIconCache = (LauncherAppState.getInstance()).getIconCache(); - mCanvas = new Canvas(); mRunningTasks = new ArrayList<AppsCustomizeAsyncTask>(); // Save the default widget preview background TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mWidgetWidthGap = mWidgetHeightGap = grid.edgeMarginPx; 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()); @@ -271,6 +235,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } + setSinglePageInViewport(); } @Override @@ -295,8 +260,9 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen void setAllAppsPadding(Rect r) { mAllAppsPadding.set(r); } + void setWidgetsPageIndicatorPadding(int pageIndicatorHeight) { - mPageLayoutPaddingBottom = pageIndicatorHeight; + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), pageIndicatorHeight); } WidgetPreviewLoader getWidgetPreviewLoader() { @@ -375,8 +341,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // use for each page LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mWidgetSpacingLayout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, - mPageLayoutPaddingRight, mPageLayoutPaddingBottom); mCellCountX = (int) grid.allAppsNumCols; mCellCountY = (int) grid.allAppsNumRows; updatePageCounts(); @@ -388,54 +352,32 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); mWidgetSpacingLayout.measure(widthSpec, heightSpec); - AppsCustomizeTabHost host = (AppsCustomizeTabHost) getTabHost(); - final boolean hostIsTransitioning = host.isTransitioning(); - - // Restore the page + final boolean hostIsTransitioning = getTabHost().isInTransition(); 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(); - } - } + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); if (!isDataReady()) { if ((LauncherAppState.isDisableAllApps() || !mApps.isEmpty()) && !mWidgets.isEmpty()) { - setDataIsReady(); - setMeasuredDimension(width, height); - onDataReady(width, height); + post(new Runnable() { + // This code triggers requestLayout so must be posted outside of the + // layout pass. + public void run() { + boolean attached = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + attached = isAttachedToWindow(); + } + if (attached) { + setDataIsReady(); + onDataReady(getMeasuredWidth(), getMeasuredHeight()); + } + } + }); } } - - super.onMeasure(widthMeasureSpec, heightMeasureSpec); } public void onPackagesUpdated(ArrayList<Object> widgetsAndShortcuts) { @@ -450,7 +392,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen if (!app.shouldShowAppOrWidgetProvider(widget.provider)) { continue; } - 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); @@ -500,40 +441,29 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen @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 AppInfo appInfo = (AppInfo) v.getTag(); + if (!mLauncher.isAllAppsVisible() + || mLauncher.getWorkspace().isSwitchingState() + || !(v instanceof PagedViewWidget)) return; - // Lock the drawable state to pressed until we return to Launcher - if (mPressedIcon != null) { - mPressedIcon.lockDrawableState(); - } - mLauncher.startActivitySafely(v, appInfo.intent, appInfo); - mLauncher.getStats().recordLaunch(appInfo.intent); - } 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(); + // 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) { @@ -549,30 +479,29 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } private void beginDraggingApplication(View v) { - mLauncher.getWorkspace().onDragStartedWithItem(v); mLauncher.getWorkspace().beginDragShared(v, this); } - Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { + static 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, + AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, sTmpRect); + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher, info.componentName, null); - float density = getResources().getDisplayMetrics().density; + float density = launcher.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); + sTmpRect.left - xPaddingDips); options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, - mTmpRect.top - yPaddingDips); + sTmpRect.top - yPaddingDips); options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, - mTmpRect.right - xPaddingDips); + sTmpRect.right - xPaddingDips); options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, - mTmpRect.bottom - yPaddingDips); + sTmpRect.bottom - yPaddingDips); } return options; } @@ -591,18 +520,9 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen @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; - } + if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed( + mWidgetLoadingId, pInfo, options)) { + mWidgetCleanupState = WIDGET_BOUND; } } }; @@ -730,9 +650,8 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen int[] previewSizeBeforeScale = new int[1]; - preview = getWidgetPreviewLoader().generateWidgetPreview(createWidgetInfo.componentName, - createWidgetInfo.previewImage, createWidgetInfo.icon, spanX, spanY, - maxWidth, maxHeight, null, previewSizeBeforeScale); + preview = getWidgetPreviewLoader().generateWidgetPreview(createWidgetInfo.info, + 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], @@ -749,15 +668,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } 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); + preview = Utilities.createIconBitmap(icon, mLauncher); createItemInfo.spanX = createItemInfo.spanY = 1; } @@ -783,7 +694,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen protected boolean beginDragging(final View v) { if (!super.beginDragging(v)) return false; - if (v instanceof PagedViewIcon) { + if (v instanceof BubbleTextView) { beginDraggingApplication(v); } else if (v instanceof PagedViewWidget) { if (!beginDraggingWidget(v)) { @@ -798,9 +709,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen public void run() { // We don't enter spring-loaded mode if the drag has been cancelled if (mLauncher.getDragController().isDragging()) { - // Reset the alpha on the dragged icon before we drag - resetDrawableState(); - // Go into spring loaded mode (must happen before we startDrag()) mLauncher.enterSpringLoadedDragMode(); } @@ -820,13 +728,8 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { // Exit spring loaded mode if we have not successfully dropped or have not handled the // drop in Workspace - mLauncher.getWorkspace().removeExtraEmptyScreen(true, new Runnable() { - @Override - public void run() { - mLauncher.exitSpringLoadedDragMode(); - mLauncher.unlockScreenOrientation(false); - } - }); + mLauncher.exitSpringLoadedDragMode(); + mLauncher.unlockScreenOrientation(false); } else { mLauncher.unlockScreenOrientation(false); } @@ -1019,13 +922,28 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen setVisibilityOnChildren(layout, View.GONE); int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - layout.setMinimumWidth(getPageContentWidth()); layout.measure(widthSpec, heightSpec); - layout.setPadding(mAllAppsPadding.left, mAllAppsPadding.top, mAllAppsPadding.right, - mAllAppsPadding.bottom); + + Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel); + if (bg != null) { + bg.setAlpha(mPageBackgroundsVisible ? 255: 0); + layout.setBackground(bg); + } + setVisibilityOnChildren(layout, View.VISIBLE); } + public void setPageBackgroundsVisible(boolean visible) { + mPageBackgroundsVisible = visible; + int childCount = getChildCount(); + for (int i = 0; i < childCount; ++i) { + Drawable bg = getChildAt(i).getBackground(); + if (bg != null) { + bg.setAlpha(visible ? 255 : 0); + } + } + } + public void syncAppsPageItems(int page, boolean immediate) { // ensure that we have the right number of items on the pages final boolean isRtl = isLayoutRtl(); @@ -1039,13 +957,14 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen ArrayList<Bitmap> images = new ArrayList<Bitmap>(); for (int i = startIndex; i < endIndex; ++i) { AppInfo info = mApps.get(i); - PagedViewIcon icon = (PagedViewIcon) mLayoutInflater.inflate( + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( R.layout.apps_customize_application, layout, false); - icon.applyFromApplicationInfo(info, true, this); - icon.setOnClickListener(this); + icon.applyFromApplicationInfo(info); + icon.setOnClickListener(mLauncher); icon.setOnLongClickListener(this); icon.setOnTouchListener(this); icon.setOnKeyListener(this); + icon.setOnFocusChangeListener(layout.mFocusHandlerView); int index = i - startIndex; int x = index % mCellCountX; @@ -1168,21 +1087,27 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // immediately after syncing, we don't have a proper width. int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - layout.setMinimumWidth(getPageContentWidth()); + + Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel_dark); + if (bg != null) { + bg.setAlpha(mPageBackgroundsVisible ? 255 : 0); + layout.setBackground(bg); + } layout.measure(widthSpec, heightSpec); } public void syncWidgetPageItems(final int page, final boolean immediate) { int numItemsPerPage = mWidgetCountX * mWidgetCountY; + final PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); + // Calculate the dimensions of each cell we are giving to each widget final ArrayList<Object> items = new ArrayList<Object>(); - int contentWidth = mContentWidth; - final int cellWidth = ((contentWidth - mPageLayoutPaddingLeft - mPageLayoutPaddingRight - - ((mWidgetCountX - 1) * mWidgetWidthGap)) / mWidgetCountX); - int contentHeight = mContentHeight; - final int cellHeight = ((contentHeight - mPageLayoutPaddingTop - mPageLayoutPaddingBottom - - ((mWidgetCountY - 1) * mWidgetHeightGap)) / mWidgetCountY); + int contentWidth = mContentWidth - layout.getPaddingLeft() - layout.getPaddingRight(); + final int cellWidth = contentWidth / mWidgetCountX; + int contentHeight = mContentHeight - layout.getPaddingTop() - layout.getPaddingBottom(); + + final int cellHeight = contentHeight / mWidgetCountY; // Prepare the set of widgets to load previews for in the background int offset = page * numItemsPerPage; @@ -1191,7 +1116,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } // 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); @@ -1232,14 +1156,22 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // Layout each widget int ix = i % mWidgetCountX; int iy = i / mWidgetCountX; + + if (ix > 0) { + View border = widget.findViewById(R.id.left_border); + border.setVisibility(View.VISIBLE); + } + if (ix < mWidgetCountX - 1) { + View border = widget.findViewById(R.id.right_border); + border.setVisibility(View.VISIBLE); + } + 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); } @@ -1389,86 +1321,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // 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(); } @@ -1513,7 +1366,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } protected void overScroll(float amount) { - acceleratedOverScroll(amount); + dampedOverScroll(amount); } /** @@ -1587,7 +1440,8 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen int length = list.size(); for (int i = 0; i < length; ++i) { AppInfo info = list.get(i); - if (info.intent.getComponent().equals(removeComponent)) { + if (info.user.equals(item.user) + && info.intent.getComponent().equals(removeComponent)) { return i; } } @@ -1625,12 +1479,8 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // 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 (mContentType != ContentType.Applications) { + setContentType(ContentType.Applications); } if (mCurrentPage != 0) { @@ -1674,23 +1524,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen 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. diff --git a/src/com/android/launcher3/AppsCustomizeTabHost.java b/src/com/android/launcher3/AppsCustomizeTabHost.java index bb7f045ce..9a516fd41 100644 --- a/src/com/android/launcher3/AppsCustomizeTabHost.java +++ b/src/com/android/launcher3/AppsCustomizeTabHost.java @@ -29,6 +29,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TabHost; @@ -37,35 +38,20 @@ import android.widget.TextView; import java.util.ArrayList; -public class AppsCustomizeTabHost extends TabHost implements LauncherTransitionable, - TabHost.OnTabChangeListener, Insettable { +public class AppsCustomizeTabHost extends FrameLayout implements LauncherTransitionable, Insettable { 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; + private AppsCustomizePagedView mPagedView; + private View mContent; + private boolean mInTransition = false; + private final Rect mInsets = new Rect(); public AppsCustomizeTabHost(Context context, AttributeSet attrs) { super(context, attrs); - mLayoutInflater = LayoutInflater.from(context); - mRelayoutAndMakeVisible = new Runnable() { - public void run() { - mTabs.requestLayout(); - mTabsContainer.setAlpha(1f); - } - }; } /** @@ -75,17 +61,17 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona * tabs manually). */ void setContentTypeImmediate(AppsCustomizePagedView.ContentType type) { - setOnTabChangedListener(null); - onTabChangedStart(); - onTabChangedEnd(type); - setCurrentTabByTag(getTabTagForContentType(type)); - setOnTabChangedListener(this); + mPagedView.setContentType(type); + } + + public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) { + setContentTypeImmediate(type); } @Override public void setInsets(Rect insets) { mInsets.set(insets); - FrameLayout.LayoutParams flp = (LayoutParams) mContent.getLayoutParams(); + LayoutParams flp = (LayoutParams) mContent.getLayoutParams(); flp.topMargin = insets.top; flp.bottomMargin = insets.bottom; flp.leftMargin = insets.left; @@ -98,212 +84,12 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona */ @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() { + mPagedView = (AppsCustomizePagedView) findViewById(R.id.apps_customize_pane_content); + mContent = findViewById(R.id.content); } - private void reloadCurrentPage() { - mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); - mAppsCustomizePane.requestFocus(); - } - - private void onTabChangedEnd(AppsCustomizePagedView.ContentType type) { - int bgAlpha = (int) (255 * (getResources().getInteger( - R.integer.config_appsCustomizeSpringLoadedBgAlpha) / 100f)); - setBackgroundColor(Color.argb(bgAlpha, 0, 0, 0)); - 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 AppsCustomizeCellLayout) { - ((AppsCustomizeCellLayout) 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); + public String getContentTag() { + return getTabTagForContentType(mPagedView.getContentType()); } /** @@ -342,44 +128,41 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona } void reset() { - if (mInTransition) { - // Defer to after the transition to reset - mResetAfterTransition = true; - } else { - // Reset immediately - mAppsCustomizePane.reset(); - } + // Reset immediately + mPagedView.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(); + 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 + mPagedView.loadAssociatedPages(mPagedView.getCurrentPage(), true); + mPagedView.loadAssociatedPages(mPagedView.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 + mPagedView.clearAllWidgetPages(); + } + @Override - public View getContent() { - View appsCustomizeContent = mAppsCustomizePane.getContent(); - if (appsCustomizeContent != null) { - return appsCustomizeContent; - } - return mContent; + public ViewGroup getContent() { + return mPagedView; + } + + public boolean isInTransition() { + return mInTransition; } /* LauncherTransitionable overrides */ @Override public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { - mAppsCustomizePane.onLauncherTransitionPrepare(l, animated, toWorkspace); + mPagedView.onLauncherTransitionPrepare(l, animated, toWorkspace); mInTransition = true; - mTransitioningToWorkspace = toWorkspace; if (toWorkspace) { // Going from All Apps -> Workspace @@ -390,45 +173,38 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona // 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 (mResetAfterTransition) { - mAppsCustomizePane.reset(); - mResetAfterTransition = false; + // TODO: revisit this + mPagedView.loadAssociatedPages(mPagedView.getCurrentPage()); } } @Override public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { - mAppsCustomizePane.onLauncherTransitionStart(l, animated, toWorkspace); - if (animated) { - enableAndBuildHardwareLayer(); - } - - // Dismiss the workspace cling - l.getLauncherClings().dismissWorkspaceCling(null); + mPagedView.onLauncherTransitionStart(l, animated, toWorkspace); } @Override public void onLauncherTransitionStep(Launcher l, float t) { - mAppsCustomizePane.onLauncherTransitionStep(l, t); + mPagedView.onLauncherTransitionStep(l, t); } @Override public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { - mAppsCustomizePane.onLauncherTransitionEnd(l, animated, toWorkspace); + mPagedView.onLauncherTransitionEnd(l, animated, toWorkspace); mInTransition = false; - if (animated) { - setLayerType(LAYER_TYPE_NONE, null); - } if (!toWorkspace) { - // 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()); + mPagedView.loadAssociatedPages(mPagedView.getCurrentPage()); + + // Opening apps, need to announce what page we are on. + AccessibilityManager am = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (am.isEnabled()) { + // Notify the user when the page changes + announceForAccessibility(mPagedView.getCurrentPageDescription()); + } // Going from Workspace -> All Apps // NOTE: We should do this at the end since we check visibility state in some of the @@ -459,25 +235,4 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona 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/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java new file mode 100644 index 000000000..00f0cf36f --- /dev/null +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2014 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.AppWidgetManager; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; +import android.util.Patterns; + +import com.android.launcher3.LauncherProvider.SqlArguments; +import com.android.launcher3.LauncherProvider.WorkspaceLoader; +import com.android.launcher3.LauncherSettings.Favorites; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This class contains contains duplication of functionality as found in + * LauncherProvider#DatabaseHelper. It has been isolated and differentiated in order + * to cleanly and separately represent AutoInstall default layout format and policy. + */ +public class AutoInstallsLayout implements WorkspaceLoader { + private static final String TAG = "AutoInstalls"; + private static final boolean LOGD = true; + + /** Marker action used to discover a package which defines launcher customization */ + static final String ACTION_LAUNCHER_CUSTOMIZATION = + "android.autoinstalls.config.action.PLAY_AUTO_INSTALL"; + + private static final String LAYOUT_RES = "default_layout"; + + static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost, + LayoutParserCallback callback) { + Pair<String, Resources> customizationApkInfo = Utilities.findSystemApk( + ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager()); + if (customizationApkInfo == null) { + return null; + } + + String pkg = customizationApkInfo.first; + Resources res = customizationApkInfo.second; + int layoutId = res.getIdentifier(LAYOUT_RES, "xml", pkg); + if (layoutId == 0) { + Log.e(TAG, "Layout definition not found in package: " + pkg); + return null; + } + return new AutoInstallsLayout(context, appWidgetHost, callback, pkg, res, layoutId); + } + + // Object Tags + private static final String TAG_WORKSPACE = "workspace"; + private static final String TAG_APP_ICON = "appicon"; + private static final String TAG_AUTO_INSTALL = "autoinstall"; + private static final String TAG_FOLDER = "folder"; + private static final String TAG_APPWIDGET = "appwidget"; + private static final String TAG_SHORTCUT = "shortcut"; + private static final String TAG_EXTRA = "extra"; + + private static final String ATTR_CONTAINER = "container"; + private static final String ATTR_RANK = "rank"; + + private static final String ATTR_PACKAGE_NAME = "packageName"; + private static final String ATTR_CLASS_NAME = "className"; + private static final String ATTR_TITLE = "title"; + private static final String ATTR_SCREEN = "screen"; + private static final String ATTR_X = "x"; + private static final String ATTR_Y = "y"; + private static final String ATTR_SPAN_X = "spanX"; + private static final String ATTR_SPAN_Y = "spanY"; + private static final String ATTR_ICON = "icon"; + private static final String ATTR_URL = "url"; + + // Style attrs -- "Extra" + private static final String ATTR_KEY = "key"; + private static final String ATTR_VALUE = "value"; + + private static final String HOTSEAT_CONTAINER_NAME = + Favorites.containerToString(Favorites.CONTAINER_HOTSEAT); + + private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE = + "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE"; + + private final Context mContext; + private final AppWidgetHost mAppWidgetHost; + private final LayoutParserCallback mCallback; + + private final PackageManager mPackageManager; + private final ContentValues mValues; + + private final Resources mRes; + private final int mLayoutId; + + private SQLiteDatabase mDb; + + public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, + LayoutParserCallback callback, String packageName, Resources res, int layoutId) { + mContext = context; + mAppWidgetHost = appWidgetHost; + mCallback = callback; + + mPackageManager = context.getPackageManager(); + mValues = new ContentValues(); + + mRes = res; + mLayoutId = layoutId; + } + + @Override + public int loadLayout(SQLiteDatabase db, ArrayList<Long> screenIds) { + mDb = db; + try { + return parseLayout(mRes, mLayoutId, screenIds); + } catch (XmlPullParserException | IOException | RuntimeException e) { + Log.w(TAG, "Got exception parsing layout.", e); + return -1; + } + } + + private int parseLayout(Resources res, int layoutId, ArrayList<Long> screenIds) + throws XmlPullParserException, IOException { + final int hotseatAllAppsRank = LauncherAppState.getInstance() + .getDynamicGrid().getDeviceProfile().hotseatAllAppsRank; + + XmlResourceParser parser = res.getXml(layoutId); + beginDocument(parser, TAG_WORKSPACE); + final int depth = parser.getDepth(); + int type; + HashMap<String, TagParser> tagParserMap = getLayoutElementsMap(); + int count = 0; + + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + + mValues.clear(); + final int container; + final long screenId; + + if (HOTSEAT_CONTAINER_NAME.equals(getAttributeValue(parser, ATTR_CONTAINER))) { + container = Favorites.CONTAINER_HOTSEAT; + + // Hack: hotseat items are stored using screen ids + long rank = Long.parseLong(getAttributeValue(parser, ATTR_RANK)); + screenId = (rank < hotseatAllAppsRank) ? rank : (rank + 1); + + } else { + container = Favorites.CONTAINER_DESKTOP; + screenId = Long.parseLong(getAttributeValue(parser, ATTR_SCREEN)); + + mValues.put(Favorites.CELLX, getAttributeValue(parser, ATTR_X)); + mValues.put(Favorites.CELLY, getAttributeValue(parser, ATTR_Y)); + } + + mValues.put(Favorites.CONTAINER, container); + mValues.put(Favorites.SCREEN, screenId); + + TagParser tagParser = tagParserMap.get(parser.getName()); + if (tagParser == null) { + if (LOGD) Log.d(TAG, "Ignoring unknown element tag: " + parser.getName()); + continue; + } + long newElementId = tagParser.parseAndAdd(parser, res); + if (newElementId >= 0) { + // Keep track of the set of screens which need to be added to the db. + if (!screenIds.contains(screenId) && + container == Favorites.CONTAINER_DESKTOP) { + screenIds.add(screenId); + } + count++; + } + } + return count; + } + + protected long addShortcut(String title, Intent intent, int type) { + long id = mCallback.generateNewItemId(); + mValues.put(Favorites.INTENT, intent.toUri(0)); + mValues.put(Favorites.TITLE, title); + mValues.put(Favorites.ITEM_TYPE, type); + mValues.put(Favorites.SPANX, 1); + mValues.put(Favorites.SPANY, 1); + mValues.put(Favorites._ID, id); + if (mCallback.insertAndCheck(mDb, mValues) < 0) { + return -1; + } else { + return id; + } + } + + protected HashMap<String, TagParser> getFolderElementsMap() { + HashMap<String, TagParser> parsers = new HashMap<String, TagParser>(); + parsers.put(TAG_APP_ICON, new AppShortcutParser()); + parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser()); + parsers.put(TAG_SHORTCUT, new ShortcutParser()); + return parsers; + } + + protected HashMap<String, TagParser> getLayoutElementsMap() { + HashMap<String, TagParser> parsers = new HashMap<String, TagParser>(); + parsers.put(TAG_APP_ICON, new AppShortcutParser()); + parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser()); + parsers.put(TAG_FOLDER, new FolderParser()); + parsers.put(TAG_APPWIDGET, new AppWidgetParser()); + parsers.put(TAG_SHORTCUT, new ShortcutParser()); + return parsers; + } + + private interface TagParser { + /** + * Parses the tag and adds to the db + * @return the id of the row added or -1; + */ + long parseAndAdd(XmlResourceParser parser, Resources res) + throws XmlPullParserException, IOException; + } + + private class AppShortcutParser implements TagParser { + + @Override + public long parseAndAdd(XmlResourceParser parser, Resources res) { + final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); + final String className = getAttributeValue(parser, ATTR_CLASS_NAME); + + if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) { + ActivityInfo info; + try { + ComponentName cn; + try { + cn = new ComponentName(packageName, className); + info = mPackageManager.getActivityInfo(cn, 0); + } catch (PackageManager.NameNotFoundException nnfe) { + String[] packages = mPackageManager.currentToCanonicalPackageNames( + new String[] { packageName }); + cn = new ComponentName(packages[0], className); + info = mPackageManager.getActivityInfo(cn, 0); + } + final Intent intent = new Intent(Intent.ACTION_MAIN, null) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(cn) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + + return addShortcut(info.loadLabel(mPackageManager).toString(), + intent, Favorites.ITEM_TYPE_APPLICATION); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Unable to add favorite: " + packageName + "/" + className, e); + } + return -1; + } else { + if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component or uri"); + return -1; + } + } + } + + private class AutoInstallParser implements TagParser { + + @Override + public long parseAndAdd(XmlResourceParser parser, Resources res) { + final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); + final String className = getAttributeValue(parser, ATTR_CLASS_NAME); + if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) { + if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component"); + return -1; + } + + mValues.put(Favorites.RESTORED, ShortcutInfo.FLAG_AUTOINTALL_ICON); + final Intent intent = new Intent(Intent.ACTION_MAIN, null) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(new ComponentName(packageName, className)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + return addShortcut(mContext.getString(R.string.package_state_unknown), intent, + Favorites.ITEM_TYPE_APPLICATION); + } + } + + private class ShortcutParser implements TagParser { + + @Override + public long parseAndAdd(XmlResourceParser parser, Resources res) { + final String url = getAttributeValue(parser, ATTR_URL); + final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0); + final int iconId = getAttributeResourceValue(parser, ATTR_ICON, 0); + + if (titleResId == 0 || iconId == 0) { + if (LOGD) Log.d(TAG, "Ignoring shortcut"); + return -1; + } + + if (TextUtils.isEmpty(url) || !Patterns.WEB_URL.matcher(url).matches()) { + if (LOGD) Log.d(TAG, "Ignoring shortcut, invalid url: " + url); + return -1; + } + Drawable icon = res.getDrawable(iconId); + if (icon == null) { + if (LOGD) Log.d(TAG, "Ignoring shortcut, can't load icon"); + return -1; + } + + ItemInfo.writeBitmap(mValues, Utilities.createIconBitmap(icon, mContext)); + final Intent intent = new Intent(Intent.ACTION_VIEW, null) + .setData(Uri.parse(url)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + return addShortcut(res.getString(titleResId), intent, Favorites.ITEM_TYPE_SHORTCUT); + } + } + + private class AppWidgetParser implements TagParser { + + @Override + public long parseAndAdd(XmlResourceParser parser, Resources res) + throws XmlPullParserException, IOException { + final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); + final String className = getAttributeValue(parser, ATTR_CLASS_NAME); + if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) { + if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component"); + return -1; + } + + ComponentName cn = new ComponentName(packageName, className); + try { + mPackageManager.getReceiverInfo(cn, 0); + } catch (Exception e) { + String[] packages = mPackageManager.currentToCanonicalPackageNames( + new String[] { packageName }); + cn = new ComponentName(packages[0], className); + try { + mPackageManager.getReceiverInfo(cn, 0); + } catch (Exception e1) { + if (LOGD) Log.d(TAG, "Can't find widget provider: " + className); + return -1; + } + } + + mValues.put(Favorites.SPANX, getAttributeValue(parser, ATTR_SPAN_X)); + mValues.put(Favorites.SPANY, getAttributeValue(parser, ATTR_SPAN_Y)); + + // Read the extras + Bundle extras = new Bundle(); + int widgetDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > widgetDepth) { + if (type != XmlPullParser.START_TAG) { + continue; + } + + if (TAG_EXTRA.equals(parser.getName())) { + String key = getAttributeValue(parser, ATTR_KEY); + String value = getAttributeValue(parser, ATTR_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"); + } + } + + final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); + long insertedId = -1; + try { + int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); + + if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn)) { + if (LOGD) Log.e(TAG, "Unable to bind app widget id " + cn); + return -1; + } + + mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); + mValues.put(Favorites.APPWIDGET_ID, appWidgetId); + mValues.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString()); + mValues.put(Favorites._ID, mCallback.generateNewItemId()); + insertedId = mCallback.insertAndCheck(mDb, mValues); + if (insertedId < 0) { + mAppWidgetHost.deleteAppWidgetId(appWidgetId); + return insertedId; + } + + // Send a broadcast to configure the widget + if (!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) { + if (LOGD) Log.e(TAG, "Problem allocating appWidgetId", ex); + } + return insertedId; + } + } + + private class FolderParser implements TagParser { + private final HashMap<String, TagParser> mFolderElements = getFolderElementsMap(); + + @Override + public long parseAndAdd(XmlResourceParser parser, Resources res) + throws XmlPullParserException, IOException { + final String title; + final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0); + if (titleResId != 0) { + title = res.getString(titleResId); + } else { + title = mContext.getResources().getString(R.string.folder_name); + } + + mValues.put(Favorites.TITLE, title); + mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER); + mValues.put(Favorites.SPANX, 1); + mValues.put(Favorites.SPANY, 1); + mValues.put(Favorites._ID, mCallback.generateNewItemId()); + long folderId = mCallback.insertAndCheck(mDb, mValues); + if (folderId < 0) { + if (LOGD) Log.e(TAG, "Unable to add folder"); + return -1; + } + + final ContentValues myValues = new ContentValues(mValues); + ArrayList<Long> folderItems = new ArrayList<Long>(); + + int type; + int folderDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > folderDepth) { + if (type != XmlPullParser.START_TAG) { + continue; + } + mValues.clear(); + mValues.put(Favorites.CONTAINER, folderId); + + TagParser tagParser = mFolderElements.get(parser.getName()); + if (tagParser != null) { + final long id = tagParser.parseAndAdd(parser, res); + if (id >= 0) { + folderItems.add(id); + } + } else { + throw new RuntimeException("Invalid folder item " + parser.getName()); + } + } + + long addedId = folderId; + + // 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) { + // Delete the folder + Uri uri = Favorites.getContentUri(folderId, false); + SqlArguments args = new SqlArguments(uri, null, null); + mDb.delete(args.table, args.where, args.args); + addedId = -1; + + // If we have a single item, promote it to where the folder + // would have been. + if (folderItems.size() == 1) { + final ContentValues childValues = new ContentValues(); + copyInteger(myValues, childValues, Favorites.CONTAINER); + copyInteger(myValues, childValues, Favorites.SCREEN); + copyInteger(myValues, childValues, Favorites.CELLX); + copyInteger(myValues, childValues, Favorites.CELLY); + + addedId = folderItems.get(0); + mDb.update(LauncherProvider.TABLE_FAVORITES, childValues, + Favorites._ID + "=" + addedId, null); + } + } + return addedId; + } + } + + 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); + } + } + + /** + * Return attribute value, attempting launcher-specific namespace first + * before falling back to anonymous attribute. + */ + private static String getAttributeValue(XmlResourceParser parser, String attribute) { + String value = parser.getAttributeValue( + "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute); + if (value == null) { + value = parser.getAttributeValue(null, attribute); + } + return value; + } + + /** + * Return attribute resource value, attempting launcher-specific namespace + * first before falling back to anonymous attribute. + */ + private static int getAttributeResourceValue(XmlResourceParser parser, String attribute, + int defaultValue) { + int value = parser.getAttributeResourceValue( + "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute, + defaultValue); + if (value == defaultValue) { + value = parser.getAttributeResourceValue(null, attribute, defaultValue); + } + return value; + } + + public static interface LayoutParserCallback { + long generateNewItemId(); + + long insertAndCheck(SQLiteDatabase db, ContentValues values); + } + + private static void copyInteger(ContentValues from, ContentValues to, String key) { + to.put(key, from.getAsInteger(key)); + } +} diff --git a/src/com/android/launcher3/BorderCropDrawable.java b/src/com/android/launcher3/BorderCropDrawable.java new file mode 100644 index 000000000..caf497d9b --- /dev/null +++ b/src/com/android/launcher3/BorderCropDrawable.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2014 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.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +public class BorderCropDrawable extends Drawable { + + private final Drawable mChild; + private final Rect mBoundsShift; + private final Rect mPadding; + + BorderCropDrawable(Drawable child, boolean cropLeft, + boolean cropTop, boolean cropRight, boolean cropBottom) { + mChild = child; + + mBoundsShift = new Rect(); + mPadding = new Rect(); + mChild.getPadding(mPadding); + + if (cropLeft) { + mBoundsShift.left = -mPadding.left; + mPadding.left = 0; + } + if (cropTop) { + mBoundsShift.top = -mPadding.top; + mPadding.top = 0; + } + if (cropRight) { + mBoundsShift.right = mPadding.right; + mPadding.right = 0; + } + if (cropBottom) { + mBoundsShift.bottom = mPadding.bottom; + mPadding.bottom = 0; + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + mChild.setBounds( + bounds.left + mBoundsShift.left, + bounds.top + mBoundsShift.top, + bounds.right + mBoundsShift.right, + bounds.bottom + mBoundsShift.bottom); + } + + @Override + public boolean getPadding(Rect padding) { + padding.set(mPadding); + return (padding.left | padding.top | padding.right | padding.bottom) != 0; + } + + @Override + public void draw(Canvas canvas) { + mChild.draw(canvas); + } + + @Override + public int getOpacity() { + return mChild.getOpacity(); + } + + @Override + public void setAlpha(int alpha) { + mChild.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mChild.setColorFilter(cf); + } +} diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index ee42904dd..a368796bd 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -17,16 +17,20 @@ package com.android.launcher3; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; 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.util.SparseArray; import android.util.TypedValue; +import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.ViewConfiguration; import android.widget.TextView; /** @@ -35,48 +39,57 @@ import android.widget.TextView; * too aggressive. */ public class BubbleTextView extends TextView { - 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 static SparseArray<Theme> sPreloaderThemes = new SparseArray<>(2); + + private static final float SHADOW_LARGE_RADIUS = 4.0f; + private static final float SHADOW_SMALL_RADIUS = 1.75f; + private static final float SHADOW_Y_OFFSET = 2.0f; + private static final int SHADOW_LARGE_COLOUR = 0xDD000000; + private static final int SHADOW_SMALL_COLOUR = 0xCC000000; + static final float PADDING_V = 3.0f; private HolographicOutlineHelper mOutlineHelper; - 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 Bitmap mPressedBackground; + + private float mSlop; private int mTextColor; - private boolean mShadowsEnabled = true; + private final boolean mCustomShadowsEnabled; private boolean mIsTextVisible; + // TODO: Remove custom background handling code, as no instance of BubbleTextView use any + // background. private boolean mBackgroundSizeChanged; - private Drawable mBackground; + private final Drawable mBackground; private boolean mStayPressed; + private boolean mIgnorePressedStateChange; private CheckLongPressHelper mLongPressHelper; public BubbleTextView(Context context) { - super(context); - init(); + this(context, null, 0); } public BubbleTextView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); + this(context, attrs, 0); } public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.BubbleTextView, defStyle, 0); + mCustomShadowsEnabled = a.getBoolean(R.styleable.BubbleTextView_customShadows, true); + a.recycle(); + + if (mCustomShadowsEnabled) { + // Draw the background itself as the parent is drawn twice. + mBackground = getBackground(); + setBackground(null); + } else { + mBackground = null; + } init(); } @@ -87,34 +100,62 @@ public class BubbleTextView extends TextView { LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); - setTextColor(getResources().getColor(R.color.workspace_icon_text_color)); } private void init() { mLongPressHelper = new CheckLongPressHelper(this); - mBackground = getBackground(); mOutlineHelper = HolographicOutlineHelper.obtain(getContext()); + if (mCustomShadowsEnabled) { + setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); + } + } - final Resources res = getContext().getResources(); - mFocusedOutlineColor = mFocusedGlowColor = mPressedOutlineColor = mPressedGlowColor = - res.getColor(R.color.outline_color); - - setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); + public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, + boolean setDefaultPadding) { + applyFromShortcutInfo(info, iconCache, setDefaultPadding, false); } - public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) { + public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, + boolean setDefaultPadding, boolean promiseStateChanged) { Bitmap b = info.getIcon(iconCache); LauncherAppState app = LauncherAppState.getInstance(); + + FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b); + iconDrawable.setGhostModeEnabled(info.isDisabled); + + setCompoundDrawables(null, iconDrawable, null, null); + if (setDefaultPadding) { + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + setCompoundDrawablePadding(grid.iconDrawablePaddingPx); + } + if (info.contentDescription != null) { + setContentDescription(info.contentDescription); + } + setText(info.title); + setTag(info); + + if (promiseStateChanged || info.isPromise()) { + applyState(promiseStateChanged); + } + } + + public void applyFromApplicationInfo(AppInfo info) { + LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setCompoundDrawables(null, - Utilities.createIconDrawable(b), null, null); + Drawable topDrawable = Utilities.createIconDrawable(info.iconBitmap); + topDrawable.setBounds(0, 0, grid.allAppsIconSizePx, grid.allAppsIconSizePx); + setCompoundDrawables(null, topDrawable, null, null); setCompoundDrawablePadding(grid.iconDrawablePaddingPx); setText(info.title); + if (info.contentDescription != null) { + setContentDescription(info.contentDescription); + } setTag(info); } + @Override protected boolean setFrame(int left, int top, int right, int bottom) { if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) { @@ -137,90 +178,19 @@ public class BubbleTextView extends TextView { } @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(); - } - } + public void setPressed(boolean pressed) { + super.setPressed(pressed); - Drawable d = mBackground; - if (d != null && d.isStateful()) { - d.setState(getDrawableState()); + if (!mIgnorePressedStateChange) { + updateIconState(); } - 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(); - } - - public void setGlowColor(int color) { - mFocusedOutlineColor = mFocusedGlowColor = mPressedOutlineColor = mPressedGlowColor = color; - } - - /** - * 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 = mOutlineHelper.mMaxOuterBlurRadius; - 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; + private void updateIconState() { + Drawable top = getCompoundDrawables()[1]; + if (top instanceof FastBitmapDrawable) { + ((FastBitmapDrawable) top).setPressed(isPressed() || mStayPressed); + } } @Override @@ -231,20 +201,11 @@ public class BubbleTextView extends TextView { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - // So that the pressed outline is visible immediately when isPressed() is true, + // So that the pressed outline is visible immediately on setStayPressed(), // 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; + if (mPressedBackground == null) { + mPressedBackground = mOutlineHelper.createMediumDropShadow(this); } mLongPressHelper.postCheckForLongPress(); @@ -254,11 +215,16 @@ public class BubbleTextView extends TextView { // 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; + mPressedBackground = null; } mLongPressHelper.cancelLongPress(); break; + case MotionEvent.ACTION_MOVE: + if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) { + mLongPressHelper.cancelLongPress(); + } + break; } return result; } @@ -266,37 +232,52 @@ public class BubbleTextView extends TextView { void setStayPressed(boolean stayPressed) { mStayPressed = stayPressed; if (!stayPressed) { - mPressedOrFocusedBackground = null; + mPressedBackground = null; } - setCellLayoutPressedOrFocusedIcon(); - } - void setCellLayoutPressedOrFocusedIcon() { + // Only show the shadow effect when persistent pressed state is set. if (getParent() instanceof ShortcutAndWidgetContainer) { - ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) getParent(); - if (parent != null) { - CellLayout layout = (CellLayout) parent.getParent(); - layout.setPressedOrFocusedIcon((mPressedOrFocusedBackground != null) ? this : null); - } + CellLayout layout = (CellLayout) getParent().getParent(); + layout.setPressedIcon(this, mPressedBackground, mOutlineHelper.shadowBitmapPadding); } + + updateIconState(); } - void clearPressedOrFocusedBackground() { - mPressedOrFocusedBackground = null; - setCellLayoutPressedOrFocusedIcon(); + void clearPressedBackground() { + setPressed(false); + setStayPressed(false); } - Bitmap getPressedOrFocusedBackground() { - return mPressedOrFocusedBackground; + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (super.onKeyDown(keyCode, event)) { + // Pre-create shadow so show immediately on click. + if (mPressedBackground == null) { + mPressedBackground = mOutlineHelper.createMediumDropShadow(this); + } + return true; + } + return false; } - int getPressedOrFocusedBackgroundPadding() { - return mOutlineHelper.mMaxOuterBlurRadius / 2; + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + // Unlike touch events, keypress event propagate pressed state change immediately, + // without waiting for onClickHandler to execute. Disable pressed state changes here + // to avoid flickering. + mIgnorePressedStateChange = true; + boolean result = super.onKeyUp(keyCode, event); + + mPressedBackground = null; + mIgnorePressedStateChange = false; + updateIconState(); + return result; } @Override public void draw(Canvas canvas) { - if (!mShadowsEnabled) { + if (!mCustomShadowsEnabled) { super.draw(canvas); return; } @@ -342,7 +323,14 @@ public class BubbleTextView extends TextView { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + if (mBackground != null) mBackground.setCallback(this); + Drawable top = getCompoundDrawables()[1]; + + if (top instanceof PreloadIconDrawable) { + ((PreloadIconDrawable) top).applyTheme(getPreloaderTheme()); + } + mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @Override @@ -357,10 +345,10 @@ public class BubbleTextView extends TextView { super.setTextColor(color); } - public void setShadowsEnabled(boolean enabled) { - mShadowsEnabled = enabled; - getPaint().clearShadowLayer(); - invalidate(); + @Override + public void setTextColor(ColorStateList colors) { + mTextColor = colors.getDefaultColor(); + super.setTextColor(colors); } public void setTextVisibility(boolean visible) { @@ -379,10 +367,6 @@ public class BubbleTextView extends TextView { @Override protected boolean onSetAlpha(int alpha) { - if (mPrevAlpha != alpha) { - mPrevAlpha = alpha; - super.onSetAlpha(alpha); - } return true; } @@ -392,4 +376,45 @@ public class BubbleTextView extends TextView { mLongPressHelper.cancelLongPress(); } + + public void applyState(boolean promiseStateChanged) { + if (getTag() instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) getTag(); + final boolean isPromise = info.isPromise(); + final int progressLevel = isPromise ? + ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ? + info.getInstallProgress() : 0)) : 100; + + Drawable[] drawables = getCompoundDrawables(); + Drawable top = drawables[1]; + if (top != null) { + final PreloadIconDrawable preloadDrawable; + if (top instanceof PreloadIconDrawable) { + preloadDrawable = (PreloadIconDrawable) top; + } else { + preloadDrawable = new PreloadIconDrawable(top, getPreloaderTheme()); + setCompoundDrawables(drawables[0], preloadDrawable, drawables[2], drawables[3]); + } + + preloadDrawable.setLevel(progressLevel); + if (promiseStateChanged) { + preloadDrawable.maybePerformFinishedAnimation(); + } + } + } + } + + private Theme getPreloaderTheme() { + Object tag = getTag(); + int style = ((tag != null) && (tag instanceof ShortcutInfo) && + (((ShortcutInfo) tag).container >= 0)) ? R.style.PreloadIcon_Folder + : R.style.PreloadIcon; + Theme theme = sPreloaderThemes.get(style); + if (theme == null) { + theme = getResources().newTheme(); + theme.applyStyle(style, true); + sPreloaderThemes.put(style, theme); + } + return theme; + } } diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 2436a51a3..0ff1ef4ad 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -30,8 +30,6 @@ 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; @@ -73,11 +71,8 @@ public class CellLayout extends ViewGroup { private int mWidthGap; private int mHeightGap; private int mMaxGap; - private boolean mScrollingTransformsDirty = false; private boolean mDropPending = false; - - private final Rect mRect = new Rect(); - private final CellInfo mCellInfo = new CellInfo(); + private boolean mIsDragTarget = true; // 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. @@ -128,7 +123,7 @@ public class CellLayout extends ViewGroup { private int mDragOutlineCurrent = 0; private final Paint mDragOutlinePaint = new Paint(); - private BubbleTextView mPressedOrFocusedIcon; + private final FastBitmapView mTouchFeedbackView; private HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new HashMap<CellLayout.LayoutParams, Animator>(); @@ -172,8 +167,6 @@ public class CellLayout extends ViewGroup { private Rect mTempRect = new Rect(); - private final static PorterDuffXfermode sAddBlendMode = - new PorterDuffXfermode(PorterDuff.Mode.ADD); private final static Paint sPaint = new Paint(); public CellLayout(Context context) { @@ -295,6 +288,9 @@ public class CellLayout extends ViewGroup { mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap, mCountX, mCountY); + mTouchFeedbackView = new FastBitmapView(context); + // Make the feedback view large enough to hold the blur bitmap. + addView(mTouchFeedbackView, (int) (grid.cellWidthPx * 1.5), (int) (grid.cellHeightPx * 1.5)); addView(mShortcutsAndWidgets); } @@ -341,14 +337,6 @@ public class CellLayout extends ViewGroup { return mDropPending; } - 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; @@ -362,24 +350,23 @@ public class CellLayout extends ViewGroup { 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; - setUseActiveGlowBackground(mIsDragOverlapping); - invalidate(); + void setPressedIcon(BubbleTextView icon, Bitmap background, int padding) { + if (icon == null || background == null) { + mTouchFeedbackView.setBitmap(null); + mTouchFeedbackView.animate().cancel(); + } else { + int offset = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() + - (mCountX * mCellWidth); + mTouchFeedbackView.setTranslationX(icon.getLeft() + (int) Math.ceil(offset / 2f) + - padding); + mTouchFeedbackView.setTranslationY(icon.getTop() - padding); + if (mTouchFeedbackView.setBitmap(background)) { + mTouchFeedbackView.setAlpha(0); + mTouchFeedbackView.animate().alpha(1) + .setDuration(FastBitmapDrawable.CLICK_FEEDBACK_DURATION) + .setInterpolator(FastBitmapDrawable.CLICK_FEEDBACK_INTERPOLATOR) + .start(); + } } } @@ -391,27 +378,26 @@ public class CellLayout extends ViewGroup { mDrawBackground = false; } - boolean getIsDragOverlapping() { - return mIsDragOverlapping; + void disableDragTarget() { + mIsDragTarget = false; } - protected void setOverscrollTransformsDirty(boolean dirty) { - mScrollingTransformsDirty = dirty; + boolean isDragTarget() { + return mIsDragTarget; } - 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); + void setIsDragOverlapping(boolean isDragOverlapping) { + if (mIsDragOverlapping != isDragOverlapping) { + mIsDragOverlapping = isDragOverlapping; + setUseActiveGlowBackground(mIsDragOverlapping); + invalidate(); } } + boolean getIsDragOverlapping() { + return mIsDragOverlapping; + } + @Override protected void onDraw(Canvas canvas) { // When we're large, we are either drawn in a "hover" state (ie when dragging an item to @@ -447,23 +433,6 @@ public class CellLayout extends ViewGroup { } } - // 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) { - int offset = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - - (mCountX * mCellWidth); - int left = getPaddingLeft() + (int) Math.ceil(offset / 2f); - int top = getPaddingTop(); - canvas.drawBitmap(b, - mPressedOrFocusedIcon.getLeft() + left - padding, - mPressedOrFocusedIcon.getTop() + top - padding, - null); - } - } - if (DEBUG_VISUALIZE_OCCUPIED) { int[] pt = new int[2]; ColorDrawable cd = new ColorDrawable(Color.RED); @@ -582,7 +551,15 @@ public class CellLayout extends ViewGroup { } public void restoreInstanceState(SparseArray<Parcelable> states) { - dispatchRestoreInstanceState(states); + try { + dispatchRestoreInstanceState(states); + } catch (IllegalArgumentException ex) { + if (LauncherAppState.isDogfoodBuild()) { + throw ex; + } + // Mismatched viewId / viewType preventing restore. Skip restore on production builds. + Log.e(TAG, "Ignoring an error while restoring a view instance state", ex); + } } @Override @@ -699,103 +676,17 @@ public class CellLayout extends ViewGroup { } @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - if (getParent() instanceof Workspace) { - Workspace workspace = (Workspace) getParent(); - mCellInfo.screenId = workspace.getIdForScreen(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 @@ -1049,6 +940,7 @@ public class CellLayout extends ViewGroup { } public void setBackgroundAlphaMultiplier(float multiplier) { + if (mBackgroundAlphaMultiplier != multiplier) { mBackgroundAlphaMultiplier = multiplier; invalidate(); @@ -1067,17 +959,11 @@ public class CellLayout extends ViewGroup { } public void setShortcutAndWidgetAlpha(float alpha) { - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - getChildAt(i).setAlpha(alpha); - } + mShortcutsAndWidgets.setAlpha(alpha); } public ShortcutAndWidgetContainer getShortcutsAndWidgets() { - if (getChildCount() > 0) { - return (ShortcutAndWidgetContainer) getChildAt(0); - } - return null; + return mShortcutsAndWidgets; } public View getChildAt(int x, int y) { @@ -3360,6 +3246,16 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { long screenId; long container; + CellInfo(View v, ItemInfo info) { + cell = v; + cellX = info.cellX; + cellY = info.cellY; + spanX = info.spanX; + spanY = info.spanY; + screenId = info.screenId; + container = info.container; + } + @Override public String toString() { return "Cell[view=" + (cell == null ? "null" : cell.getClass()) diff --git a/src/com/android/launcher3/Cling.java b/src/com/android/launcher3/Cling.java deleted file mode 100644 index a6139ccbc..000000000 --- a/src/com/android/launcher3/Cling.java +++ /dev/null @@ -1,571 +0,0 @@ -/* - * 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.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.*; -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.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; -import android.widget.FrameLayout; -import android.widget.TextView; - -public class Cling extends FrameLayout implements Insettable, View.OnClickListener, - View.OnLongClickListener, View.OnTouchListener { - - private static String FIRST_RUN_PORTRAIT = "first_run_portrait"; - private static String FIRST_RUN_LANDSCAPE = "first_run_landscape"; - - 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 MIGRATION_PORTRAIT = "migration_portrait"; - private static String MIGRATION_LANDSCAPE = "migration_landscape"; - - private static String MIGRATION_WORKSPACE_PORTRAIT = "migration_workspace_portrait"; - private static String MIGRATION_WORKSPACE_LARGE_PORTRAIT = "migration_workspace_large_portrait"; - private static String MIGRATION_WORKSPACE_LANDSCAPE = "migration_workspace_landscape"; - - private static String FOLDER_PORTRAIT = "folder_portrait"; - private static String FOLDER_LANDSCAPE = "folder_landscape"; - private static String FOLDER_LARGE = "folder_large"; - - private static float FIRST_RUN_CIRCLE_BUFFER_DPS = 60; - private static float FIRST_RUN_MAX_CIRCLE_RADIUS_DPS = 180; - private static float WORKSPACE_INNER_CIRCLE_RADIUS_DPS = 50; - private static float WORKSPACE_OUTER_CIRCLE_RADIUS_DPS = 60; - private static float WORKSPACE_CIRCLE_Y_OFFSET_DPS = 30; - private static float MIGRATION_WORKSPACE_INNER_CIRCLE_RADIUS_DPS = 42; - private static float MIGRATION_WORKSPACE_OUTER_CIRCLE_RADIUS_DPS = 46; - - private Launcher mLauncher; - private boolean mIsInitialized; - private String mDrawIdentifier; - private Drawable mBackground; - - private int[] mTouchDownPt = new int[2]; - - private Drawable mFocusedHotseatApp; - private ComponentName mFocusedHotseatAppComponent; - private Rect mFocusedHotseatAppBounds; - - private Paint mErasePaint; - private Paint mBorderPaint; - private Paint mBubblePaint; - private Paint mDotPaint; - - private View mScrimView; - private int mBackgroundColor; - - private final Rect mInsets = new Rect(); - - 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, View scrim) { - if (!mIsInitialized) { - mLauncher = l; - mScrimView = scrim; - mBackgroundColor = 0xcc000000; - setOnLongClickListener(this); - setOnClickListener(this); - setOnTouchListener(this); - - mErasePaint = new Paint(); - mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); - mErasePaint.setColor(0xFFFFFF); - mErasePaint.setAlpha(0); - mErasePaint.setAntiAlias(true); - - mBorderPaint = new Paint(); - mBorderPaint.setColor(0xFFFFFFFF); - mBorderPaint.setAntiAlias(true); - - int circleColor = getResources().getColor( - R.color.first_run_cling_circle_background_color); - mBubblePaint = new Paint(); - mBubblePaint.setColor(circleColor); - mBubblePaint.setAntiAlias(true); - - mDotPaint = new Paint(); - mDotPaint.setColor(0x72BBED); - mDotPaint.setAntiAlias(true); - - mIsInitialized = true; - } - } - - void setFocusedHotseatApp(int drawableId, int appRank, ComponentName cn, String title, - String description) { - // Get the app to draw - Resources r = getResources(); - int appIconId = drawableId; - Hotseat hotseat = mLauncher.getHotseat(); - // Skip the focused app in the large layouts - if (!mDrawIdentifier.equals(WORKSPACE_LARGE) && - hotseat != null && appIconId > -1 && appRank > -1 && !title.isEmpty() && - !description.isEmpty()) { - // Set the app bounds - int x = hotseat.getCellXFromOrder(appRank); - int y = hotseat.getCellYFromOrder(appRank); - Rect pos = hotseat.getCellCoordinates(x, y); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mFocusedHotseatApp = getResources().getDrawable(appIconId); - mFocusedHotseatAppComponent = cn; - mFocusedHotseatAppBounds = new Rect(pos.left, pos.top, - pos.left + Utilities.sIconTextureWidth, - pos.top + Utilities.sIconTextureHeight); - Utilities.scaleRectAboutCenter(mFocusedHotseatAppBounds, - ((float) grid.hotseatIconSizePx / grid.iconSizePx)); - - // Set the title - TextView v = (TextView) findViewById(R.id.focused_hotseat_app_title); - if (v != null) { - v.setText(title); - } - - // Set the description - v = (TextView) findViewById(R.id.focused_hotseat_app_description); - if (v != null) { - v.setText(description); - } - - // Show the bubble - View bubble = findViewById(R.id.focused_hotseat_app_bubble); - bubble.setVisibility(View.VISIBLE); - } - } - - void setOpenFolderRect(Rect r) { - if (mDrawIdentifier.equals(FOLDER_LANDSCAPE) || - mDrawIdentifier.equals(FOLDER_LARGE)) { - ViewGroup vg = (ViewGroup) findViewById(R.id.folder_bubble); - ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) vg.getLayoutParams(); - lp.topMargin = r.top - mInsets.bottom; - lp.leftMargin = r.right; - vg.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); - vg.requestLayout(); - } - } - - void updateMigrationWorkspaceBubblePosition() { - DisplayMetrics metrics = new DisplayMetrics(); - mLauncher.getWindowManager().getDefaultDisplay().getMetrics(metrics); - - // Get the page indicator bounds - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - Rect pageIndicatorBounds = grid.getWorkspacePageIndicatorBounds(mInsets); - - if (mDrawIdentifier.equals(MIGRATION_WORKSPACE_PORTRAIT)) { - View bubble = findViewById(R.id.migration_workspace_cling_bubble); - ViewGroup.MarginLayoutParams lp = - (ViewGroup.MarginLayoutParams) bubble.getLayoutParams(); - lp.bottomMargin = grid.heightPx - pageIndicatorBounds.top; - bubble.requestLayout(); - } else if (mDrawIdentifier.equals(MIGRATION_WORKSPACE_LARGE_PORTRAIT)) { - View bubble = findViewById(R.id.content); - ViewGroup.MarginLayoutParams lp = - (ViewGroup.MarginLayoutParams) bubble.getLayoutParams(); - lp.bottomMargin = grid.heightPx - pageIndicatorBounds.top; - bubble.requestLayout(); - } else if (mDrawIdentifier.equals(MIGRATION_WORKSPACE_LANDSCAPE)) { - View bubble = findViewById(R.id.content); - ViewGroup.MarginLayoutParams lp = - (ViewGroup.MarginLayoutParams) bubble.getLayoutParams(); - if (grid.isLayoutRtl) { - lp.leftMargin = pageIndicatorBounds.right; - } else { - lp.rightMargin = (grid.widthPx - pageIndicatorBounds.left); - } - bubble.requestLayout(); - } - } - - void updateWorkspaceBubblePosition() { - DisplayMetrics metrics = new DisplayMetrics(); - mLauncher.getWindowManager().getDefaultDisplay().getMetrics(metrics); - - // Get the cut-out bounds - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - Rect cutOutBounds = getWorkspaceCutOutBounds(metrics); - - if (mDrawIdentifier.equals(WORKSPACE_LARGE)) { - View bubble = findViewById(R.id.workspace_cling_bubble); - ViewGroup.MarginLayoutParams lp = - (ViewGroup.MarginLayoutParams) bubble.getLayoutParams(); - lp.bottomMargin = grid.heightPx - cutOutBounds.top - mInsets.bottom; - bubble.requestLayout(); - } - } - - private Rect getWorkspaceCutOutBounds(DisplayMetrics metrics) { - int halfWidth = metrics.widthPixels / 2; - int halfHeight = metrics.heightPixels / 2; - int yOffset = DynamicGrid.pxFromDp(WORKSPACE_CIRCLE_Y_OFFSET_DPS, metrics); - if (mDrawIdentifier.equals(WORKSPACE_LARGE)) { - yOffset = 0; - } - int radius = DynamicGrid.pxFromDp(WORKSPACE_OUTER_CIRCLE_RADIUS_DPS, metrics); - return new Rect(halfWidth - radius, halfHeight - yOffset - radius, halfWidth + radius, - halfHeight - yOffset + radius); - } - - void show(boolean animate, int duration) { - setVisibility(View.VISIBLE); - setLayerType(View.LAYER_TYPE_HARDWARE, null); - if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || - mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || - mDrawIdentifier.equals(WORKSPACE_LARGE) || - mDrawIdentifier.equals(WORKSPACE_CUSTOM) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_PORTRAIT) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_LARGE_PORTRAIT) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_LANDSCAPE)) { - View content = getContent(); - content.setAlpha(0f); - content.animate() - .alpha(1f) - .setDuration(duration) - .setListener(null) - .start(); - setAlpha(1f); - } else { - if (animate) { - buildLayer(); - setAlpha(0f); - animate() - .alpha(1f) - .setInterpolator(new AccelerateInterpolator()) - .setDuration(duration) - .setListener(null) - .start(); - } else { - setAlpha(1f); - } - } - - // Show the scrim if necessary - if (mScrimView != null) { - mScrimView.setVisibility(View.VISIBLE); - mScrimView.setAlpha(0f); - mScrimView.animate() - .alpha(1f) - .setDuration(duration) - .setListener(null) - .start(); - } - - setFocusableInTouchMode(true); - post(new Runnable() { - public void run() { - setFocusable(true); - requestFocus(); - } - }); - } - - void hide(final int duration, final Runnable postCb) { - if (mDrawIdentifier.equals(FIRST_RUN_PORTRAIT) || - mDrawIdentifier.equals(FIRST_RUN_LANDSCAPE) || - mDrawIdentifier.equals(MIGRATION_PORTRAIT) || - mDrawIdentifier.equals(MIGRATION_LANDSCAPE)) { - View content = getContent(); - content.animate() - .alpha(0f) - .setDuration(duration) - .setListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - // We are about to trigger the workspace cling, so don't do anything else - setVisibility(View.GONE); - postCb.run(); - }; - }) - .start(); - } else { - animate() - .alpha(0f) - .setDuration(duration) - .setListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - // We are about to trigger the workspace cling, so don't do anything else - setVisibility(View.GONE); - postCb.run(); - }; - }) - .start(); - } - - // Show the scrim if necessary - if (mScrimView != null) { - mScrimView.animate() - .alpha(0f) - .setDuration(duration) - .setListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - mScrimView.setVisibility(View.GONE); - }; - }) - .start(); - } - } - - void cleanup() { - mBackground = null; - mIsInitialized = false; - } - - void bringScrimToFront() { - if (mScrimView != null) { - mScrimView.bringToFront(); - } - } - - @Override - public void setInsets(Rect insets) { - mInsets.set(insets); - setPadding(insets.left, insets.top, insets.right, insets.bottom); - } - - View getContent() { - return findViewById(R.id.content); - } - - String getDrawIdentifier() { - return mDrawIdentifier; - } - - @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(WORKSPACE_CUSTOM)); - } - - @Override - public boolean onTouchEvent(android.view.MotionEvent event) { - 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 super.onTouchEvent(event); - }; - - @Override - public boolean onTouch(View v, MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mTouchDownPt[0] = (int) ev.getX(); - mTouchDownPt[1] = (int) ev.getY(); - } - return false; - } - - @Override - public void onClick(View v) { - if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || - mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || - mDrawIdentifier.equals(WORKSPACE_LARGE)) { - if (mFocusedHotseatAppBounds != null && - mFocusedHotseatAppBounds.contains(mTouchDownPt[0], mTouchDownPt[1])) { - // Launch the activity that is being highlighted - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setComponent(mFocusedHotseatAppComponent); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - mLauncher.startActivity(intent, null); - mLauncher.getLauncherClings().dismissWorkspaceCling(this); - } - } - } - - @Override - public boolean onLongClick(View v) { - if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || - mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || - mDrawIdentifier.equals(WORKSPACE_LARGE)) { - mLauncher.getLauncherClings().dismissWorkspaceCling(null); - return true; - } else if (mDrawIdentifier.equals(MIGRATION_WORKSPACE_PORTRAIT) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_LARGE_PORTRAIT) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_LANDSCAPE)) { - mLauncher.getLauncherClings().dismissMigrationWorkspaceCling(null); - return true; - } - return false; - } - - @Override - protected void dispatchDraw(Canvas canvas) { - if (mIsInitialized) { - canvas.save(); - - // Get the page indicator bounds - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - Rect pageIndicatorBounds = grid.getWorkspacePageIndicatorBounds(mInsets); - - // Get the background override if there is one - if (mBackground == null) { - if (mDrawIdentifier.equals(WORKSPACE_CUSTOM)) { - mBackground = getResources().getDrawable(R.drawable.bg_cling5); - } - } - // Draw the background - Bitmap eraseBg = null; - Canvas eraseCanvas = null; - if (mScrimView != null) { - // Skip drawing the background - mScrimView.setBackgroundColor(mBackgroundColor); - } else if (mBackground != null) { - mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); - mBackground.draw(canvas); - } else if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || - mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || - mDrawIdentifier.equals(WORKSPACE_LARGE) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_PORTRAIT) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_LARGE_PORTRAIT) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_LANDSCAPE)) { - // Initialize the draw buffer (to allow punching through) - eraseBg = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), - Bitmap.Config.ARGB_8888); - eraseCanvas = new Canvas(eraseBg); - eraseCanvas.drawColor(mBackgroundColor); - } else { - canvas.drawColor(mBackgroundColor); - } - - // Draw everything else - DisplayMetrics metrics = new DisplayMetrics(); - mLauncher.getWindowManager().getDefaultDisplay().getMetrics(metrics); - float alpha = getAlpha(); - View content = getContent(); - if (content != null) { - alpha *= content.getAlpha(); - } - if (mDrawIdentifier.equals(FIRST_RUN_PORTRAIT) || - mDrawIdentifier.equals(FIRST_RUN_LANDSCAPE)) { - // Draw the circle - View bubbleContent = findViewById(R.id.bubble_content); - Rect bubbleRect = new Rect(); - bubbleContent.getGlobalVisibleRect(bubbleRect); - mBubblePaint.setAlpha((int) (255 * alpha)); - float buffer = DynamicGrid.pxFromDp(FIRST_RUN_CIRCLE_BUFFER_DPS, metrics); - float maxRadius = DynamicGrid.pxFromDp(FIRST_RUN_MAX_CIRCLE_RADIUS_DPS, metrics); - float radius = Math.min(maxRadius, (bubbleContent.getMeasuredWidth() + buffer) / 2); - canvas.drawCircle(metrics.widthPixels / 2, - bubbleRect.centerY(), radius, - mBubblePaint); - } else if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || - mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || - mDrawIdentifier.equals(WORKSPACE_LARGE)) { - Rect cutOutBounds = getWorkspaceCutOutBounds(metrics); - // Draw the outer circle - mErasePaint.setAlpha(128); - eraseCanvas.drawCircle(cutOutBounds.centerX(), cutOutBounds.centerY(), - DynamicGrid.pxFromDp(WORKSPACE_OUTER_CIRCLE_RADIUS_DPS, metrics), - mErasePaint); - // Draw the inner circle - mErasePaint.setAlpha(0); - eraseCanvas.drawCircle(cutOutBounds.centerX(), cutOutBounds.centerY(), - DynamicGrid.pxFromDp(WORKSPACE_INNER_CIRCLE_RADIUS_DPS, metrics), - mErasePaint); - canvas.drawBitmap(eraseBg, 0, 0, null); - eraseCanvas.setBitmap(null); - eraseBg = null; - - // Draw the focused hotseat app icon - if (mFocusedHotseatAppBounds != null && mFocusedHotseatApp != null) { - mFocusedHotseatApp.setBounds(mFocusedHotseatAppBounds.left, - mFocusedHotseatAppBounds.top, mFocusedHotseatAppBounds.right, - mFocusedHotseatAppBounds.bottom); - mFocusedHotseatApp.setAlpha((int) (255 * alpha)); - mFocusedHotseatApp.draw(canvas); - } - } else if (mDrawIdentifier.equals(MIGRATION_WORKSPACE_PORTRAIT) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_LARGE_PORTRAIT) || - mDrawIdentifier.equals(MIGRATION_WORKSPACE_LANDSCAPE)) { - int offset = DynamicGrid.pxFromDp(WORKSPACE_CIRCLE_Y_OFFSET_DPS, metrics); - // Draw the outer circle - eraseCanvas.drawCircle(pageIndicatorBounds.centerX(), - pageIndicatorBounds.centerY(), - DynamicGrid.pxFromDp(MIGRATION_WORKSPACE_OUTER_CIRCLE_RADIUS_DPS, metrics), - mBorderPaint); - // Draw the inner circle - mErasePaint.setAlpha(0); - eraseCanvas.drawCircle(pageIndicatorBounds.centerX(), - pageIndicatorBounds.centerY(), - DynamicGrid.pxFromDp(MIGRATION_WORKSPACE_INNER_CIRCLE_RADIUS_DPS, metrics), - mErasePaint); - canvas.drawBitmap(eraseBg, 0, 0, null); - eraseCanvas.setBitmap(null); - eraseBg = null; - } - canvas.restore(); - } - - // Draw the rest of the cling - super.dispatchDraw(canvas); - }; -} diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index 75d906bc2..05e8906cb 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -30,6 +30,9 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.TransitionDrawable; import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.UserManager; import android.util.AttributeSet; import android.view.View; import android.view.ViewConfiguration; @@ -38,6 +41,9 @@ import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; import android.view.animation.LinearInterpolator; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.UserHandleCompat; + import java.util.List; import java.util.Set; @@ -125,11 +131,15 @@ public class DeleteDropTarget extends ButtonDropTarget { } private void setHoverColor() { - mCurrentDrawable.startTransition(mTransitionDuration); + if (mCurrentDrawable != null) { + mCurrentDrawable.startTransition(mTransitionDuration); + } setTextColor(mHoverColor); } private void resetHoverColor() { - mCurrentDrawable.resetTransition(); + if (mCurrentDrawable != null) { + mCurrentDrawable.resetTransition(); + } setTextColor(mOriginalTextColor); } @@ -184,6 +194,17 @@ public class DeleteDropTarget extends ButtonDropTarget { if (!willAcceptDrop(info) || isAllAppsWidget(source, info)) { isVisible = false; } + if (useUninstallLabel) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + UserManager userManager = (UserManager) + getContext().getSystemService(Context.USER_SERVICE); + Bundle restrictions = userManager.getUserRestrictions(); + if (restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false) + || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false)) { + isVisible = false; + } + } + } if (useUninstallLabel) { setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null); @@ -230,8 +251,11 @@ public class DeleteDropTarget extends ButtonDropTarget { final DragLayer dragLayer = mLauncher.getDragLayer(); final Rect from = new Rect(); dragLayer.getViewRectRelativeToSelf(d.dragView, from); + + int width = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicWidth(); + int height = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicHeight(); final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), - mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); + width, height); final float scale = (float) to.width() / from.width(); mSearchDropTargetBar.deferOnDragEnd(); @@ -279,25 +303,24 @@ public class DeleteDropTarget extends ButtonDropTarget { if (isAllAppsApplication(d.dragSource, item)) { // Uninstall the application if it is being dragged from AppsCustomize AppInfo appInfo = (AppInfo) item; - mLauncher.startApplicationUninstallActivity(appInfo.componentName, appInfo.flags); + mLauncher.startApplicationUninstallActivity(appInfo.componentName, appInfo.flags, + appInfo.user); } else if (isUninstallFromWorkspace(d)) { ShortcutInfo shortcut = (ShortcutInfo) item; if (shortcut.intent != null && shortcut.intent.getComponent() != null) { final ComponentName componentName = shortcut.intent.getComponent(); final DragSource dragSource = d.dragSource; - int flags = AppInfo.initFlags( - ShortcutInfo.getPackageInfo(getContext(), componentName.getPackageName())); - mWaitingForUninstall = - mLauncher.startApplicationUninstallActivity(componentName, flags); + final UserHandleCompat user = shortcut.user; + mWaitingForUninstall = mLauncher.startApplicationUninstallActivity( + componentName, shortcut.flags, user); if (mWaitingForUninstall) { final Runnable checkIfUninstallWasSuccess = new Runnable() { @Override public void run() { mWaitingForUninstall = false; String packageName = componentName.getPackageName(); - List<ResolveInfo> activities = - AllAppsList.findActivitiesForPackage(getContext(), packageName); - boolean uninstallSuccessful = activities.size() == 0; + boolean uninstallSuccessful = !AllAppsList.packageHasActivities( + getContext(), packageName, user); if (dragSource instanceof Folder) { ((Folder) dragSource). onUninstallActivityReturned(uninstallSuccessful); @@ -324,7 +347,7 @@ public class DeleteDropTarget extends ButtonDropTarget { final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); - if (appWidgetHost != null) { + if ((appWidgetHost != null) && launcherAppWidgetInfo.isWidgetIdValid()) { // Deleting an app widget ID is a void call but writes to disk before returning // to the caller... new AsyncTask<Void, Void, Void>() { @@ -353,8 +376,11 @@ public class DeleteDropTarget extends ButtonDropTarget { */ private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer, DragObject d, PointF vel, ViewConfiguration config) { + + int width = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicWidth(); + int height = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicHeight(); final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), - mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); + width, height); final Rect from = new Rect(); dragLayer.getViewRectRelativeToSelf(d.dragView, from); diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index e67ec197a..daf5556d4 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -43,16 +43,18 @@ import java.util.Comparator; class DeviceProfileQuery { + DeviceProfile profile; float widthDps; float heightDps; float value; PointF dimens; - DeviceProfileQuery(float w, float h, float v) { - widthDps = w; - heightDps = h; + DeviceProfileQuery(DeviceProfile p, float v) { + widthDps = p.minWidthDps; + heightDps = p.minHeightDps; value = v; - dimens = new PointF(w, h); + dimens = new PointF(widthDps, heightDps); + profile = p; } } @@ -67,11 +69,14 @@ public class DeviceProfile { float numRows; float numColumns; float numHotseatIcons; - private float iconSize; + float iconSize; private float iconTextSize; private int iconDrawablePaddingOriginalPx; private float hotseatIconSize; + int defaultLayoutId; + int defaultNoAllAppsLayoutId; + boolean isLandscape; boolean isTablet; boolean isLargeTablet; @@ -121,13 +126,17 @@ public class DeviceProfile { int searchBarSpaceHeightPx; int searchBarHeightPx; int pageIndicatorHeightPx; + int allAppsButtonVisualSize; float dragViewScale; + int allAppsShortEdgeCount = -1; + int allAppsLongEdgeCount = -1; + private ArrayList<DeviceProfileCallbacks> mCallbacks = new ArrayList<DeviceProfileCallbacks>(); DeviceProfile(String n, float w, float h, float r, float c, - float is, float its, float hs, float his) { + float is, float its, float hs, float his, int dlId, int dnalId) { // Ensure that we have an odd number of hotseat items (since we need to place all apps) if (!LauncherAppState.isDisableAllApps() && hs % 2 == 0) { throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); @@ -142,6 +151,11 @@ public class DeviceProfile { iconTextSize = its; numHotseatIcons = hs; hotseatIconSize = his; + defaultLayoutId = dlId; + defaultNoAllAppsLayoutId = dnalId; + } + + DeviceProfile() { } DeviceProfile(Context context, @@ -182,38 +196,42 @@ public class DeviceProfile { overviewModeScaleFactor = res.getInteger(R.integer.config_dynamic_grid_overview_scale_percentage) / 100f; - // Interpolate the rows + // Find the closes profile given the width/height for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numRows)); + points.add(new DeviceProfileQuery(p, 0f)); } - numRows = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); - // Interpolate the columns - points.clear(); - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numColumns)); - } - numColumns = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); - // Interpolate the hotseat length - points.clear(); - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.numHotseatIcons)); - } - numHotseatIcons = Math.round(invDistWeightedInterpolate(minWidth, minHeight, points)); + DeviceProfile closestProfile = findClosestDeviceProfile(minWidth, minHeight, points); + + // Snap to the closest row count + numRows = closestProfile.numRows; + + // Snap to the closest column count + numColumns = closestProfile.numColumns; + + // Snap to the closest hotseat size + numHotseatIcons = closestProfile.numHotseatIcons; hotseatAllAppsRank = (int) (numHotseatIcons / 2); + // Snap to the closest default layout id + defaultLayoutId = closestProfile.defaultLayoutId; + + // Snap to the closest default no all-apps layout id + defaultNoAllAppsLayoutId = closestProfile.defaultNoAllAppsLayoutId; + // Interpolate the icon size points.clear(); for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.iconSize)); + points.add(new DeviceProfileQuery(p, p.iconSize)); } iconSize = invDistWeightedInterpolate(minWidth, minHeight, points); + // AllApps uses the original non-scaled icon size allAppsIconSizePx = DynamicGrid.pxFromDp(iconSize, dm); // Interpolate the icon text size points.clear(); for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.iconTextSize)); + points.add(new DeviceProfileQuery(p, p.iconTextSize)); } iconTextSize = invDistWeightedInterpolate(minWidth, minHeight, points); iconDrawablePaddingOriginalPx = @@ -224,14 +242,56 @@ public class DeviceProfile { // Interpolate the hotseat icon size points.clear(); for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p.minWidthDps, p.minHeightDps, p.hotseatIconSize)); + points.add(new DeviceProfileQuery(p, p.hotseatIconSize)); } // Hotseat hotseatIconSize = invDistWeightedInterpolate(minWidth, minHeight, points); + // If the partner customization apk contains any grid overrides, apply them + applyPartnerDeviceProfileOverrides(context, dm); + // Calculate the remaining vars updateFromConfiguration(context, res, wPx, hPx, awPx, ahPx); updateAvailableDimensions(context); + computeAllAppsButtonSize(context); + } + + /** + * Apply any Partner customization grid overrides. + * + * Currently we support: all apps row / column count. + */ + private void applyPartnerDeviceProfileOverrides(Context ctx, DisplayMetrics dm) { + Partner p = Partner.get(ctx.getPackageManager()); + if (p != null) { + DeviceProfile partnerDp = p.getDeviceProfileOverride(dm); + if (partnerDp != null) { + if (partnerDp.numRows > 0 && partnerDp.numColumns > 0) { + numRows = partnerDp.numRows; + numColumns = partnerDp.numColumns; + } + if (partnerDp.allAppsShortEdgeCount > 0 && partnerDp.allAppsLongEdgeCount > 0) { + allAppsShortEdgeCount = partnerDp.allAppsShortEdgeCount; + allAppsLongEdgeCount = partnerDp.allAppsLongEdgeCount; + } + if (partnerDp.iconSize > 0) { + iconSize = partnerDp.iconSize; + // AllApps uses the original non-scaled icon size + allAppsIconSizePx = DynamicGrid.pxFromDp(iconSize, dm); + } + } + } + } + + /** + * Determine the exact visual footprint of the all apps button, taking into account scaling + * and internal padding of the drawable. + */ + private void computeAllAppsButtonSize(Context context) { + Resources res = context.getResources(); + float padding = res.getInteger(R.integer.config_allAppsButtonPaddingPercent) / 100f; + LauncherAppState app = LauncherAppState.getInstance(); + allAppsButtonVisualSize = (int) (hotseatIconSizePx * (1 - padding)); } void addCallback(DeviceProfileCallbacks cb) { @@ -357,12 +417,17 @@ public class DeviceProfile { int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount); int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount); - allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) / - (allAppsCellHeightPx + allAppsCellPaddingPx); - allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows)); - allAppsNumCols = (availableWidthPx) / - (allAppsCellWidthPx + allAppsCellPaddingPx); - allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols)); + if (allAppsShortEdgeCount > 0 && allAppsLongEdgeCount > 0) { + allAppsNumRows = isLandscape ? allAppsShortEdgeCount : allAppsLongEdgeCount; + allAppsNumCols = isLandscape ? allAppsLongEdgeCount : allAppsShortEdgeCount; + } else { + allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) / + (allAppsCellHeightPx + allAppsCellPaddingPx); + allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows)); + allAppsNumCols = (availableWidthPx) / + (allAppsCellWidthPx + allAppsCellPaddingPx); + allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols)); + } } void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx, @@ -398,14 +463,18 @@ public class DeviceProfile { return (float) (1f / Math.pow(d, pow)); } - private float invDistWeightedInterpolate(float width, float height, - ArrayList<DeviceProfileQuery> points) { - float sum = 0; - float weights = 0; - float pow = 5; - float kNearestNeighbors = 3; + /** Returns the closest device profile given the width and height and a list of profiles */ + private DeviceProfile findClosestDeviceProfile(float width, float height, + ArrayList<DeviceProfileQuery> points) { + return findClosestDeviceProfiles(width, height, points).get(0).profile; + } + + /** Returns the closest device profiles ordered by closeness to the specified width and height */ + private ArrayList<DeviceProfileQuery> findClosestDeviceProfiles(float width, float height, + ArrayList<DeviceProfileQuery> points) { final PointF xy = new PointF(width, height); + // Sort the profiles by their closeness to the dimensions ArrayList<DeviceProfileQuery> pointsByNearness = points; Collections.sort(pointsByNearness, new Comparator<DeviceProfileQuery>() { public int compare(DeviceProfileQuery a, DeviceProfileQuery b) { @@ -413,6 +482,20 @@ public class DeviceProfile { } }); + return pointsByNearness; + } + + private float invDistWeightedInterpolate(float width, float height, + ArrayList<DeviceProfileQuery> points) { + float sum = 0; + float weights = 0; + float pow = 5; + float kNearestNeighbors = 3; + final PointF xy = new PointF(width, height); + + ArrayList<DeviceProfileQuery> pointsByNearness = findClosestDeviceProfiles(width, height, + points); + for (int i = 0; i < pointsByNearness.size(); ++i) { DeviceProfileQuery p = pointsByNearness.get(i); if (i < kNearestNeighbors) { @@ -739,15 +822,19 @@ public class DeviceProfile { (allAppsIconSizePx / DynamicGrid.DEFAULT_ICON_SIZE_PX))); pageIndicator = host.findViewById(R.id.apps_customize_page_indicator); if (pageIndicator != null) { - lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams(); - lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; - lp.width = LayoutParams.WRAP_CONTENT; - lp.height = pageIndicatorHeight; - pageIndicator.setLayoutParams(lp); + LinearLayout.LayoutParams lllp = (LinearLayout.LayoutParams) pageIndicator.getLayoutParams(); + lllp.width = LayoutParams.WRAP_CONTENT; + lllp.height = pageIndicatorHeight; + pageIndicator.setLayoutParams(lllp); } AppsCustomizePagedView pagedView = (AppsCustomizePagedView) host.findViewById(R.id.apps_customize_pane_content); + + FrameLayout fakePageContainer = (FrameLayout) + host.findViewById(R.id.fake_page_container); + FrameLayout fakePage = (FrameLayout) host.findViewById(R.id.fake_page); + padding = new Rect(); if (pagedView != null) { // Constrain the dimensions of all apps so that it does not span the full width @@ -763,11 +850,24 @@ public class DeviceProfile { if ((isTablet() || isLandscape) && gridPaddingLR > (allAppsCellWidthPx / 4)) { padding.left = padding.right = gridPaddingLR; } + // The icons are centered, so we can't just offset by the page indicator height // because the empty space will actually be pageIndicatorHeight + paddingTB padding.bottom = Math.max(0, pageIndicatorHeight - paddingTB); - pagedView.setAllAppsPadding(padding); + pagedView.setWidgetsPageIndicatorPadding(pageIndicatorHeight); + fakePage.setBackground(res.getDrawable(R.drawable.quantum_panel)); + + // Horizontal padding for the whole paged view + int pagedFixedViewPadding = + res.getDimensionPixelSize(R.dimen.apps_customize_horizontal_padding); + + padding.left += pagedFixedViewPadding; + padding.right += pagedFixedViewPadding; + + pagedView.setPadding(padding.left, padding.top, padding.right, padding.bottom); + fakePageContainer.setPadding(padding.left, padding.top, padding.right, padding.bottom); + } } diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java index 4c3ea2a0a..6d0a2be63 100644 --- a/src/com/android/launcher3/DragController.java +++ b/src/com/android/launcher3/DragController.java @@ -329,8 +329,8 @@ public class DragController { if (dragInfo != null && dragInfo.intent != null && info != null) { ComponentName cn = dragInfo.intent.getComponent(); - boolean isSameComponent = cn.equals(info.componentName) || - packageNames.contains(cn.getPackageName()); + boolean isSameComponent = cn != null && (cn.equals(info.componentName) || + packageNames.contains(cn.getPackageName())); if (isSameComponent) { cancelDrag(); return; diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index 862ceca28..a8a61ea89 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -73,7 +73,21 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang private final Rect mInsets = new Rect(); - private int mDragViewIndex; + private View mOverlayView; + private int mTopViewIndex; + private int mChildCountOnLastUpdate = -1; + + // Darkening scrim + private Drawable mBackground; + private float mBackgroundAlpha = 0; + + // Related to adjacent page hints + private boolean mInScrollArea; + private boolean mShowPageHints; + private Drawable mLeftHoverDrawable; + private Drawable mRightHoverDrawable; + private Drawable mLeftHoverDrawableActive; + private Drawable mRightHoverDrawableActive; /** * Used to create a new DragLayer from XML. @@ -89,8 +103,12 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang setChildrenDrawingOrderEnabled(true); setOnHierarchyChangeListener(this); - mLeftHoverDrawable = getResources().getDrawable(R.drawable.page_hover_left_holo); - mRightHoverDrawable = getResources().getDrawable(R.drawable.page_hover_right_holo); + final Resources res = getResources(); + mLeftHoverDrawable = res.getDrawable(R.drawable.page_hover_left); + mRightHoverDrawable = res.getDrawable(R.drawable.page_hover_right); + mLeftHoverDrawableActive = res.getDrawable(R.drawable.page_hover_left_active); + mRightHoverDrawableActive = res.getDrawable(R.drawable.page_hover_right_active); + mBackground = res.getDrawable(R.drawable.apps_customize_bg); } public void setup(Launcher launcher, DragController controller) { @@ -114,12 +132,30 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang return true; // I'll take it from here } + Rect getInsets() { + return mInsets; + } + @Override public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) { super.addView(child, index, params); setInsets(child, mInsets, new Rect()); } + public void showOverlayView(View overlayView) { + LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + mOverlayView = overlayView; + addView(overlayView, lp); + + // ensure that the overlay view stays on top. we can't use drawing order for this + // because in API level 16 touch dispatch doesn't respect drawing order. + mOverlayView.bringToFront(); + } + + public void dismissOverlayView() { + removeView(mOverlayView); + } + private void setInsets(View child, Rect newInsets, Rect oldInsets) { final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams(); if (child instanceof Insettable) { @@ -168,8 +204,7 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang } Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); - if (currentFolder != null && !mLauncher.getLauncherClings().isFolderClingVisible() && - intercept) { + if (currentFolder != null && intercept) { if (currentFolder.isEditingName()) { if (!isEventOverFolderTextRegion(currentFolder, ev)) { currentFolder.dismissEditingName(); @@ -544,6 +579,10 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang // the drag view about the scaled child view. toY += Math.round(toScale * tv.getPaddingTop()); toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2; + if (dragView.getDragVisualizeOffset() != null) { + toY -= Math.round(toScale * dragView.getDragVisualizeOffset().y); + } + toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; } else if (child instanceof FolderIcon) { // Account for holographic blur padding on the drag view @@ -762,6 +801,11 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang @Override public void onChildViewAdded(View parent, View child) { + if (mOverlayView != null) { + // ensure that the overlay view stays on top. we can't use drawing order for this + // because in API level 16 touch dispatch doesn't respect drawing order. + mOverlayView.bringToFront(); + } updateChildIndices(); } @@ -770,34 +814,54 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang updateChildIndices(); } + @Override + public void bringChildToFront(View child) { + super.bringChildToFront(child); + if (child != mOverlayView && mOverlayView != null) { + // ensure that the overlay view stays on top. we can't use drawing order for this + // because in API level 16 touch dispatch doesn't respect drawing order. + mOverlayView.bringToFront(); + } + updateChildIndices(); + } + private void updateChildIndices() { - mDragViewIndex = -1; + mTopViewIndex = -1; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { if (getChildAt(i) instanceof DragView) { - mDragViewIndex = i; + mTopViewIndex = i; } } + mChildCountOnLastUpdate = childCount; } @Override protected int getChildDrawingOrder(int childCount, int i) { - if (mDragViewIndex == -1) { + if (mChildCountOnLastUpdate != childCount) { + // between platform versions 17 and 18, behavior for onChildViewRemoved / Added changed. + // Pre-18, the child was not added / removed by the time of those callbacks. We need to + // force update our representation of things here to avoid crashing on pre-18 devices + // in certain instances. + updateChildIndices(); + } + + // i represents the current draw iteration + if (mTopViewIndex == -1) { + // in general we do nothing return i; - } else if (i == mDragViewIndex) { - return getChildCount()-1; - } else if (i < mDragViewIndex) { + } else if (i == childCount - 1) { + // if we have a top index, we return it when drawing last item (highest z-order) + return mTopViewIndex; + } else if (i < mTopViewIndex) { return i; } else { - // i > mDragViewIndex - return i-1; + // for indexes greater than the top index, we fetch one item above to shift for the + // displacement of the top index + return i + 1; } } - private boolean mInScrollArea; - private Drawable mLeftHoverDrawable; - private Drawable mRightHoverDrawable; - void onEnterScrollArea(int direction) { mInScrollArea = true; invalidate(); @@ -808,6 +872,16 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang invalidate(); } + void showPageHints() { + mShowPageHints = true; + invalidate(); + } + + void hidePageHints() { + mShowPageHints = false; + invalidate(); + } + /** * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. */ @@ -817,31 +891,68 @@ public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChang @Override protected void dispatchDraw(Canvas canvas) { + // Draw the background gradient below children. + if (mBackground != null && mBackgroundAlpha > 0.0f) { + int alpha = (int) (mBackgroundAlpha * 255); + mBackground.setAlpha(alpha); + mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); + mBackground.draw(canvas); + } + super.dispatchDraw(canvas); + } - if (mInScrollArea && !LauncherAppState.getInstance().isScreenLarge()) { + private void drawPageHints(Canvas canvas) { + if (mShowPageHints) { Workspace workspace = mLauncher.getWorkspace(); int width = getMeasuredWidth(); Rect childRect = new Rect(); - getDescendantRectRelativeToSelf(workspace.getChildAt(0), childRect); + getDescendantRectRelativeToSelf(workspace.getChildAt(workspace.getChildCount() - 1), + 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(), + if (leftPage != null && leftPage.isDragTarget()) { + Drawable left = mInScrollArea && leftPage.getIsDragOverlapping() ? + mLeftHoverDrawableActive : mLeftHoverDrawable; + left.setBounds(0, childRect.top, + left.getIntrinsicWidth(), childRect.bottom); + left.draw(canvas); + } + if (rightPage != null && rightPage.isDragTarget()) { + Drawable right = mInScrollArea && rightPage.getIsDragOverlapping() ? + mRightHoverDrawableActive : mRightHoverDrawable; + right.setBounds(width - right.getIntrinsicWidth(), childRect.top, width, childRect.bottom); - mRightHoverDrawable.draw(canvas); + right.draw(canvas); } } } + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean ret = super.drawChild(canvas, child, drawingTime); + + // We want to draw the page hints above the workspace, but below the drag view. + if (child instanceof Workspace) { + drawPageHints(canvas); + } + return ret; + } + + public void setBackgroundAlpha(float alpha) { + if (alpha != mBackgroundAlpha) { + mBackgroundAlpha = alpha; + invalidate(); + } + } + + public float getBackgroundAlpha() { + return mBackgroundAlpha; + } + public void setTouchCompleteListener(TouchCompleteListener listener) { mTouchCompleteListener = listener; } diff --git a/src/com/android/launcher3/DynamicGrid.java b/src/com/android/launcher3/DynamicGrid.java index 447bb1cd8..94a07d706 100644 --- a/src/com/android/launcher3/DynamicGrid.java +++ b/src/com/android/launcher3/DynamicGrid.java @@ -60,36 +60,41 @@ public class DynamicGrid { DEFAULT_ICON_SIZE_PX = pxFromDp(DEFAULT_ICON_SIZE_DP, dm); // Our phone profiles include the bar sizes in each orientation deviceProfiles.add(new DeviceProfile("Super Short Stubby", - 255, 300, 2, 3, 48, 13, (hasAA ? 5 : 5), 48)); + 255, 300, 2, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4, + R.xml.default_workspace_4x4_no_all_apps)); deviceProfiles.add(new DeviceProfile("Shorter Stubby", - 255, 400, 3, 3, 48, 13, (hasAA ? 5 : 5), 48)); + 255, 400, 3, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4, + R.xml.default_workspace_4x4_no_all_apps)); deviceProfiles.add(new DeviceProfile("Short Stubby", - 275, 420, 3, 4, 48, 13, (hasAA ? 5 : 5), 48)); + 275, 420, 3, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4, + R.xml.default_workspace_4x4_no_all_apps)); deviceProfiles.add(new DeviceProfile("Stubby", - 255, 450, 3, 4, 48, 13, (hasAA ? 5 : 5), 48)); + 255, 450, 3, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4, + R.xml.default_workspace_4x4_no_all_apps)); deviceProfiles.add(new DeviceProfile("Nexus S", - 296, 491.33f, 4, 4, 48, 13, (hasAA ? 5 : 5), 48)); + 296, 491.33f, 4, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4, + R.xml.default_workspace_4x4_no_all_apps)); deviceProfiles.add(new DeviceProfile("Nexus 4", - 335, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56)); + 335, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4, + R.xml.default_workspace_4x4_no_all_apps)); deviceProfiles.add(new DeviceProfile("Nexus 5", - 359, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56)); + 359, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4, + R.xml.default_workspace_4x4_no_all_apps)); deviceProfiles.add(new DeviceProfile("Large Phone", - 406, 694, 5, 5, 64, 14.4f, 5, 56)); + 406, 694, 5, 5, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5, + R.xml.default_workspace_5x5_no_all_apps)); // The tablet profile is odd in that the landscape orientation // also includes the nav bar on the side deviceProfiles.add(new DeviceProfile("Nexus 7", - 575, 904, 5, 6, 72, 14.4f, 7, 60)); + 575, 904, 5, 6, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6, + R.xml.default_workspace_5x6_no_all_apps)); // Larger tablet profiles always have system bars on the top & bottom deviceProfiles.add(new DeviceProfile("Nexus 10", - 727, 1207, 5, 6, 76, 14.4f, 7, 64)); - /* - deviceProfiles.add(new DeviceProfile("Nexus 7", - 600, 960, 5, 5, 72, 14.4f, 5, 60)); - deviceProfiles.add(new DeviceProfile("Nexus 10", - 800, 1280, 5, 5, 80, 14.4f, (hasAA ? 7 : 6), 64)); - */ + 727, 1207, 5, 6, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6, + R.xml.default_workspace_5x6_no_all_apps)); deviceProfiles.add(new DeviceProfile("20-inch Tablet", - 1527, 2527, 7, 7, 100, 20, 7, 72)); + 1527, 2527, 7, 7, 100, 20, 7, 72, R.xml.default_workspace_4x4, + R.xml.default_workspace_4x4_no_all_apps)); mMinWidth = dpiFromPx(minWidthPx, dm); mMinHeight = dpiFromPx(minHeightPx, dm); mProfile = new DeviceProfile(context, deviceProfiles, diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index 85e90202b..ff02bbbc3 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -16,18 +16,61 @@ package com.android.launcher3; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.ColorFilter; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.util.SparseArray; class FastBitmapDrawable extends Drawable { - private Bitmap mBitmap; - private int mAlpha; + + static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { + + @Override + public float getInterpolation(float input) { + if (input < 0.05f) { + return input / 0.05f; + } else if (input < 0.3f){ + return 1; + } else { + return (1 - input) / 0.7f; + } + } + }; + static final long CLICK_FEEDBACK_DURATION = 2000; + + private static final int PRESSED_BRIGHTNESS = 100; + private static ColorMatrix sGhostModeMatrix; + private static final ColorMatrix sTempMatrix = new ColorMatrix(); + + /** + * Store the brightness colors filters to optimize animations during icon press. This + * only works for non-ghost-mode icons. + */ + private static final SparseArray<ColorFilter> sCachedBrightnessFilter = + new SparseArray<ColorFilter>(); + + private static final int GHOST_MODE_MIN_COLOR_RANGE = 130; + private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + private final Bitmap mBitmap; + private int mAlpha; + + private int mBrightness = 0; + private boolean mGhostModeEnabled = false; + + private boolean mPressed = false; + private ObjectAnimator mPressedAnimator; FastBitmapDrawable(Bitmap b) { mAlpha = 255; @@ -44,7 +87,7 @@ class FastBitmapDrawable extends Drawable { @Override public void setColorFilter(ColorFilter cf) { - mPaint.setColorFilter(cf); + // No op } @Override @@ -58,6 +101,7 @@ class FastBitmapDrawable extends Drawable { mPaint.setAlpha(alpha); } + @Override public void setFilterBitmap(boolean filterBitmap) { mPaint.setFilterBitmap(filterBitmap); mPaint.setAntiAlias(filterBitmap); @@ -69,12 +113,12 @@ class FastBitmapDrawable extends Drawable { @Override public int getIntrinsicWidth() { - return getBounds().width(); + return mBitmap.getWidth(); } @Override public int getIntrinsicHeight() { - return getBounds().height(); + return mBitmap.getHeight(); } @Override @@ -90,4 +134,98 @@ class FastBitmapDrawable extends Drawable { public Bitmap getBitmap() { return mBitmap; } + + /** + * When enabled, the icon is grayed out and the contrast is increased to give it a 'ghost' + * appearance. + */ + public void setGhostModeEnabled(boolean enabled) { + if (mGhostModeEnabled != enabled) { + mGhostModeEnabled = enabled; + updateFilter(); + } + } + + public void setPressed(boolean pressed) { + if (mPressed != pressed) { + mPressed = pressed; + if (mPressed) { + mPressedAnimator = ObjectAnimator + .ofInt(this, "brightness", PRESSED_BRIGHTNESS) + .setDuration(CLICK_FEEDBACK_DURATION); + mPressedAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR); + mPressedAnimator.start(); + } else if (mPressedAnimator != null) { + mPressedAnimator.cancel(); + setBrightness(0); + } + } + invalidateSelf(); + } + + public boolean isGhostModeEnabled() { + return mGhostModeEnabled; + } + + public int getBrightness() { + return mBrightness; + } + + public void setBrightness(int brightness) { + if (mBrightness != brightness) { + mBrightness = brightness; + updateFilter(); + invalidateSelf(); + } + } + + private void updateFilter() { + if (mGhostModeEnabled) { + if (sGhostModeMatrix == null) { + sGhostModeMatrix = new ColorMatrix(); + sGhostModeMatrix.setSaturation(0); + + // For ghost mode, set the color range to [GHOST_MODE_MIN_COLOR_RANGE, 255] + float range = (255 - GHOST_MODE_MIN_COLOR_RANGE) / 255.0f; + sTempMatrix.set(new float[] { + range, 0, 0, 0, GHOST_MODE_MIN_COLOR_RANGE, + 0, range, 0, 0, GHOST_MODE_MIN_COLOR_RANGE, + 0, 0, range, 0, GHOST_MODE_MIN_COLOR_RANGE, + 0, 0, 0, 1, 0 }); + sGhostModeMatrix.preConcat(sTempMatrix); + } + + if (mBrightness == 0) { + mPaint.setColorFilter(new ColorMatrixColorFilter(sGhostModeMatrix)); + } else { + setBrightnessMatrix(sTempMatrix, mBrightness); + sTempMatrix.postConcat(sGhostModeMatrix); + mPaint.setColorFilter(new ColorMatrixColorFilter(sTempMatrix)); + } + } else if (mBrightness != 0) { + ColorFilter filter = sCachedBrightnessFilter.get(mBrightness); + if (filter == null) { + filter = new PorterDuffColorFilter(Color.argb(mBrightness, 255, 255, 255), + PorterDuff.Mode.SRC_ATOP); + sCachedBrightnessFilter.put(mBrightness, filter); + } + mPaint.setColorFilter(filter); + } else { + mPaint.setColorFilter(null); + } + } + + private static void setBrightnessMatrix(ColorMatrix matrix, int brightness) { + // Brightness: C-new = C-old*(1-amount) + amount + float scale = 1 - brightness / 255.0f; + matrix.setScale(scale, scale, scale, 1); + float[] array = matrix.getArray(); + + // Add the amount to RGB components of the matrix, as per the above formula. + // Fifth elements in the array correspond to the constant being added to + // red, blue, green, and alpha channel respectively. + array[4] = brightness; + array[9] = brightness; + array[14] = brightness; + } } diff --git a/src/com/android/launcher3/FastBitmapView.java b/src/com/android/launcher3/FastBitmapView.java new file mode 100644 index 000000000..0937eb75e --- /dev/null +++ b/src/com/android/launcher3/FastBitmapView.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 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.graphics.Canvas; +import android.graphics.Paint; +import android.view.View; + +public class FastBitmapView extends View { + + private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + private Bitmap mBitmap; + + public FastBitmapView(Context context) { + super(context); + } + + /** + * Applies the new bitmap. + * @return true if the view was invalidated. + */ + public boolean setBitmap(Bitmap b) { + if (b != mBitmap){ + if (mBitmap != null) { + invalidate(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); + } + mBitmap = b; + if (mBitmap != null) { + invalidate(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); + } + return true; + } + return false; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mBitmap != null) { + canvas.drawBitmap(mBitmap, 0, 0, mPaint); + } + } +} diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java index bb62bac65..d529b3901 100644 --- a/src/com/android/launcher3/FocusHelper.java +++ b/src/com/android/launcher3/FocusHelper.java @@ -17,13 +17,13 @@ package com.android.launcher3; import android.content.res.Configuration; +import android.util.Log; import android.view.KeyEvent; +import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.ScrollView; -import android.widget.TabHost; -import android.widget.TabWidget; import java.util.ArrayList; import java.util.Collections; @@ -57,61 +57,16 @@ class HotseatIconKeyEventListener implements View.OnKeyListener { } } -/** - * 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) { + private static AppsCustomizeTabHost findTabHostParent(View v) { ViewParent p = v.getParent(); - while (p != null && !(p instanceof TabHost)) { + while (p != null && !(p instanceof AppsCustomizeTabHost)) { 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; + return (AppsCustomizeTabHost) p; } /** @@ -134,8 +89,6 @@ public class FocusHelper { 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)); @@ -194,8 +147,6 @@ public class FocusHelper { int newWidgetIndex = ((y - 1) * cellCountX) + x; child = parent.getChildAt(newWidgetIndex); if (child != null) child.requestFocus(); - } else { - tabs.requestFocus(); } } wasHandled = true; @@ -294,8 +245,6 @@ public class FocusHelper { // 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)); @@ -317,13 +266,17 @@ public class FocusHelper { // Select the previous icon or the last icon on the previous page if (iconIndex > 0) { itemContainer.getChildAt(iconIndex - 1).requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); } 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(); + if (child != null) { + child.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); + } } } } @@ -335,13 +288,17 @@ public class FocusHelper { // Select the next icon or the first icon on the next page if (iconIndex < (itemCount - 1)) { itemContainer.getChildAt(iconIndex + 1).requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); } 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(); + if (child != null) { + child.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); + } } } } @@ -354,31 +311,25 @@ public class FocusHelper { if (y > 0) { int newiconIndex = ((y - 1) * countX) + x; itemContainer.getChildAt(newiconIndex).requestFocus(); - } else { - tabs.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); } } wasHandled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: if (handleKeyEvent) { - // Select the closest icon in the previous row, otherwise do nothing + // Select the closest icon in the next row, otherwise do nothing if (y < (countY - 1)) { int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x); - itemContainer.getChildAt(newiconIndex).requestFocus(); + int newIconY = newiconIndex / countX; + if (newIconY != y) { + itemContainer.getChildAt(newiconIndex).requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); + } } } 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 @@ -388,10 +339,14 @@ public class FocusHelper { if (newParent != null) { container.snapToPage(pageIndex - 1); child = newParent.getChildAt(0); - if (child != null) child.requestFocus(); + if (child != null) { + child.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); + } } } else { itemContainer.getChildAt(0).requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); } } wasHandled = true; @@ -405,10 +360,14 @@ public class FocusHelper { if (newParent != null) { container.snapToPage(pageIndex + 1); child = newParent.getChildAt(0); - if (child != null) child.requestFocus(); + if (child != null) { + child.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); + } } } else { itemContainer.getChildAt(itemCount - 1).requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); } } wasHandled = true; @@ -417,6 +376,7 @@ public class FocusHelper { if (handleKeyEvent) { // Select the first icon on this page itemContainer.getChildAt(0).requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); } wasHandled = true; break; @@ -424,6 +384,7 @@ public class FocusHelper { if (handleKeyEvent) { // Select the last icon on this page itemContainer.getChildAt(itemCount - 1).requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); } wasHandled = true; break; @@ -439,8 +400,8 @@ public class FocusHelper { if (!LauncherAppState.getInstance().isScreenLarge()) return false; final FocusOnlyTabWidget parent = (FocusOnlyTabWidget) v.getParent(); - final TabHost tabHost = findTabHostParent(parent); - final ViewGroup contents = tabHost.getTabContentView(); + final AppsCustomizeTabHost tabHost = findTabHostParent(parent); + final ViewGroup contents = tabHost.getContent(); final int tabCount = parent.getTabCount(); final int tabIndex = parent.getChildTabIndex(v); @@ -490,53 +451,56 @@ public class FocusHelper { * 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(); + ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout layout = (CellLayout) parent.getParent(); // 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 + // 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); + ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); + int myIndex = views.indexOf(v); + // Select the previous button, otherwise do nothing + if (myIndex > 0) { + views.get(myIndex - 1).requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); } } 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); + ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); + int myIndex = views.indexOf(v); + // Select the next button, otherwise do nothing + if (myIndex < views.size() - 1) { + views.get(myIndex + 1).requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); } } 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(); + final Workspace workspace = (Workspace) + v.getRootView().findViewById(R.id.workspace); + if (workspace != null) { + int pageIndex = workspace.getCurrentPage(); + CellLayout topLayout = (CellLayout) workspace.getChildAt(pageIndex); + ShortcutAndWidgetContainer children = topLayout.getShortcutsAndWidgets(); + final View newIcon = getIconInDirection(layout, children, -1, 1); + // Select the first bubble text view in the current page of the workspace + if (newIcon != null) { + newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); + } else { + workspace.requestFocus(); + } } } wasHandled = true; @@ -555,8 +519,8 @@ public class FocusHelper { */ private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( ViewGroup container, int i) { - ViewGroup parent = (ViewGroup) container.getChildAt(i); - return (ShortcutAndWidgetContainer) parent.getChildAt(0); + CellLayout parent = (CellLayout) container.getChildAt(i); + return parent.getShortcutsAndWidgets(); } /** @@ -680,6 +644,7 @@ public class FocusHelper { View newIcon = getIconInDirection(layout, parent, v, -1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); } else { if (pageIndex > 0) { parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); @@ -691,6 +656,7 @@ public class FocusHelper { // Snap to the previous page workspace.snapToPage(pageIndex - 1); } + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); } } } @@ -702,6 +668,7 @@ public class FocusHelper { View newIcon = getIconInDirection(layout, parent, v, 1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); } else { if (pageIndex < (pageCount - 1)) { parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); @@ -712,6 +679,7 @@ public class FocusHelper { // Snap to the next page workspace.snapToPage(pageIndex + 1); } + v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); } } } @@ -727,6 +695,7 @@ public class FocusHelper { } else { tabs.requestFocus(); } + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); } break; case KeyEvent.KEYCODE_DPAD_DOWN: @@ -735,9 +704,11 @@ public class FocusHelper { View newIcon = getClosestIconOnLine(layout, parent, v, 1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); wasHandled = true; } else if (hotseat != null) { hotseat.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); } } break; @@ -754,10 +725,12 @@ public class FocusHelper { // Snap to the previous page workspace.snapToPage(pageIndex - 1); } + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); } else { View newIcon = getIconInDirection(layout, parent, -1, 1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); } } } @@ -776,11 +749,13 @@ public class FocusHelper { // Snap to the next page workspace.snapToPage(pageIndex + 1); } + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); } else { View newIcon = getIconInDirection(layout, parent, parent.getChildCount(), -1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); } } } @@ -792,6 +767,7 @@ public class FocusHelper { View newIcon = getIconInDirection(layout, parent, -1, 1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); } } wasHandled = true; @@ -803,6 +779,7 @@ public class FocusHelper { parent.getChildCount(), -1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); } } wasHandled = true; @@ -832,6 +809,7 @@ public class FocusHelper { View newIcon = getIconInDirection(layout, parent, v, -1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); } } wasHandled = true; @@ -845,6 +823,7 @@ public class FocusHelper { } else { title.requestFocus(); } + v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); } wasHandled = true; break; @@ -854,6 +833,7 @@ public class FocusHelper { View newIcon = getClosestIconOnLine(layout, parent, v, -1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); } } wasHandled = true; @@ -867,6 +847,7 @@ public class FocusHelper { } else { title.requestFocus(); } + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); } wasHandled = true; break; @@ -876,6 +857,7 @@ public class FocusHelper { View newIcon = getIconInDirection(layout, parent, -1, 1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); } } wasHandled = true; @@ -887,6 +869,7 @@ public class FocusHelper { parent.getChildCount(), -1); if (newIcon != null) { newIcon.requestFocus(); + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); } } wasHandled = true; diff --git a/src/com/android/launcher3/FocusIndicatorView.java b/src/com/android/launcher3/FocusIndicatorView.java new file mode 100644 index 000000000..12b7a4076 --- /dev/null +++ b/src/com/android/launcher3/FocusIndicatorView.java @@ -0,0 +1,146 @@ +/* + * 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.util.Pair; +import android.view.View; +import android.view.ViewParent; + +public class FocusIndicatorView extends View implements View.OnFocusChangeListener { + + // It can be any number >0. The view is resized using scaleX and scaleY. + static final int DEFAULT_LAYOUT_SIZE = 100; + private static final float MIN_VISIBLE_ALPHA = 0.2f; + + private static final int[] sTempPos = new int[2]; + private static final int[] sTempShift = new int[2]; + + private final int[] mIndicatorPos = new int[2]; + private final int[] mTargetViewPos = new int[2]; + + private View mLastFocusedView; + private boolean mInitiated; + + private Pair<View, Boolean> mPendingCall; + + public FocusIndicatorView(Context context) { + this(context, null); + } + + public FocusIndicatorView(Context context, AttributeSet attrs) { + super(context, attrs); + setAlpha(0); + setBackgroundColor(getResources().getColor(R.color.focused_background)); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + // Redraw if it is already showing. This avoids a bug where the height changes by a small + // amount on connecting/disconnecting a bluetooth keyboard. + if (mLastFocusedView != null) { + mPendingCall = Pair.create(mLastFocusedView, Boolean.TRUE); + invalidate(); + } + } + + @Override + public void onFocusChange(View v, boolean hasFocus) { + mPendingCall = null; + if (!mInitiated && (getWidth() == 0)) { + // View not yet laid out. Wait until the view is ready to be drawn, so that be can + // get the location on screen. + mPendingCall = Pair.create(v, hasFocus); + invalidate(); + return; + } + + if (!mInitiated) { + getLocationRelativeToParentPagedView(this, mIndicatorPos); + mInitiated = true; + } + + if (hasFocus) { + int indicatorWidth = getWidth(); + int indicatorHeight = getHeight(); + + float scaleX = v.getScaleX() * v.getWidth() / indicatorWidth; + float scaleY = v.getScaleY() * v.getHeight() / indicatorHeight; + + getLocationRelativeToParentPagedView(v, mTargetViewPos); + float x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - scaleX) * indicatorWidth / 2; + float y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - scaleY) * indicatorHeight / 2; + + if (getAlpha() > MIN_VISIBLE_ALPHA) { + animate() + .translationX(x) + .translationY(y) + .scaleX(scaleX) + .scaleY(scaleY) + .alpha(1); + } else { + setTranslationX(x); + setTranslationY(y); + setScaleX(scaleX); + setScaleY(scaleY); + animate().alpha(1); + } + mLastFocusedView = v; + } else { + if (mLastFocusedView == v) { + mLastFocusedView = null; + animate().alpha(0); + } + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mPendingCall != null) { + onFocusChange(mPendingCall.first, mPendingCall.second); + } + } + + /** + * Gets the location of a view relative in the window, off-setting any shift due to + * page view scroll + */ + private static void getLocationRelativeToParentPagedView(View v, int[] pos) { + getPagedViewScrollShift(v, sTempShift); + v.getLocationInWindow(sTempPos); + pos[0] = sTempPos[0] + sTempShift[0]; + pos[1] = sTempPos[1] + sTempShift[1]; + } + + private static void getPagedViewScrollShift(View child, int[] shift) { + ViewParent parent = child.getParent(); + if (parent instanceof PagedView) { + View parentView = (View) parent; + child.getLocationInWindow(sTempPos); + shift[0] = parentView.getPaddingLeft() - sTempPos[0]; + shift[1] = -(int) child.getTranslationY(); + } else if (parent instanceof View) { + getPagedViewScrollShift((View) parent, shift); + } else { + shift[0] = shift[1] = 0; + } + } +} diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index b4c399266..1890af47d 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -18,6 +18,7 @@ package com.android.launcher3; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.content.Context; @@ -40,6 +41,7 @@ import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.animation.AccelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout; @@ -72,6 +74,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList private static final int CLOSE_FOLDER_DELAY_MS = 150; private int mExpandDuration; + private int mMaterialExpandDuration; + private int mMaterialExpandStagger; protected CellLayout mContent; private ScrollView mScrollView; private final LayoutInflater mInflater; @@ -112,9 +116,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList private static String sDefaultFolderName; private static String sHintText; - private int DRAG_MODE_NONE = 0; - private int DRAG_MODE_REORDER = 1; - private int mDragMode = DRAG_MODE_NONE; + private FocusIndicatorView mFocusIndicatorHandler; // We avoid measuring the scroll view with a 0 width or height, as this // results in CellLayout being measured as UNSPECIFIED, which it does @@ -157,7 +159,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mInputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - mExpandDuration = res.getInteger(R.integer.config_folderAnimDuration); + mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration); + mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); + mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger); if (sDefaultFolderName == null) { sDefaultFolderName = res.getString(R.string.folder_name); @@ -178,6 +182,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mScrollView = (ScrollView) findViewById(R.id.scroll_view); mContent = (CellLayout) findViewById(R.id.folder_content); + mFocusIndicatorHandler = new FocusIndicatorView(getContext()); + mContent.addView(mFocusIndicatorHandler, 0); + mFocusIndicatorHandler.getLayoutParams().height = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; + mFocusIndicatorHandler.getLayoutParams().width = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; + LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); @@ -239,9 +248,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return false; } - mLauncher.getLauncherClings().dismissFolderCling(null); - - mLauncher.getWorkspace().onDragStartedWithItem(v); mLauncher.getWorkspace().beginDragShared(v, this); mCurrentDragInfo = item; @@ -303,6 +309,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mFolderName; } + public CellLayout getContent() { + return mContent; + } + /** * We need to handle touch events to prevent them from falling through to the workspace below. */ @@ -387,7 +397,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // 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 + // 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) { @@ -439,18 +449,93 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mState = STATE_SMALL; } - public void animateOpen() { - positionAndSizeAsIcon(); + private void prepareReveal() { + setScaleX(1f); + setScaleY(1f); + setAlpha(1f); + mState = STATE_SMALL; + } + public void animateOpen() { 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() { + Animator openFolderAnim = null; + final Runnable onCompleteRunnable; + if (!Utilities.isLmpOrAbove()) { + positionAndSizeAsIcon(); + 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.setDuration(mExpandDuration); + openFolderAnim = oa; + + setLayerType(LAYER_TYPE_HARDWARE, null); + onCompleteRunnable = new Runnable() { + @Override + public void run() { + setLayerType(LAYER_TYPE_NONE, null); + } + }; + } else { + prepareReveal(); + centerAboutIcon(); + + int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + int height = getFolderHeight(); + + float transX = - 0.075f * (width / 2 - getPivotX()); + float transY = - 0.075f * (height / 2 - getPivotY()); + setTranslationX(transX); + setTranslationY(transY); + PropertyValuesHolder tx = PropertyValuesHolder.ofFloat("translationX", transX, 0); + PropertyValuesHolder ty = PropertyValuesHolder.ofFloat("translationY", transY, 0); + + int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX()); + int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY()); + float radius = (float) Math.sqrt(rx * rx + ry * ry); + AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); + Animator reveal = LauncherAnimUtils.createCircularReveal(this, (int) getPivotX(), + (int) getPivotY(), 0, radius); + reveal.setDuration(mMaterialExpandDuration); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + + mContent.setAlpha(0f); + Animator iconsAlpha = LauncherAnimUtils.ofFloat(mContent, "alpha", 0f, 1f); + iconsAlpha.setDuration(mMaterialExpandDuration); + iconsAlpha.setStartDelay(mMaterialExpandStagger); + iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); + + mFolderName.setAlpha(0f); + Animator textAlpha = LauncherAnimUtils.ofFloat(mFolderName, "alpha", 0f, 1f); + textAlpha.setDuration(mMaterialExpandDuration); + textAlpha.setStartDelay(mMaterialExpandStagger); + textAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); + + Animator drift = LauncherAnimUtils.ofPropertyValuesHolder(this, tx, ty); + drift.setDuration(mMaterialExpandDuration); + drift.setStartDelay(mMaterialExpandStagger); + drift.setInterpolator(new LogDecelerateInterpolator(60, 0)); + + anim.play(drift); + anim.play(iconsAlpha); + anim.play(textAlpha); + anim.play(reveal); + + openFolderAnim = anim; + + mContent.setLayerType(LAYER_TYPE_HARDWARE, null); + onCompleteRunnable = new Runnable() { + @Override + public void run() { + mContent.setLayerType(LAYER_TYPE_NONE, null); + } + }; + } + openFolderAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, @@ -461,23 +546,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void onAnimationEnd(Animator animation) { mState = STATE_OPEN; - setLayerType(LAYER_TYPE_NONE, null); - // Only show cling if we are not in the middle of a drag - this would be quite jarring. - if (!mDragController.isDragging()) { - Cling cling = mLauncher.getLauncherClings().showFoldersCling(); - if (cling != null) { - cling.bringScrimToFront(); - bringToFront(); - cling.bringToFront(); - } + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); } + setFocusOnFirstChild(); } }); - oa.setDuration(mExpandDuration); - setLayerType(LAYER_TYPE_HARDWARE, null); - oa.start(); + openFolderAnim.start(); // Make sure the folder picks up the last drag move even if the finger doesn't move. if (mDragController.isDragging()) { @@ -563,23 +640,18 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList protected View createAndAddShortcut(ShortcutInfo item) { final BubbleTextView textView = - (BubbleTextView) mInflater.inflate(R.layout.application, this, false); - textView.setCompoundDrawables(null, - Utilities.createIconDrawable(item.getIcon(mIconCache)), null, null); - textView.setText(item.title); - textView.setTag(item); - textView.setTextColor(getResources().getColor(R.color.folder_items_text_color)); - textView.setShadowsEnabled(false); - textView.setGlowColor(getResources().getColor(R.color.folder_items_glow_color)); + (BubbleTextView) mInflater.inflate(R.layout.folder_application, this, false); + textView.applyFromShortcutInfo(item, mIconCache, false); textView.setOnClickListener(this); textView.setOnLongClickListener(this); + textView.setOnFocusChangeListener(mFocusIndicatorHandler); // 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. + // This shouldn't happen, log it. Log.e(TAG, "Folder order not properly persisted during bind"); if (!findAndSetEmptyCells(item)) { return null; @@ -695,9 +767,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mReorderAlarm.setAlarm(REORDER_DELAY); mPreviousTargetCell[0] = mTargetCell[0]; mPreviousTargetCell[1] = mTargetCell[1]; - mDragMode = DRAG_MODE_REORDER; - } else { - mDragMode = DRAG_MODE_NONE; } } } @@ -753,7 +822,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); } mReorderAlarm.cancelAlarm(); - mDragMode = DRAG_MODE_NONE; } public void onDropCompleted(final View target, final DragObject d, @@ -793,12 +861,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } - // This is kind of hacky, but in general, dropping on the workspace handles removing - // the extra screen, but dropping elsewhere (back to self, or onto delete) doesn't. - if (target != mLauncher.getWorkspace()) { - mLauncher.getWorkspace().removeExtraEmptyScreen(true, null); - } - mDeleteFolderOnDropCompleted = false; mDragInProgress = false; mItemAddedBackToSelfViaIcon = false; @@ -1172,21 +1234,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void onDrop(DragObject d) { Runnable cleanUpRunnable = null; - // If we are coming from All Apps space, we need to remove the extra empty screen (which is - // normally done in Workspace#onDropExternal, as well zoom back in and close the folder. + // If we are coming from All Apps space, we defer removing the extra empty screen + // until the folder closes if (d.dragSource != mLauncher.getWorkspace() && !(d.dragSource instanceof Folder)) { cleanUpRunnable = new Runnable() { @Override public void run() { - mLauncher.getWorkspace().removeExtraEmptyScreen(false, new Runnable() { - @Override - public void run() { - mLauncher.closeFolder(); - mLauncher.exitSpringLoadedDragModeDelayed(true, - Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT_FOLDER_CLOSE, - null); - } - }, CLOSE_FOLDER_DELAY_MS, false); + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, + null); } }; } @@ -1196,6 +1252,18 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList if (mIsExternalDrag) { si.cellX = mEmptyCell[0]; si.cellY = mEmptyCell[1]; + + // Actually move the item in the database if it was an external drag. Call this + // before creating the view, so that ShortcutInfo is updated appropriately. + LauncherModel.addOrMoveItemInDatabase( + mLauncher, si, mInfo.id, 0, si.cellX, si.cellY); + + // We only need to update the locations if it doesn't get handled in #onDropCompleted. + if (d.dragSource != this) { + updateItemLocationsInDatabaseBatch(); + } + mIsExternalDrag = false; + currentDragView = createAndAddShortcut(si); } else { currentDragView = mCurrentDragView; @@ -1223,22 +1291,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mItemsInvalidated = true; setupContentDimensions(getItemCount()); - // Actually move the item in the database if it was an external drag. - if (mIsExternalDrag) { - LauncherModel.addOrMoveItemInDatabase( - mLauncher, si, mInfo.id, 0, si.cellX, si.cellY); - - // We only need to update the locations if it doesn't get handled in #onDropCompleted. - if (d.dragSource != this) { - updateItemLocationsInDatabaseBatch(); - } - mIsExternalDrag = false; - } - // Temporarily suppress the listener, as we did all the work already here. mSuppressOnAdd = true; mInfo.add(si); mSuppressOnAdd = false; + // Clear the drag info, as it is no longer being dragged. + mCurrentDragInfo = null; } // This is used so the item doesn't immediately appear in the folder when added. In one case diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index 78026f162..a359f1180 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -33,6 +33,7 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; @@ -70,7 +71,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { 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; + private static final float PERSPECTIVE_SHIFT_FACTOR = 0.18f; // Flag as to whether or not to draw an outer ring. Currently none is designed. public static final boolean HAS_OUTER_RING = true; @@ -105,6 +106,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { boolean mAnimating = false; private Rect mOldBounds = new Rect(); + private float mSlop; + 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>(); @@ -130,7 +133,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { final ViewGroup cellLayoutChildren = (ViewGroup) getParent(); final ViewGroup cellLayout = (ViewGroup) cellLayoutChildren.getParent(); final Workspace workspace = (Workspace) cellLayout.getParent(); - return !workspace.isSmall(); + return !workspace.workspaceInModalState(); } static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group, @@ -175,6 +178,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { icon.mFolderRingAnimator = new FolderRingAnimator(launcher, icon); folderInfo.addListener(icon); + icon.setOnFocusChangeListener(launcher.mFocusHandler); return icon; } @@ -308,7 +312,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { } } - Folder getFolder() { + public Folder getFolder() { return mFolder; } @@ -341,7 +345,12 @@ public class FolderIcon extends FrameLayout implements FolderListener { mFolderRingAnimator.animateToAcceptState(); layout.showFolderAccept(mFolderRingAnimator); mOpenAlarm.setOnAlarmListener(mOnOpenListener); - if (SPRING_LOADING_ENABLED) { + if (SPRING_LOADING_ENABLED && + ((dragInfo instanceof AppInfo) || (dragInfo instanceof ShortcutInfo))) { + // TODO: we currently don't support spring-loading for PendingAddShortcutInfos even + // though widget-style shortcuts can be added to folders. The issue is that we need + // to deal with configuration activities which are currently handled in + // Workspace#onDropExternal. mOpenAlarm.setAlarm(ON_OPEN_DELAY); } mDragInfo = (ItemInfo) dragInfo; @@ -359,6 +368,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { item.spanX = 1; item.spanY = 1; } else { + // ShortcutInfo item = (ShortcutInfo) mDragInfo; } mFolder.beginExternalDrag(item); @@ -371,7 +381,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { float scaleRelativeToDragLayer, Runnable postAnimationRunnable) { // These correspond two the drawable and view that the icon was dropped _onto_ - Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1]; + Drawable animateDrawable = getTopDrawable((TextView) destView); computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), destView.getMeasuredWidth()); @@ -385,8 +395,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { } public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) { - Drawable animateDrawable = ((TextView) finalView).getCompoundDrawables()[1]; - computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), + Drawable animateDrawable = getTopDrawable((TextView) finalView); + computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), finalView.getMeasuredWidth()); // This will animate the first item from it's position as an icon into its @@ -492,6 +502,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { 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); @@ -546,7 +557,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { // 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) + getPaddingTop(); - float transX = offset + scaleOffsetCorrection; + float transX = (mAvailableSpaceInPreview - scaledSize) / 2; float totalScale = mBaselineIconScale * scale; final int overlayAlpha = (int) (80 * (1 - r)); @@ -570,10 +581,18 @@ public class FolderIcon extends FrameLayout implements FolderListener { if (d != null) { mOldBounds.set(d.getBounds()); d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize); - d.setColorFilter(Color.argb(params.overlayAlpha, 255, 255, 255), - PorterDuff.Mode.SRC_ATOP); - d.draw(canvas); - d.clearColorFilter(); + if (d instanceof FastBitmapDrawable) { + FastBitmapDrawable fd = (FastBitmapDrawable) d; + int oldBrightness = fd.getBrightness(); + fd.setBrightness(params.overlayAlpha); + d.draw(canvas); + fd.setBrightness(oldBrightness); + } else { + d.setColorFilter(Color.argb(params.overlayAlpha, 255, 255, 255), + PorterDuff.Mode.SRC_ATOP); + d.draw(canvas); + d.clearColorFilter(); + } d.setBounds(mOldBounds); } canvas.restore(); @@ -595,7 +614,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { computePreviewDrawingParams(mAnimParams.drawable); } else { v = (TextView) items.get(0); - d = v.getCompoundDrawables()[1]; + d = getTopDrawable(v); computePreviewDrawingParams(d); } @@ -604,7 +623,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { for (int i = nItemsInPreview - 1; i >= 0; i--) { v = (TextView) items.get(i); if (!mHiddenItems.contains(v.getTag())) { - d = v.getCompoundDrawables()[1]; + d = getTopDrawable(v); mParams = computePreviewItemDrawingParams(i, mParams); mParams.drawable = d; drawPreviewItem(canvas, mParams); @@ -615,6 +634,11 @@ public class FolderIcon extends FrameLayout implements FolderListener { } } + private Drawable getTopDrawable(TextView v) { + Drawable d = v.getCompoundDrawables()[1]; + return (d instanceof PreloadIconDrawable) ? ((PreloadIconDrawable) d).mIcon : d; + } + private void animateFirstItem(final Drawable d, int duration, final boolean reverse, final Runnable onCompleteRunnable) { final PreviewItemDrawingParams finalParams = computePreviewItemDrawingParams(0, null); @@ -703,11 +727,22 @@ public class FolderIcon extends FrameLayout implements FolderListener { case MotionEvent.ACTION_UP: mLongPressHelper.cancelLongPress(); break; + case MotionEvent.ACTION_MOVE: + if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) { + mLongPressHelper.cancelLongPress(); + } + break; } return result; } @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + } + + @Override public void cancelLongPress() { super.cancelLongPress(); diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java index d45e4e47b..85a792f4b 100644 --- a/src/com/android/launcher3/FolderInfo.java +++ b/src/com/android/launcher3/FolderInfo.java @@ -17,13 +17,17 @@ package com.android.launcher3; import android.content.ContentValues; +import android.content.Context; + +import com.android.launcher3.compat.UserHandleCompat; import java.util.ArrayList; +import java.util.Arrays; /** * Represents a folder containing shortcuts or apps. */ -class FolderInfo extends ItemInfo { +public class FolderInfo extends ItemInfo { /** * Whether this folder has been opened @@ -39,6 +43,7 @@ class FolderInfo extends ItemInfo { FolderInfo() { itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; + user = UserHandleCompat.myUserHandle(); } /** @@ -75,8 +80,8 @@ class FolderInfo extends ItemInfo { } @Override - void onAddToDatabase(ContentValues values) { - super.onAddToDatabase(values); + void onAddToDatabase(Context context, ContentValues values) { + super.onAddToDatabase(context, values); values.put(LauncherSettings.Favorites.TITLE, title.toString()); } @@ -114,6 +119,6 @@ class FolderInfo extends ItemInfo { return "FolderInfo(id=" + this.id + " type=" + this.itemType + " container=" + this.container + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX - + " spanY=" + spanY + " dropPos=" + dropPos + ")"; + + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + ")"; } } diff --git a/src/com/android/launcher3/HideFromAccessibilityHelper.java b/src/com/android/launcher3/HideFromAccessibilityHelper.java deleted file mode 100644 index 75cbb1b1e..000000000 --- a/src/com/android/launcher3/HideFromAccessibilityHelper.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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) { - Integer important = mPreviousValues.get(v); - v.setImportantForAccessibility(important); - 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/HolographicOutlineHelper.java b/src/com/android/launcher3/HolographicOutlineHelper.java index d7b960aba..b1e0e68a4 100644 --- a/src/com/android/launcher3/HolographicOutlineHelper.java +++ b/src/com/android/launcher3/HolographicOutlineHelper.java @@ -20,48 +20,49 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.Region.Op; public class HolographicOutlineHelper { - private final Paint mHolographicPaint = new Paint(); + + private static final Rect sTempRect = new Rect(); + + private final Canvas mCanvas = new Canvas(); + private final Paint mDrawPaint = new Paint(); private final Paint mBlurPaint = new Paint(); private final Paint mErasePaint = new Paint(); - public int mMaxOuterBlurRadius; - public int mMinOuterBlurRadius; + private final BlurMaskFilter mMediumOuterBlurMaskFilter; + private final BlurMaskFilter mThinOuterBlurMaskFilter; + private final BlurMaskFilter mMediumInnerBlurMaskFilter; - private BlurMaskFilter mExtraThickOuterBlurMaskFilter; - private BlurMaskFilter mThickOuterBlurMaskFilter; - private BlurMaskFilter mMediumOuterBlurMaskFilter; - private BlurMaskFilter mThinOuterBlurMaskFilter; - private BlurMaskFilter mThickInnerBlurMaskFilter; - private BlurMaskFilter mExtraThickInnerBlurMaskFilter; - private BlurMaskFilter mMediumInnerBlurMaskFilter; + private final BlurMaskFilter mShaowBlurMaskFilter; + private final int mShadowOffset; - private static final int THICK = 0; - private static final int MEDIUM = 1; - private static final int EXTRA_THICK = 2; + /** + * Padding used when creating shadow bitmap; + */ + final int shadowBitmapPadding; static HolographicOutlineHelper INSTANCE; private HolographicOutlineHelper(Context context) { final float scale = LauncherAppState.getInstance().getScreenDensity(); - mMinOuterBlurRadius = (int) (scale * 1.0f); - mMaxOuterBlurRadius = (int) (scale * 12.0f); - - mExtraThickOuterBlurMaskFilter = new BlurMaskFilter(scale * 12.0f, BlurMaskFilter.Blur.OUTER); - mThickOuterBlurMaskFilter = new BlurMaskFilter(scale * 6.0f, BlurMaskFilter.Blur.OUTER); mMediumOuterBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.OUTER); mThinOuterBlurMaskFilter = new BlurMaskFilter(scale * 1.0f, BlurMaskFilter.Blur.OUTER); - mExtraThickInnerBlurMaskFilter = new BlurMaskFilter(scale * 6.0f, BlurMaskFilter.Blur.NORMAL); - mThickInnerBlurMaskFilter = new BlurMaskFilter(scale * 4.0f, BlurMaskFilter.Blur.NORMAL); mMediumInnerBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.NORMAL); - mHolographicPaint.setFilterBitmap(true); - mHolographicPaint.setAntiAlias(true); + mShaowBlurMaskFilter = new BlurMaskFilter(scale * 4.0f, BlurMaskFilter.Blur.NORMAL); + mShadowOffset = (int) (scale * 2.0f); + shadowBitmapPadding = (int) (scale * 4.0f); + + mDrawPaint.setFilterBitmap(true); + mDrawPaint.setAntiAlias(true); mBlurPaint.setFilterBitmap(true); mBlurPaint.setAntiAlias(true); mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); @@ -77,37 +78,15 @@ public class HolographicOutlineHelper { } /** - * 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); + int outlineColor) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, true); } void applyExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, - int outlineColor, boolean clipAlpha, int thickness) { + int outlineColor, boolean clipAlpha) { // 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 @@ -127,50 +106,18 @@ public class HolographicOutlineHelper { Bitmap glowShape = srcDst.extractAlpha(); // calculate the outer blur first - BlurMaskFilter outerBlurMaskFilter; - switch (thickness) { - case EXTRA_THICK: - outerBlurMaskFilter = mExtraThickOuterBlurMaskFilter; - break; - case THICK: - outerBlurMaskFilter = mThickOuterBlurMaskFilter; - break; - case MEDIUM: - outerBlurMaskFilter = mMediumOuterBlurMaskFilter; - break; - default: - throw new RuntimeException("Invalid blur thickness"); - } - mBlurPaint.setMaskFilter(outerBlurMaskFilter); + mBlurPaint.setMaskFilter(mMediumOuterBlurMaskFilter); int[] outerBlurOffset = new int[2]; Bitmap thickOuterBlur = glowShape.extractAlpha(mBlurPaint, outerBlurOffset); - if (thickness == EXTRA_THICK) { - mBlurPaint.setMaskFilter(mMediumOuterBlurMaskFilter); - } else { - mBlurPaint.setMaskFilter(mThinOuterBlurMaskFilter); - } + mBlurPaint.setMaskFilter(mThinOuterBlurMaskFilter); 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 = mExtraThickInnerBlurMaskFilter; - break; - case THICK: - innerBlurMaskFilter = mThickInnerBlurMaskFilter; - break; - case MEDIUM: - innerBlurMaskFilter = mMediumInnerBlurMaskFilter; - break; - default: - throw new RuntimeException("Invalid blur thickness"); - } - mBlurPaint.setMaskFilter(innerBlurMaskFilter); + mBlurPaint.setMaskFilter(mMediumInnerBlurMaskFilter); int[] thickInnerBlurOffset = new int[2]; Bitmap thickInnerBlur = glowShape.extractAlpha(mBlurPaint, thickInnerBlurOffset); @@ -186,16 +133,16 @@ public class HolographicOutlineHelper { // draw the inner and outer blur srcDstCanvas.setBitmap(srcDst); srcDstCanvas.drawColor(0, PorterDuff.Mode.CLEAR); - mHolographicPaint.setColor(color); + mDrawPaint.setColor(color); srcDstCanvas.drawBitmap(thickInnerBlur, thickInnerBlurOffset[0], thickInnerBlurOffset[1], - mHolographicPaint); + mDrawPaint); srcDstCanvas.drawBitmap(thickOuterBlur, outerBlurOffset[0], outerBlurOffset[1], - mHolographicPaint); + mDrawPaint); // draw the bright outline - mHolographicPaint.setColor(outlineColor); + mDrawPaint.setColor(outlineColor); srcDstCanvas.drawBitmap(brightOutline, brightOutlineOffset[0], brightOutlineOffset[1], - mHolographicPaint); + mDrawPaint); // cleanup srcDstCanvas.setBitmap(null); @@ -205,25 +152,52 @@ public class HolographicOutlineHelper { 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); + Bitmap createMediumDropShadow(BubbleTextView view) { + final Bitmap result = Bitmap.createBitmap( + view.getWidth() + shadowBitmapPadding + shadowBitmapPadding, + view.getHeight() + shadowBitmapPadding + shadowBitmapPadding + mShadowOffset, + Bitmap.Config.ARGB_8888); + + mCanvas.setBitmap(result); + + final Rect clipRect = sTempRect; + view.getDrawingRect(sTempRect); + // adjust the clip rect so that we don't include the text label + clipRect.bottom = view.getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V + + view.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 + mCanvas.save(); + mCanvas.scale(view.getScaleX(), view.getScaleY(), + view.getWidth() / 2 + shadowBitmapPadding, + view.getHeight() / 2 + shadowBitmapPadding); + mCanvas.translate(-view.getScrollX() + shadowBitmapPadding, + -view.getScrollY() + shadowBitmapPadding); + mCanvas.clipRect(clipRect, Op.REPLACE); + view.draw(mCanvas); + mCanvas.restore(); + + int[] blurOffst = new int[2]; + mBlurPaint.setMaskFilter(mShaowBlurMaskFilter); + Bitmap blurBitmap = result.extractAlpha(mBlurPaint, blurOffst); + + mCanvas.save(); + mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + mCanvas.translate(blurOffst[0], blurOffst[1]); + + mDrawPaint.setColor(Color.BLACK); + mDrawPaint.setAlpha(30); + mCanvas.drawBitmap(blurBitmap, 0, 0, mDrawPaint); + + mDrawPaint.setAlpha(60); + mCanvas.drawBitmap(blurBitmap, 0, mShadowOffset, mDrawPaint); + mCanvas.restore(); + + mCanvas.setBitmap(null); + blurBitmap.recycle(); + + return result; } - - 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/Hotseat.java b/src/com/android/launcher3/Hotseat.java index 59d60e381..b08272f36 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -64,7 +64,6 @@ public class Hotseat extends FrameLayout { public void setup(Launcher launcher) { mLauncher = launcher; - setOnKeyListener(new HotseatIconKeyEventListener()); } CellLayout getLayout() { @@ -150,21 +149,18 @@ public class Hotseat extends FrameLayout { TextView allAppsButton = (TextView) inflater.inflate(R.layout.all_apps_button, mContent, false); Drawable d = context.getResources().getDrawable(R.drawable.all_apps_button_icon); + Utilities.resizeIconDrawable(d); allAppsButton.setCompoundDrawables(null, d, null, null); allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label)); + allAppsButton.setOnKeyListener(new HotseatIconKeyEventListener()); if (mLauncher != null) { allAppsButton.setOnTouchListener(mLauncher.getHapticFeedbackTouchListener()); + mLauncher.setAllAppsButton(allAppsButton); + allAppsButton.setOnClickListener(mLauncher); + allAppsButton.setOnFocusChangeListener(mLauncher.mFocusHandler); } - 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 @@ -172,7 +168,7 @@ public class Hotseat extends FrameLayout { int y = getCellYFromOrder(mAllAppsButtonRank); CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1); lp.canReorder = false; - mContent.addViewToCellLayout(allAppsButton, -1, 0, lp, true); + mContent.addViewToCellLayout(allAppsButton, -1, allAppsButton.getId(), lp, true); } } @@ -180,7 +176,7 @@ public class Hotseat extends FrameLayout { public boolean onInterceptTouchEvent(MotionEvent ev) { // We don't want any clicks to go through to the hotseat unless the workspace is in // the normal state. - if (mLauncher.getWorkspace().isSmall()) { + if (mLauncher.getWorkspace().workspaceInModalState()) { return true; } return false; diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 827718b9e..bb71d776c 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -16,23 +16,29 @@ package com.android.launcher3; -import com.android.launcher3.backup.BackupProtos; - 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.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; -import android.graphics.Paint; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.text.TextUtils; import android.util.Log; +import com.android.launcher3.compat.LauncherActivityInfoCompat; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -48,24 +54,52 @@ import java.util.Map.Entry; * 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 final String RESOURCE_FILE_PREFIX = "icon_"; - private static final boolean DEBUG = true; + // Empty class name is used for storing package default entry. + private static final String EMPTY_CLASS_NAME = "."; + + private static final boolean DEBUG = false; private static class CacheEntry { public Bitmap icon; - public String title; + public CharSequence title; + public CharSequence contentDescription; + } + + private static class CacheKey { + public ComponentName componentName; + public UserHandleCompat user; + + CacheKey(ComponentName componentName, UserHandleCompat user) { + this.componentName = componentName; + this.user = user; + } + + @Override + public int hashCode() { + return componentName.hashCode() + user.hashCode(); + } + + @Override + public boolean equals(Object o) { + CacheKey other = (CacheKey) o; + return other.componentName.equals(componentName) && other.user.equals(user); + } } - private final Bitmap mDefaultIcon; + private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = + new HashMap<UserHandleCompat, Bitmap>(); private final Context mContext; private final PackageManager mPackageManager; - private final HashMap<ComponentName, CacheEntry> mCache = - new HashMap<ComponentName, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); + private final UserManagerCompat mUserManager; + private final LauncherAppsCompat mLauncherApps; + private final HashMap<CacheKey, CacheEntry> mCache = + new HashMap<CacheKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); private int mIconDpi; public IconCache(Context context) { @@ -74,10 +108,13 @@ public class IconCache { mContext = context; mPackageManager = context.getPackageManager(); + mUserManager = UserManagerCompat.getInstance(mContext); + mLauncherApps = LauncherAppsCompat.getInstance(mContext); mIconDpi = activityManager.getLauncherLargeIconDensity(); // need to set mIconDpi before getting default icon - mDefaultIcon = makeDefaultIcon(); + UserHandleCompat myUser = UserHandleCompat.myUserHandle(); + mDefaultIcons.put(myUser, makeDefaultIcon(myUser)); } public Drawable getFullResDefaultActivityIcon() { @@ -111,6 +148,10 @@ public class IconCache { return getFullResDefaultActivityIcon(); } + public int getFullResIconDpi() { + return mIconDpi; + } + public Drawable getFullResIcon(ResolveInfo info) { return getFullResIcon(info.activityInfo); } @@ -134,8 +175,9 @@ public class IconCache { return getFullResDefaultActivityIcon(); } - private Bitmap makeDefaultIcon() { - Drawable d = getFullResDefaultActivityIcon(); + private Bitmap makeDefaultIcon(UserHandleCompat user) { + Drawable unbadged = getFullResDefaultActivityIcon(); + Drawable d = mUserManager.getBadgedDrawableForUser(unbadged, user); Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1), Math.max(d.getIntrinsicHeight(), 1), Bitmap.Config.ARGB_8888); @@ -149,24 +191,25 @@ public class IconCache { /** * Remove any records for the supplied ComponentName. */ - public void remove(ComponentName componentName) { + public void remove(ComponentName componentName, UserHandleCompat user) { synchronized (mCache) { - mCache.remove(componentName); + mCache.remove(new CacheKey(componentName, user)); } } /** * Remove any records for the supplied package name. */ - public void remove(String packageName) { - HashSet<ComponentName> forDeletion = new HashSet<ComponentName>(); - for (ComponentName componentName: mCache.keySet()) { - if (componentName.getPackageName().equals(packageName)) { - forDeletion.add(componentName); + public void remove(String packageName, UserHandleCompat user) { + HashSet<CacheKey> forDeletion = new HashSet<CacheKey>(); + for (CacheKey key: mCache.keySet()) { + if (key.componentName.getPackageName().equals(packageName) + && key.user.equals(user)) { + forDeletion.add(key); } } - for (ComponentName condemned: forDeletion) { - remove(condemned); + for (CacheKey condemned: forDeletion) { + mCache.remove(condemned); } } @@ -184,10 +227,11 @@ public class IconCache { */ public void flushInvalidIcons(DeviceProfile grid) { synchronized (mCache) { - Iterator<Entry<ComponentName, CacheEntry>> it = mCache.entrySet().iterator(); + Iterator<Entry<CacheKey, CacheEntry>> it = mCache.entrySet().iterator(); while (it.hasNext()) { final CacheEntry e = it.next().getValue(); - if (e.icon.getWidth() < grid.iconSizePx || e.icon.getHeight() < grid.iconSizePx) { + if ((e.icon != null) && (e.icon.getWidth() < grid.iconSizePx + || e.icon.getHeight() < grid.iconSizePx)) { it.remove(); } } @@ -197,100 +241,193 @@ public class IconCache { /** * Fill in "application" with the icon and label for "info." */ - public void getTitleAndIcon(AppInfo application, ResolveInfo info, + public void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info, HashMap<Object, CharSequence> labelCache) { synchronized (mCache) { - CacheEntry entry = cacheLocked(application.componentName, info, labelCache); + CacheEntry entry = cacheLocked(application.componentName, info, labelCache, + info.getUser(), false); application.title = entry.title; application.iconBitmap = entry.icon; + application.contentDescription = entry.contentDescription; } } - public Bitmap getIcon(Intent intent) { - return getIcon(intent, null); + public Bitmap getIcon(Intent intent, UserHandleCompat user) { + return getIcon(intent, null, user, true); } - public Bitmap getIcon(Intent intent, String title) { + private Bitmap getIcon(Intent intent, String title, UserHandleCompat user, boolean usePkgIcon) { synchronized (mCache) { - final ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, 0); ComponentName component = intent.getComponent(); - + // null info means not installed, but if we have a component from the intent then + // we should still look in the cache for restored app icons. if (component == null) { - return mDefaultIcon; + return getDefaultIcon(user); } - CacheEntry entry = cacheLocked(component, resolveInfo, null); + LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user); + CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon); if (title != null) { entry.title = title; + entry.contentDescription = mUserManager.getBadgedLabelForUser(title, user); } return entry.icon; } } - public Bitmap getIcon(ComponentName component, ResolveInfo resolveInfo, + /** + * Fill in "shortcutInfo" with the icon and label for "info." + */ + public void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent, UserHandleCompat user, + boolean usePkgIcon) { + synchronized (mCache) { + ComponentName component = intent.getComponent(); + // null info means not installed, but if we have a component from the intent then + // we should still look in the cache for restored app icons. + if (component == null) { + shortcutInfo.setIcon(getDefaultIcon(user)); + shortcutInfo.title = ""; + shortcutInfo.usingFallbackIcon = true; + } else { + LauncherActivityInfoCompat launcherActInfo = + mLauncherApps.resolveActivity(intent, user); + CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon); + + shortcutInfo.setIcon(entry.icon); + shortcutInfo.title = entry.title; + shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + } + } + } + + + public Bitmap getDefaultIcon(UserHandleCompat user) { + if (!mDefaultIcons.containsKey(user)) { + mDefaultIcons.put(user, makeDefaultIcon(user)); + } + return mDefaultIcons.get(user); + } + + public Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info, HashMap<Object, CharSequence> labelCache) { synchronized (mCache) { - if (resolveInfo == null || component == null) { + if (info == null || component == null) { return null; } - CacheEntry entry = cacheLocked(component, resolveInfo, labelCache); + CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser(), false); return entry.icon; } } - public boolean isDefaultIcon(Bitmap icon) { - return mDefaultIcon == icon; + public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) { + return mDefaultIcons.get(user) == icon; } - private CacheEntry cacheLocked(ComponentName componentName, ResolveInfo info, - HashMap<Object, CharSequence> labelCache) { - CacheEntry entry = mCache.get(componentName); + private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info, + HashMap<Object, CharSequence> labelCache, UserHandleCompat user, boolean usePackageIcon) { + CacheKey cacheKey = new CacheKey(componentName, user); + CacheEntry entry = mCache.get(cacheKey); if (entry == null) { entry = new CacheEntry(); - mCache.put(componentName, entry); + mCache.put(cacheKey, entry); if (info != null) { - ComponentName key = LauncherModel.getComponentNameFromResolveInfo(info); - if (labelCache != null && labelCache.containsKey(key)) { - entry.title = labelCache.get(key).toString(); + ComponentName labelKey = info.getComponentName(); + if (labelCache != null && labelCache.containsKey(labelKey)) { + entry.title = labelCache.get(labelKey).toString(); } else { - entry.title = info.loadLabel(mPackageManager).toString(); + entry.title = info.getLabel().toString(); if (labelCache != null) { - labelCache.put(key, entry.title); + labelCache.put(labelKey, entry.title); } } - if (entry.title == null) { - entry.title = info.activityInfo.name; - } + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); entry.icon = Utilities.createIconBitmap( - getFullResIcon(info), mContext); + info.getBadgedIcon(mIconDpi), mContext); } else { entry.title = ""; - Bitmap preloaded = getPreloadedIcon(componentName); + Bitmap preloaded = getPreloadedIcon(componentName, user); if (preloaded != null) { if (DEBUG) Log.d(TAG, "using preloaded icon for " + componentName.toShortString()); entry.icon = preloaded; } else { - if (DEBUG) Log.d(TAG, "using default icon for " + - componentName.toShortString()); - entry.icon = mDefaultIcon; + if (usePackageIcon) { + CacheEntry packageEntry = getEntryForPackage( + componentName.getPackageName(), user); + if (packageEntry != null) { + if (DEBUG) Log.d(TAG, "using package default icon for " + + componentName.toShortString()); + entry.icon = packageEntry.icon; + entry.title = packageEntry.title; + } + } + if (entry.icon == null) { + if (DEBUG) Log.d(TAG, "using default icon for " + + componentName.toShortString()); + entry.icon = getDefaultIcon(user); + } } } } return entry; } + /** + * Adds a default package entry in the cache. This entry is not persisted and will be removed + * when the cache is flushed. + */ + public void cachePackageInstallInfo(String packageName, UserHandleCompat user, + Bitmap icon, CharSequence title) { + remove(packageName, user); + + CacheEntry entry = getEntryForPackage(packageName, user); + if (!TextUtils.isEmpty(title)) { + entry.title = title; + } + if (icon != null) { + entry.icon = Utilities.createIconBitmap( + new BitmapDrawable(mContext.getResources(), icon), mContext); + } + } + + /** + * Gets an entry for the package, which can be used as a fallback entry for various components. + */ + private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) { + ComponentName cn = getPackageComponent(packageName); + CacheKey cacheKey = new CacheKey(cn, user); + CacheEntry entry = mCache.get(cacheKey); + if (entry == null) { + entry = new CacheEntry(); + entry.title = ""; + mCache.put(cacheKey, entry); + + try { + ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0); + entry.title = info.loadLabel(mPackageManager); + entry.icon = Utilities.createIconBitmap(info.loadIcon(mPackageManager), mContext); + } catch (NameNotFoundException e) { + if (DEBUG) Log.d(TAG, "Application not installed " + packageName); + } + + if (entry.icon == null) { + entry.icon = getPreloadedIcon(cn, user); + } + } + 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); + for (CacheKey ck : mCache.keySet()) { + final CacheEntry e = mCache.get(ck); + set.put(ck.componentName, e.icon); } return set; } @@ -353,9 +490,14 @@ public class IconCache { * @param componentName the component that should own the icon * @returns a bitmap if one is cached, or null. */ - private Bitmap getPreloadedIcon(ComponentName componentName) { + private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) { final String key = componentName.flattenToShortString(); + // We don't keep icons for other profiles in persistent cache. + if (!user.equals(UserHandleCompat.myUserHandle())) { + return null; + } + if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key); Bitmap icon = null; FileInputStream resourceFile = null; @@ -374,7 +516,7 @@ public class IconCache { Log.w(TAG, "failed to decode pre-load icon for " + key); } } catch (FileNotFoundException e) { - if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key, e); + if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key); } catch (IOException e) { Log.w(TAG, "failed to read pre-load icon for: " + key, e); } finally { @@ -387,20 +529,6 @@ public class IconCache { } } - if (icon != null) { - // TODO: handle alpha mask in the view layer - Bitmap b = Bitmap.createBitmap(Math.max(icon.getWidth(), 1), - Math.max(icon.getHeight(), 1), - Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(b); - Paint paint = new Paint(); - paint.setAlpha(127); - c.drawBitmap(icon, 0, 0, paint); - c.setBitmap(null); - icon.recycle(); - icon = b; - } - return icon; } @@ -410,7 +538,11 @@ public class IconCache { * @param componentName the component that should own the icon * @returns true on success */ - public boolean deletePreloadedIcon(ComponentName componentName) { + public boolean deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) { + // We don't keep icons for other profiles in persistent cache. + if (!user.equals(UserHandleCompat.myUserHandle())) { + return false; + } if (componentName == null) { return false; } @@ -428,4 +560,8 @@ public class IconCache { String filename = resourceName.replace(File.separatorChar, '_'); return RESOURCE_FILE_PREFIX + filename; } + + static ComponentName getPackageComponent(String packageName) { + return new ComponentName(packageName, EMPTY_CLASS_NAME); + } } diff --git a/src/com/android/launcher3/InfoDropTarget.java b/src/com/android/launcher3/InfoDropTarget.java index 374238c49..7e55af228 100644 --- a/src/com/android/launcher3/InfoDropTarget.java +++ b/src/com/android/launcher3/InfoDropTarget.java @@ -26,6 +26,8 @@ import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import com.android.launcher3.compat.UserHandleCompat; + public class InfoDropTarget extends ButtonDropTarget { private ColorStateList mOriginalTextColor; @@ -49,6 +51,13 @@ public class InfoDropTarget extends ButtonDropTarget { Resources r = getResources(); mHoverColor = r.getColor(R.color.info_target_hover_tint); mDrawable = (TransitionDrawable) getCurrentDrawable(); + + if (mDrawable == null) { + // TODO: investigate why this is ever happening. Presently only on one known device. + mDrawable = (TransitionDrawable) r.getDrawable(R.drawable.info_target_selector); + setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null); + } + if (null != mDrawable) { mDrawable.setCrossFadeEnabled(true); } @@ -75,8 +84,15 @@ public class InfoDropTarget extends ButtonDropTarget { } else if (d.dragInfo instanceof PendingAddItemInfo) { componentName = ((PendingAddItemInfo) d.dragInfo).componentName; } + final UserHandleCompat user; + if (d.dragInfo instanceof ItemInfo) { + user = ((ItemInfo) d.dragInfo).user; + } else { + user = UserHandleCompat.myUserHandle(); + } + if (componentName != null) { - mLauncher.startApplicationDetailsActivity(componentName); + mLauncher.startApplicationDetailsActivity(componentName, user); } // There is no post-drop animation, so clean up the DragView now diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index 28cef1346..2edde4fae 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -17,6 +17,7 @@ package com.android.launcher3; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -29,6 +30,8 @@ import android.util.Base64; import android.util.Log; import android.widget.Toast; +import com.android.launcher3.compat.UserHandleCompat; + import org.json.JSONObject; import org.json.JSONStringer; import org.json.JSONTokener; @@ -280,19 +283,27 @@ public class InstallShortcutReceiver extends BroadcastReceiver { final boolean exists = LauncherModel.shortcutExists(context, name, intent); //final boolean allowDuplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); - // TODO-XXX: Disable duplicates for now - if (!exists /* && allowDuplicate */) { + // If the intent specifies a package, make sure the package exists + String packageName = intent.getPackage(); + if (packageName == null) { + packageName = intent.getComponent() == null ? null : + intent.getComponent().getPackageName(); + } + if (packageName != null && !packageName.isEmpty()) { + UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); + if (!LauncherModel.isValidPackage(context, packageName, myUserHandle)) { + if (DBG) Log.d(TAG, "Ignoring shortcut for absent package:" + intent); + continue; + } + } + + if (!exists) { // Generate a shortcut info to add into the model ShortcutInfo info = getShortcutInfo(context, pendingInfo.data, pendingInfo.launchIntent); addShortcuts.add(info); } - /* - else if (exists && !allowDuplicate) { - result = INSTALL_SHORTCUT_IS_DUPLICATE; - duplicateName = name; - } - */ + } // Notify the user once if we weren't able to place any duplicates diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java index 3dc92c9c2..09b77f756 100644 --- a/src/com/android/launcher3/ItemInfo.java +++ b/src/com/android/launcher3/ItemInfo.java @@ -17,24 +17,34 @@ package com.android.launcher3; import android.content.ContentValues; +import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.util.Log; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; /** * Represents an item in the launcher. */ public class ItemInfo { + + /** + * Intent extra to store the profile. Format: UserHandle + */ + static final String EXTRA_PROFILE = "profile"; static final int NO_ID = -1; /** * The id in the settings database for this item */ - long id = NO_ID; + public long id = NO_ID; /** * One of {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION}, @@ -42,7 +52,7 @@ public class ItemInfo { * {@link LauncherSettings.Favorites#ITEM_TYPE_FOLDER}, or * {@link LauncherSettings.Favorites#ITEM_TYPE_APPWIDGET}. */ - int itemType; + public int itemType; /** * The id of the container that holds this item. For the desktop, this will be @@ -50,27 +60,27 @@ public class ItemInfo { * 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; + public long container = NO_ID; /** * Iindicates the screen in which the shortcut appears. */ - long screenId = -1; + public long screenId = -1; /** * Indicates the X position of the associated cell. */ - int cellX = -1; + public int cellX = -1; /** * Indicates the Y position of the associated cell. */ - int cellY = -1; + public int cellY = -1; /** * Indicates the X cell span. */ - int spanX = 1; + public int spanX = 1; /** * Indicates the Y cell span. @@ -80,17 +90,17 @@ public class ItemInfo { /** * Indicates the minimum X cell span. */ - int minSpanX = 1; + public int minSpanX = 1; /** * Indicates the minimum Y cell span. */ - int minSpanY = 1; + public int minSpanY = 1; /** * Indicates that this item needs to be updated in the db */ - boolean requiresDbUpdate = false; + public boolean requiresDbUpdate = false; /** * Title of the item @@ -98,14 +108,28 @@ public class ItemInfo { CharSequence title; /** + * Content description of the item. + */ + CharSequence contentDescription; + + /** * The position of the item in a drag-and-drop operation. */ int[] dropPos = null; + UserHandleCompat user; + ItemInfo() { + user = UserHandleCompat.myUserHandle(); } ItemInfo(ItemInfo info) { + copyFrom(info); + // tempdebug: + LauncherModel.checkItemInfo(this); + } + + public void copyFrom(ItemInfo info) { id = info.id; cellX = info.cellX; cellY = info.cellY; @@ -114,24 +138,22 @@ public class ItemInfo { screenId = info.screenId; itemType = info.itemType; container = info.container; - // tempdebug: - LauncherModel.checkItemInfo(this); - } - - protected Intent getIntent() { - throw new RuntimeException("Unexpected Intent"); + user = info.user; + contentDescription = info.contentDescription; } - protected Intent getRestoredIntent() { + public Intent getIntent() { throw new RuntimeException("Unexpected Intent"); } /** * Write the fields of this item to the DB * + * @param context A context object to use for getting UserManagerCompat * @param values */ - void onAddToDatabase(ContentValues values) { + + void onAddToDatabase(Context context, ContentValues values) { values.put(LauncherSettings.BaseLauncherColumns.ITEM_TYPE, itemType); values.put(LauncherSettings.Favorites.CONTAINER, container); values.put(LauncherSettings.Favorites.SCREEN, screenId); @@ -139,6 +161,13 @@ public class ItemInfo { values.put(LauncherSettings.Favorites.CELLY, cellY); values.put(LauncherSettings.Favorites.SPANX, spanX); values.put(LauncherSettings.Favorites.SPANY, spanY); + long serialNumber = UserManagerCompat.getInstance(context).getSerialNumberForUser(user); + values.put(LauncherSettings.Favorites.PROFILE_ID, serialNumber); + + if (screenId == Workspace.EXTRA_EMPTY_SCREEN_ID) { + // We should never persist an item on the extra empty screen. + throw new RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID"); + } } void updateValuesWithCoordinates(ContentValues values, int cellX, int cellY) { @@ -182,6 +211,7 @@ public class ItemInfo { public String toString() { return "Item(id=" + this.id + " type=" + this.itemType + " container=" + this.container + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX - + " spanY=" + spanY + " dropPos=" + dropPos + ")"; + + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + + " user=" + user + ")"; } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index c22a6bf3f..42ec4fb48 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -22,11 +22,13 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityOptions; +import android.app.AlertDialog; import android.app.SearchManager; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; @@ -37,6 +39,7 @@ import android.content.ComponentCallbacks2; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; @@ -44,25 +47,25 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; 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.ContentObserver; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; 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; @@ -70,6 +73,7 @@ import android.text.TextUtils; import android.text.method.TextKeyListener; import android.util.DisplayMetrics; import android.util.Log; +import android.view.ContextThemeWrapper; import android.view.Display; import android.view.Gravity; import android.view.HapticFeedbackConstants; @@ -81,13 +85,16 @@ import android.view.Surface; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; +import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.Window; 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.animation.Interpolator; import android.view.inputmethod.InputMethodManager; import android.widget.Advanceable; import android.widget.FrameLayout; @@ -96,6 +103,14 @@ import android.widget.TextView; import android.widget.Toast; import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.PagedView.PageSwitchListener; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.LauncherActivityInfoCompat; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.PackageInstallerCompat; +import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -105,6 +120,9 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collection; @@ -113,13 +131,12 @@ import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; - /** * Default launcher application. */ public class Launcher extends Activity implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, - View.OnTouchListener { + View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener { static final String TAG = "Launcher"; static final boolean LOGD = false; @@ -133,12 +150,12 @@ public class Launcher extends Activity 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; + private static final int REQUEST_RECONFIGURE_APPWIDGET = 12; /** * IntentStarter uses request codes starting with this. This must be greater than all activity @@ -189,9 +206,13 @@ public class Launcher extends Activity // Type: int[] private static final String RUNTIME_STATE_VIEW_IDS = "launcher.view_ids"; - + static final String INTRO_SCREEN_DISMISSED = "launcher.intro_screen_dismissed"; static final String FIRST_RUN_ACTIVITY_DISPLAYED = "launcher.first_run_activity_displayed"; + static final String FIRST_LOAD_COMPLETE = "launcher.first_load_complete"; + static final String ACTION_FIRST_LOAD_COMPLETE = + "com.android.launcher3.action.FIRST_LOAD_COMPLETE"; + private static final String TOOLBAR_ICON_METADATA_NAME = "com.android.launcher.toolbar_icon"; private static final String TOOLBAR_SEARCH_ICON_METADATA_NAME = "com.android.launcher.toolbar_search_icon"; @@ -208,10 +229,12 @@ public class Launcher extends Activity private State mState = State.WORKSPACE; private AnimatorSet mStateAnimation; + private boolean mIsSafeModeEnabled; + static final int APPWIDGET_HOST_ID = 1024; public static final int EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT = 300; - public static final int EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT_FOLDER_CLOSE = 400; private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500; + private static final int ACTIVITY_START_DELAY = 1000; private static final Object sLock = new Object(); private static int sScreen = DEFAULT_SCREEN; @@ -223,6 +246,7 @@ public class Launcher extends Activity private static int NEW_APPS_PAGE_MOVE_DELAY = 500; private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5; private static int NEW_APPS_ANIMATION_DELAY = 500; + private static final int SINGLE_FRAME_DELAY = 16; private final BroadcastReceiver mCloseSystemDialogsReceiver = new CloseSystemDialogsIntentReceiver(); @@ -236,9 +260,8 @@ public class Launcher extends Activity private DragLayer mDragLayer; private DragController mDragController; private View mWeightWatcher; - private LauncherClings mLauncherClings; - private AppWidgetManager mAppWidgetManager; + private AppWidgetManagerCompat mAppWidgetManager; private LauncherAppWidgetHost mAppWidgetHost; private ItemInfo mPendingAddInfo = new ItemInfo(); @@ -278,9 +301,6 @@ public class Launcher extends Activity private ArrayList<Runnable> mBindOnResumeCallbacks = new ArrayList<Runnable>(); 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; @@ -312,10 +332,6 @@ public class Launcher extends Activity // 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 Intent mAppMarketIntent = null; - private static final boolean DISABLE_MARKET_BUTTON = true; private Drawable mWorkspaceBackgroundDrawable; @@ -352,8 +368,7 @@ public class Launcher extends Activity } }; - private static ArrayList<PendingAddArguments> sPendingAddList - = new ArrayList<PendingAddArguments>(); + private static PendingAddArguments sPendingAddItem; public static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); @@ -364,10 +379,13 @@ public class Launcher extends Activity long screenId; int cellX; int cellY; + int appWidgetId; } private Stats mStats; + FocusIndicatorView mFocusHandler; + static boolean isPropertyEnabled(String propertyName) { return Log.isLoggable(propertyName, Log.VERBOSE); } @@ -393,7 +411,7 @@ public class Launcher extends Activity LauncherAppState.setApplicationContext(getApplicationContext()); LauncherAppState app = LauncherAppState.getInstance(); - + LauncherAppState.getLauncherProvider().setLauncherProviderChangeListener(this); // Determine the dynamic grid properties Point smallestSize = new Point(); Point largestSize = new Point(); @@ -414,16 +432,16 @@ public class Launcher extends Activity // the LauncherApplication should call this, but in case of Instrumentation it might not be present yet mSharedPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); + mIsSafeModeEnabled = getPackageManager().isSafeMode(); mModel = app.setLauncher(this); mIconCache = app.getIconCache(); mIconCache.flushInvalidIcons(grid); mDragController = new DragController(this); - mLauncherClings = new LauncherClings(this); mInflater = getLayoutInflater(); mStats = new Stats(this); - mAppWidgetManager = AppWidgetManager.getInstance(this); + mAppWidgetManager = AppWidgetManagerCompat.getInstance(this); mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID); mAppWidgetHost.startListening(); @@ -438,7 +456,6 @@ public class Launcher extends Activity Environment.getExternalStorageDirectory() + "/launcher"); } - checkForLocaleChange(); setContentView(R.layout.launcher); @@ -457,7 +474,7 @@ public class Launcher extends Activity } if (!mRestoring) { - if (DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE || sPausedFromUserAction) { + if (DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE) { // If the user leaves launcher, then we should just load items asynchronously when // they return. mModel.startLoader(true, PagedView.INVALID_RESTORE_PAGE); @@ -480,25 +497,16 @@ public class Launcher extends Activity // On large interfaces, we want the screen to auto-rotate based on the current orientation unlockScreenOrientation(true); - // The two first run cling paths are mutually exclusive, if the launcher is preinstalled - // on the device, then we always show the first run cling experience (or if there is no - // launcher2). Otherwise, we prompt the user upon started for migration - showFirstRunActivity(); - if (mLauncherClings.shouldShowFirstRunOrMigrationClings()) { - if (mModel.canMigrateFromOldLauncherDb(this)) { - mLauncherClings.showMigrationCling(); - } else { - mLauncherClings.showFirstRunCling(); - } + if (shouldShowIntroScreen()) { + showIntroScreen(); } else { - mLauncherClings.removeFirstRunAndMigrationClings(); + showFirstRunActivity(); + showFirstRunClings(); } } - protected void onUserLeaveHint() { - super.onUserLeaveHint(); - sPausedFromUserAction = true; - } + @Override + public void onLauncherProviderChange() { } /** To be overriden by subclasses to hint to Launcher that we have custom content */ protected boolean hasCustomContentToLeft() { @@ -514,21 +522,6 @@ public class Launcher extends Activity } /** - * To be overridden by subclasses to indicate that there is an activity to launch - * before showing the standard launcher experience. - */ - protected boolean hasFirstRunActivity() { - return false; - } - - /** - * To be overridden by subclasses to launch any first run activity - */ - protected Intent getFirstRunActivity() { - return null; - } - - /** * Invoked by subclasses to signal a change to the {@link #addCustomContentToLeft} value to * ensure the custom content page is added or removed if necessary. */ @@ -552,11 +545,7 @@ public class Launcher extends Activity 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) { - if (!DISABLE_MARKET_BUTTON) { - updateAppMarketIcon(); - } + if (sGlobalSearchIcon[coi] == null || sVoiceSearchIcon[coi] == null) { searchVisible = updateGlobalSearchIcon(); voiceVisible = updateVoiceSearchIcon(searchVisible); } @@ -568,9 +557,6 @@ public class Launcher extends Activity updateVoiceSearchIcon(sVoiceSearchIcon[coi]); voiceVisible = true; } - if (!DISABLE_MARKET_BUTTON && sAppMarketIcon[coi] != null) { - updateAppMarketIcon(sAppMarketIcon[coi]); - } if (mSearchDropTargetBar != null) { mSearchDropTargetBar.onSearchPackagesChanged(searchVisible, voiceVisible); } @@ -703,19 +689,20 @@ public class Launcher extends Activity } } - /** - * Copied from View -- the View version of the method isn't called - * anywhere else in our process and only exists for API level 17+, - * so it's ok to keep our own version with no API requirement. - */ public static int generateViewId() { - for (;;) { - final int result = sNextGeneratedId.get(); - // aapt-generated IDs have the high byte nonzero; clamp to the range under that. - int newValue = result + 1; - if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0. - if (sNextGeneratedId.compareAndSet(result, newValue)) { - return result; + if (Build.VERSION.SDK_INT >= 17) { + return View.generateViewId(); + } else { + // View.generateViewId() is not available. The following fallback logic is a copy + // of its implementation. + for (;;) { + final int result = sNextGeneratedId.get(); + // aapt-generated IDs have the high byte nonzero; clamp to the range under that. + int newValue = result + 1; + if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0. + if (sNextGeneratedId.compareAndSet(result, newValue)) { + return result; + } } } } @@ -735,40 +722,39 @@ public class Launcher extends Activity * 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; + private long completeAdd(PendingAddArguments args) { + long screenId = args.screenId; + if (args.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + // When the screen id represents an actual screen (as opposed to a rank) we make sure + // that the drop page actually exists. + screenId = ensurePendingDropLayoutExists(args.screenId); + } + switch (args.requestCode) { - case REQUEST_PICK_APPLICATION: - completeAddApplication(args.intent, args.container, args.screenId, args.cellX, - args.cellY); - break; - case REQUEST_PICK_SHORTCUT: - processShortcut(args.intent); - break; case REQUEST_CREATE_SHORTCUT: - completeAddShortcut(args.intent, args.container, args.screenId, args.cellX, + completeAddShortcut(args.intent, args.container, screenId, 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.screenId, null, null); - result = true; + completeAddAppWidget(args.appWidgetId, args.container, screenId, null, null); + break; + case REQUEST_RECONFIGURE_APPWIDGET: + completeRestoreAppWidget(args.appWidgetId); 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; + return screenId; } @Override protected void onActivityResult( final int requestCode, final int resultCode, final Intent data) { // Reset the startActivity waiting flag - mWaitingForResult = false; - int pendingAddWidgetId = mPendingAddWidgetId; + setWaitingForResult(false); + final int pendingAddWidgetId = mPendingAddWidgetId; mPendingAddWidgetId = -1; Runnable exitSpringLoaded = new Runnable() { @@ -784,7 +770,7 @@ public class Launcher extends Activity data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1; if (resultCode == RESULT_CANCELED) { completeTwoStageWidgetDrop(RESULT_CANCELED, appWidgetId); - mWorkspace.removeExtraEmptyScreen(true, exitSpringLoaded, + mWorkspace.removeExtraEmptyScreenDelayed(true, exitSpringLoaded, ON_ACTIVITY_RESULT_ANIMATION_DELAY, false); } else if (resultCode == RESULT_OK) { addAppWidgetImpl(appWidgetId, mPendingAddInfo, null, @@ -801,6 +787,7 @@ public class Launcher extends Activity boolean isWidgetDrop = (requestCode == REQUEST_PICK_APPWIDGET || requestCode == REQUEST_CREATE_APPWIDGET); + final boolean workspaceLocked = isWorkspaceLocked(); // We have special handling for widgets if (isWidgetDrop) { final int appWidgetId; @@ -813,33 +800,66 @@ public class Launcher extends Activity } final int result; - final Runnable onComplete; if (appWidgetId < 0 || resultCode == RESULT_CANCELED) { - Log.e(TAG, "Error: appWidgetId (EXTRA_APPWIDGET_ID) was not returned from the \\" + - "widget configuration activity."); + Log.e(TAG, "Error: appWidgetId (EXTRA_APPWIDGET_ID) was not " + + "returned from the widget configuration activity."); result = RESULT_CANCELED; completeTwoStageWidgetDrop(result, appWidgetId); - onComplete = new Runnable() { + final Runnable onComplete = new Runnable() { @Override public void run() { exitSpringLoadedDragModeDelayed(false, 0, null); } }; + if (workspaceLocked) { + // No need to remove the empty screen if we're mid-binding, as the + // the bind will not add the empty screen. + mWorkspace.postDelayed(onComplete, ON_ACTIVITY_RESULT_ANIMATION_DELAY); + } else { + mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete, + ON_ACTIVITY_RESULT_ANIMATION_DELAY, false); + } } else { - result = resultCode; - final CellLayout dropLayout = - (CellLayout) mWorkspace.getScreenWithId(mPendingAddInfo.screenId); - dropLayout.setDropPending(true); - onComplete = new Runnable() { - @Override - public void run() { - completeTwoStageWidgetDrop(result, appWidgetId); - dropLayout.setDropPending(false); + if (!workspaceLocked) { + if (mPendingAddInfo.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + // When the screen id represents an actual screen (as opposed to a rank) + // we make sure that the drop page actually exists. + mPendingAddInfo.screenId = + ensurePendingDropLayoutExists(mPendingAddInfo.screenId); } - }; + final CellLayout dropLayout = mWorkspace.getScreenWithId(mPendingAddInfo.screenId); + + dropLayout.setDropPending(true); + final Runnable onComplete = new Runnable() { + @Override + public void run() { + completeTwoStageWidgetDrop(resultCode, appWidgetId); + dropLayout.setDropPending(false); + } + }; + mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete, + ON_ACTIVITY_RESULT_ANIMATION_DELAY, false); + } else { + PendingAddArguments args = preparePendingAddArgs(requestCode, data, appWidgetId, + mPendingAddInfo); + sPendingAddItem = args; + } + } + return; + } + + if (requestCode == REQUEST_RECONFIGURE_APPWIDGET) { + if (resultCode == RESULT_OK) { + // Update the widget view. + PendingAddArguments args = preparePendingAddArgs(requestCode, data, + pendingAddWidgetId, mPendingAddInfo); + if (workspaceLocked) { + sPendingAddItem = args; + } else { + completeAdd(args); + } } - mWorkspace.removeExtraEmptyScreen(true, onComplete, ON_ACTIVITY_RESULT_ANIMATION_DELAY, - false); + // Leave the widget in the pending state if the user canceled the configure. return; } @@ -849,27 +869,54 @@ public class Launcher extends Activity // 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.screenId = mPendingAddInfo.screenId; - args.cellX = mPendingAddInfo.cellX; - args.cellY = mPendingAddInfo.cellY; + final PendingAddArguments args = preparePendingAddArgs(requestCode, data, -1, + mPendingAddInfo); if (isWorkspaceLocked()) { - sPendingAddList.add(args); + sPendingAddItem = args; } else { completeAdd(args); + mWorkspace.removeExtraEmptyScreenDelayed(true, exitSpringLoaded, + ON_ACTIVITY_RESULT_ANIMATION_DELAY, false); } - mWorkspace.removeExtraEmptyScreen(true, exitSpringLoaded, - ON_ACTIVITY_RESULT_ANIMATION_DELAY, false); } else if (resultCode == RESULT_CANCELED) { - mWorkspace.removeExtraEmptyScreen(true, exitSpringLoaded, + mWorkspace.removeExtraEmptyScreenDelayed(true, exitSpringLoaded, ON_ACTIVITY_RESULT_ANIMATION_DELAY, false); } mDragLayer.clearAnimatedView(); } + private PendingAddArguments preparePendingAddArgs(int requestCode, Intent data, int + appWidgetId, ItemInfo info) { + PendingAddArguments args = new PendingAddArguments(); + args.requestCode = requestCode; + args.intent = data; + args.container = info.container; + args.screenId = info.screenId; + args.cellX = info.cellX; + args.cellY = info.cellY; + args.appWidgetId = appWidgetId; + return args; + } + + /** + * Check to see if a given screen id exists. If not, create it at the end, return the new id. + * + * @param screenId the screen id to check + * @return the new screen, or screenId if it exists + */ + private long ensurePendingDropLayoutExists(long screenId) { + CellLayout dropLayout = + (CellLayout) mWorkspace.getScreenWithId(screenId); + if (dropLayout == null) { + // it's possible that the add screen was removed because it was + // empty and a re-bind occurred + mWorkspace.addExtraEmptyScreen(); + return mWorkspace.commitExtraEmptyScreen(); + } else { + return screenId; + } + } + private void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { CellLayout cellLayout = (CellLayout) mWorkspace.getScreenWithId(mPendingAddInfo.screenId); @@ -938,9 +985,8 @@ public class Launcher extends Activity setWorkspaceBackground(mState == State.WORKSPACE); mPaused = false; - sPausedFromUserAction = false; if (mRestoring || mOnResumeNeedsLoad) { - mWorkspaceLoading = true; + setWorkspaceLoading(true); mModel.startLoader(true, PagedView.INVALID_RESTORE_PAGE); mRestoring = false; mOnResumeNeedsLoad = false; @@ -981,10 +1027,6 @@ public class Launcher extends Activity // 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 @@ -1010,17 +1052,20 @@ public class Launcher extends Activity // It is also poassible that onShow will instead be called slightly after first layout // if PagedView#setRestorePage was set to the custom content page in onCreate(). if (mWorkspace.isOnOrMovingToCustomContent()) { - mWorkspace.getCustomContentCallbacks().onShow(); + mWorkspace.getCustomContentCallbacks().onShow(true); } } mWorkspace.updateInteractionForState(); mWorkspace.onResume(); + + PackageInstallerCompat.getInstance(this).onResume(); } @Override protected void onPause() { // Ensure that items added to Launcher are queued until Launcher returns InstallShortcutReceiver.enableInstallQueue(); + PackageInstallerCompat.getInstance(this).onPause(); super.onPause(); mPaused = true; @@ -1054,23 +1099,24 @@ public class Launcher extends Activity } public interface CustomContentCallbacks { - // Custom content is completely shown - public void onShow(); + // Custom content is completely shown. {@code fromResume} indicates whether this was caused + // by a onResume or by scrolling otherwise. + public void onShow(boolean fromResume); // Custom content is completely hidden public void onHide(); // Custom content scroll progress changed. From 0 (not showing) to 1 (fully showing). public void onScrollProgressChanged(float progress); + + // Indicates whether the user is allowed to scroll away from the custom content. + boolean isScrollingAllowed(); } protected boolean hasSettings() { return false; } - protected void startSettings() { - } - public interface QSBScroller { public void setScrollY(int scrollY); } @@ -1089,7 +1135,9 @@ public class Launcher extends Activity @Override public Object onRetainNonConfigurationInstance() { // Flag the loader to stop early before switching - mModel.stopLoader(); + if (mModel.isCurrentCallbacks(this)) { + mModel.stopLoader(); + } if (mAppsCustomizeContent != null) { mAppsCustomizeContent.surrender(); } @@ -1196,7 +1244,7 @@ public class Launcher extends Activity mPendingAddInfo.spanY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y); mPendingAddWidgetInfo = savedState.getParcelable(RUNTIME_STATE_PENDING_ADD_WIDGET_INFO); mPendingAddWidgetId = savedState.getInt(RUNTIME_STATE_PENDING_ADD_WIDGET_ID); - mWaitingForResult = true; + setWaitingForResult(true); mRestoring = true; } @@ -1231,8 +1279,10 @@ public class Launcher extends Activity final DragController dragController = mDragController; mLauncherView = findViewById(R.id.launcher); + mFocusHandler = (FocusIndicatorView) findViewById(R.id.focus_indicator); mDragLayer = (DragLayer) findViewById(R.id.drag_layer); mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace); + mWorkspace.setPageSwitchListener(this); mPageIndicators = mDragLayer.findViewById(R.id.page_indicator); mLauncherView.setSystemUiVisibility( @@ -1255,7 +1305,7 @@ public class Launcher extends Activity @Override public void onClick(View arg0) { if (!mWorkspace.isSwitchingState()) { - showAllApps(true, AppsCustomizePagedView.ContentType.Widgets, true); + onClickAddWidgetButton(arg0); } } }); @@ -1266,7 +1316,7 @@ public class Launcher extends Activity @Override public void onClick(View arg0) { if (!mWorkspace.isSwitchingState()) { - startWallpaper(); + onClickWallpaperPicker(arg0); } } }); @@ -1278,9 +1328,9 @@ public class Launcher extends Activity @Override public void onClick(View arg0) { if (!mWorkspace.isSwitchingState()) { - startSettings(); - } + onClickSettingsButton(arg0); } + } }); settingsButton.setOnTouchListener(getHapticFeedbackTouchListener()); } else { @@ -1334,6 +1384,17 @@ public class Launcher extends Activity } /** + * Sets the all apps button. This method is called from {@link Hotseat}. + */ + public void setAllAppsButton(View allAppsButton) { + mAllAppsButton = allAppsButton; + } + + public View getAllAppsButton() { + return mAllAppsButton; + } + + /** * Creates a view representing a shortcut. * * @param info The data structure describing the shortcut. @@ -1356,44 +1417,13 @@ public class Launcher extends Activity */ View createShortcut(int layoutResId, ViewGroup parent, ShortcutInfo info) { BubbleTextView favorite = (BubbleTextView) mInflater.inflate(layoutResId, parent, false); - favorite.applyFromShortcutInfo(info, mIconCache); + favorite.applyFromShortcutInfo(info, mIconCache, true); favorite.setOnClickListener(this); + favorite.setOnFocusChangeListener(mFocusHandler); 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, long screenId, int cellX, int cellY) { - final int[] cellXY = mTmpAddItemCellCoordinates; - final CellLayout layout = getCellLayout(container, screenId); - - // 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(this, data.getComponent(), Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - info.container = ItemInfo.NO_ID; - mWorkspace.addApplicationShortcut(info, layout, container, screenId, 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. @@ -1543,6 +1573,7 @@ public class Launcher extends Activity launcherInfo.spanY = spanXY[1]; launcherInfo.minSpanX = mPendingAddInfo.minSpanX; launcherInfo.minSpanY = mPendingAddInfo.minSpanY; + launcherInfo.user = mAppWidgetManager.getUser(appWidgetInfo); LauncherModel.addItemToDatabase(this, launcherInfo, container, screenId, cellXY[0], cellXY[1], false); @@ -1595,6 +1626,9 @@ public class Launcher extends Activity mModel.startLoader(false, PagedView.INVALID_RESTORE_PAGE, LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE | LauncherModel.LOADER_FLAG_MIGRATE_SHORTCUTS); + } else if (LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED.equals(action) + || LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { + getModel().forceReload(); } } }; @@ -1607,16 +1641,61 @@ public class Launcher extends Activity final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_USER_PRESENT); + // For handling managed profiles + filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED); + filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED); if (ENABLE_DEBUG_INTENTS) { filter.addAction(DebugIntents.DELETE_DATABASE); filter.addAction(DebugIntents.MIGRATE_DATABASE); } registerReceiver(mReceiver, filter); FirstFrameAnimatorHelper.initializeDrawListener(getWindow().getDecorView()); + setupTransparentSystemBarsForLmp(); mAttached = true; mVisible = true; } + /** + * Sets up transparent navigation and status bars in LMP. + * This method is a no-op for other platform versions. + */ + @TargetApi(19) + private void setupTransparentSystemBarsForLmp() { + // TODO(sansid): use the APIs directly when compiling against L sdk. + // Currently we use reflection to access the flags and the API to set the transparency + // on the System bars. + if (Utilities.isLmpOrAbove()) { + try { + getWindow().getAttributes().systemUiVisibility |= + (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + Field drawsSysBackgroundsField = WindowManager.LayoutParams.class.getField( + "FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS"); + getWindow().addFlags(drawsSysBackgroundsField.getInt(null)); + + Method setStatusBarColorMethod = + Window.class.getDeclaredMethod("setStatusBarColor", int.class); + Method setNavigationBarColorMethod = + Window.class.getDeclaredMethod("setNavigationBarColor", int.class); + setStatusBarColorMethod.invoke(getWindow(), Color.TRANSPARENT); + setNavigationBarColorMethod.invoke(getWindow(), Color.TRANSPARENT); + } catch (NoSuchFieldException e) { + Log.w(TAG, "NoSuchFieldException while setting up transparent bars"); + } catch (NoSuchMethodException ex) { + Log.w(TAG, "NoSuchMethodException while setting up transparent bars"); + } catch (IllegalAccessException e) { + Log.w(TAG, "IllegalAccessException while setting up transparent bars"); + } catch (IllegalArgumentException e) { + Log.w(TAG, "IllegalArgumentException while setting up transparent bars"); + } catch (InvocationTargetException e) { + Log.w(TAG, "InvocationTargetException while setting up transparent bars"); + } finally {} + } + } + @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); @@ -1667,11 +1746,6 @@ public class Launcher extends Activity } }); } - // When Launcher comes back to foreground, a different Activity might be responsible for - // the app market intent, so refresh the icon - if (!DISABLE_MARKET_BUTTON) { - updateAppMarketIcon(); - } clearTypedText(); } } @@ -1778,10 +1852,6 @@ public class Launcher extends Activity return mModel; } - public LauncherClings getLauncherClings() { - return mLauncherClings; - } - protected SharedPreferences getSharedPrefs() { return mSharedPrefs; } @@ -1790,7 +1860,7 @@ public class Launcher extends Activity getWindow().closeAllPanels(); // Whatever we were doing is hereby canceled. - mWaitingForResult = false; + setWaitingForResult(false); } @Override @@ -1930,8 +2000,13 @@ public class Launcher extends Activity // Stop callbacks from LauncherModel LauncherAppState app = (LauncherAppState.getInstance()); - mModel.stopLoader(); - app.setLauncher(null); + + // It's possible to receive onDestroy after a new Launcher activity has + // been created. In this case, don't interfere with the new Launcher. + if (mModel.isCurrentCallbacks(this)) { + mModel.stopLoader(); + app.setLauncher(null); + } try { mAppWidgetHost.stopListening(); @@ -1959,6 +2034,7 @@ public class Launcher extends Activity mWorkspace = null; mDragController = null; + PackageInstallerCompat.getInstance(this).onStop(); LauncherAnimUtils.onDestroyActivity(); } @@ -1968,7 +2044,9 @@ public class Launcher extends Activity @Override public void startActivityForResult(Intent intent, int requestCode) { - if (requestCode >= 0) mWaitingForResult = true; + if (requestCode >= 0) { + setWaitingForResult(true); + } super.startActivityForResult(intent, requestCode); } @@ -1995,14 +2073,25 @@ public class Launcher extends Activity sourceBounds = mSearchDropTargetBar.getSearchBarBounds(); } - startSearch(initialQuery, selectInitialQuery, + boolean clearTextImmediately = startSearch(initialQuery, selectInitialQuery, appSearchData, sourceBounds); + if (clearTextImmediately) { + clearTypedText(); + } } - public void startSearch(String initialQuery, + /** + * Start a text search. + * + * @return {@code true} if the search will start immediately, so any further keypresses + * will be handled directly by the search UI. {@code false} if {@link Launcher} should continue + * to buffer keypresses. + */ + public boolean startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, Rect sourceBounds) { startGlobalSearch(initialQuery, selectInitialQuery, appSearchData, sourceBounds); + return false; } /** @@ -2082,6 +2171,24 @@ public class Launcher extends Activity return mWorkspaceLoading; } + private void setWorkspaceLoading(boolean value) { + boolean isLocked = isWorkspaceLocked(); + mWorkspaceLoading = value; + if (isLocked != isWorkspaceLocked()) { + onWorkspaceLockedChanged(); + } + } + + private void setWaitingForResult(boolean value) { + boolean isLocked = isWorkspaceLocked(); + mWaitingForResult = value; + if (isLocked != isWorkspaceLocked()) { + onWorkspaceLockedChanged(); + } + } + + protected void onWorkspaceLockedChanged() { } + private void resetAddInfo() { mPendingAddInfo.container = ItemInfo.NO_ID; mPendingAddInfo.screenId = -1; @@ -2104,10 +2211,9 @@ public class Launcher extends Activity mPendingAddWidgetId = appWidgetId; // 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); - Utilities.startActivityForResultSafely(this, intent, REQUEST_CREATE_APPWIDGET); + mAppWidgetManager.startConfigActivity(appWidgetInfo, appWidgetId, this, + mAppWidgetHost, REQUEST_CREATE_APPWIDGET); + } else { // Otherwise just add it Runnable onComplete = new Runnable() { @@ -2120,7 +2226,7 @@ public class Launcher extends Activity }; completeAddAppWidget(appWidgetId, info.container, info.screenId, boundWidget, appWidgetInfo); - mWorkspace.removeExtraEmptyScreen(true, onComplete, delay, false); + mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete, delay, false); } } @@ -2191,14 +2297,8 @@ public class Launcher extends Activity 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); - } + boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( + appWidgetId, info.info, options); if (success) { addAppWidgetImpl(appWidgetId, info, null, info.info); } else { @@ -2206,6 +2306,8 @@ public class Launcher extends Activity Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.componentName); + mAppWidgetManager.getUser(mPendingAddWidgetInfo) + .addToIntent(intent, AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE); // 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); @@ -2214,21 +2316,7 @@ public class Launcher extends Activity } 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)); - Utilities.startActivityForResultSafely(this, pickIntent, REQUEST_PICK_APPLICATION); - } else { - Utilities.startActivityForResultSafely(this, intent, REQUEST_CREATE_SHORTCUT); - } + Utilities.startActivityForResultSafely(this, intent, REQUEST_CREATE_SHORTCUT); } void processWallpaper(Intent intent) { @@ -2260,12 +2348,6 @@ public class Launcher extends Activity sFolders.remove(folder.id); } - protected void startWallpaper() { - final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER); - pickWallpaper.setComponent(getWallpaperPickerComponent()); - startActivityForResult(pickWallpaper, REQUEST_PICK_WALLPAPER); - } - protected ComponentName getWallpaperPickerComponent() { return new ComponentName(getPackageName(), LauncherWallpaperPickerActivity.class.getName()); } @@ -2369,59 +2451,63 @@ public class Launcher extends Activity Object tag = v.getTag(); if (tag instanceof ShortcutInfo) { - // Open shortcut - final ShortcutInfo shortcut = (ShortcutInfo) tag; - final Intent intent = shortcut.intent; - - // Check for special shortcuts - if (intent.getComponent() != null) { - final String shortcutClass = intent.getComponent().getClassName(); - - if (shortcutClass.equals(WidgetAdder.class.getName())) { - onClickAddWidgetButton(); - return; - } else if (shortcutClass.equals(MemoryDumpActivity.class.getName())) { - MemoryDumpActivity.startDump(this); - return; - } else if (shortcutClass.equals(ToggleWeightWatcher.class.getName())) { - toggleShowWeightWatcher(); - return; - } - } - - // Start activities - 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); - - mStats.recordLaunch(intent, shortcut); - - if (success && v instanceof BubbleTextView) { - mWaitingForResume = (BubbleTextView) v; - mWaitingForResume.setStayPressed(true); - } + onClickAppShortcut(v); } else if (tag instanceof FolderInfo) { if (v instanceof FolderIcon) { - FolderIcon fi = (FolderIcon) v; - handleFolderClick(fi); + onClickFolderIcon(v); } } else if (v == mAllAppsButton) { - if (isAllAppsVisible()) { - showWorkspace(true); - } else { - onClickAllAppsButton(v); + onClickAllAppsButton(v); + } else if (tag instanceof AppInfo) { + startAppShortcutOrInfoActivity(v); + } else if (tag instanceof LauncherAppWidgetInfo) { + if (v instanceof PendingAppWidgetHostView) { + onClickPendingWidget((PendingAppWidgetHostView) v); } } } + public void onClickPagedViewIcon(View v) { + startAppShortcutOrInfoActivity(v); + } + public boolean onTouch(View v, MotionEvent event) { return false; } /** + * Event handler for the app widget view which has not fully restored. + */ + public void onClickPendingWidget(final PendingAppWidgetHostView v) { + final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag(); + if (v.isReadyForClickSetup()) { + int widgetId = info.appWidgetId; + AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(widgetId); + if (appWidgetInfo != null) { + mPendingAddWidgetInfo = appWidgetInfo; + mPendingAddInfo.copyFrom(info); + mPendingAddWidgetId = widgetId; + + AppWidgetManagerCompat.getInstance(this).startConfigActivity(appWidgetInfo, + info.appWidgetId, this, mAppWidgetHost, REQUEST_RECONFIGURE_APPWIDGET); + } + } else if (info.installProgress < 0) { + // The install has not been queued + final String packageName = info.providerName.getPackageName(); + showBrokenAppInstallDialog(packageName, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + startActivitySafely(v, LauncherModel.getMarketIntent(packageName), info); + } + }); + } else { + // Download has started. + final String packageName = info.providerName.getPackageName(); + startActivitySafely(v, LauncherModel.getMarketIntent(packageName), info); + } + } + + /** * Event handler for the search button * * @param v The view that was clicked. @@ -2467,18 +2553,180 @@ public class Launcher extends Activity * * @param v The view that was clicked. */ - public void onClickAllAppsButton(View v) { - showAllApps(true, AppsCustomizePagedView.ContentType.Applications, false); + protected void onClickAllAppsButton(View v) { + if (LOGD) Log.d(TAG, "onClickAllAppsButton"); + if (isAllAppsVisible()) { + showWorkspace(true); + } else { + showAllApps(true, AppsCustomizePagedView.ContentType.Applications, false); + } + } + + private void showBrokenAppInstallDialog(final String packageName, + DialogInterface.OnClickListener onSearchClickListener) { + new AlertDialog.Builder(new ContextThemeWrapper(this, android.R.style.Theme_DeviceDefault)) + .setTitle(R.string.abandoned_promises_title) + .setMessage(R.string.abandoned_promise_explanation) + .setPositiveButton(R.string.abandoned_search, onSearchClickListener) + .setNeutralButton(R.string.abandoned_clean_this, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + final UserHandleCompat user = UserHandleCompat.myUserHandle(); + mWorkspace.removeAbandonedPromise(packageName, user); + } + }) + .create().show(); + return; + } + + /** + * Event handler for an app shortcut click. + * + * @param v The view that was clicked. Must be a tagged with a {@link ShortcutInfo}. + */ + protected void onClickAppShortcut(final View v) { + if (LOGD) Log.d(TAG, "onClickAppShortcut"); + Object tag = v.getTag(); + if (!(tag instanceof ShortcutInfo)) { + throw new IllegalArgumentException("Input must be a Shortcut"); + } + + // Open shortcut + final ShortcutInfo shortcut = (ShortcutInfo) tag; + final Intent intent = shortcut.intent; + + // Check for special shortcuts + if (intent.getComponent() != null) { + final String shortcutClass = intent.getComponent().getClassName(); + + if (shortcutClass.equals(MemoryDumpActivity.class.getName())) { + MemoryDumpActivity.startDump(this); + return; + } else if (shortcutClass.equals(ToggleWeightWatcher.class.getName())) { + toggleShowWeightWatcher(); + return; + } + } + + // Check for abandoned promise + if ((v instanceof BubbleTextView) + && shortcut.isPromise() + && !shortcut.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE)) { + showBrokenAppInstallDialog( + shortcut.getTargetComponent().getPackageName(), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + startAppShortcutOrInfoActivity(v); + } + }); + return; + } + + // Start activities + startAppShortcutOrInfoActivity(v); + } + + private void startAppShortcutOrInfoActivity(View v) { + Object tag = v.getTag(); + final ShortcutInfo shortcut; + final Intent intent; + if (tag instanceof ShortcutInfo) { + shortcut = (ShortcutInfo) tag; + intent = shortcut.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())); + + } else if (tag instanceof AppInfo) { + shortcut = null; + intent = ((AppInfo) tag).intent; + } else { + throw new IllegalArgumentException("Input must be a Shortcut or AppInfo"); + } + + boolean success = startActivitySafely(v, intent, tag); + mStats.recordLaunch(intent, shortcut); + + if (success && v instanceof BubbleTextView) { + mWaitingForResume = (BubbleTextView) v; + mWaitingForResume.setStayPressed(true); + } + } + + /** + * Event handler for a folder icon click. + * + * @param v The view that was clicked. Must be an instance of {@link FolderIcon}. + */ + protected void onClickFolderIcon(View v) { + if (LOGD) Log.d(TAG, "onClickFolder"); + if (!(v instanceof FolderIcon)){ + throw new IllegalArgumentException("Input must be a FolderIcon"); + } + + FolderIcon folderIcon = (FolderIcon) v; + 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.screenId + " (" + 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); + } + } + } } /** * Event handler for the (Add) Widgets button that appears after a long press * on the home screen. */ - protected void onClickAddWidgetButton() { + protected void onClickAddWidgetButton(View view) { + if (LOGD) Log.d(TAG, "onClickAddWidgetButton"); showAllApps(true, AppsCustomizePagedView.ContentType.Widgets, true); } + /** + * Event handler for the wallpaper picker button that appears after a long press + * on the home screen. + */ + protected void onClickWallpaperPicker(View v) { + if (LOGD) Log.d(TAG, "onClickWallpaperPicker"); + final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER); + pickWallpaper.setComponent(getWallpaperPickerComponent()); + startActivityForResult(pickWallpaper, REQUEST_PICK_WALLPAPER); + } + + /** + * Event handler for a click on the settings button that appears after a long press + * on the home screen. + */ + protected void onClickSettingsButton(View v) { + if (LOGD) Log.d(TAG, "onClickSettingsButton"); + } + public void onTouchDownAllAppsButton(View v) { // Provide the same haptic feedback that the system offers for virtual keys. v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); @@ -2504,15 +2752,7 @@ public class Launcher extends Activity return mHapticFeedbackTouchListener; } - public void onClickAppMarketButton(View v) { - if (!DISABLE_MARKET_BUTTON) { - if (mAppMarketIntent != null) { - startActivitySafely(v, mAppMarketIntent, "app market"); - } else { - Log.e(TAG, "Invalid app market intent."); - } - } - } + public void onDragStarted(View view) {} /** * Called when the user stops interacting with the launcher. @@ -2531,17 +2771,24 @@ public class Launcher extends Activity */ protected void onInteractionBegin() {} - void startApplicationDetailsActivity(ComponentName componentName) { + void startApplicationDetailsActivity(ComponentName componentName, UserHandleCompat user) { 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_CLEAR_TASK | - Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - startActivitySafely(null, intent, "startApplicationDetailsActivity"); + try { + LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(this); + UserManagerCompat userManager = UserManagerCompat.getInstance(this); + launcherApps.showAppDetailsForProfile(componentName, user); + } catch (SecurityException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Launcher does not have permission to launch settings"); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Unable to launch settings"); + } } // returns true if the activity was started - boolean startApplicationUninstallActivity(ComponentName componentName, int flags) { + boolean startApplicationUninstallActivity(ComponentName componentName, int flags, + UserHandleCompat user) { if ((flags & AppInfo.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. @@ -2555,6 +2802,9 @@ public class Launcher extends Activity Intent.ACTION_DELETE, Uri.fromParts("package", packageName, className)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + if (user != null) { + user.addToIntent(intent, Intent.EXTRA_USER); + } startActivity(intent); return true; } @@ -2562,19 +2812,35 @@ public class Launcher extends Activity 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); + LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(this); + UserManagerCompat userManager = UserManagerCompat.getInstance(this); + + UserHandleCompat user = null; + if (intent.hasExtra(AppInfo.EXTRA_PROFILE)) { + long serialNumber = intent.getLongExtra(AppInfo.EXTRA_PROFILE, -1); + user = userManager.getUserForSerialNumber(serialNumber); + } + + Bundle optsBundle = null; if (useLaunchAnimation) { - ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, - v.getMeasuredWidth(), v.getMeasuredHeight()); + ActivityOptions opts = Utilities.isLmpOrAbove() ? + ActivityOptions.makeCustomAnimation(this, R.anim.task_open_enter, R.anim.no_anim) : + ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getMeasuredWidth(), v.getMeasuredHeight()); + optsBundle = opts.toBundle(); + } - startActivity(intent, opts.toBundle()); + if (user == null || user.equals(UserHandleCompat.myUserHandle())) { + // Could be launching some bookkeeping activity + startActivity(intent, optsBundle); } else { - startActivity(intent); + // TODO Component can be null when shortcuts are supported for secondary user + launcherApps.startActivityForProfile(intent.getComponent(), user, + intent.getSourceBounds(), optsBundle); } return true; } catch (SecurityException e) { @@ -2589,6 +2855,10 @@ public class Launcher extends Activity boolean startActivitySafely(View v, Intent intent, Object tag) { boolean success = false; + if (mIsSafeModeEnabled && !Utilities.isSystemApp(this, intent)) { + Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show(); + return false; + } try { success = startActivity(v, intent, tag); } catch (ActivityNotFoundException e) { @@ -2598,40 +2868,6 @@ public class Launcher extends Activity return success; } - 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.screenId + " (" + 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. @@ -2703,7 +2939,10 @@ public class Launcher extends Activity ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(mFolderIconImageView, alpha, scaleX, scaleY); - oa.setDuration(getResources().getInteger(R.integer.config_folderAnimDuration)); + if (Utilities.isLmpOrAbove()) { + oa.setInterpolator(new LogDecelerateInterpolator(100, 0)); + } + oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration)); oa.start(); } @@ -2720,7 +2959,7 @@ public class Launcher extends Activity copyFolderIconToImage(fi); ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(mFolderIconImageView, alpha, scaleX, scaleY); - oa.setDuration(getResources().getInteger(R.integer.config_folderAnimDuration)); + oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration)); oa.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -2773,9 +3012,6 @@ public class Launcher extends Activity folder.dismissEditingName(); } closeFolder(folder); - - // Dismiss the folder cling - mLauncherClings.dismissFolderCling(null); } } @@ -2808,23 +3044,22 @@ public class Launcher extends Activity } else { return false; } + } else { + 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; + CellLayout.CellInfo longClickCellInfo = null; + View itemUnderLongClick = null; + if (v.getTag() instanceof ItemInfo) { + ItemInfo info = (ItemInfo) v.getTag(); + longClickCellInfo = new CellLayout.CellInfo(v, info);; + itemUnderLongClick = longClickCellInfo.cell; + resetAddInfo(); } // The hotseat touch handling does not go through Workspace, and we always allow long press // on hotseat items. - final View itemUnderLongClick = longClickCellInfo.cell; final boolean inHotseat = isHotseatLayout(v); boolean allowLongPress = inHotseat || mWorkspace.allowLongPress(); if (allowLongPress && !mDragController.isDragging()) { @@ -2832,7 +3067,6 @@ public class Launcher extends Activity // User long pressed on empty space mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); - // Disabling reordering until we sort out some issues. if (mWorkspace.isInOverviewMode()) { mWorkspace.startReordering(v); } else { @@ -2876,22 +3110,12 @@ public class Launcher extends Activity return (mState == State.APPS_CUSTOMIZE) || (mOnResumeState == State.APPS_CUSTOMIZE); } - /** - * 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); - } - private void setWorkspaceBackground(boolean workspace) { mLauncherView.setBackground(workspace ? mWorkspaceBackgroundDrawable : null); } - void updateWallpaperVisibility(boolean visible) { + protected void changeWallpaperVisiblity(boolean visible) { int wpflags = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0; int curflags = getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; @@ -2980,6 +3204,7 @@ public class Launcher extends Activity AppsCustomizePagedView.ContentType contentType = mAppsCustomizeContent.getContentType(); showAppsCustomizeHelper(animated, springLoaded, contentType); } + private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded, final AppsCustomizePagedView.ContentType contentType) { if (mStateAnimation != null) { @@ -2987,98 +3212,178 @@ public class Launcher extends Activity mStateAnimation.cancel(); mStateAnimation = null; } + + boolean material = Utilities.isLmpOrAbove(); + final Resources res = getResources(); final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime); final int fadeDuration = res.getInteger(R.integer.config_appsCustomizeFadeInTime); + final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime); + final int itemsAlphaStagger = + res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); + 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); + final ArrayList<View> layerViews = new ArrayList<View>(); - // Shrink workspaces away if going to AppsCustomize from workspace + Workspace.State workspaceState = contentType == AppsCustomizePagedView.ContentType.Widgets ? + Workspace.State.OVERVIEW_HIDDEN : Workspace.State.NORMAL_HIDDEN; Animator workspaceAnim = - mWorkspace.getChangeStateAnimation(Workspace.State.SMALL, animated); + mWorkspace.getChangeStateAnimation(workspaceState, animated, layerViews); if (!LauncherAppState.isDisableAllApps() || contentType == AppsCustomizePagedView.ContentType.Widgets) { // Set the content type for the all apps/widgets space mAppsCustomizeTabHost.setContentTypeImmediate(contentType); } - 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); - } - }); + // If for some reason our views aren't initialized, don't animate + boolean initialized = getAllAppsButton() != null; - // toView should appear right at the end of the workspace shrink - // animation + if (animated && initialized) { mStateAnimation = LauncherAnimUtils.createAnimatorSet(); - mStateAnimation.play(scaleAnim).after(startDelay); - mStateAnimation.play(alphaAnim).after(startDelay); + final AppsCustomizePagedView content = (AppsCustomizePagedView) + toView.findViewById(R.id.apps_customize_pane_content); + + final View page = content.getPageAt(content.getCurrentPage()); + final View revealView = toView.findViewById(R.id.fake_page); + + final float initialPanelAlpha = 1f; + + final boolean isWidgetTray = contentType == AppsCustomizePagedView.ContentType.Widgets; + if (isWidgetTray) { + revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); + } else { + revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); + } + + // Hide the real page background, and swap in the fake one + content.setPageBackgroundsVisible(false); + revealView.setVisibility(View.VISIBLE); + // We need to hide this view as the animation start will be posted. + revealView.setAlpha(0); + + int width = revealView.getMeasuredWidth(); + int height = revealView.getMeasuredHeight(); + float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4); + + revealView.setTranslationY(0); + revealView.setTranslationX(0); + + // Get the y delta between the center of the page and the center of the all apps button + int[] allAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, + getAllAppsButton(), null); + + float alpha = 0; + float xDrift = 0; + float yDrift = 0; + if (material) { + alpha = isWidgetTray ? 0.3f : 1f; + yDrift = isWidgetTray ? height / 2 : allAppsToPanelDelta[1]; + xDrift = isWidgetTray ? 0 : allAppsToPanelDelta[0]; + } else { + yDrift = 2 * height / 3; + xDrift = 0; + } + final float initAlpha = alpha; + + revealView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + layerViews.add(revealView); + PropertyValuesHolder panelAlpha = PropertyValuesHolder.ofFloat("alpha", initAlpha, 1f); + PropertyValuesHolder panelDriftY = + PropertyValuesHolder.ofFloat("translationY", yDrift, 0); + PropertyValuesHolder panelDriftX = + PropertyValuesHolder.ofFloat("translationX", xDrift, 0); + + ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView, + panelAlpha, panelDriftY, panelDriftX); + + panelAlphaAndDrift.setDuration(revealDuration); + panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + + mStateAnimation.play(panelAlphaAndDrift); + + if (page != null) { + page.setVisibility(View.VISIBLE); + page.setLayerType(View.LAYER_TYPE_HARDWARE, null); + layerViews.add(page); + + ObjectAnimator pageDrift = ObjectAnimator.ofFloat(page, "translationY", yDrift, 0); + page.setTranslationY(yDrift); + pageDrift.setDuration(revealDuration); + pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + pageDrift.setStartDelay(itemsAlphaStagger); + mStateAnimation.play(pageDrift); + + page.setAlpha(0f); + ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(page, "alpha", 0f, 1f); + itemsAlpha.setDuration(revealDuration); + itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); + itemsAlpha.setStartDelay(itemsAlphaStagger); + mStateAnimation.play(itemsAlpha); + } + + View pageIndicators = toView.findViewById(R.id.apps_customize_page_indicator); + pageIndicators.setAlpha(0.01f); + ObjectAnimator indicatorsAlpha = + ObjectAnimator.ofFloat(pageIndicators, "alpha", 1f); + indicatorsAlpha.setDuration(revealDuration); + mStateAnimation.play(indicatorsAlpha); + + if (material) { + final View allApps = getAllAppsButton(); + int allAppsButtonSize = LauncherAppState.getInstance(). + getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize; + float startRadius = isWidgetTray ? 0 : allAppsButtonSize / 2; + Animator reveal = ViewAnimationUtils.createCircularReveal(revealView, width / 2, + height / 2, startRadius, revealRadius); + reveal.setDuration(revealDuration); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + + reveal.addListener(new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + if (!isWidgetTray) { + allApps.setVisibility(View.INVISIBLE); + } + } + public void onAnimationEnd(Animator animation) { + if (!isWidgetTray) { + allApps.setVisibility(View.VISIBLE); + } + } + }); + mStateAnimation.play(reveal); + } mStateAnimation.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationStart(Animator animation) { - // 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); + revealView.setVisibility(View.INVISIBLE); + revealView.setLayerType(View.LAYER_TYPE_NONE, null); + if (page != null) { + page.setLayerType(View.LAYER_TYPE_NONE, null); + } + content.setPageBackgroundsVisible(true); + // Hide the search bar if (mSearchDropTargetBar != null) { mSearchDropTargetBar.hideSearchBar(false); } } + }); 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() { @@ -3086,23 +3391,28 @@ public class Launcher extends Activity // 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); + + revealView.setAlpha(initAlpha); + if (Utilities.isLmpOrAbove()) { + for (int i = 0; i < layerViews.size(); i++) { + View v = layerViews.get(i); + if (v != null) { + boolean attached = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + attached = v.isAttachedToWindow(); + } + if (attached) v.buildLayer(); + } + } + } + mStateAnimation.start(); } }; - if (delayAnim) { - final ViewTreeObserver observer = toView.getViewTreeObserver(); - observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - public void onGlobalLayout() { - startAnimRunnable.run(); - toView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - }); - } else { - startAnimRunnable.run(); - } + toView.bringToFront(); + toView.setVisibility(View.VISIBLE); + toView.post(startAnimRunnable); } else { toView.setTranslationX(0.0f); toView.setTranslationY(0.0f); @@ -3139,54 +3449,184 @@ public class Launcher extends Activity mStateAnimation.cancel(); mStateAnimation = null; } + + boolean material = Utilities.isLmpOrAbove(); Resources res = getResources(); final int duration = res.getInteger(R.integer.config_appsCustomizeZoomOutTime); - final int fadeOutDuration = - res.getInteger(R.integer.config_appsCustomizeFadeOutTime); + final int fadeOutDuration = res.getInteger(R.integer.config_appsCustomizeFadeOutTime); + final int revealDuration = res.getInteger(R.integer.config_appsCustomizeConcealTime); + final int itemsAlphaStagger = + res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); + final float scaleFactor = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); final View fromView = mAppsCustomizeTabHost; final View toView = mWorkspace; Animator workspaceAnim = null; + final ArrayList<View> layerViews = new ArrayList<View>(); + if (toState == Workspace.State.NORMAL) { - int stagger = res.getInteger(R.integer.config_appsCustomizeWorkspaceAnimationStagger); workspaceAnim = mWorkspace.getChangeStateAnimation( - toState, animated, stagger, -1); + toState, animated, layerViews); } else if (toState == Workspace.State.SPRING_LOADED || toState == Workspace.State.OVERVIEW) { workspaceAnim = mWorkspace.getChangeStateAnimation( - toState, animated); - } - - setPivotsForZoom(fromView, scaleFactor); - 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); - } - }); + toState, animated, layerViews); + } + + // If for some reason our views aren't initialized, don't animate + boolean initialized = getAllAppsButton() != null; + if (animated && initialized) { mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } - dispatchOnLauncherTransitionPrepare(fromView, animated, true); - dispatchOnLauncherTransitionPrepare(toView, animated, true); - mAppsCustomizeContent.stopScrolling(); + final AppsCustomizePagedView content = (AppsCustomizePagedView) + fromView.findViewById(R.id.apps_customize_pane_content); + + final View page = content.getPageAt(content.getNextPage()); + + // We need to hide side pages of the Apps / Widget tray to avoid some ugly edge cases + int count = content.getChildCount(); + for (int i = 0; i < count; i++) { + View child = content.getChildAt(i); + if (child != page) { + child.setVisibility(View.INVISIBLE); + } + } + final View revealView = fromView.findViewById(R.id.fake_page); + + // hideAppsCustomizeHelper is called in some cases when it is already hidden + // don't perform all these no-op animations. In particularly, this was causing + // the all-apps button to pop in and out. + if (fromView.getVisibility() == View.VISIBLE) { + AppsCustomizePagedView.ContentType contentType = content.getContentType(); + final boolean isWidgetTray = + contentType == AppsCustomizePagedView.ContentType.Widgets; + + if (isWidgetTray) { + revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); + } else { + revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); + } + + int width = revealView.getMeasuredWidth(); + int height = revealView.getMeasuredHeight(); + float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4); + + // Hide the real page background, and swap in the fake one + revealView.setVisibility(View.VISIBLE); + content.setPageBackgroundsVisible(false); + + final View allAppsButton = getAllAppsButton(); + revealView.setTranslationY(0); + int[] allAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, + allAppsButton, null); + + float xDrift = 0; + float yDrift = 0; + if (material) { + yDrift = isWidgetTray ? height / 2 : allAppsToPanelDelta[1]; + xDrift = isWidgetTray ? 0 : allAppsToPanelDelta[0]; + } else { + yDrift = 5 * height / 4; + xDrift = 0; + } + + revealView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + TimeInterpolator decelerateInterpolator = material ? + new LogDecelerateInterpolator(100, 0) : + new LogDecelerateInterpolator(30, 0); + + // The vertical motion of the apps panel should be delayed by one frame + // from the conceal animation in order to give the right feel. We correpsondingly + // shorten the duration so that the slide and conceal end at the same time. + ObjectAnimator panelDriftY = LauncherAnimUtils.ofFloat(revealView, "translationY", + 0, yDrift); + panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY); + panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelDriftY.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelDriftY); + + ObjectAnimator panelDriftX = LauncherAnimUtils.ofFloat(revealView, "translationX", + 0, xDrift); + panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY); + panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelDriftX.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelDriftX); + + if (isWidgetTray || !material) { + float finalAlpha = material ? 0.4f : 0f; + revealView.setAlpha(1f); + ObjectAnimator panelAlpha = LauncherAnimUtils.ofFloat(revealView, "alpha", + 1f, finalAlpha); + panelAlpha.setDuration(revealDuration); + panelAlpha.setInterpolator(material ? decelerateInterpolator : + new AccelerateInterpolator(1.5f)); + mStateAnimation.play(panelAlpha); + } + + if (page != null) { + page.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + ObjectAnimator pageDrift = LauncherAnimUtils.ofFloat(page, "translationY", + 0, yDrift); + page.setTranslationY(0); + pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY); + pageDrift.setInterpolator(decelerateInterpolator); + pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + mStateAnimation.play(pageDrift); + + page.setAlpha(1f); + ObjectAnimator itemsAlpha = LauncherAnimUtils.ofFloat(page, "alpha", 1f, 0f); + itemsAlpha.setDuration(100); + itemsAlpha.setInterpolator(decelerateInterpolator); + mStateAnimation.play(itemsAlpha); + } + + View pageIndicators = fromView.findViewById(R.id.apps_customize_page_indicator); + pageIndicators.setAlpha(1f); + ObjectAnimator indicatorsAlpha = + LauncherAnimUtils.ofFloat(pageIndicators, "alpha", 0f); + indicatorsAlpha.setDuration(revealDuration); + indicatorsAlpha.setInterpolator(new DecelerateInterpolator(1.5f)); + mStateAnimation.play(indicatorsAlpha); + + width = revealView.getMeasuredWidth(); + + if (material) { + if (!isWidgetTray) { + allAppsButton.setVisibility(View.INVISIBLE); + } + int allAppsButtonSize = LauncherAppState.getInstance(). + getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize; + float finalRadius = isWidgetTray ? 0 : allAppsButtonSize / 2; + Animator reveal = + LauncherAnimUtils.createCircularReveal(revealView, width / 2, + height / 2, revealRadius, finalRadius); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + reveal.setDuration(revealDuration); + reveal.setStartDelay(itemsAlphaStagger); + + reveal.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + revealView.setVisibility(View.INVISIBLE); + if (!isWidgetTray) { + allAppsButton.setVisibility(View.VISIBLE); + } + } + }); + + mStateAnimation.play(reveal); + } + + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + mAppsCustomizeContent.stopScrolling(); + } mStateAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -3197,17 +3637,57 @@ public class Launcher extends Activity if (onCompleteRunnable != null) { onCompleteRunnable.run(); } + + revealView.setLayerType(View.LAYER_TYPE_NONE, null); + if (page != null) { + page.setLayerType(View.LAYER_TYPE_NONE, null); + } + content.setPageBackgroundsVisible(true); + // Unhide side pages + int count = content.getChildCount(); + for (int i = 0; i < count; i++) { + View child = content.getChildAt(i); + child.setVisibility(View.VISIBLE); + } + + // Reset page transforms + if (page != null) { + page.setTranslationX(0); + page.setTranslationY(0); + page.setAlpha(1); + } + content.setCurrentPage(content.getNextPage()); + mAppsCustomizeContent.updateCurrentPageScroll(); } }); - mStateAnimation.playTogether(scaleAnim, alphaAnim); - if (workspaceAnim != null) { - mStateAnimation.play(workspaceAnim); - } - dispatchOnLauncherTransitionStart(fromView, animated, true); - dispatchOnLauncherTransitionStart(toView, animated, true); - LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView); + 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; + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + + if (Utilities.isLmpOrAbove()) { + for (int i = 0; i < layerViews.size(); i++) { + View v = layerViews.get(i); + if (v != null) { + boolean attached = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + attached = v.isAttachedToWindow(); + } + if (attached) v.buildLayer(); + } + } + } + mStateAnimation.start(); + } + }; + fromView.post(startAnimRunnable); } else { fromView.setVisibility(View.GONE); dispatchOnLauncherTransitionPrepare(fromView, animated, true); @@ -3236,10 +3716,7 @@ public class Launcher extends Activity } void showWorkspace(boolean animated, Runnable onCompleteRunnable) { - if (mWorkspace.isInOverviewMode()) { - mWorkspace.exitOverviewMode(animated); - } - if (mState != State.WORKSPACE) { + if (mState != State.WORKSPACE || mWorkspace.getState() != Workspace.State.NORMAL) { boolean wasInSpringLoadedMode = (mState != State.WORKSPACE); mWorkspace.setVisibility(View.VISIBLE); hideAppsCustomizeHelper(Workspace.State.NORMAL, animated, false, onCompleteRunnable); @@ -3288,7 +3765,13 @@ public class Launcher extends Activity mAppsCustomizeTabHost.reset(); } showAppsCustomizeHelper(animated, false, contentType); - mAppsCustomizeTabHost.requestFocus(); + mAppsCustomizeTabHost.post(new Runnable() { + @Override + public void run() { + // We post this in-case the all apps view isn't yet constructed. + mAppsCustomizeTabHost.requestFocus(); + } + }); // Change the state *after* we've called all the transition code mState = State.APPS_CUSTOMIZE; @@ -3328,7 +3811,6 @@ public class Launcher extends Activity } } }, delay); - } void exitSpringLoadedDragMode() { @@ -3350,25 +3832,6 @@ public class Launcher extends Activity } /** - * Shows the hotseat area. - */ - void showHotseat(boolean animated) { - if (!LauncherAppState.getInstance().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) { @@ -3621,44 +4084,6 @@ public class Launcher extends Activity public void disableVoiceButtonProxy(boolean disabled) { updateVoiceButtonProxyVisible(disabled); } - /** - * Sets the app market icon - */ - private void updateAppMarketIcon() { - if (!DISABLE_MARKET_BUTTON) { - 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) { - if (!DISABLE_MARKET_BUTTON) { - // 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) { @@ -3667,7 +4092,7 @@ public class Launcher extends Activity text.clear(); // Populate event with a fake title based on the current state. if (mState == State.APPS_CUSTOMIZE) { - text.add(mAppsCustomizeTabHost.getCurrentTabView().getContentDescription()); + text.add(mAppsCustomizeTabHost.getContentTag()); } else { text.add(getString(R.string.all_apps_home_button_label)); } @@ -3775,7 +4200,7 @@ public class Launcher extends Activity * Implementation of the method from LauncherModel.Callbacks. */ public void startBinding() { - mWorkspaceLoading = true; + setWorkspaceLoading(true); // 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 @@ -3875,7 +4300,7 @@ public class Launcher extends Activity } // Remove the extra empty screen - mWorkspace.removeExtraEmptyScreen(false, null); + mWorkspace.removeExtraEmptyScreen(false, false); if (!LauncherAppState.isDisableAllApps() && addedApps != null && mAppsCustomizeContent != null) { @@ -3926,7 +4351,15 @@ public class Launcher extends Activity if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { CellLayout cl = mWorkspace.getScreenWithId(item.screenId); if (cl != null && cl.isOccupied(item.cellX, item.cellY)) { - throw new RuntimeException("OCCUPIED"); + View v = cl.getChildAt(item.cellX, item.cellY); + Object tag = v.getTag(); + String desc = "Collision while binding workspace item: " + item + + ". Collides with " + tag; + if (LauncherAppState.isDogfoodBuild()) { + throw (new RuntimeException(desc)); + } else { + Log.d(TAG, desc); + } } } @@ -4021,13 +4454,74 @@ public class Launcher extends Activity } 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); + AppWidgetProviderInfo appWidgetInfo; + if (((item.restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0) && + ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) != 0)) { + + appWidgetInfo = mModel.findAppWidgetProviderInfoWithComponent(this, item.providerName); + if (appWidgetInfo == null) { + if (DEBUG_WIDGETS) { + Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId + + " belongs to component " + item.providerName + + ", as the povider is null"); + } + LauncherModel.deleteItemFromDatabase(this, item); + return; + } + // Note: This assumes that the id remap broadcast is received before this step. + // If that is not the case, the id remap will be ignored and user may see the + // click to setup view. + PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(appWidgetInfo, null, null); + pendingInfo.spanX = item.spanX; + pendingInfo.spanY = item.spanY; + pendingInfo.minSpanX = item.minSpanX; + pendingInfo.minSpanY = item.minSpanY; + Bundle options = + AppsCustomizePagedView.getDefaultOptionsForWidget(this, pendingInfo); + + int newWidgetId = mAppWidgetHost.allocateAppWidgetId(); + boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( + newWidgetId, appWidgetInfo, options); + + // TODO consider showing a permission dialog when the widget is clicked. + if (!success) { + mAppWidgetHost.deleteAppWidgetId(newWidgetId); + if (DEBUG_WIDGETS) { + Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId + + " belongs to component " + item.providerName + + ", as the launcher is unable to bing a new widget id"); + } + LauncherModel.deleteItemFromDatabase(this, item); + return; + } + + item.appWidgetId = newWidgetId; + + // If the widget has a configure activity, it is still needs to set it up, otherwise + // the widget is ready to go. + item.restoreStatus = (appWidgetInfo.configure == null) + ? LauncherAppWidgetInfo.RESTORE_COMPLETED + : LauncherAppWidgetInfo.FLAG_UI_NOT_READY; + + LauncherModel.updateItemInDatabase(this, item); } - item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) { + final int appWidgetId = item.appWidgetId; + 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); + } else { + appWidgetInfo = null; + PendingAppWidgetHostView view = new PendingAppWidgetHostView(this, item); + view.updateIcon(mIconCache); + item.hostView = view; + item.hostView.updateAppWidget(null); + item.hostView.setOnClickListener(this); + } item.hostView.setTag(item); item.onBindAppWidget(this); @@ -4044,6 +4538,26 @@ public class Launcher extends Activity } } + /** + * Restores a pending widget. + * + * @param appWidgetId The app widget id + * @param cellInfo The position on screen where to create the widget. + */ + private void completeRestoreAppWidget(final int appWidgetId) { + LauncherAppWidgetHostView view = mWorkspace.getWidgetForAppWidgetId(appWidgetId); + if ((view == null) || !(view instanceof PendingAppWidgetHostView)) { + Log.e(TAG, "Widget update called, when the widget no longer exists."); + return; + } + + LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag(); + info.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED; + + mWorkspace.reinflateWidgetsIfNecessary(); + LauncherModel.updateItemInDatabase(this, info); + } + public void onPageBoundSynchronously(int page) { mSynchronouslyBoundPages.add(page); } @@ -4071,24 +4585,44 @@ public class Launcher extends Activity mWorkspace.restoreInstanceStateForRemainingPages(); + setWorkspaceLoading(false); + sendLoadingCompleteBroadcastIfNecessary(); + // 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(); + if (sPendingAddItem != null) { + final long screenId = completeAdd(sPendingAddItem); - // Update the market app icon as necessary (the other icons will be managed in response to - // package changes in bindSearchablesChanged() - if (!DISABLE_MARKET_BUTTON) { - updateAppMarketIcon(); + // TODO: this moves the user to the page where the pending item was added. Ideally, + // the screen would be guaranteed to exist after bind, and the page would be set through + // the workspace restore process. + mWorkspace.post(new Runnable() { + @Override + public void run() { + mWorkspace.snapToScreenId(screenId); + } + }); + sPendingAddItem = null; } - mWorkspaceLoading = false; if (upgradePath) { mWorkspace.getUniqueComponents(true, null); mIntentsOnWorkspaceFromUpgradePath = mWorkspace.getUniqueComponents(true, null); } + PackageInstallerCompat.getInstance(this).onFinishBind(); + mModel.recheckRestoredItems(this); + } + + private void sendLoadingCompleteBroadcastIfNecessary() { + if (!mSharedPrefs.getBoolean(FIRST_LOAD_COMPLETE, false)) { + String permission = + getResources().getString(R.string.receive_first_load_broadcast_permission); + Intent intent = new Intent(ACTION_FIRST_LOAD_COMPLETE); + sendBroadcast(intent, permission); + SharedPreferences.Editor editor = mSharedPrefs.edit(); + editor.putBoolean(FIRST_LOAD_COMPLETE, true); + editor.apply(); + } } public boolean isAllAppsButtonRank(int rank) { @@ -4176,7 +4710,7 @@ public class Launcher extends Activity } if (mWorkspace != null) { - mWorkspace.updateShortcuts(apps); + mWorkspace.updateShortcutsAndWidgets(apps); } if (!LauncherAppState.isDisableAllApps() && @@ -4186,6 +4720,48 @@ public class Launcher extends Activity } /** + * Packages were restored + */ + public void bindAppsRestored(final ArrayList<AppInfo> apps) { + Runnable r = new Runnable() { + public void run() { + bindAppsRestored(apps); + } + }; + if (waitUntilResume(r)) { + return; + } + + if (mWorkspace != null) { + mWorkspace.updateShortcutsAndWidgets(apps); + } + } + + /** + * Update the state of a package, typically related to install state. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + @Override + public void updatePackageState(ArrayList<PackageInstallInfo> installInfo) { + if (mWorkspace != null) { + mWorkspace.updatePackageState(installInfo); + } + } + + /** + * Update the label and icon of all the icons in a package + * + * Implementation of the method from LauncherModel.Callbacks. + */ + @Override + public void updatePackageBadge(String packageName) { + if (mWorkspace != null) { + mWorkspace.updatePackageBadge(packageName, UserHandleCompat.myUserHandle()); + } + } + + /** * 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, @@ -4195,10 +4771,10 @@ public class Launcher extends Activity * Implementation of the method from LauncherModel.Callbacks. */ public void bindComponentsRemoved(final ArrayList<String> packageNames, - final ArrayList<AppInfo> appInfos) { + final ArrayList<AppInfo> appInfos, final UserHandleCompat user) { Runnable r = new Runnable() { public void run() { - bindComponentsRemoved(packageNames, appInfos); + bindComponentsRemoved(packageNames, appInfos, user); } }; if (waitUntilResume(r)) { @@ -4206,10 +4782,10 @@ public class Launcher extends Activity } if (!packageNames.isEmpty()) { - mWorkspace.removeItemsByPackageName(packageNames); + mWorkspace.removeItemsByPackageName(packageNames, user); } if (!appInfos.isEmpty()) { - mWorkspace.removeItemsByApplicationInfo(appInfos); + mWorkspace.removeItemsByApplicationInfo(appInfos, user); } // Notify the drag controller @@ -4307,7 +4883,7 @@ public class Launcher extends Activity * @param hint the hint to be displayed in the search bar. */ protected void onSearchBarHintChanged(String hint) { - mLauncherClings.updateSearchBarHint(hint); + } protected boolean isLauncherPreinstalled() { @@ -4325,6 +4901,17 @@ public class Launcher extends Activity } } + /** + * This method indicates whether or not we should suggest default wallpaper dimensions + * when our wallpaper cropper was not yet used to set a wallpaper. + */ + protected boolean overrideWallpaperDimensions() { + return true; + } + + protected boolean shouldClingFocusHotseatApp() { + return false; + } protected String getFirstRunClingSearchBarHint() { return ""; } @@ -4347,23 +4934,19 @@ public class Launcher extends Activity return ""; } - public void dismissFirstRunCling(View v) { - mLauncherClings.dismissFirstRunCling(v); - } - public void dismissMigrationClingCopyApps(View v) { - mLauncherClings.dismissMigrationClingCopyApps(v); - } - public void dismissMigrationClingUseDefault(View v) { - mLauncherClings.dismissMigrationClingUseDefault(v); - } - public void dismissMigrationWorkspaceCling(View v) { - mLauncherClings.dismissMigrationWorkspaceCling(v); - } - public void dismissWorkspaceCling(View v) { - mLauncherClings.dismissWorkspaceCling(v); + /** + * To be overridden by subclasses to indicate that there is an activity to launch + * before showing the standard launcher experience. + */ + protected boolean hasFirstRunActivity() { + return false; } - public void dismissFolderCling(View v) { - mLauncherClings.dismissFolderCling(v); + + /** + * To be overridden by subclasses to launch any first run activity + */ + protected Intent getFirstRunActivity() { + return null; } private boolean shouldRunFirstRunActivity() { @@ -4371,15 +4954,21 @@ public class Launcher extends Activity !mSharedPrefs.getBoolean(FIRST_RUN_ACTIVITY_DISPLAYED, false); } - public void showFirstRunActivity() { + protected boolean hasRunFirstRunActivity() { + return mSharedPrefs.getBoolean(FIRST_RUN_ACTIVITY_DISPLAYED, false); + } + + public boolean showFirstRunActivity() { if (shouldRunFirstRunActivity() && hasFirstRunActivity()) { Intent firstRunIntent = getFirstRunActivity(); if (firstRunIntent != null) { startActivity(firstRunIntent); markFirstRunActivityShown(); + return true; } } + return false; } private void markFirstRunActivityShown() { @@ -4388,6 +4977,77 @@ public class Launcher extends Activity editor.apply(); } + /** + * To be overridden by subclasses to indicate that there is an in-activity full-screen intro + * screen that must be displayed and dismissed. + */ + protected boolean hasDismissableIntroScreen() { + return false; + } + + /** + * Full screen intro screen to be shown and dismissed before the launcher can be used. + */ + protected View getIntroScreen() { + return null; + } + + /** + * To be overriden by subclasses to indicate whether the in-activity intro screen has been + * dismissed. This method is ignored if #hasDismissableIntroScreen returns false. + */ + private boolean shouldShowIntroScreen() { + return hasDismissableIntroScreen() && + !mSharedPrefs.getBoolean(INTRO_SCREEN_DISMISSED, false); + } + + protected void showIntroScreen() { + View introScreen = getIntroScreen(); + changeWallpaperVisiblity(false); + if (introScreen != null) { + mDragLayer.showOverlayView(introScreen); + } + } + + public void dismissIntroScreen() { + markIntroScreenDismissed(); + if (showFirstRunActivity()) { + // We delay hiding the intro view until the first run activity is showing. This + // avoids a blip. + mWorkspace.postDelayed(new Runnable() { + @Override + public void run() { + mDragLayer.dismissOverlayView(); + showFirstRunClings(); + } + }, ACTIVITY_START_DELAY); + } else { + mDragLayer.dismissOverlayView(); + showFirstRunClings(); + } + changeWallpaperVisiblity(true); + } + + private void markIntroScreenDismissed() { + SharedPreferences.Editor editor = mSharedPrefs.edit(); + editor.putBoolean(INTRO_SCREEN_DISMISSED, true); + editor.apply(); + } + + private void showFirstRunClings() { + // The two first run cling paths are mutually exclusive, if the launcher is preinstalled + // on the device, then we always show the first run cling experience (or if there is no + // launcher2). Otherwise, we prompt the user upon started for migration + LauncherClings launcherClings = new LauncherClings(this); + if (launcherClings.shouldShowFirstRunOrMigrationClings()) { + if (mModel.canMigrateFromOldLauncherDb(this)) { + launcherClings.showMigrationCling(); + } else { + launcherClings.showLongPressCling(true); + } + } + } + void showWorkspaceSearchAndHotseat() { if (mWorkspace != null) mWorkspace.setAlpha(1f); if (mHotseat != null) mHotseat.setAlpha(1f); @@ -4402,24 +5062,44 @@ public class Launcher extends Activity if (mSearchDropTargetBar != null) mSearchDropTargetBar.hideSearchBar(false); } - public ItemInfo createAppDragInfo(Intent appLaunchIntent) { - ResolveInfo ri = getPackageManager().resolveActivity(appLaunchIntent, 0); - if (ri == null) { + // Called from search suggestion, not supported in other profiles. + final UserHandleCompat myUser = UserHandleCompat.myUserHandle(); + LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(this); + LauncherActivityInfoCompat activityInfo = launcherApps.resolveActivity(appLaunchIntent, + myUser); + if (activityInfo == null) { return null; } - return new AppInfo(getPackageManager(), ri, mIconCache, null); + return new AppInfo(this, activityInfo, myUser, mIconCache, null); } public ItemInfo createShortcutDragInfo(Intent shortcutIntent, CharSequence caption, Bitmap icon) { - return new ShortcutInfo(shortcutIntent, caption, icon); + // Called from search suggestion, not supported in other profiles. + return createShortcutDragInfo(shortcutIntent, caption, icon, + UserHandleCompat.myUserHandle()); + } + + public ItemInfo createShortcutDragInfo(Intent shortcutIntent, CharSequence caption, + Bitmap icon, UserHandleCompat user) { + UserManagerCompat userManager = UserManagerCompat.getInstance(this); + CharSequence contentDescription = userManager.getBadgedLabelForUser(caption, user); + return new ShortcutInfo(shortcutIntent, caption, contentDescription, icon, user); + } + + protected void moveWorkspaceToDefaultScreen() { + mWorkspace.moveToDefaultScreen(false); } public void startDrag(View dragView, ItemInfo dragInfo, DragSource source) { dragView.setTag(dragInfo); - mWorkspace.onDragStartedWithItem(dragView); - mWorkspace.beginDragShared(dragView, source); + mWorkspace.onExternalDragStartedWithItem(dragView); + mWorkspace.beginExternalDragShared(dragView, source); + } + + @Override + public void onPageSwitch(View newPage, int newPageIndex) { } /** diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java index e6c220b2a..be295f8b3 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -22,6 +22,7 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.view.View; +import android.view.ViewAnimationUtils; import android.view.ViewTreeObserver; import java.util.HashSet; @@ -126,4 +127,14 @@ public class LauncherAnimUtils { new FirstFrameAnimatorHelper(anim, view); return anim; } + + public static Animator createCircularReveal(View view, int centerX, + int centerY, float startRadius, float endRadius) { + Animator anim = ViewAnimationUtils.createCircularReveal(view, centerX, + centerY, startRadius, endRadius); + if (anim instanceof ValueAnimator) { + new FirstFrameAnimatorHelper((ValueAnimator) anim, view); + } + return anim; + } } diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 29e18f9c0..246278fa2 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -17,19 +17,29 @@ package com.android.launcher3; import android.app.SearchManager; -import android.content.*; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; import android.os.Handler; import android.util.Log; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; + import java.lang.ref.WeakReference; +import java.util.ArrayList; public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { private static final String TAG = "LauncherAppState"; private static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs"; + private static final boolean DEBUG = false; + private final AppFilter mAppFilter; private final BuildInfo mBuildInfo; private LauncherModel mModel; @@ -90,16 +100,11 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class)); mBuildInfo = BuildInfo.loadByName(sContext.getString(R.string.build_info_class)); mModel = new LauncherModel(this, mIconCache, mAppFilter); + final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(sContext); + launcherApps.addOnAppsChangedCallback(mModel); // 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"); - sContext.registerReceiver(mModel, filter); - filter = new IntentFilter(); - filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_LOCALE_CHANGED); filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); sContext.registerReceiver(mModel, filter); @@ -115,7 +120,7 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true, mFavoritesObserver); } - + public void recreateWidgetPreviewDb() { if (mWidgetPreviewCacheDb != null) { mWidgetPreviewCacheDb.close(); @@ -128,6 +133,8 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { */ public void onTerminate() { sContext.unregisterReceiver(mModel); + final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(sContext); + launcherApps.removeOnAppsChangedCallback(mModel); ContentResolver resolver = sContext.getContentResolver(); resolver.unregisterContentObserver(mFavoritesObserver); @@ -154,7 +161,7 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return mModel; } - IconCache getIconCache() { + public IconCache getIconCache() { return mIconCache; } @@ -249,4 +256,15 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { public static boolean isDogfoodBuild() { return getInstance().mBuildInfo.isDogfoodBuild(); } + + public void setPackageState(ArrayList<PackageInstallInfo> installInfo) { + mModel.setPackageState(installInfo); + } + + /** + * Updates the icons and label of all icons for the provided package name. + */ + public void updatePackageBadge(String packageName) { + mModel.updatePackageBadge(packageName); + } } diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java index 7b08d4403..a309f268c 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHost.java +++ b/src/com/android/launcher3/LauncherAppWidgetHost.java @@ -20,6 +20,9 @@ import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; +import android.os.TransactionTooLargeException; + +import java.util.ArrayList; /** * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView} @@ -28,6 +31,8 @@ import android.content.Context; */ public class LauncherAppWidgetHost extends AppWidgetHost { + private final ArrayList<Runnable> mProviderChangeListeners = new ArrayList<Runnable>(); + Launcher mLauncher; public LauncherAppWidgetHost(Launcher launcher, int hostId) { @@ -42,14 +47,42 @@ public class LauncherAppWidgetHost extends AppWidgetHost { } @Override + public void startListening() { + try { + super.startListening(); + } catch (Exception e) { + if (e.getCause() instanceof TransactionTooLargeException) { + // We're willing to let this slide. The exception is being caused by the list of + // RemoteViews which is being passed back. The startListening relationship will + // have been established by this point, and we will end up populating the + // widgets upon bind anyway. See issue 14255011 for more context. + } else { + throw new RuntimeException(e); + } + } + } + + @Override public void stopListening() { super.stopListening(); clearViews(); } + public void addProviderChangeListener(Runnable callback) { + mProviderChangeListeners.add(callback); + } + + public void removeProviderChangeListener(Runnable callback) { + mProviderChangeListeners.remove(callback); + } + 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)); + + for (Runnable callback : mProviderChangeListeners) { + callback.run(); + } } } diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java index 51a649a07..e39727b17 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java @@ -21,6 +21,7 @@ import android.content.Context; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.RemoteViews; @@ -30,12 +31,16 @@ import com.android.launcher3.DragLayer.TouchCompleteListener; * {@inheritDoc} */ public class LauncherAppWidgetHostView extends AppWidgetHostView implements TouchCompleteListener { + + LayoutInflater mInflater; + private CheckLongPressHelper mLongPressHelper; - private LayoutInflater mInflater; private Context mContext; private int mPreviousOrientation; private DragLayer mDragLayer; + private float mSlop; + public LauncherAppWidgetHostView(Context context) { super(context); mContext = context; @@ -56,7 +61,8 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc super.updateAppWidget(remoteViews); } - public boolean orientationChangedSincedInflation() { + public boolean isReinflateRequired() { + // Re-inflate is required if the orientation has changed since last inflated. int orientation = mContext.getResources().getConfiguration().orientation; if (mPreviousOrientation != orientation) { return true; @@ -90,6 +96,11 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc case MotionEvent.ACTION_CANCEL: mLongPressHelper.cancelLongPress(); break; + case MotionEvent.ACTION_MOVE: + if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { + mLongPressHelper.cancelLongPress(); + } + break; } // Otherwise continue letting touch events fall through to children @@ -104,11 +115,22 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc case MotionEvent.ACTION_CANCEL: mLongPressHelper.cancelLongPress(); break; + case MotionEvent.ACTION_MOVE: + if (!Utilities.pointInView(this, ev.getX(), ev.getY(), mSlop)) { + mLongPressHelper.cancelLongPress(); + } + break; } return false; } @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + } + + @Override public void cancelLongPress() { super.cancelLongPress(); mLongPressHelper.cancelLongPress(); diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java index 28df90fb0..5c6535a24 100644 --- a/src/com/android/launcher3/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java @@ -19,11 +19,36 @@ package com.android.launcher3; import android.appwidget.AppWidgetHostView; import android.content.ComponentName; import android.content.ContentValues; +import android.content.Context; + +import com.android.launcher3.compat.UserHandleCompat; /** * Represents a widget (either instantiated or about to be) in the Launcher. */ -class LauncherAppWidgetInfo extends ItemInfo { +public class LauncherAppWidgetInfo extends ItemInfo { + + public static final int RESTORE_COMPLETED = 0; + + /** + * This is set during the package backup creation. + */ + public static final int FLAG_ID_NOT_VALID = 1; + + /** + * Indicates that the provider is not available yet. + */ + public static final int FLAG_PROVIDER_NOT_READY = 2; + + /** + * Indicates that the widget UI is not yet ready, and user needs to set it up again. + */ + public static final int FLAG_UI_NOT_READY = 4; + + /** + * Indicates that the widget restore has started. + */ + public static final int FLAG_RESTORE_STARTED = 8; /** * Indicates that the widget hasn't been instantiated yet. @@ -42,6 +67,16 @@ class LauncherAppWidgetInfo extends ItemInfo { int minWidth = -1; int minHeight = -1; + /** + * Indicates the restore status of the widget. + */ + int restoreStatus; + + /** + * Indicates the installation progress of the widget provider + */ + int installProgress = -1; + private boolean mHasNotifiedInitialWidgetSizeChanged; /** @@ -59,13 +94,17 @@ class LauncherAppWidgetInfo extends ItemInfo { // to indicate that they should be calculated based on the layout and minWidth/minHeight spanX = -1; spanY = -1; + // We only support app widgets on current user. + user = UserHandleCompat.myUserHandle(); + restoreStatus = RESTORE_COMPLETED; } @Override - void onAddToDatabase(ContentValues values) { - super.onAddToDatabase(values); + void onAddToDatabase(Context context, ContentValues values) { + super.onAddToDatabase(context, values); values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId); values.put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, providerName.flattenToString()); + values.put(LauncherSettings.Favorites.RESTORED, restoreStatus); } /** @@ -96,4 +135,12 @@ class LauncherAppWidgetInfo extends ItemInfo { super.unbind(); hostView = null; } + + public final boolean isWidgetIdValid() { + return (restoreStatus & FLAG_ID_NOT_VALID) == 0; + } + + public final boolean hasRestoreFlag(int flag) { + return (restoreStatus & flag) == flag; + } } diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java index de6aedddd..c20c6939d 100644 --- a/src/com/android/launcher3/LauncherBackupAgentHelper.java +++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java @@ -17,13 +17,16 @@ package com.android.launcher3; import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInput; import android.app.backup.BackupManager; -import android.app.backup.SharedPreferencesBackupHelper; import android.content.Context; -import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.ParcelFileDescriptor; import android.provider.Settings; import android.util.Log; +import java.io.IOException; + public class LauncherBackupAgentHelper extends BackupAgentHelper { private static final String TAG = "LauncherBackupAgentHelper"; @@ -54,14 +57,14 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { // modifies the file outside the normal codepaths, so it looks like another // process. This forces a reload of the file, in case this process persists. String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); + getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); super.onDestroy(); } @Override public void onCreate() { boolean restoreEnabled = 0 != Settings.Secure.getInt( - getContentResolver(), SETTING_RESTORE_ENABLED, 0); + getContentResolver(), SETTING_RESTORE_ENABLED, 1); if (VERBOSE) Log.v(TAG, "restore is " + (restoreEnabled ? "enabled" : "disabled")); addHelper(LauncherBackupHelper.LAUNCHER_PREFS_PREFIX, @@ -71,4 +74,21 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { addHelper(LauncherBackupHelper.LAUNCHER_PREFIX, new LauncherBackupHelper(this, restoreEnabled)); } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) + throws IOException { + super.onRestore(data, appVersionCode, newState); + + // If no favorite was migrated, clear the data and start fresh. + final Cursor c = getContentResolver().query( + LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, null, null, null, null); + boolean hasData = c.moveToNext(); + c.close(); + + if (!hasData) { + if (VERBOSE) Log.v(TAG, "Nothing was restored, clearing DB"); + LauncherAppState.getLauncherProvider().createEmptyDB(); + } + } } diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index 62e6f3102..201f3e9bb 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -28,6 +28,8 @@ import com.android.launcher3.backup.BackupProtos.Key; import com.android.launcher3.backup.BackupProtos.Resource; import com.android.launcher3.backup.BackupProtos.Screen; import com.android.launcher3.backup.BackupProtos.Widget; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; import android.app.backup.BackupDataInputStream; import android.app.backup.BackupDataOutput; @@ -108,6 +110,7 @@ public class LauncherBackupHelper implements BackupHelper { Favorites.SPANX, // 14 Favorites.SPANY, // 15 Favorites.TITLE, // 16 + Favorites.PROFILE_ID, // 17 }; private static final int ID_INDEX = 0; @@ -127,6 +130,7 @@ public class LauncherBackupHelper implements BackupHelper { private static final int SPANX_INDEX = 14; private static final int SPANY_INDEX = 15; private static final int TITLE_INDEX = 16; + private static final int PROFILE_ID_INDEX = 17; private static final String[] SCREEN_PROJECTION = { WorkspaceScreens._ID, // 0 @@ -144,11 +148,12 @@ public class LauncherBackupHelper implements BackupHelper { private HashMap<ComponentName, AppWidgetProviderInfo> mWidgetMap; - private ArrayList<Key> mKeys; + private final ArrayList<Key> mKeys; public LauncherBackupHelper(Context context, boolean restoreEnabled) { mContext = context; mRestoreEnabled = restoreEnabled; + mKeys = new ArrayList<Key>(); } private void dataChanged() { @@ -214,9 +219,6 @@ public class LauncherBackupHelper implements BackupHelper { @Override public void restoreEntity(BackupDataInputStream data) { if (VERBOSE) Log.v(TAG, "restoreEntity"); - if (mKeys == null) { - mKeys = new ArrayList<Key>(); - } byte[] buffer = new byte[512]; String backupKey = data.getKey(); int dataSize = data.size(); @@ -297,8 +299,9 @@ public class LauncherBackupHelper implements BackupHelper { // persist things that have changed since the last backup ContentResolver cr = mContext.getContentResolver(); + // Don't backup apps in other profiles for now. Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, - null, null, null); + getUserSelectionArg(), null, null); Set<String> currentIds = new HashSet<String>(cursor.getCount()); try { cursor.moveToPosition(-1); @@ -349,7 +352,7 @@ public class LauncherBackupHelper implements BackupHelper { try { ContentResolver cr = mContext.getContentResolver(); ContentValues values = unpackFavorite(buffer, 0, dataSize); - cr.insert(Favorites.CONTENT_URI, values); + cr.insert(Favorites.CONTENT_URI_NO_NOTIFICATION, values); } catch (InvalidProtocolBufferNanoException e) { Log.e(TAG, "failed to decode favorite", e); } @@ -454,14 +457,19 @@ public class LauncherBackupHelper implements BackupHelper { } final ContentResolver cr = mContext.getContentResolver(); final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; + final UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); // read the old ID set Set<String> savedIds = getSavedIdsByType(Key.ICON, in); if (DEBUG) Log.d(TAG, "icon savedIds.size()=" + savedIds.size()); + // Don't backup apps in other profiles for now. int startRows = out.rows; if (DEBUG) Log.d(TAG, "starting here: " + startRows); - String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION; + + String where = "(" + Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPLICATION + " OR " + + Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_SHORTCUT + ") AND " + + getUserSelectionArg(); Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, where, null, null); Set<String> currentIds = new HashSet<String>(cursor.getCount()); @@ -491,9 +499,9 @@ public class LauncherBackupHelper implements BackupHelper { if (DEBUG) Log.d(TAG, "I can count this high: " + out.rows); if ((out.rows - startRows) < MAX_ICONS_PER_PASS) { if (VERBOSE) Log.v(TAG, "saving icon " + backupKey); - Bitmap icon = mIconCache.getIcon(intent); + Bitmap icon = mIconCache.getIcon(intent, myUserHandle); keys.add(key); - if (icon != null && !mIconCache.isDefaultIcon(icon)) { + if (icon != null && !mIconCache.isDefaultIcon(icon, myUserHandle)) { byte[] blob = packIcon(dpi, icon); writeRowToBackup(key, blob, out, data); } @@ -556,6 +564,7 @@ public class LauncherBackupHelper implements BackupHelper { } return; } else { + if (VERBOSE) Log.v(TAG, "saving restored icon as: " + key.name); IconCache.preloadIcon(mContext, ComponentName.unflattenFromString(key.name), icon, res.dpi); } @@ -595,7 +604,8 @@ public class LauncherBackupHelper implements BackupHelper { int startRows = out.rows; if (DEBUG) Log.d(TAG, "starting here: " + startRows); - String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPWIDGET; + String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPWIDGET + " AND " + + getUserSelectionArg(); Cursor cursor = cr.query(Favorites.CONTENT_URI, FAVORITE_PROJECTION, where, null, null); Set<String> currentIds = new HashSet<String>(cursor.getCount()); @@ -670,6 +680,9 @@ public class LauncherBackupHelper implements BackupHelper { .decodeByteArray(widget.icon.data, 0, widget.icon.data.length); if (icon == null) { Log.w(TAG, "failed to unpack widget icon for " + key.name); + } else { + IconCache.preloadIcon(mContext, ComponentName.unflattenFromString(widget.provider), + icon, widget.icon.dpi); } } @@ -798,9 +811,15 @@ public class LauncherBackupHelper implements BackupHelper { if (!TextUtils.isEmpty(title)) { favorite.title = title; } - String intent = c.getString(INTENT_INDEX); - if (!TextUtils.isEmpty(intent)) { - favorite.intent = intent; + String intentDescription = c.getString(INTENT_INDEX); + if (!TextUtils.isEmpty(intentDescription)) { + try { + Intent intent = Intent.parseUri(intentDescription, 0); + intent.removeExtra(ItemInfo.EXTRA_PROFILE); + favorite.intent = intent.toUri(0); + } catch (URISyntaxException e) { + Log.e(TAG, "Invalid intent", e); + } } favorite.itemType = c.getInt(ITEM_TYPE_INDEX); if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) { @@ -846,16 +865,26 @@ public class LauncherBackupHelper implements BackupHelper { values.put(Favorites.INTENT, favorite.intent); } values.put(Favorites.ITEM_TYPE, favorite.itemType); + + UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); + long userSerialNumber = + UserManagerCompat.getInstance(mContext).getSerialNumberForUser(myUserHandle); + values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber); + if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) { if (!TextUtils.isEmpty(favorite.appWidgetProvider)) { values.put(Favorites.APPWIDGET_PROVIDER, favorite.appWidgetProvider); } values.put(Favorites.APPWIDGET_ID, favorite.appWidgetId); + values.put(LauncherSettings.Favorites.RESTORED, + LauncherAppWidgetInfo.FLAG_ID_NOT_VALID | + LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY | + LauncherAppWidgetInfo.FLAG_UI_NOT_READY); + } else { + // Let LauncherModel know we've been here. + values.put(LauncherSettings.Favorites.RESTORED, 1); } - // Let LauncherModel know we've been here. - values.put(LauncherSettings.Favorites.RESTORED, 1); - return values; } @@ -1151,6 +1180,11 @@ public class LauncherBackupHelper implements BackupHelper { return true; } + private String getUserSelectionArg() { + return Favorites.PROFILE_ID + '=' + UserManagerCompat.getInstance(mContext) + .getSerialNumberForUser(UserHandleCompat.myUserHandle()); + } + private class KeyParsingException extends Throwable { private KeyParsingException(Throwable cause) { super(cause); diff --git a/src/com/android/launcher3/LauncherClings.java b/src/com/android/launcher3/LauncherClings.java index 952edfd06..458d81f61 100644 --- a/src/com/android/launcher3/LauncherClings.java +++ b/src/com/android/launcher3/LauncherClings.java @@ -18,324 +18,186 @@ package com.android.launcher3; import android.accounts.Account; import android.accounts.AccountManager; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; import android.app.ActivityManager; import android.content.Context; import android.content.SharedPreferences; -import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserManager; +import android.provider.Settings; +import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.accessibility.AccessibilityManager; -import android.widget.TextView; -class LauncherClings { - private static final String FIRST_RUN_CLING_DISMISSED_KEY = "cling_gel.first_run.dismissed"; +class LauncherClings implements OnClickListener { private static final String MIGRATION_CLING_DISMISSED_KEY = "cling_gel.migration.dismissed"; - private static final String MIGRATION_WORKSPACE_CLING_DISMISSED_KEY = - "cling_gel.migration_workspace.dismissed"; private static final String WORKSPACE_CLING_DISMISSED_KEY = "cling_gel.workspace.dismissed"; - private static final String FOLDER_CLING_DISMISSED_KEY = "cling_gel.folder.dismissed"; + + private static final String TAG_CROP_TOP_AND_SIDES = "crop_bg_top_and_sides"; private static final boolean DISABLE_CLINGS = false; private static final int SHOW_CLING_DURATION = 250; private static final int DISMISS_CLING_DURATION = 200; + // New Secure Setting in L + private static final String SKIP_FIRST_USE_HINTS = "skip_first_use_hints"; + private Launcher mLauncher; private LayoutInflater mInflater; - private HideFromAccessibilityHelper mHideFromAccessibilityHelper - = new HideFromAccessibilityHelper(); /** Ctor */ public LauncherClings(Launcher launcher) { mLauncher = launcher; - mInflater = mLauncher.getLayoutInflater(); + mInflater = LayoutInflater.from(new + ContextThemeWrapper(mLauncher, android.R.style.Theme_DeviceDefault)); } - /** Initializes a cling */ - private Cling initCling(int clingId, int scrimId, boolean animate, - boolean dimNavBarVisibilty) { - Cling cling = (Cling) mLauncher.findViewById(clingId); - View scrim = null; - if (scrimId > 0) { - scrim = mLauncher.findViewById(scrimId); - } - if (cling != null) { - cling.init(mLauncher, scrim); - cling.show(animate, SHOW_CLING_DURATION); - - if (dimNavBarVisibilty) { - cling.setSystemUiVisibility(cling.getSystemUiVisibility() | - View.SYSTEM_UI_FLAG_LOW_PROFILE); - } + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.cling_dismiss_migration_use_default) { + // Disable the migration cling + dismissMigrationCling(); + } else if (id == R.id.cling_dismiss_migration_copy_apps) { + // Copy the shortcuts from the old database + LauncherModel model = mLauncher.getModel(); + model.resetLoadedState(false, true); + model.startLoader(false, PagedView.INVALID_RESTORE_PAGE, + LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE + | LauncherModel.LOADER_FLAG_MIGRATE_SHORTCUTS); + // Set the flag to skip the folder cling + String spKey = LauncherAppState.getSharedPreferencesKey(); + SharedPreferences sp = mLauncher.getSharedPreferences(spKey, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putBoolean(Launcher.USER_HAS_MIGRATED, true); + editor.apply(); + // Disable the migration cling + dismissMigrationCling(); + } else if (id == R.id.cling_dismiss_longpress_info) { + dismissLongPressCling(); } - return cling; } - /** Returns whether the clings are enabled or should be shown */ - private boolean areClingsEnabled() { - if (DISABLE_CLINGS) { - return false; - } - - // disable clings when running in a test harness - if(ActivityManager.isRunningInTestHarness()) return false; + /** + * Shows the migration cling. + * + * This flow is mutually exclusive with showFirstRunCling, and only runs if this Launcher + * package was not preinstalled and there exists a db to migrate from. + */ + public void showMigrationCling() { + mLauncher.hideWorkspaceSearchAndHotseat(); - // Disable clings for accessibility when explore by touch is enabled - final AccessibilityManager a11yManager = (AccessibilityManager) mLauncher.getSystemService( - Launcher.ACCESSIBILITY_SERVICE); - if (a11yManager.isTouchExplorationEnabled()) { - return false; - } + ViewGroup root = (ViewGroup) mLauncher.findViewById(R.id.launcher); + View inflated = mInflater.inflate(R.layout.migration_cling, root); + inflated.findViewById(R.id.cling_dismiss_migration_copy_apps).setOnClickListener(this); + inflated.findViewById(R.id.cling_dismiss_migration_use_default).setOnClickListener(this); + } - // 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(mLauncher).getAccounts(); - if (supportsLimitedUsers && accounts.length == 0) { - UserManager um = (UserManager) mLauncher.getSystemService(Context.USER_SERVICE); - Bundle restrictions = um.getUserRestrictions(); - if (restrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false)) { - return false; + private void dismissMigrationCling() { + mLauncher.showWorkspaceSearchAndHotseat(); + Runnable dismissCb = new Runnable() { + public void run() { + Runnable cb = new Runnable() { + public void run() { + // Show the longpress cling next + showLongPressCling(false); + } + }; + dismissCling(mLauncher.findViewById(R.id.migration_cling), cb, + MIGRATION_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION); } - } - return true; + }; + mLauncher.getWorkspace().post(dismissCb); } - /** Returns whether the folder cling is visible. */ - public boolean isFolderClingVisible() { - Cling cling = (Cling) mLauncher.findViewById(R.id.folder_cling); - if (cling != null) { - return cling.getVisibility() == View.VISIBLE; - } - return false; - } + public void showLongPressCling(boolean showWelcome) { + ViewGroup root = (ViewGroup) mLauncher.findViewById(R.id.launcher); + View cling = mInflater.inflate(R.layout.longpress_cling, root, false); - private boolean skipCustomClingIfNoAccounts() { - Cling cling = (Cling) mLauncher.findViewById(R.id.workspace_cling); - boolean customCling = cling.getDrawIdentifier().equals("workspace_custom"); - if (customCling) { - AccountManager am = AccountManager.get(mLauncher); - if (am == null) return false; - Account[] accounts = am.getAccountsByType("com.google"); - return accounts.length == 0; - } - return false; - } + cling.setOnLongClickListener(new OnLongClickListener() { - /** Updates the first run cling custom content hint */ - private void setCustomContentHintVisibility(Cling cling, String ccHintStr, boolean visible, - boolean animate) { - final TextView ccHint = (TextView) cling.findViewById(R.id.custom_content_hint); - if (ccHint != null) { - if (visible && !ccHintStr.isEmpty()) { - ccHint.setText(ccHintStr); - ccHint.setVisibility(View.VISIBLE); - if (animate) { - ccHint.setAlpha(0f); - ccHint.animate().alpha(1f) - .setDuration(SHOW_CLING_DURATION) - .start(); - } else { - ccHint.setAlpha(1f); - } - } else { - if (animate) { - ccHint.animate().alpha(0f) - .setDuration(SHOW_CLING_DURATION) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - ccHint.setVisibility(View.GONE); - } - }) - .start(); - } else { - ccHint.setAlpha(0f); - ccHint.setVisibility(View.GONE); - } + @Override + public boolean onLongClick(View v) { + mLauncher.getWorkspace().enterOverviewMode(); + dismissLongPressCling(); + return true; } - } - } + }); - /** Updates the first run cling custom content hint */ - public void updateCustomContentHintVisibility() { - Cling cling = (Cling) mLauncher.findViewById(R.id.first_run_cling); - String ccHintStr = mLauncher.getFirstRunCustomContentHint(); + final ViewGroup content = (ViewGroup) cling.findViewById(R.id.cling_content); + mInflater.inflate(showWelcome ? R.layout.longpress_cling_welcome_content + : R.layout.longpress_cling_content, content); + content.findViewById(R.id.cling_dismiss_longpress_info).setOnClickListener(this); - if (mLauncher.getWorkspace().hasCustomContent()) { - // Show the custom content hint if ccHintStr is not empty - if (cling != null) { - setCustomContentHintVisibility(cling, ccHintStr, true, true); - } - } else { - // Hide the custom content hint - if (cling != null) { - setCustomContentHintVisibility(cling, ccHintStr, false, true); - } + if (TAG_CROP_TOP_AND_SIDES.equals(content.getTag())) { + Drawable bg = new BorderCropDrawable(mLauncher.getResources().getDrawable(R.drawable.cling_bg), + true, true, true, false); + content.setBackground(bg); } - } - /** Updates the first run cling search bar hint. */ - public void updateSearchBarHint(String hint) { - Cling cling = (Cling) mLauncher.findViewById(R.id.first_run_cling); - if (cling != null && cling.getVisibility() == View.VISIBLE && !hint.isEmpty()) { - TextView sbHint = (TextView) cling.findViewById(R.id.search_bar_hint); - sbHint.setText(hint); - sbHint.setVisibility(View.VISIBLE); - } - } + root.addView(cling); - public boolean shouldShowFirstRunOrMigrationClings() { - SharedPreferences sharedPrefs = mLauncher.getSharedPrefs(); - return areClingsEnabled() && - !sharedPrefs.getBoolean(FIRST_RUN_CLING_DISMISSED_KEY, false) && - !sharedPrefs.getBoolean(MIGRATION_CLING_DISMISSED_KEY, false); - } - - public void removeFirstRunAndMigrationClings() { - removeCling(R.id.first_run_cling); - removeCling(R.id.migration_cling); - } - - /** - * Shows the first run cling. - * - * This flow is mutually exclusive with showMigrationCling, and only runs if this Launcher - * package was preinstalled or there is no db to migrate from. - */ - public void showFirstRunCling() { - if (!skipCustomClingIfNoAccounts()) { - Cling cling = (Cling) mLauncher.findViewById(R.id.first_run_cling); - if (cling != null) { - String sbHintStr = mLauncher.getFirstRunClingSearchBarHint(); - String ccHintStr = mLauncher.getFirstRunCustomContentHint(); - if (!sbHintStr.isEmpty()) { - TextView sbHint = (TextView) cling.findViewById(R.id.search_bar_hint); - sbHint.setText(sbHintStr); - sbHint.setVisibility(View.VISIBLE); - } - setCustomContentHintVisibility(cling, ccHintStr, true, false); - } - initCling(R.id.first_run_cling, 0, false, true); - } else { - removeFirstRunAndMigrationClings(); + if (showWelcome) { + // This is the first cling being shown. No need to animate. + return; } - } - /** - * Shows the migration cling. - * - * This flow is mutually exclusive with showFirstRunCling, and only runs if this Launcher - * package was not preinstalled and there exists a db to migrate from. - */ - public void showMigrationCling() { - mLauncher.hideWorkspaceSearchAndHotseat(); - - Cling c = initCling(R.id.migration_cling, 0, false, true); - c.bringScrimToFront(); - c.bringToFront(); - } + // Animate + content.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - public void showMigrationWorkspaceCling() { - // Enable the clings only if they have not been dismissed before - if (areClingsEnabled() && !mLauncher.getSharedPrefs().getBoolean( - MIGRATION_WORKSPACE_CLING_DISMISSED_KEY, false)) { - Cling c = initCling(R.id.migration_workspace_cling, 0, false, true); - c.updateMigrationWorkspaceBubblePosition(); - c.bringScrimToFront(); - c.bringToFront(); - } else { - removeCling(R.id.migration_workspace_cling); - } - } + @Override + public void onGlobalLayout() { + content.getViewTreeObserver().removeOnGlobalLayoutListener(this); - public void showWorkspaceCling() { - // Enable the clings only if they have not been dismissed before - if (areClingsEnabled() && !mLauncher.getSharedPrefs().getBoolean( - WORKSPACE_CLING_DISMISSED_KEY, false)) { - Cling c = initCling(R.id.workspace_cling, 0, false, true); - c.updateWorkspaceBubblePosition(); - - // Set the focused hotseat app if there is one - c.setFocusedHotseatApp(mLauncher.getFirstRunFocusedHotseatAppDrawableId(), - mLauncher.getFirstRunFocusedHotseatAppRank(), - mLauncher.getFirstRunFocusedHotseatAppComponentName(), - mLauncher.getFirstRunFocusedHotseatAppBubbleTitle(), - mLauncher.getFirstRunFocusedHotseatAppBubbleDescription()); - } else { - removeCling(R.id.workspace_cling); - } - } + ObjectAnimator anim; + if (TAG_CROP_TOP_AND_SIDES.equals(content.getTag())) { + content.setTranslationY(-content.getMeasuredHeight()); + anim = LauncherAnimUtils.ofFloat(content, "translationY", 0); + } else { + content.setScaleX(0); + content.setScaleY(0); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1); + anim = LauncherAnimUtils.ofPropertyValuesHolder(content, scaleX, scaleY); + } - public Cling showFoldersCling() { - SharedPreferences sharedPrefs = mLauncher.getSharedPrefs(); - // Enable the clings only if they have not been dismissed before - if (areClingsEnabled() && - !sharedPrefs.getBoolean(FOLDER_CLING_DISMISSED_KEY, false) && - !sharedPrefs.getBoolean(Launcher.USER_HAS_MIGRATED, false)) { - Cling cling = initCling(R.id.folder_cling, R.id.cling_scrim, - true, true); - Folder openFolder = mLauncher.getWorkspace().getOpenFolder(); - if (openFolder != null) { - Rect openFolderRect = new Rect(); - openFolder.getHitRect(openFolderRect); - cling.setOpenFolderRect(openFolderRect); - openFolder.bringToFront(); + anim.setDuration(SHOW_CLING_DURATION); + anim.setInterpolator(new LogDecelerateInterpolator(100, 0)); + anim.start(); } - return cling; - } else { - removeCling(R.id.folder_cling); - return null; - } + }); } - public static void synchonouslyMarkFirstRunClingDismissed(Context ctx) { - SharedPreferences prefs = ctx.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(LauncherClings.FIRST_RUN_CLING_DISMISSED_KEY, true); - editor.commit(); - } - - public void markFolderClingDismissed() { - SharedPreferences.Editor editor = mLauncher.getSharedPrefs().edit(); - editor.putBoolean(LauncherClings.FOLDER_CLING_DISMISSED_KEY, true); - editor.apply(); - } - - /** Removes the cling outright from the DragLayer */ - private void removeCling(int id) { - final View cling = mLauncher.findViewById(id); - if (cling != null) { - final ViewGroup parent = (ViewGroup) cling.getParent(); - parent.post(new Runnable() { - @Override - public void run() { - parent.removeView(cling); - } - }); - mHideFromAccessibilityHelper.restoreImportantForAccessibility(mLauncher.getDragLayer()); - } + private void dismissLongPressCling() { + Runnable dismissCb = new Runnable() { + public void run() { + dismissCling(mLauncher.findViewById(R.id.longpress_cling), null, + WORKSPACE_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION); + } + }; + mLauncher.getWorkspace().post(dismissCb); } /** Hides the specified Cling */ - private void dismissCling(final Cling cling, final Runnable postAnimationCb, - final String flag, int duration, boolean restoreNavBarVisibilty) { + private void dismissCling(final View cling, final Runnable postAnimationCb, + 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) { final Runnable cleanUpClingCb = new Runnable() { public void run() { - cling.cleanup(); - SharedPreferences.Editor editor = mLauncher.getSharedPrefs().edit(); - editor.putBoolean(flag, true); - editor.apply(); + cling.setVisibility(View.GONE); + mLauncher.getSharedPrefs().edit() + .putBoolean(flag, true) + .apply(); if (postAnimationCb != null) { postAnimationCb.run(); } @@ -344,108 +206,58 @@ class LauncherClings { if (duration <= 0) { cleanUpClingCb.run(); } else { - cling.hide(duration, cleanUpClingCb); - } - mHideFromAccessibilityHelper.restoreImportantForAccessibility(mLauncher.getDragLayer()); - - if (restoreNavBarVisibilty) { - cling.setSystemUiVisibility(cling.getSystemUiVisibility() & - ~View.SYSTEM_UI_FLAG_LOW_PROFILE); + cling.animate().alpha(0).setDuration(duration).withEndAction(cleanUpClingCb); } } } - public void dismissFirstRunCling(View v) { - Cling cling = (Cling) mLauncher.findViewById(R.id.first_run_cling); - Runnable cb = new Runnable() { - public void run() { - // Show the workspace cling next - showWorkspaceCling(); - } - }; - dismissCling(cling, cb, FIRST_RUN_CLING_DISMISSED_KEY, - DISMISS_CLING_DURATION, false); - - // Fade out the search bar for the workspace cling coming up - mLauncher.getSearchBar().hideSearchBar(true); - } - - private void dismissMigrationCling() { - mLauncher.showWorkspaceSearchAndHotseat(); - Runnable dismissCb = new Runnable() { - public void run() { - Cling cling = (Cling) mLauncher.findViewById(R.id.migration_cling); - Runnable cb = new Runnable() { - public void run() { - // Show the migration workspace cling next - showMigrationWorkspaceCling(); - } - }; - dismissCling(cling, cb, MIGRATION_CLING_DISMISSED_KEY, - DISMISS_CLING_DURATION, true); - } - }; - mLauncher.getWorkspace().post(dismissCb); - } - - private void dismissAnyWorkspaceCling(Cling cling, String key, View v) { - Runnable cb = null; - if (v == null) { - cb = new Runnable() { - public void run() { - mLauncher.getWorkspace().enterOverviewMode(); - } - }; + /** Returns whether the clings are enabled or should be shown */ + private boolean areClingsEnabled() { + if (DISABLE_CLINGS) { + return false; } - dismissCling(cling, cb, key, DISMISS_CLING_DURATION, true); - - // Fade in the search bar - mLauncher.getSearchBar().showSearchBar(true); - } - - public void dismissMigrationClingCopyApps(View v) { - // Copy the shortcuts from the old database - LauncherModel model = mLauncher.getModel(); - model.resetLoadedState(false, true); - model.startLoader(false, PagedView.INVALID_RESTORE_PAGE, - LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE - | LauncherModel.LOADER_FLAG_MIGRATE_SHORTCUTS); - - // Set the flag to skip the folder cling - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = mLauncher.getSharedPreferences(spKey, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putBoolean(Launcher.USER_HAS_MIGRATED, true); - editor.apply(); - - // Disable the migration cling - dismissMigrationCling(); - } - public void dismissMigrationClingUseDefault(View v) { - // Clear the workspace - LauncherModel model = mLauncher.getModel(); - model.resetLoadedState(false, true); - model.startLoader(false, PagedView.INVALID_RESTORE_PAGE, - LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE); + // disable clings when running in a test harness + if(ActivityManager.isRunningInTestHarness()) return false; - // Disable the migration cling - dismissMigrationCling(); - } + // Disable clings for accessibility when explore by touch is enabled + final AccessibilityManager a11yManager = (AccessibilityManager) mLauncher.getSystemService( + Launcher.ACCESSIBILITY_SERVICE); + if (a11yManager.isTouchExplorationEnabled()) { + return false; + } - public void dismissMigrationWorkspaceCling(View v) { - Cling cling = (Cling) mLauncher.findViewById(R.id.migration_workspace_cling); - dismissAnyWorkspaceCling(cling, MIGRATION_WORKSPACE_CLING_DISMISSED_KEY, v); + // 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(mLauncher).getAccounts(); + if (supportsLimitedUsers && accounts.length == 0) { + UserManager um = (UserManager) mLauncher.getSystemService(Context.USER_SERVICE); + Bundle restrictions = um.getUserRestrictions(); + if (restrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false)) { + return false; + } + } + if (Settings.Secure.getInt(mLauncher.getContentResolver(), SKIP_FIRST_USE_HINTS, 0) + == 1) { + return false; + } + return true; } - public void dismissWorkspaceCling(View v) { - Cling cling = (Cling) mLauncher.findViewById(R.id.workspace_cling); - dismissAnyWorkspaceCling(cling, WORKSPACE_CLING_DISMISSED_KEY, v); + public boolean shouldShowFirstRunOrMigrationClings() { + SharedPreferences sharedPrefs = mLauncher.getSharedPrefs(); + return areClingsEnabled() && + !sharedPrefs.getBoolean(WORKSPACE_CLING_DISMISSED_KEY, false) && + !sharedPrefs.getBoolean(MIGRATION_CLING_DISMISSED_KEY, false); } - public void dismissFolderCling(View v) { - Cling cling = (Cling) mLauncher.findViewById(R.id.folder_cling); - dismissCling(cling, null, FOLDER_CLING_DISMISSED_KEY, - DISMISS_CLING_DURATION, true); + public static void synchonouslyMarkFirstRunClingDismissed(Context ctx) { + SharedPreferences prefs = ctx.getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(WORKSPACE_CLING_DISMISSED_KEY, true); + editor.commit(); } } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 007fd7a4a..c64506d80 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -19,12 +19,19 @@ package com.android.launcher3; import android.app.SearchManager; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; -import android.content.*; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +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.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; @@ -44,10 +51,17 @@ import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import com.android.launcher3.InstallWidgetReceiver.WidgetMimeTypeHandlerData; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.LauncherActivityInfoCompat; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.PackageInstallerCompat; +import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; import java.lang.ref.WeakReference; import java.net.URISyntaxException; +import java.security.InvalidParameterException; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; @@ -58,6 +72,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicBoolean; @@ -67,14 +82,17 @@ import java.util.concurrent.atomic.AtomicBoolean; * LauncherModel object held in a static. Also provide APIs for updating the database state * for the Launcher. */ -public class LauncherModel extends BroadcastReceiver { +public class LauncherModel extends BroadcastReceiver + implements LauncherAppsCompat.OnAppsChangedCallbackCompat { static final boolean DEBUG_LOADERS = false; + private static final boolean DEBUG_RECEIVER = false; + private static final boolean REMOVE_UNRESTORED_ICONS = true; + static final String TAG = "Launcher.Model"; // true = use a "More Apps" folder for non-workspace apps on upgrade // false = strew non-workspace apps across the workspace on upgrade public static final boolean UPGRADE_USE_MORE_APPS_FOLDER = false; - public static final int LOADER_FLAG_NONE = 0; public static final int LOADER_FLAG_CLEAR_WORKSPACE = 1 << 0; public static final int LOADER_FLAG_MIGRATE_SHORTCUTS = 1 << 1; @@ -97,6 +115,7 @@ public class LauncherModel extends BroadcastReceiver { private static final int MAIN_THREAD_NORMAL_RUNNABLE = 0; private static final int MAIN_THREAD_BINDING_RUNNABLE = 1; + private static final String MIGRATE_AUTHORITY = "com.android.launcher2.settings"; private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); static { @@ -149,13 +168,19 @@ public class LauncherModel extends BroadcastReceiver { // sBgWorkspaceScreens is the ordered set of workspace screens. static final ArrayList<Long> sBgWorkspaceScreens = new ArrayList<Long>(); + // sPendingPackages is a set of packages which could be on sdcard and are not available yet + static final HashMap<UserHandleCompat, HashSet<String>> sPendingPackages = + new HashMap<UserHandleCompat, HashSet<String>>(); + // </ only access in worker thread > private IconCache mIconCache; - private Bitmap mDefaultIcon; protected int mPreviousConfigMcc; + private final LauncherAppsCompat mLauncherApps; + private final UserManagerCompat mUserManager; + public interface Callbacks { public boolean setLoadOnResume(); public int getCurrentWorkspaceScreen(); @@ -173,8 +198,11 @@ public class LauncherModel extends BroadcastReceiver { ArrayList<ItemInfo> addAnimated, ArrayList<AppInfo> addedApps); public void bindAppsUpdated(ArrayList<AppInfo> apps); + public void bindAppsRestored(ArrayList<AppInfo> apps); + public void updatePackageState(ArrayList<PackageInstallInfo> installInfo); + public void updatePackageBadge(String packageName); public void bindComponentsRemoved(ArrayList<String> packageNames, - ArrayList<AppInfo> appInfos); + ArrayList<AppInfo> appInfos, UserHandleCompat user); public void bindPackagesUpdated(ArrayList<Object> widgetsAndShortcuts); public void bindSearchablesChanged(); public boolean isAllAppsButtonRank(int rank); @@ -188,11 +216,26 @@ public class LauncherModel extends BroadcastReceiver { LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) { Context context = app.getContext(); - ContentResolver contentResolver = context.getContentResolver(); mAppsCanBeOnRemoveableStorage = Environment.isExternalStorageRemovable(); - mOldContentProviderExists = (contentResolver.acquireContentProviderClient( - LauncherSettings.Favorites.OLD_CONTENT_URI) != null); + String oldProvider = context.getString(R.string.old_launcher_provider_uri); + // This may be the same as MIGRATE_AUTHORITY, or it may be replaced by a different + // resource string. + String redirectAuthority = Uri.parse(oldProvider).getAuthority(); + ProviderInfo providerInfo = + context.getPackageManager().resolveContentProvider(MIGRATE_AUTHORITY, 0); + ProviderInfo redirectProvider = + context.getPackageManager().resolveContentProvider(redirectAuthority, 0); + + Log.d(TAG, "Old launcher provider: " + oldProvider); + mOldContentProviderExists = (providerInfo != null) && (redirectProvider != null); + + if (mOldContentProviderExists) { + Log.d(TAG, "Old launcher provider exists."); + } else { + Log.d(TAG, "Old launcher provider does not exist."); + } + mApp = app; mBgAllAppsList = new AllAppsList(iconCache, appFilter); mIconCache = iconCache; @@ -200,6 +243,8 @@ public class LauncherModel extends BroadcastReceiver { final Resources res = context.getResources(); Configuration config = res.getConfiguration(); mPreviousConfigMcc = config.mcc; + mLauncherApps = LauncherAppsCompat.getInstance(context); + mUserManager = UserManagerCompat.getInstance(context); } /** Runs the specified runnable immediately if called from the main thread, otherwise it is @@ -292,6 +337,32 @@ public class LauncherModel extends BroadcastReceiver { return null; } + public void setPackageState(final ArrayList<PackageInstallInfo> installInfo) { + // Process the updated package state + Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks != null) { + callbacks.updatePackageState(installInfo); + } + } + }; + mHandler.post(r); + } + + public void updatePackageBadge(final String packageName) { + // Process the updated package badge + Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks != null) { + callbacks.updatePackageBadge(packageName); + } + } + }; + mHandler.post(r); + } + public void addAppsToAllApps(final Context ctx, final ArrayList<AppInfo> allAppsApps) { final Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null; @@ -306,7 +377,7 @@ public class LauncherModel extends BroadcastReceiver { Iterator<AppInfo> iter = allAppsApps.iterator(); while (iter.hasNext()) { ItemInfo a = iter.next(); - if (LauncherModel.appWasRestored(ctx, a.getIntent())) { + if (LauncherModel.appWasPromise(ctx, a.getIntent(), a.user)) { restoredAppsFinal.add((AppInfo) a); } } @@ -322,7 +393,8 @@ public class LauncherModel extends BroadcastReceiver { for (AppInfo info : restoredAppsFinal) { final Intent intent = info.getIntent(); if (intent != null) { - mIconCache.deletePreloadedIcon(intent.getComponent()); + mIconCache.deletePreloadedIcon(intent.getComponent(), + info.user); } } callbacks.bindAppsUpdated(restoredAppsFinal); @@ -374,7 +446,7 @@ public class LauncherModel extends BroadcastReceiver { if (LauncherModel.shortcutExists(context, name, launchIntent)) { // Only InstallShortcutReceiver sends us shortcutInfos, ignore them if (a instanceof AppInfo && - LauncherModel.appWasRestored(context, launchIntent)) { + LauncherModel.appWasPromise(context, launchIntent, a.user)) { restoredAppsFinal.add((AppInfo) a); } continue; @@ -464,15 +536,6 @@ public class LauncherModel extends BroadcastReceiver { runOnWorkerThread(r); } - public Bitmap getFallbackIcon() { - if (mDefaultIcon == null) { - final Context context = LauncherAppState.getInstance().getContext(); - mDefaultIcon = Utilities.createIconBitmap( - mIconCache.getFullResDefaultActivityIcon(), context); - } - return Bitmap.createBitmap(mDefaultIcon); - } - public void unbindItemInfosAndClearQueuedBindRunnables() { if (sWorkerThread.getThreadId() == Process.myTid()) { throw new RuntimeException("Expected unbindLauncherItemInfos() to be called from the " + @@ -480,7 +543,9 @@ public class LauncherModel extends BroadcastReceiver { } // Clear any deferred bind runnables - mDeferredBindRunnables.clear(); + synchronized (mDeferredBindRunnables) { + mDeferredBindRunnables.clear(); + } // Remove any queued bind runnables mHandler.cancelAllRunnablesOfType(MAIN_THREAD_BINDING_RUNNABLE); // Unbind all the workspace items @@ -796,7 +861,7 @@ public class LauncherModel extends BroadcastReceiver { */ static void updateItemInDatabase(Context context, final ItemInfo item) { final ContentValues values = new ContentValues(); - item.onAddToDatabase(values); + item.onAddToDatabase(context, values); item.updateValuesWithCoordinates(values, item.cellX, item.cellY); updateItemInDatabaseHelper(context, values, item, "updateItemInDatabase"); } @@ -807,9 +872,26 @@ public class LauncherModel extends BroadcastReceiver { */ static boolean shortcutExists(Context context, String title, Intent intent) { final ContentResolver cr = context.getContentResolver(); + final Intent intentWithPkg, intentWithoutPkg; + + if (intent.getComponent() != null) { + // If component is not null, an intent with null package will produce + // the same result and should also be a match. + if (intent.getPackage() != null) { + intentWithPkg = intent; + intentWithoutPkg = new Intent(intent).setPackage(null); + } else { + intentWithPkg = new Intent(intent).setPackage( + intent.getComponent().getPackageName()); + intentWithoutPkg = intent; + } + } else { + intentWithPkg = intent; + intentWithoutPkg = intent; + } Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, - new String[] { "title", "intent" }, "title=? and intent=?", - new String[] { title, intent.toUri(0) }, null); + new String[] { "title", "intent" }, "title=? and (intent=? or intent=?)", + new String[] { title, intentWithPkg.toUri(0), intentWithoutPkg.toUri(0) }, null); boolean result = false; try { result = c.moveToFirst(); @@ -820,27 +902,14 @@ public class LauncherModel extends BroadcastReceiver { } /** - * Returns true if the shortcuts already exists in the database. - * we identify a shortcut by the component name of the intent. + * Returns true if the promise shortcuts with the same package name exists on the workspace. */ - static boolean appWasRestored(Context context, Intent intent) { - final ContentResolver cr = context.getContentResolver(); + static boolean appWasPromise(Context context, Intent intent, UserHandleCompat user) { final ComponentName component = intent.getComponent(); if (component == null) { return false; } - String componentName = component.flattenToString(); - final String where = "intent glob \"*component=" + componentName + "*\" and restored = 1"; - Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, - new String[]{"intent", "restored"}, where, null, null); - boolean result = false; - try { - result = c.moveToFirst(); - } finally { - c.close(); - } - Log.d(TAG, "shortcutWasRestored is " + result + " for " + componentName); - return result; + return !getItemsByPackageName(component.getPackageName(), user).isEmpty(); } /** @@ -852,8 +921,10 @@ public class LauncherModel extends BroadcastReceiver { 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); + LauncherSettings.Favorites.SCREEN, + LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY, + LauncherSettings.Favorites.SPANX, LauncherSettings.Favorites.SPANY, + LauncherSettings.Favorites.PROFILE_ID }, null, null, null); final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); @@ -862,7 +933,8 @@ public class LauncherModel extends BroadcastReceiver { 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 profileIdIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID); + UserManagerCompat userManager = UserManagerCompat.getInstance(context); try { while (c.moveToNext()) { ItemInfo item = new ItemInfo(); @@ -873,8 +945,12 @@ public class LauncherModel extends BroadcastReceiver { item.container = c.getInt(containerIndex); item.itemType = c.getInt(itemTypeIndex); item.screenId = c.getInt(screenIndex); - - items.add(item); + long serialNumber = c.getInt(profileIdIndex); + item.user = userManager.getUserForSerialNumber(serialNumber); + // Skip if user has been deleted. + if (item.user != null) { + items.add(item); + } } } catch (Exception e) { items.clear(); @@ -947,12 +1023,13 @@ public class LauncherModel extends BroadcastReceiver { final ContentValues values = new ContentValues(); final ContentResolver cr = context.getContentResolver(); - item.onAddToDatabase(values); + item.onAddToDatabase(context, values); item.id = LauncherAppState.getLauncherProvider().generateNewItemId(); values.put(LauncherSettings.Favorites._ID, item.id); item.updateValuesWithCoordinates(values, item.cellX, item.cellY); + final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); Runnable r = new Runnable() { public void run() { cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI : @@ -960,7 +1037,7 @@ public class LauncherModel extends BroadcastReceiver { // Lock on mBgLock *after* the db operation synchronized (sBgLock) { - checkItemInfoLocked(item.id, item, null); + checkItemInfoLocked(item.id, item, stackTrace); sBgItemsIdMap.put(item.id, item); switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: @@ -999,45 +1076,77 @@ public class LauncherModel extends BroadcastReceiver { | ((int) screen & 0xFF) << 16 | (localCellX & 0xFF) << 8 | (localCellY & 0xFF); } + private static ArrayList<ItemInfo> getItemsByPackageName( + final String pn, final UserHandleCompat user) { + ItemInfoFilter filter = new ItemInfoFilter() { + @Override + public boolean filterItem(ItemInfo parent, ItemInfo info, ComponentName cn) { + return cn.getPackageName().equals(pn) && info.user.equals(user); + } + }; + return filterItemInfos(sBgItemsIdMap.values(), filter); + } + + /** + * Removes all the items from the database corresponding to the specified package. + */ + static void deletePackageFromDatabase(Context context, final String pn, + final UserHandleCompat user) { + deleteItemsFromDatabase(context, getItemsByPackageName(pn, user)); + } + /** * Removes the specified item from the database * @param context * @param item */ static void deleteItemFromDatabase(Context context, final ItemInfo item) { + ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); + items.add(item); + deleteItemsFromDatabase(context, items); + } + + /** + * Removes the specified items from the database + * @param context + * @param item + */ + static void deleteItemsFromDatabase(Context context, final ArrayList<ItemInfo> items) { final ContentResolver cr = context.getContentResolver(); - final Uri uriToDelete = LauncherSettings.Favorites.getContentUri(item.id, false); Runnable r = new Runnable() { public void run() { - cr.delete(uriToDelete, null, null); + for (ItemInfo item : items) { + final Uri uri = LauncherSettings.Favorites.getContentUri(item.id, false); + cr.delete(uri, 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); + // 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); + } } - } - 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; + 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); } - sBgItemsIdMap.remove(item.id); - sBgDbIconCache.remove(item); } } }; @@ -1136,74 +1245,67 @@ public class LauncherModel extends BroadcastReceiver { } } - /** - * 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(); + public void onPackageChanged(String packageName, UserHandleCompat user) { + int op = PackageUpdatedTask.OP_UPDATE; + enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName }, + user)); + } - 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); + @Override + public void onPackageRemoved(String packageName, UserHandleCompat user) { + int op = PackageUpdatedTask.OP_REMOVE; + enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName }, + user)); + } - int op = PackageUpdatedTask.OP_NONE; + @Override + public void onPackageAdded(String packageName, UserHandleCompat user) { + int op = PackageUpdatedTask.OP_ADD; + enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName }, + user)); + } - if (packageName == null || packageName.length() == 0) { - // they sent us a bad intent - return; + @Override + public void onPackagesAvailable(String[] packageNames, UserHandleCompat user, + boolean replacing) { + if (!replacing) { + enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packageNames, + user)); + if (mAppsCanBeOnRemoveableStorage) { + // Only rebind if we support removable storage. It catches the + // case where + // apps on the external sd card need to be reloaded + startLoaderFromBackground(); } + } else { + // If we are replacing then just update the packages in the list + enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, + packageNames, user)); + } + } - 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; - } - } + @Override + public void onPackagesUnavailable(String[] packageNames, UserHandleCompat user, + boolean replacing) { + if (!replacing) { + enqueuePackageUpdated(new PackageUpdatedTask( + PackageUpdatedTask.OP_UNAVAILABLE, packageNames, + user)); + } - if (op != PackageUpdatedTask.OP_NONE) { - enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName })); - } + } - } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { - final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); - String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); - if (!replacing) { - enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packages)); - if (mAppsCanBeOnRemoveableStorage) { - // Only rebind if we support removable storage. It catches the case where - // apps on the external sd card need to be reloaded - startLoaderFromBackground(); - } - } else { - // If we are replacing then just update the packages in the list - enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, - packages)); - } - } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { - final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); - if (!replacing) { - String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); - enqueuePackageUpdated(new PackageUpdatedTask( - PackageUpdatedTask.OP_UNAVAILABLE, packages)); - } - // else, we are replacing the packages, so ignore this event and wait for - // EXTERNAL_APPLICATIONS_AVAILABLE to update the packages at that time - } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { + /** + * 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_RECEIVER) Log.d(TAG, "onReceive intent=" + intent); + + final String action = intent.getAction(); + 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)) { @@ -1229,7 +1331,7 @@ public class LauncherModel extends BroadcastReceiver { } } - private void forceReload() { + void forceReload() { resetLoadedState(true, true); // Do this here because if the launcher activity is running it will be restarted. @@ -1284,6 +1386,10 @@ public class LauncherModel extends BroadcastReceiver { return isLaunching; } + public boolean isCurrentCallbacks(Callbacks callbacks) { + return (mCallbacks != null && mCallbacks.get() == callbacks); + } + public void startLoader(boolean isLaunching, int synchronousBindPage) { startLoader(isLaunching, synchronousBindPage, LOADER_FLAG_NONE); } @@ -1296,7 +1402,9 @@ public class LauncherModel extends BroadcastReceiver { // Clear any deferred bind-runnables from the synchronized load process // We must do this before any loading/binding is scheduled below. - mDeferredBindRunnables.clear(); + synchronized (mDeferredBindRunnables) { + 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) { @@ -1318,10 +1426,15 @@ public class LauncherModel extends BroadcastReceiver { void bindRemainingSynchronousPages() { // Post the remaining side pages to be loaded if (!mDeferredBindRunnables.isEmpty()) { - for (final Runnable r : mDeferredBindRunnables) { + Runnable[] deferredBindRunnables = null; + synchronized (mDeferredBindRunnables) { + deferredBindRunnables = mDeferredBindRunnables.toArray( + new Runnable[mDeferredBindRunnables.size()]); + mDeferredBindRunnables.clear(); + } + for (final Runnable r : deferredBindRunnables) { mHandler.post(r, MAIN_THREAD_BINDING_RUNNABLE); } - mDeferredBindRunnables.clear(); } } @@ -1630,7 +1743,7 @@ public class LauncherModel extends BroadcastReceiver { ArrayList<ItemInfo> added = new ArrayList<ItemInfo>(); synchronized (sBgLock) { for (AppInfo app : mBgAllAppsList.data) { - tmpInfos = getItemInfoForComponentName(app.componentName); + tmpInfos = getItemInfoForComponentName(app.componentName, app.user); if (tmpInfos.isEmpty()) { // We are missing an application icon, so add this to the workspace added.add(app); @@ -1760,6 +1873,9 @@ public class LauncherModel extends BroadcastReceiver { final PackageManager manager = context.getPackageManager(); final AppWidgetManager widgets = AppWidgetManager.getInstance(context); final boolean isSafeMode = manager.isSafeMode(); + final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); + final boolean isSdCardReady = context.registerReceiver(null, + new IntentFilter(StartupReceiver.SYSTEM_READY)) != null; LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); @@ -1778,22 +1894,23 @@ public class LauncherModel extends BroadcastReceiver { } else { // Make sure the default workspace is loaded Launcher.addDumpLog(TAG, "loadWorkspace: loading default favorites", false); - LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary(0); + LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary(); } - // Check if we need to do any upgrade-path logic - // (Includes having just imported default favorites) - boolean loadedOldDb = LauncherAppState.getLauncherProvider().justLoadedOldDb(); + // This code path is for our old migration code and should no longer be exercised + boolean loadedOldDb = false; // Log to disk Launcher.addDumpLog(TAG, "11683562 - loadedOldDb: " + loadedOldDb, true); synchronized (sBgLock) { clearSBgDataStructures(); + final HashSet<String> installingPkgs = PackageInstallerCompat + .getInstance(mContext).updateAndGetActiveSessionCache(); final ArrayList<Long> itemsToRemove = new ArrayList<Long>(); final ArrayList<Long> restoredRows = new ArrayList<Long>(); - final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI; + final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION; if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri); final Cursor c = contentResolver.query(contentUri, null, null, null, null); @@ -1835,6 +1952,8 @@ public class LauncherModel extends BroadcastReceiver { LauncherSettings.Favorites.SPANY); final int restoredIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.RESTORED); + final int profileIdIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.PROFILE_ID); //final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); //final int displayModeIndex = c.getColumnIndexOrThrow( // LauncherSettings.Favorites.DISPLAY_MODE); @@ -1845,43 +1964,119 @@ public class LauncherModel extends BroadcastReceiver { int container; long id; Intent intent; + UserHandleCompat user; while (!mStopped && c.moveToNext()) { AtomicBoolean deleteOnInvalidPlacement = new AtomicBoolean(false); try { int itemType = c.getInt(itemTypeIndex); boolean restored = 0 != c.getInt(restoredIndex); + boolean allowMissingTarget = false; switch (itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: id = c.getLong(idIndex); intentDescription = c.getString(intentIndex); + long serialNumber = c.getInt(profileIdIndex); + user = mUserManager.getUserForSerialNumber(serialNumber); + int promiseType = c.getInt(restoredIndex); + if (user == null) { + // User has been deleted remove the item. + itemsToRemove.add(id); + continue; + } try { intent = Intent.parseUri(intentDescription, 0); ComponentName cn = intent.getComponent(); - if (cn != null && !isValidPackageComponent(manager, cn)) { - if (restored) { - // might be installed later - Launcher.addDumpLog(TAG, - "package not yet restored: " + cn, true); - } else { - if (!mAppsCanBeOnRemoveableStorage) { - // Log the invalid package, and remove it + if (cn != null && cn.getPackageName() != null) { + boolean validPkg = launcherApps.isPackageEnabledForProfile( + cn.getPackageName(), user); + boolean validComponent = validPkg && + launcherApps.isActivityEnabledForProfile(cn, user); + + if (validComponent) { + if (restored) { + // no special handling necessary for this item + restoredRows.add(id); + restored = false; + } + } else if (validPkg) { + intent = null; + if ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) { + // We allow auto install apps to have their intent + // updated after an install. + intent = manager.getLaunchIntentForPackage( + cn.getPackageName()); + if (intent != null) { + ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.INTENT, + intent.toUri(0)); + String where = BaseColumns._ID + "= ?"; + String[] args = {Long.toString(id)}; + contentResolver.update(contentUri, values, where, args); + } + } + + if (intent == null) { + // The app is installed but the component is no + // longer available. Launcher.addDumpLog(TAG, - "Invalid package removed: " + cn, true); + "Invalid component removed: " + cn, true); itemsToRemove.add(id); + continue; } else { - // If apps can be on external storage, then we just - // leave them for the user to remove (maybe add - // visual treatment to it) + // no special handling necessary for this item + restoredRows.add(id); + restored = false; + } + } else if (restored) { + // Package is not yet available but might be + // installed later. + Launcher.addDumpLog(TAG, + "package not yet restored: " + cn, true); + + if ((promiseType & ShortcutInfo.FLAG_RESTORE_STARTED) != 0) { + // Restore has started once. + } else if (installingPkgs.contains(cn.getPackageName())) { + // App restore has started. Update the flag + promiseType |= ShortcutInfo.FLAG_RESTORE_STARTED; + ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.RESTORED, + promiseType); + String where = BaseColumns._ID + "= ?"; + String[] args = {Long.toString(id)}; + contentResolver.update(contentUri, values, where, args); + + } else if (REMOVE_UNRESTORED_ICONS) { Launcher.addDumpLog(TAG, - "Invalid package found: " + cn, true); + "Unrestored package removed: " + cn, true); + itemsToRemove.add(id); + continue; } + } else if (isSdCardReady) { + // Do not wait for external media load anymore. + // Log the invalid package, and remove it + Launcher.addDumpLog(TAG, + "Invalid package removed: " + cn, true); + itemsToRemove.add(id); continue; + } else { + // SdCard is not ready yet. Package might get available, + // once it is ready. + Launcher.addDumpLog(TAG, "Invalid package: " + cn + + " (check again later)", true); + HashSet<String> pkgs = sPendingPackages.get(user); + if (pkgs == null) { + pkgs = new HashSet<String>(); + sPendingPackages.put(user, pkgs); + } + pkgs.add(cn.getPackageName()); + allowMissingTarget = true; + // Add the icon on the workspace anyway. } - } else if (restored) { - // no special handling necessary for this restored item + } else if (cn == null) { + // For shortcuts with no component, keep them as they are restoredRows.add(id); restored = false; } @@ -1892,15 +2087,21 @@ public class LauncherModel extends BroadcastReceiver { } if (restored) { - Launcher.addDumpLog(TAG, - "constructing info for partially restored package", - true); - info = getRestoredItemInfo(c, titleIndex, intent); - intent = getRestoredItemIntent(c, context, intent); + if (user.equals(UserHandleCompat.myUserHandle())) { + Launcher.addDumpLog(TAG, + "constructing info for partially restored package", + true); + info = getRestoredItemInfo(c, titleIndex, intent, promiseType); + intent = getRestoredItemIntent(c, context, intent); + } else { + // Don't restore items for other profiles. + itemsToRemove.add(id); + continue; + } } else if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { - info = getShortcutInfo(manager, intent, context, c, iconIndex, - titleIndex, mLabelCache); + info = getShortcutInfo(manager, intent, user, context, c, + iconIndex, titleIndex, mLabelCache, allowMissingTarget); } else { info = getShortcutInfo(c, context, iconTypeIndex, iconPackageIndex, iconResourceIndex, iconIndex, @@ -1929,6 +2130,9 @@ public class LauncherModel extends BroadcastReceiver { info.cellY = c.getInt(cellYIndex); info.spanX = 1; info.spanY = 1; + info.intent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber); + info.isDisabled = isSafeMode + && !Utilities.isSystemApp(context, intent); // check & update map of what's occupied deleteOnInvalidPlacement.set(false); @@ -2005,31 +2209,79 @@ public class LauncherModel extends BroadcastReceiver { // Read all Launcher-specific widget details int appWidgetId = c.getInt(appWidgetIdIndex); String savedProvider = c.getString(appWidgetProviderIndex); - id = c.getLong(idIndex); + final ComponentName component = + ComponentName.unflattenFromString(savedProvider); + + final int restoreStatus = c.getInt(restoredIndex); + final boolean isIdValid = (restoreStatus & + LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) == 0; - final AppWidgetProviderInfo provider = - widgets.getAppWidgetInfo(appWidgetId); + final boolean wasProviderReady = (restoreStatus & + LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0; - if (!isSafeMode && (provider == null || provider.provider == null || - provider.provider.getPackageName() == null)) { - String log = "Deleting widget that isn't installed anymore: id=" - + id + " appWidgetId=" + appWidgetId; + final AppWidgetProviderInfo provider = isIdValid + ? widgets.getAppWidgetInfo(appWidgetId) + : findAppWidgetProviderInfoWithComponent(context, component); + + final boolean isProviderReady = isValidProvider(provider); + if (!isSafeMode && wasProviderReady && !isProviderReady) { + String log = "Deleting widget that isn't installed anymore: " + + "id=" + id + " appWidgetId=" + appWidgetId; Log.e(TAG, log); Launcher.addDumpLog(TAG, log, false); itemsToRemove.add(id); } else { - appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, - provider.provider); + if (isProviderReady) { + appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, + provider.provider); + int[] minSpan = + Launcher.getMinSpanForWidget(context, provider); + appWidgetInfo.minSpanX = minSpan[0]; + appWidgetInfo.minSpanY = minSpan[1]; + + int status = restoreStatus; + if (!wasProviderReady) { + // If provider was not previously ready, update the + // status and UI flag. + + // Id would be valid only if the widget restore broadcast was received. + if (isIdValid) { + status = LauncherAppWidgetInfo.RESTORE_COMPLETED; + } else { + status &= ~LauncherAppWidgetInfo + .FLAG_PROVIDER_NOT_READY; + } + } + appWidgetInfo.restoreStatus = status; + } else { + Log.v(TAG, "Widget restore pending id=" + id + + " appWidgetId=" + appWidgetId + + " status =" + restoreStatus); + appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, + component); + appWidgetInfo.restoreStatus = restoreStatus; + + if ((restoreStatus & LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) != 0) { + // Restore has started once. + } else if (installingPkgs.contains(component.getPackageName())) { + // App restore has started. Update the flag + appWidgetInfo.restoreStatus |= + LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; + } else if (REMOVE_UNRESTORED_ICONS) { + Launcher.addDumpLog(TAG, + "Unrestored widget removed: " + component, true); + itemsToRemove.add(id); + continue; + } + } + appWidgetInfo.id = id; appWidgetInfo.screenId = 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 && @@ -2049,13 +2301,17 @@ public class LauncherModel extends BroadcastReceiver { } break; } - String providerName = provider.provider.flattenToString(); - if (!providerName.equals(savedProvider)) { + + String providerName = appWidgetInfo.providerName.flattenToString(); + if (!providerName.equals(savedProvider) || + (appWidgetInfo.restoreStatus != restoreStatus)) { ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, providerName); + values.put(LauncherSettings.Favorites.RESTORED, + appWidgetInfo.restoreStatus); String where = BaseColumns._ID + "= ?"; - String[] args = {Integer.toString(c.getInt(idIndex))}; + String[] args = {Long.toString(id)}; contentResolver.update(contentUri, values, where, args); } sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); @@ -2081,7 +2337,7 @@ public class LauncherModel extends BroadcastReceiver { if (itemsToRemove.size() > 0) { ContentProviderClient client = contentResolver.acquireContentProviderClient( - LauncherSettings.Favorites.CONTENT_URI); + contentUri); // Remove dead items for (long id : itemsToRemove) { if (DEBUG_LOADERS) { @@ -2099,7 +2355,7 @@ public class LauncherModel extends BroadcastReceiver { if (restoredRows.size() > 0) { ContentProviderClient updater = contentResolver.acquireContentProviderClient( - LauncherSettings.Favorites.CONTENT_URI); + contentUri); // Update restored items that no longer require special handling try { StringBuilder selectionBuilder = new StringBuilder(); @@ -2109,13 +2365,19 @@ public class LauncherModel extends BroadcastReceiver { selectionBuilder.append(")"); ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites.RESTORED, 0); - updater.update(LauncherSettings.Favorites.CONTENT_URI, + updater.update(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values, selectionBuilder.toString(), null); } catch (RemoteException e) { Log.w(TAG, "Could not update restored rows"); } } + if (!isSdCardReady && !sPendingPackages.isEmpty()) { + context.registerReceiver(new AppsAvailabilityCheck(), + new IntentFilter(StartupReceiver.SYSTEM_READY), + null, sWorker); + } + if (loadedOldDb) { long maxScreenId = 0; // If we're importing we use the old screen order. @@ -2189,7 +2451,12 @@ public class LauncherModel extends BroadcastReceiver { line += " | "; } for (int x = 0; x < countX; x++) { - line += ((occupied.get(screenId)[x][y] != null) ? "#" : "."); + ItemInfo[][] screen = occupied.get(screenId); + if (x < screen.length && y < screen[x].length) { + line += (screen[x][y] != null) ? "#" : "."; + } else { + line += "!"; + } } } Log.d(TAG, "[ " + line + " ]"); @@ -2343,7 +2610,9 @@ public class LauncherModel extends BroadcastReceiver { } }; if (postOnMainThread) { - deferredBindRunnables.add(r); + synchronized (deferredBindRunnables) { + deferredBindRunnables.add(r); + } } else { runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); } @@ -2360,7 +2629,9 @@ public class LauncherModel extends BroadcastReceiver { } }; if (postOnMainThread) { - deferredBindRunnables.add(r); + synchronized (deferredBindRunnables) { + deferredBindRunnables.add(r); + } } else { runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); } @@ -2482,7 +2753,9 @@ public class LauncherModel extends BroadcastReceiver { // Load all the remaining pages (if we are loading synchronously, we want to defer this // work until after the first render) - mDeferredBindRunnables.clear(); + synchronized (mDeferredBindRunnables) { + mDeferredBindRunnables.clear(); + } bindWorkspaceItems(oldCallbacks, otherWorkspaceItems, otherAppWidgets, otherFolders, (isLoadingSynchronously ? mDeferredBindRunnables : null)); @@ -2504,7 +2777,9 @@ public class LauncherModel extends BroadcastReceiver { } }; if (isLoadingSynchronously) { - mDeferredBindRunnables.add(r); + synchronized (mDeferredBindRunnables) { + mDeferredBindRunnables.add(r); + } } else { runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); } @@ -2570,42 +2845,42 @@ public class LauncherModel extends BroadcastReceiver { return; } - final PackageManager packageManager = mContext.getPackageManager(); final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + final List<UserHandleCompat> profiles = mUserManager.getUserProfiles(); + // Clear the list of apps mBgAllAppsList.clear(); + for (UserHandleCompat user : profiles) { + // Query for the set of apps + final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user); + if (DEBUG_LOADERS) { + Log.d(TAG, "getActivityList took " + + (SystemClock.uptimeMillis()-qiaTime) + "ms for user " + user); + Log.d(TAG, "getActivityList got " + apps.size() + " apps for user " + user); + } + // Fail if we don't have any apps + if (apps == null || apps.isEmpty()) { + return; + } + // Sort the applications by name + final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + Collections.sort(apps, + new LauncherModel.ShortcutNameComparator(mLabelCache)); + if (DEBUG_LOADERS) { + Log.d(TAG, "sort took " + + (SystemClock.uptimeMillis()-sortTime) + "ms"); + } - // Query for the set of apps - final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; - List<ResolveInfo> apps = packageManager.queryIntentActivities(mainIntent, 0); - if (DEBUG_LOADERS) { - Log.d(TAG, "queryIntentActivities took " - + (SystemClock.uptimeMillis()-qiaTime) + "ms"); - Log.d(TAG, "queryIntentActivities got " + apps.size() + " apps"); - } - // Fail if we don't have any apps - if (apps == null || apps.isEmpty()) { - return; - } - // Sort the applications by name - 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"); - } - - // Create the ApplicationInfos - for (int i = 0; i < apps.size(); i++) { - ResolveInfo app = apps.get(i); - // This builds the icon bitmaps. - mBgAllAppsList.add(new AppInfo(packageManager, app, - mIconCache, mLabelCache)); + // Create the ApplicationInfos + for (int i = 0; i < apps.size(); i++) { + LauncherActivityInfoCompat app = apps.get(i); + // This builds the icon bitmaps. + mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache, mLabelCache)); + } } - // Huh? Shouldn't this be inside the Runnable below? final ArrayList<AppInfo> added = mBgAllAppsList.added; mBgAllAppsList.added = new ArrayList<AppInfo>(); @@ -2648,9 +2923,95 @@ public class LauncherModel extends BroadcastReceiver { sWorker.post(task); } + private class AppsAvailabilityCheck extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + synchronized (sBgLock) { + final LauncherAppsCompat launcherApps = LauncherAppsCompat + .getInstance(mApp.getContext()); + ArrayList<String> packagesRemoved; + for (Entry<UserHandleCompat, HashSet<String>> entry : sPendingPackages.entrySet()) { + UserHandleCompat user = entry.getKey(); + packagesRemoved = new ArrayList<String>(); + for (String pkg : entry.getValue()) { + if (!launcherApps.isPackageEnabledForProfile(pkg, user)) { + Launcher.addDumpLog(TAG, "Package not found: " + pkg, true); + packagesRemoved.add(pkg); + } + } + if (!packagesRemoved.isEmpty()) { + enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_REMOVE, + packagesRemoved.toArray(new String[packagesRemoved.size()]), user)); + } + } + sPendingPackages.clear(); + } + } + } + + /** + * Workaround to re-check unrestored items, in-case they were installed but the Package-ADD + * runnable was missed by the launcher. + */ + public void recheckRestoredItems(final Context context) { + Runnable r = new Runnable() { + + @Override + public void run() { + LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); + HashSet<String> installedPackages = new HashSet<String>(); + UserHandleCompat user = UserHandleCompat.myUserHandle(); + synchronized(sBgLock) { + for (ItemInfo info : sBgItemsIdMap.values()) { + if (info instanceof ShortcutInfo) { + ShortcutInfo si = (ShortcutInfo) info; + if (si.isPromise() && si.getTargetComponent() != null + && launcherApps.isPackageEnabledForProfile( + si.getTargetComponent().getPackageName(), user)) { + installedPackages.add(si.getTargetComponent().getPackageName()); + } + } else if (info instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo widget = (LauncherAppWidgetInfo) info; + if (widget.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) + && launcherApps.isPackageEnabledForProfile( + widget.providerName.getPackageName(), user)) { + installedPackages.add(widget.providerName.getPackageName()); + } + } + } + } + + if (!installedPackages.isEmpty()) { + final ArrayList<AppInfo> restoredApps = new ArrayList<AppInfo>(); + for (String pkg : installedPackages) { + for (LauncherActivityInfoCompat info : launcherApps.getActivityList(pkg, user)) { + restoredApps.add(new AppInfo(context, info, user, mIconCache, null)); + } + } + + final Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null; + if (!restoredApps.isEmpty()) { + mHandler.post(new Runnable() { + public void run() { + Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == cb && cb != null) { + callbacks.bindAppsRestored(restoredApps); + } + } + }); + } + + } + } + }; + sWorker.post(r); + } + private class PackageUpdatedTask implements Runnable { int mOp; String[] mPackages; + UserHandleCompat mUser; public static final int OP_NONE = 0; public static final int OP_ADD = 1; @@ -2659,9 +3020,10 @@ public class LauncherModel extends BroadcastReceiver { public static final int OP_UNAVAILABLE = 4; // external media unmounted - public PackageUpdatedTask(int op, String[] packages) { + public PackageUpdatedTask(int op, String[] packages, UserHandleCompat user) { mOp = op; mPackages = packages; + mUser = user; } public void run() { @@ -2673,14 +3035,14 @@ public class LauncherModel extends BroadcastReceiver { case OP_ADD: for (int i=0; i<N; i++) { if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]); - mIconCache.remove(packages[i]); - mBgAllAppsList.addPackage(context, packages[i]); + mIconCache.remove(packages[i], mUser); + mBgAllAppsList.addPackage(context, packages[i], mUser); } 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]); + mBgAllAppsList.updatePackage(context, packages[i], mUser); WidgetPreviewLoader.removePackageFromDb( mApp.getWidgetPreviewCacheDb(), packages[i]); } @@ -2689,7 +3051,7 @@ public class LauncherModel extends BroadcastReceiver { case OP_UNAVAILABLE: for (int i=0; i<N; i++) { if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); - mBgAllAppsList.removePackage(packages[i]); + mBgAllAppsList.removePackage(packages[i], mUser); WidgetPreviewLoader.removePackageFromDb( mApp.getWidgetPreviewCacheDb(), packages[i]); } @@ -2735,11 +3097,12 @@ public class LauncherModel extends BroadcastReceiver { // Update the launcher db to reflect the changes for (AppInfo a : modifiedFinal) { ArrayList<ItemInfo> infos = - getItemInfoForComponentName(a.componentName); + getItemInfoForComponentName(a.componentName, mUser); for (ItemInfo i : infos) { if (isShortcutInfoUpdateable(i)) { ShortcutInfo info = (ShortcutInfo) i; info.title = a.title.toString(); + info.contentDescription = a.contentDescription; updateItemInDatabase(context, info); } } @@ -2764,24 +3127,19 @@ public class LauncherModel extends BroadcastReceiver { // Mark disabled packages in the broadcast to be removed final PackageManager pm = context.getPackageManager(); for (int i=0; i<N; i++) { - if (isPackageDisabled(pm, packages[i])) { + if (isPackageDisabled(context, packages[i], mUser)) { removedPackageNames.add(packages[i]); } } } // Remove all the components associated with this package for (String pn : removedPackageNames) { - ArrayList<ItemInfo> infos = getItemInfoForPackageName(pn); - for (ItemInfo i : infos) { - deleteItemFromDatabase(context, i); - } + deletePackageFromDatabase(context, pn, mUser); } // Remove all the specific components for (AppInfo a : removedApps) { - ArrayList<ItemInfo> infos = getItemInfoForComponentName(a.componentName); - for (ItemInfo i : infos) { - deleteItemFromDatabase(context, i); - } + ArrayList<ItemInfo> infos = getItemInfoForComponentName(a.componentName, mUser); + deleteItemsFromDatabase(context, infos); } if (!removedPackageNames.isEmpty() || !removedApps.isEmpty()) { // Remove any queued items from the install queue @@ -2794,14 +3152,14 @@ public class LauncherModel extends BroadcastReceiver { public void run() { Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; if (callbacks == cb && cb != null) { - callbacks.bindComponentsRemoved(removedPackageNames, removedApps); + callbacks.bindComponentsRemoved(removedPackageNames, removedApps, mUser); } } }); } final ArrayList<Object> widgetsAndShortcuts = - getSortedWidgetsAndShortcuts(context); + getSortedWidgetsAndShortcuts(context); mHandler.post(new Runnable() { @Override public void run() { @@ -2828,55 +3186,70 @@ public class LauncherModel extends BroadcastReceiver { public static ArrayList<Object> getSortedWidgetsAndShortcuts(Context context) { PackageManager packageManager = context.getPackageManager(); final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>(); - widgetsAndShortcuts.addAll(AppWidgetManager.getInstance(context).getInstalledProviders()); + widgetsAndShortcuts.addAll(AppWidgetManagerCompat.getInstance(context).getAllProviders()); + Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0)); - Collections.sort(widgetsAndShortcuts, - new LauncherModel.WidgetAndShortcutNameComparator(packageManager)); + Collections.sort(widgetsAndShortcuts, new WidgetAndShortcutNameComparator(context)); return widgetsAndShortcuts; } - private static boolean isPackageDisabled(PackageManager pm, String packageName) { - try { - PackageInfo pi = pm.getPackageInfo(packageName, 0); - return !pi.applicationInfo.enabled; - } catch (NameNotFoundException e) { - // Fall through - } - return false; + private static boolean isPackageDisabled(Context context, String packageName, + UserHandleCompat user) { + final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); + return !launcherApps.isPackageEnabledForProfile(packageName, user); } - public static boolean isValidPackageComponent(PackageManager pm, ComponentName cn) { + public static boolean isValidPackageActivity(Context context, ComponentName cn, + UserHandleCompat user) { if (cn == null) { return false; } - if (isPackageDisabled(pm, cn.getPackageName())) { + final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); + if (!launcherApps.isPackageEnabledForProfile(cn.getPackageName(), user)) { return false; } + return launcherApps.isActivityEnabledForProfile(cn, user); + } - try { - // Check the activity - PackageInfo pi = pm.getPackageInfo(cn.getPackageName(), 0); - return (pm.getActivityInfo(cn, 0) != null); - } catch (NameNotFoundException e) { + public static boolean isValidPackage(Context context, String packageName, + UserHandleCompat user) { + if (packageName == null) { return false; } + final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); + return launcherApps.isPackageEnabledForProfile(packageName, user); } /** * Make an ShortcutInfo object for a restored application or shortcut item that points * to a package that is not yet installed on the system. */ - public ShortcutInfo getRestoredItemInfo(Cursor cursor, int titleIndex, Intent intent) { + public ShortcutInfo getRestoredItemInfo(Cursor cursor, int titleIndex, Intent intent, + int promiseType) { final ShortcutInfo info = new ShortcutInfo(); - if (cursor != null) { - info.title = cursor.getString(titleIndex); + info.user = UserHandleCompat.myUserHandle(); + mIconCache.getTitleAndIcon(info, intent, info.user, true); + + if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) { + String title = (cursor != null) ? cursor.getString(titleIndex) : null; + if (!TextUtils.isEmpty(title)) { + info.title = title; + } + info.status = ShortcutInfo.FLAG_RESTORED_ICON; + } else if ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) { + if (TextUtils.isEmpty(info.title)) { + info.title = (cursor != null) ? cursor.getString(titleIndex) : ""; + } + info.status = ShortcutInfo.FLAG_AUTOINTALL_ICON; } else { - info.title = ""; + throw new InvalidParameterException("Invalid restoreType " + promiseType); } - info.setIcon(mIconCache.getIcon(intent, info.title.toString())); + + info.contentDescription = mUserManager.getBadgedLabelForUser( + info.title.toString(), info.user); info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; - info.restoredIntent = intent; + info.promisedIntent = intent; return info; } @@ -2885,25 +3258,26 @@ public class LauncherModel extends BroadcastReceiver { * to the market page for the item. */ private Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) { - final boolean debug = false; ComponentName componentName = intent.getComponent(); - Intent marketIntent = new Intent(Intent.ACTION_VIEW); - Uri marketUri = new Uri.Builder() + return getMarketIntent(componentName.getPackageName()); + } + + static Intent getMarketIntent(String packageName) { + return new Intent(Intent.ACTION_VIEW) + .setData(new Uri.Builder() .scheme("market") .authority("details") - .appendQueryParameter("id", componentName.getPackageName()) - .build(); - if (debug) Log.d(TAG, "manufactured intent uri: " + marketUri.toString()); - marketIntent.setData(marketUri); - return marketIntent; + .appendQueryParameter("id", packageName) + .build()); } /** * 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); + public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, + UserHandleCompat user, Context context) { + return getShortcutInfo(manager, intent, user, context, null, -1, -1, null, false); } /** @@ -2911,54 +3285,37 @@ public class LauncherModel extends BroadcastReceiver { * * 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) { + public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, + UserHandleCompat user, Context context, Cursor c, int iconIndex, int titleIndex, + HashMap<Object, CharSequence> labelCache, boolean allowMissingTarget) { + if (user == null) { + Log.d(TAG, "Null user found in getShortcutInfo"); + return null; + } + ComponentName componentName = intent.getComponent(); - final ShortcutInfo info = new ShortcutInfo(); - if (componentName != null && !isValidPackageComponent(manager, componentName)) { - Log.d(TAG, "Invalid package found in getShortcutInfo: " + componentName); + if (componentName == null) { + Log.d(TAG, "Missing component found in getShortcutInfo: " + componentName); + return null; + } + + Intent newIntent = new Intent(intent.getAction(), null); + newIntent.addCategory(Intent.CATEGORY_LAUNCHER); + newIntent.setComponent(componentName); + LauncherActivityInfoCompat lai = mLauncherApps.resolveActivity(newIntent, user); + if ((lai == null) && !allowMissingTarget) { + Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName); return null; - } else { - try { - PackageInfo pi = manager.getPackageInfo(componentName.getPackageName(), 0); - info.initFlagsAndFirstInstallTime(pi); - } 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. + final ShortcutInfo info = new ShortcutInfo(); // 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. + Bitmap icon = mIconCache.getIcon(componentName, lai, labelCache); - // 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(). - Bitmap icon = null; - 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) { @@ -2967,21 +3324,21 @@ public class LauncherModel extends BroadcastReceiver { } // the fallback icon if (icon == null) { - icon = getFallbackIcon(); + icon = mIconCache.getDefaultIcon(user); info.usingFallbackIcon = true; } info.setIcon(icon); + // From the cache. + if (labelCache != null) { + info.title = labelCache.get(componentName); + } + // 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); - } + if (info.title == null && lai != null) { + info.title = lai.getLabel(); + if (labelCache != null) { + labelCache.put(componentName, info.title); } } // from the db @@ -2995,6 +3352,9 @@ public class LauncherModel extends BroadcastReceiver { info.title = componentName.getClassName(); } info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; + info.user = user; + info.contentDescription = mUserManager.getBadgedLabelForUser( + info.title.toString(), info.user); return info; } @@ -3004,14 +3364,14 @@ public class LauncherModel extends BroadcastReceiver { for (ItemInfo i : infos) { if (i instanceof ShortcutInfo) { ShortcutInfo info = (ShortcutInfo) i; - ComponentName cn = info.intent.getComponent(); + ComponentName cn = info.getTargetComponent(); if (cn != null && f.filterItem(null, info, cn)) { filtered.add(info); } } else if (i instanceof FolderInfo) { FolderInfo info = (FolderInfo) i; for (ShortcutInfo s : info.contents) { - ComponentName cn = s.intent.getComponent(); + ComponentName cn = s.getTargetComponent(); if (cn != null && f.filterItem(info, s, cn)) { filtered.add(s); } @@ -3027,21 +3387,16 @@ public class LauncherModel extends BroadcastReceiver { return new ArrayList<ItemInfo>(filtered); } - private ArrayList<ItemInfo> getItemInfoForPackageName(final String pn) { + private ArrayList<ItemInfo> getItemInfoForComponentName(final ComponentName cname, + final UserHandleCompat user) { ItemInfoFilter filter = new ItemInfoFilter() { @Override public boolean filterItem(ItemInfo parent, ItemInfo info, ComponentName cn) { - return cn.getPackageName().equals(pn); - } - }; - return filterItemInfos(sBgItemsIdMap.values(), filter); - } - - private ArrayList<ItemInfo> getItemInfoForComponentName(final ComponentName cname) { - ItemInfoFilter filter = new ItemInfoFilter() { - @Override - public boolean filterItem(ItemInfo parent, ItemInfo info, ComponentName cn) { - return cn.equals(cname); + if (info.user == null) { + return cn.equals(cname); + } else { + return cn.equals(cname) && info.user.equals(user); + } } }; return filterItemInfos(sBgItemsIdMap.values(), filter); @@ -3060,7 +3415,7 @@ public class LauncherModel extends BroadcastReceiver { return true; } // placeholder shortcuts get special treatment, let them through too. - if (info.getRestoredIntent() != null) { + if (info.isPromise()) { return true; } } @@ -3076,6 +3431,8 @@ public class LauncherModel extends BroadcastReceiver { Bitmap icon = null; final ShortcutInfo info = new ShortcutInfo(); + // Non-app shortcuts are only supported for current user. + info.user = UserHandleCompat.myUserHandle(); info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; // TODO: If there's an explicit component and we can't install that, delete it. @@ -3106,14 +3463,14 @@ public class LauncherModel extends BroadcastReceiver { } // the fallback icon if (icon == null) { - icon = getFallbackIcon(); + icon = mIconCache.getDefaultIcon(info.user); info.usingFallbackIcon = true; } break; case LauncherSettings.Favorites.ICON_TYPE_BITMAP: icon = getIconFromCursor(c, iconIndex, context); if (icon == null) { - icon = getFallbackIcon(); + icon = mIconCache.getDefaultIcon(info.user); info.customIcon = false; info.usingFallbackIcon = true; } else { @@ -3121,7 +3478,7 @@ public class LauncherModel extends BroadcastReceiver { } break; default: - icon = getFallbackIcon(); + icon = mIconCache.getDefaultIcon(info.user); info.usingFallbackIcon = true; info.customIcon = false; break; @@ -3160,7 +3517,7 @@ public class LauncherModel extends BroadcastReceiver { /** * Attempts to find an AppWidgetProviderInfo that matches the given component. */ - AppWidgetProviderInfo findAppWidgetProviderInfoWithComponent(Context context, + static AppWidgetProviderInfo findAppWidgetProviderInfoWithComponent(Context context, ComponentName component) { List<AppWidgetProviderInfo> widgets = AppWidgetManager.getInstance(context).getInstalledProviders(); @@ -3172,44 +3529,6 @@ public class LauncherModel extends BroadcastReceiver { 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); @@ -3238,7 +3557,8 @@ public class LauncherModel extends BroadcastReceiver { iconResource.packageName); final int id = resources.getIdentifier(iconResource.resourceName, null, null); icon = Utilities.createIconBitmap( - mIconCache.getFullResIcon(resources, id), context); + mIconCache.getFullResIcon(resources, id), + context); } catch (Exception e) { Log.w(TAG, "Could not load shortcut icon: " + extra); } @@ -3247,17 +3567,22 @@ public class LauncherModel extends BroadcastReceiver { final ShortcutInfo info = new ShortcutInfo(); + // Only support intents for current user for now. Intents sent from other + // users wouldn't get here without intent forwarding anyway. + info.user = UserHandleCompat.myUserHandle(); if (icon == null) { if (fallbackIcon != null) { icon = fallbackIcon; } else { - icon = getFallbackIcon(); + icon = mIconCache.getDefaultIcon(info.user); info.usingFallbackIcon = true; } } info.setIcon(icon); info.title = name; + info.contentDescription = mUserManager.getBadgedLabelForUser( + info.title.toString(), info.user); info.intent = intent; info.customIcon = customIcon; info.iconResource = iconResource; @@ -3323,12 +3648,18 @@ public class LauncherModel extends BroadcastReceiver { final Collator collator = Collator.getInstance(); return new Comparator<AppInfo>() { public final int compare(AppInfo a, AppInfo b) { - int result = collator.compare(a.title.toString().trim(), - b.title.toString().trim()); - if (result == 0) { - result = a.componentName.compareTo(b.componentName); + if (a.user.equals(b.user)) { + int result = collator.compare(a.title.toString().trim(), + b.title.toString().trim()); + if (result == 0) { + result = a.componentName.compareTo(b.componentName); + } + return result; + } else { + // TODO Need to figure out rules for sorting + // profiles, this puts work second. + return a.user.toString().compareTo(b.user.toString()); } - return result; } }; } @@ -3340,14 +3671,6 @@ public class LauncherModel extends BroadcastReceiver { 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().trim(), b.label.toString().trim()); - } - }; - } static ComponentName getComponentNameFromResolveInfo(ResolveInfo info) { if (info.activityInfo != null) { return new ComponentName(info.activityInfo.packageName, info.activityInfo.name); @@ -3355,35 +3678,32 @@ public class LauncherModel extends BroadcastReceiver { return new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name); } } - public static class ShortcutNameComparator implements Comparator<ResolveInfo> { + public static class ShortcutNameComparator implements Comparator<LauncherActivityInfoCompat> { 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; + ShortcutNameComparator(HashMap<Object, CharSequence> labelCache) { 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); + public final int compare(LauncherActivityInfoCompat a, LauncherActivityInfoCompat b) { + String labelA, labelB; + ComponentName keyA = a.getComponentName(); + ComponentName keyB = b.getComponentName(); if (mLabelCache.containsKey(keyA)) { - labelA = mLabelCache.get(keyA); + labelA = mLabelCache.get(keyA).toString(); } else { - labelA = a.loadLabel(mPackageManager).toString().trim(); + labelA = a.getLabel().toString().trim(); mLabelCache.put(keyA, labelA); } if (mLabelCache.containsKey(keyB)) { - labelB = mLabelCache.get(keyB); + labelB = mLabelCache.get(keyB).toString(); } else { - labelB = b.loadLabel(mPackageManager).toString().trim(); + labelB = b.getLabel().toString().trim(); mLabelCache.put(keyB, labelB); } @@ -3391,11 +3711,14 @@ public class LauncherModel extends BroadcastReceiver { } }; public static class WidgetAndShortcutNameComparator implements Comparator<Object> { - private Collator mCollator; - private PackageManager mPackageManager; - private HashMap<Object, String> mLabelCache; - WidgetAndShortcutNameComparator(PackageManager pm) { - mPackageManager = pm; + private final AppWidgetManagerCompat mManager; + private final PackageManager mPackageManager; + private final HashMap<Object, String> mLabelCache; + private final Collator mCollator; + + WidgetAndShortcutNameComparator(Context context) { + mManager = AppWidgetManagerCompat.getInstance(context); + mPackageManager = context.getPackageManager(); mLabelCache = new HashMap<Object, String>(); mCollator = Collator.getInstance(); } @@ -3404,23 +3727,28 @@ public class LauncherModel extends BroadcastReceiver { if (mLabelCache.containsKey(a)) { labelA = mLabelCache.get(a); } else { - labelA = (a instanceof AppWidgetProviderInfo) ? - ((AppWidgetProviderInfo) a).label : - ((ResolveInfo) a).loadLabel(mPackageManager).toString().trim(); + labelA = (a instanceof AppWidgetProviderInfo) + ? mManager.loadLabel((AppWidgetProviderInfo) a) + : ((ResolveInfo) a).loadLabel(mPackageManager).toString().trim(); 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().trim(); + labelB = (b instanceof AppWidgetProviderInfo) + ? mManager.loadLabel((AppWidgetProviderInfo) b) + : ((ResolveInfo) b).loadLabel(mPackageManager).toString().trim(); mLabelCache.put(b, labelB); } return mCollator.compare(labelA, labelB); } }; + static boolean isValidProvider(AppWidgetProviderInfo provider) { + return (provider != null) && (provider.provider != null) + && (provider.provider.getPackageName() != null); + } + public void dumpState() { Log.d(TAG, "mCallbacks=" + mCallbacks); AppInfo.dumpApplicationInfoList(TAG, "mAllAppsList.data", mBgAllAppsList.data); diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index a080dd8ca..6cc1688de 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -32,9 +32,10 @@ import android.content.Intent; import android.content.OperationApplicationException; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.res.Resources; -import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.database.Cursor; import android.database.SQLException; @@ -48,12 +49,13 @@ 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.SparseArray; -import android.util.Xml; +import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.config.ProviderConfig; import org.xmlpull.v1.XmlPullParser; @@ -63,6 +65,7 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -72,7 +75,7 @@ public class LauncherProvider extends ContentProvider { private static final String DATABASE_NAME = "launcher.db"; - private static final int DATABASE_VERSION = 17; + private static final int DATABASE_VERSION = 20; static final String OLD_AUTHORITY = "com.android.launcher2.settings"; static final String AUTHORITY = ProviderConfig.AUTHORITY; @@ -87,12 +90,14 @@ public class LauncherProvider extends ContentProvider { "UPGRADED_FROM_OLD_DATABASE"; static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; - static final String DEFAULT_WORKSPACE_RESOURCE_ID = - "DEFAULT_WORKSPACE_RESOURCE_ID"; private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE = "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE"; + private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd"; + + private LauncherProviderChangeListener mListener; + /** * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when * {@link AppWidgetHost#deleteHost()} is called during database creation. @@ -116,6 +121,10 @@ public class LauncherProvider extends ContentProvider { return mOpenHelper.wasNewDbCreated(); } + public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) { + mListener = listener; + } + @Override public String getType(Uri uri) { SqlArguments args = new SqlArguments(uri, null, null); @@ -146,7 +155,7 @@ public class LauncherProvider extends ContentProvider { if (values == null) { throw new RuntimeException("Error: attempting to insert null values"); } - if (!values.containsKey(LauncherSettings.BaseLauncherColumns._ID)) { + if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) { throw new RuntimeException("Error: attempting to add item without specifying an id"); } helper.checkId(table, values); @@ -163,6 +172,14 @@ public class LauncherProvider extends ContentProvider { public Uri insert(Uri uri, ContentValues initialValues) { SqlArguments args = new SqlArguments(uri); + // In very limited cases, we support system|signature permission apps to add to the db + String externalAdd = uri.getQueryParameter(URI_PARAM_IS_EXTERNAL_ADD); + if (externalAdd != null && "true".equals(externalAdd)) { + if (!mOpenHelper.initializeExternalAdd(initialValues)) { + return null; + } + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); addModifiedTime(initialValues); final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); @@ -174,6 +191,7 @@ public class LauncherProvider extends ContentProvider { return uri; } + @Override public int bulkInsert(Uri uri, ContentValues[] values) { SqlArguments args = new SqlArguments(uri); @@ -242,6 +260,9 @@ public class LauncherProvider extends ContentProvider { // always notify the backup agent LauncherBackupAgentHelper.dataChanged(getContext()); + if (mListener != null) { + mListener.onLauncherProviderChange(); + } } private void addModifiedTime(ContentValues values) { @@ -287,45 +308,64 @@ public class LauncherProvider extends ContentProvider { } /** - * @param workspaceResId that can be 0 to use default or non-zero for specific resource + * Clears all the data for a fresh start. */ - synchronized public void loadDefaultFavoritesIfNecessary(int origWorkspaceResId) { + synchronized public void createEmptyDB() { + mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); + } + + /** + * Loads the default workspace based on the following priority scheme: + * 1) From a package provided by play store + * 2) From a partner configuration APK, already in the system image + * 3) The default configuration for the particular device + */ + synchronized public void loadDefaultFavoritesIfNecessary() { String spKey = LauncherAppState.getSharedPreferencesKey(); SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE); if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) { Log.d(TAG, "loading default workspace"); - int workspaceResId = origWorkspaceResId; - // Use default workspace resource if none provided - if (workspaceResId == 0) { - workspaceResId = - sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, getDefaultWorkspaceResourceId()); + WorkspaceLoader loader = AutoInstallsLayout.get(getContext(), + mOpenHelper.mAppWidgetHost, mOpenHelper); + + if (loader == null) { + final Partner partner = Partner.get(getContext().getPackageManager()); + if (partner != null && partner.hasDefaultLayout()) { + final Resources partnerRes = partner.getResources(); + int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT, + "xml", partner.getPackageName()); + if (workspaceResId != 0) { + loader = new SimpleWorkspaceLoader(mOpenHelper, partnerRes, workspaceResId); + } + } } - // Populate favorites table with initial favorites - SharedPreferences.Editor editor = sp.edit(); - editor.remove(EMPTY_DATABASE_CREATED); - if (origWorkspaceResId != 0) { - editor.putInt(DEFAULT_WORKSPACE_RESOURCE_ID, origWorkspaceResId); + if (loader == null) { + loader = new SimpleWorkspaceLoader(mOpenHelper, getContext().getResources(), + getDefaultWorkspaceResourceId()); } - mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), workspaceResId); - mOpenHelper.setFlagJustLoadedOldDb(); + // Populate favorites table with initial favorites + SharedPreferences.Editor editor = sp.edit().remove(EMPTY_DATABASE_CREATED); + mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader); editor.commit(); } } public void migrateLauncher2Shortcuts() { mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(), - LauncherSettings.Favorites.OLD_CONTENT_URI); + Uri.parse(getContext().getString(R.string.old_launcher_provider_uri))); } private static int getDefaultWorkspaceResourceId() { + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); if (LauncherAppState.isDisableAllApps()) { - return R.xml.default_workspace_no_all_apps; + return grid.defaultNoAllAppsLayoutId; } else { - return R.xml.default_workspace; + return grid.defaultLayoutId; } } @@ -351,18 +391,39 @@ public class LauncherProvider extends ContentProvider { mOpenHelper = new DatabaseHelper(getContext()); } - private static class DatabaseHelper extends SQLiteOpenHelper { + private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback { + private static final String TAG_RESOLVE = "resolve"; 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_PARTNER_FOLDER = "partner-folder"; private static final String TAG_EXTRA = "extra"; private static final String TAG_INCLUDE = "include"; + // Style attrs -- "Favorite" + private static final String ATTR_CLASS_NAME = "className"; + private static final String ATTR_PACKAGE_NAME = "packageName"; + private static final String ATTR_CONTAINER = "container"; + private static final String ATTR_SCREEN = "screen"; + private static final String ATTR_X = "x"; + private static final String ATTR_Y = "y"; + private static final String ATTR_SPAN_X = "spanX"; + private static final String ATTR_SPAN_Y = "spanY"; + private static final String ATTR_ICON = "icon"; + private static final String ATTR_TITLE = "title"; + private static final String ATTR_URI = "uri"; + + // Style attrs -- "Include" + private static final String ATTR_WORKSPACE = "workspace"; + + // Style attrs -- "Extra" + private static final String ATTR_KEY = "key"; + private static final String ATTR_VALUE = "value"; + private final Context mContext; + private final PackageManager mPackageManager; private final AppWidgetHost mAppWidgetHost; private long mMaxItemId = -1; private long mMaxScreenId = -1; @@ -372,6 +433,7 @@ public class LauncherProvider extends ContentProvider { DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; + mPackageManager = context.getPackageManager(); mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from @@ -407,6 +469,10 @@ public class LauncherProvider extends ContentProvider { mMaxScreenId = 0; mNewDbCreated = true; + UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); + long userSerialNumber = userManager.getSerialNumberForUser( + UserHandleCompat.myUserHandle()); + db.execSQL("CREATE TABLE favorites (" + "_id INTEGER PRIMARY KEY," + "title TEXT," + @@ -428,7 +494,8 @@ public class LauncherProvider extends ContentProvider { "displayMode INTEGER," + "appWidgetProvider TEXT," + "modified INTEGER NOT NULL DEFAULT 0," + - "restored INTEGER NOT NULL DEFAULT 0" + + "restored INTEGER NOT NULL DEFAULT 0," + + "profileId INTEGER DEFAULT " + userSerialNumber + ");"); addWorkspacesTable(db); @@ -454,7 +521,7 @@ public class LauncherProvider extends ContentProvider { "/old_favorites?notify=true"); if (!convertDatabase(db, uri, permuteScreensCb, true)) { // Try and upgrade from the Launcher2 db - uri = LauncherSettings.Favorites.OLD_CONTENT_URI; + uri = Uri.parse(mContext.getString(R.string.old_launcher_provider_uri)); if (!convertDatabase(db, uri, permuteScreensCb, false)) { // If we fail, then set a flag to load the default workspace setFlagEmptyDbCreated(); @@ -480,6 +547,37 @@ public class LauncherProvider extends ContentProvider { ");"); } + private void removeOrphanedItems(SQLiteDatabase db) { + // Delete items directly on the workspace who's screen id doesn't exist + // "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens) + // AND container = -100" + String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES + + " WHERE " + + LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " + + LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" + + " AND " + + LauncherSettings.Favorites.CONTAINER + " = " + + LauncherSettings.Favorites.CONTAINER_DESKTOP; + db.execSQL(removeOrphanedDesktopItems); + + // Delete items contained in folders which no longer exist (after above statement) + // "DELETE FROM favorites WHERE container <> -100 AND container <> -101 AND container + // NOT IN (SELECT _id FROM favorites WHERE itemType = 2)" + String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES + + " WHERE " + + LauncherSettings.Favorites.CONTAINER + " <> " + + LauncherSettings.Favorites.CONTAINER_DESKTOP + + " AND " + + LauncherSettings.Favorites.CONTAINER + " <> " + + LauncherSettings.Favorites.CONTAINER_HOTSEAT + + " AND " + + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " + + LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES + + " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " + + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")"; + db.execSQL(removeOrphanedFolderItems); + } + private void setFlagJustLoadedOldDb() { String spKey = LauncherAppState.getSharedPreferencesKey(); SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); @@ -691,7 +789,8 @@ public class LauncherProvider extends ContentProvider { } // Add default hotseat icons - loadFavorites(db, R.xml.update_workspace); + loadFavorites(db, new SimpleWorkspaceLoader(this, mContext.getResources(), + R.xml.update_workspace)); version = 9; } @@ -780,6 +879,28 @@ public class LauncherProvider extends ContentProvider { version = 17; } + if (version < 18) { + // No-op + version = 18; + } + + if (version < 19) { + // Due to a data loss bug, some users may have items associated with screen ids + // which no longer exist. Since this can cause other problems, and since the user + // will never see these items anyway, we use database upgrade as an opportunity to + // clean things up. + removeOrphanedItems(db); + version = 19; + } + + if (version < 20) { + // Add userId column + if (addProfileColumn(db)) { + version = 20; + } + // else old version remains, which means we wipe old data + } + if (version != DATABASE_VERSION) { Log.w(TAG, "Destroying all old data."); db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES); @@ -789,6 +910,47 @@ public class LauncherProvider extends ContentProvider { } } + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // This shouldn't happen -- throw our hands up in the air and start over. + Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion + + ". Wiping databse."); + createEmptyDB(db); + } + + + /** + * Clears all the data for a fresh start. + */ + public void createEmptyDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS); + onCreate(db); + } + + private boolean addProfileColumn(SQLiteDatabase db) { + db.beginTransaction(); + try { + UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); + // Default to the serial number of this user, for older + // shortcuts. + long userSerialNumber = userManager.getSerialNumberForUser( + UserHandleCompat.myUserHandle()); + // Insert new column for holding user serial number + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN profileId INTEGER DEFAULT " + + userSerialNumber + ";"); + db.setTransactionSuccessful(); + } catch (SQLException ex) { + // Old version remains, which means we wipe old data + Log.e(TAG, ex.getMessage(), ex); + return false; + } finally { + db.endTransaction(); + } + return true; + } + private boolean updateContactsShortcuts(SQLiteDatabase db) { final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, new int[] { Favorites.ITEM_TYPE_SHORTCUT }); @@ -930,6 +1092,7 @@ public class LauncherProvider extends ContentProvider { // 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 + @Override public long generateNewItemId() { if (mMaxItemId < 0) { throw new RuntimeException("Error: max item id was not initialized"); @@ -938,6 +1101,11 @@ public class LauncherProvider extends ContentProvider { return mMaxItemId; } + @Override + public long insertAndCheck(SQLiteDatabase db, ContentValues values) { + return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values); + } + public void updateMaxItemId(long id) { mMaxItemId = id + 1; } @@ -1102,6 +1270,93 @@ public class LauncherProvider extends ContentProvider { if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId); } + private boolean initializeExternalAdd(ContentValues values) { + // 1. Ensure that externally added items have a valid item id + long id = generateNewItemId(); + values.put(LauncherSettings.Favorites._ID, id); + + // 2. In the case of an app widget, and if no app widget id is specified, we + // attempt allocate and bind the widget. + Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE); + if (itemType != null && + itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && + !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) { + + final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); + ComponentName cn = ComponentName.unflattenFromString( + values.getAsString(Favorites.APPWIDGET_PROVIDER)); + + if (cn != null) { + try { + int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); + values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId); + if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) { + return false; + } + } catch (RuntimeException e) { + Log.e(TAG, "Failed to initialize external widget", e); + return false; + } + } else { + return false; + } + } + + // Add screen id if not present + long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN); + if (!addScreenIdIfNecessary(screenId)) { + return false; + } + return true; + } + + // Returns true of screen id exists, or if successfully added + private boolean addScreenIdIfNecessary(long screenId) { + if (!hasScreenId(screenId)) { + int rank = getMaxScreenRank() + 1; + + ContentValues v = new ContentValues(); + v.put(LauncherSettings.WorkspaceScreens._ID, screenId); + v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank); + if (dbInsertAndCheck(this, getWritableDatabase(), + TABLE_WORKSPACE_SCREENS, null, v) < 0) { + return false; + } + } + return true; + } + + private boolean hasScreenId(long screenId) { + SQLiteDatabase db = getWritableDatabase(); + Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE " + + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null); + if (c != null) { + int count = c.getCount(); + c.close(); + return count > 0; + } else { + return false; + } + } + + private int getMaxScreenRank() { + SQLiteDatabase db = getWritableDatabase(); + Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK + + ") FROM " + TABLE_WORKSPACE_SCREENS, null); + + // get the result + final int maxRankIndex = 0; + int rank = -1; + if (c != null && c.moveToNext()) { + rank = c.getInt(maxRankIndex); + } + if (c != null) { + c.close(); + } + + return rank; + } + private static final void beginDocument(XmlPullParser parser, String firstElementName) throws XmlPullParserException, IOException { int type; @@ -1120,24 +1375,55 @@ public class LauncherProvider extends ContentProvider { } } + private static Intent buildMainIntent() { + Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + return intent; + } + + private int loadFavorites(SQLiteDatabase db, WorkspaceLoader loader) { + ArrayList<Long> screenIds = new ArrayList<Long>(); + // TODO: Use multiple loaders with fall-back and transaction. + int count = loader.loadLayout(db, screenIds); + + // Add the screens specified by the items above + Collections.sort(screenIds); + int rank = 0; + ContentValues values = new ContentValues(); + for (Long id : screenIds) { + values.clear(); + values.put(LauncherSettings.WorkspaceScreens._ID, id); + values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank); + if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) { + throw new RuntimeException("Failed initialize screen table" + + "from default layout"); + } + rank++; + } + + // Ensure that the max ids are initialized + mMaxItemId = initializeMaxItemId(db); + mMaxScreenId = initializeMaxScreenId(db); + + return count; + } + /** * 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 + * @param the set of screenIds which are used by the favorites */ - private int loadFavorites(SQLiteDatabase db, int workspaceResourceId) { - Intent intent = new Intent(Intent.ACTION_MAIN, null); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - ContentValues values = new ContentValues(); + private int loadFavoritesRecursive(SQLiteDatabase db, Resources res, int workspaceResourceId, + ArrayList<Long> screenIds) { + ContentValues values = new ContentValues(); if (LOGD) Log.v(TAG, String.format("Loading favorites from resid=0x%08x", workspaceResourceId)); - PackageManager packageManager = mContext.getPackageManager(); - int i = 0; + int count = 0; try { - XmlResourceParser parser = mContext.getResources().getXml(workspaceResourceId); - AttributeSet attrs = Xml.asAttributeSet(parser); + XmlResourceParser parser = res.getXml(workspaceResourceId); beginDocument(parser, TAG_FAVORITES); final int depth = parser.getDepth(); @@ -1154,38 +1440,34 @@ public class LauncherProvider extends ContentProvider { final String name = parser.getName(); if (TAG_INCLUDE.equals(name)) { - final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Include); - final int resId = a.getResourceId(R.styleable.Include_workspace, 0); + final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0); if (LOGD) Log.v(TAG, String.format(("%" + (2*(depth+1)) + "s<include workspace=%08x>"), "", resId)); if (resId != 0 && resId != workspaceResourceId) { // recursively load some more favorites, why not? - i += loadFavorites(db, resId); + count += loadFavoritesRecursive(db, res, resId, screenIds); added = false; } else { Log.w(TAG, String.format("Skipping <include workspace=0x%08x>", resId)); } - a.recycle(); - if (LOGD) Log.v(TAG, String.format(("%" + (2*(depth+1)) + "s</include>"), "")); continue; } // Assuming it's a <favorite> at this point - 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 strContainer = getAttributeValue(parser, ATTR_CONTAINER); + if (strContainer != null) { + container = Long.valueOf(strContainer); } - String screen = a.getString(R.styleable.Favorite_screen); - String x = a.getString(R.styleable.Favorite_x); - String y = a.getString(R.styleable.Favorite_y); + String screen = getAttributeValue(parser, ATTR_SCREEN); + String x = getAttributeValue(parser, ATTR_X); + String y = getAttributeValue(parser, ATTR_Y); values.clear(); values.put(LauncherSettings.Favorites.CONTAINER, container); @@ -1194,8 +1476,8 @@ public class LauncherProvider extends ContentProvider { values.put(LauncherSettings.Favorites.CELLY, y); if (LOGD) { - final String title = a.getString(R.styleable.Favorite_title); - final String pkg = a.getString(R.styleable.Favorite_packageName); + final String title = getAttributeValue(parser, ATTR_TITLE); + final String pkg = getAttributeValue(parser, ATTR_PACKAGE_NAME); final String something = title != null ? title : pkg; Log.v(TAG, String.format( ("%" + (2*(depth+1)) + "s<%s%s c=%d s=%s x=%s y=%s>"), @@ -1205,82 +1487,62 @@ public class LauncherProvider extends ContentProvider { } if (TAG_FAVORITE.equals(name)) { - long id = addAppShortcut(db, values, a, packageManager, intent); + long id = addAppShortcut(db, values, parser); 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); + added = addAppWidget(parser, type, db, values); } else if (TAG_SHORTCUT.equals(name)) { - long id = addUriShortcut(db, values, a); + long id = addUriShortcut(db, values, res, parser); 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(); + } else if (TAG_RESOLVE.equals(name)) { + // This looks through the contained favorites (or meta-favorites) and + // attempts to add them as shortcuts in the fallback group's location + // until one is added successfully. + added = false; + final int groupDepth = parser.getDepth(); while ((type = parser.next()) != XmlPullParser.END_TAG || - parser.getDepth() > folderDepth) { + parser.getDepth() > groupDepth) { 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 (LOGD) { - final String pkg = ar.getString(R.styleable.Favorite_packageName); - final String uri = ar.getString(R.styleable.Favorite_uri); - Log.v(TAG, String.format(("%" + (2*(folderDepth+1)) + "s<%s \"%s\">"), "", - folder_item_name, uri != null ? uri : pkg)); - } - - if (TAG_FAVORITE.equals(folder_item_name) && folderId >= 0) { - long id = - addAppShortcut(db, values, ar, packageManager, intent); - if (id >= 0) { - folderItems.add(id); + final String fallback_item_name = parser.getName(); + if (!added) { + if (TAG_FAVORITE.equals(fallback_item_name)) { + final long id = addAppShortcut(db, values, parser); + added = id >= 0; + } else { + Log.e(TAG, "Fallback groups can contain only favorites, found " + + fallback_item_name); } - } 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)); + } else if (TAG_FOLDER.equals(name)) { + // Folder contents are nested in this XML file + added = loadFolder(db, values, res, parser); + + } else if (TAG_PARTNER_FOLDER.equals(name)) { + // Folder contents come from an external XML resource + final Partner partner = Partner.get(mPackageManager); + if (partner != null) { + final Resources partnerRes = partner.getResources(); + final int resId = partnerRes.getIdentifier(Partner.RES_FOLDER, + "xml", partner.getPackageName()); + if (resId != 0) { + final XmlResourceParser partnerParser = partnerRes.getXml(resId); + beginDocument(partnerParser, TAG_FOLDER); + added = loadFolder(db, values, partnerRes, partnerParser); } - added = false; } } - if (added) i++; - a.recycle(); + if (added) { + long screenId = Long.parseLong(screen); + // Keep track of the set of screens which need to be added to the db. + if (!screenIds.contains(screenId) && + container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + screenIds.add(screenId); + } + count++; + } } } catch (XmlPullParserException e) { Log.w(TAG, "Got exception parsing favorites.", e); @@ -1289,50 +1551,231 @@ public class LauncherProvider extends ContentProvider { } catch (RuntimeException e) { Log.w(TAG, "Got exception parsing favorites.", e); } + return count; + } - // Update the max item id after we have loaded the database - if (mMaxItemId == -1) { - mMaxItemId = initializeMaxItemId(db); + /** + * Parse folder items starting at {@link XmlPullParser} location. Allow recursive + * includes of items. + */ + private void addToFolder(SQLiteDatabase db, Resources res, XmlResourceParser parser, + ArrayList<Long> folderItems, long folderId) throws IOException, XmlPullParserException { + int type; + int folderDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > folderDepth) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final String tag = parser.getName(); + + final ContentValues childValues = new ContentValues(); + childValues.put(LauncherSettings.Favorites.CONTAINER, folderId); + + if (LOGD) { + final String pkg = getAttributeValue(parser, ATTR_PACKAGE_NAME); + final String uri = getAttributeValue(parser, ATTR_URI); + Log.v(TAG, String.format(("%" + (2*(folderDepth+1)) + "s<%s \"%s\">"), "", + tag, uri != null ? uri : pkg)); + } + + if (TAG_FAVORITE.equals(tag) && folderId >= 0) { + final long id = addAppShortcut(db, childValues, parser); + if (id >= 0) { + folderItems.add(id); + } + } else if (TAG_SHORTCUT.equals(tag) && folderId >= 0) { + final long id = addUriShortcut(db, childValues, res, parser); + if (id >= 0) { + folderItems.add(id); + } + } else if (TAG_INCLUDE.equals(tag) && folderId >= 0) { + addToFolder(db, res, parser, folderItems, folderId); + } else { + throw new RuntimeException("Folders can contain only shortcuts"); + } } + } - return i; + /** + * Parse folder starting at current {@link XmlPullParser} location. + */ + private boolean loadFolder(SQLiteDatabase db, ContentValues values, Resources res, + XmlResourceParser parser) throws IOException, XmlPullParserException { + final String title; + final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0); + if (titleResId != 0) { + title = res.getString(titleResId); + } else { + title = mContext.getResources().getString(R.string.folder_name); + } + + values.put(LauncherSettings.Favorites.TITLE, title); + long folderId = addFolder(db, values); + boolean added = folderId >= 0; + + ArrayList<Long> folderItems = new ArrayList<Long>(); + addToFolder(db, res, parser, folderItems, folderId); + + // 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) { + // Delete the folder + deleteId(db, folderId); + + // If we have a single item, promote it to where the folder + // would have been. + if (folderItems.size() == 1) { + final ContentValues childValues = new ContentValues(); + copyInteger(values, childValues, LauncherSettings.Favorites.CONTAINER); + copyInteger(values, childValues, LauncherSettings.Favorites.SCREEN); + copyInteger(values, childValues, LauncherSettings.Favorites.CELLX); + copyInteger(values, childValues, LauncherSettings.Favorites.CELLY); + + final long id = folderItems.get(0); + db.update(TABLE_FAVORITES, childValues, + LauncherSettings.Favorites._ID + "=" + id, null); + } else { + added = false; + } + } + return added; } - 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); + // A meta shortcut attempts to resolve an intent specified as a URI in the XML, if a + // logical choice for what shortcut should be used for that intent exists, then it is + // added. Otherwise add nothing. + private long addAppShortcutByUri(SQLiteDatabase db, ContentValues values, + String intentUri) { + Intent metaIntent; try { - ComponentName cn; + metaIntent = Intent.parseUri(intentUri, 0); + } catch (URISyntaxException e) { + Log.e(TAG, "Unable to add meta-favorite: " + intentUri, e); + return -1; + } + + ResolveInfo resolved = mPackageManager.resolveActivity(metaIntent, + PackageManager.MATCH_DEFAULT_ONLY); + final List<ResolveInfo> appList = mPackageManager.queryIntentActivities( + metaIntent, PackageManager.MATCH_DEFAULT_ONLY); + + // Verify that the result is an app and not just the resolver dialog asking which + // app to use. + if (wouldLaunchResolverActivity(resolved, appList)) { + // If only one of the results is a system app then choose that as the default. + final ResolveInfo systemApp = getSingleSystemActivity(appList); + if (systemApp == null) { + // There is no logical choice for this meta-favorite, so rather than making + // a bad choice just add nothing. + Log.w(TAG, "No preference or single system activity found for " + + metaIntent.toString()); + return -1; + } + resolved = systemApp; + } + final ActivityInfo info = resolved.activityInfo; + final Intent intent = mPackageManager.getLaunchIntentForPackage(info.packageName); + if (intent == null) { + return -1; + } + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + + return addAppShortcut(db, values, info.loadLabel(mPackageManager).toString(), intent); + } + + private ResolveInfo getSingleSystemActivity(List<ResolveInfo> appList) { + ResolveInfo systemResolve = null; + final int N = appList.size(); + for (int i = 0; i < N; ++i) { 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); + ApplicationInfo info = mPackageManager.getApplicationInfo( + appList.get(i).activityInfo.packageName, 0); + if ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + if (systemResolve != null) { + return null; + } else { + systemResolve = appList.get(i); + } + } + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Unable to get info about resolve results", e); + return null; } - id = generateNewItemId(); - 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, generateNewItemId()); - if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) { - return -1; + } + return systemResolve; + } + + private boolean wouldLaunchResolverActivity(ResolveInfo resolved, + List<ResolveInfo> appList) { + // If the list contains the above resolved activity, then it can't be + // ResolverActivity itself. + for (int i = 0; i < appList.size(); ++i) { + ResolveInfo tmp = appList.get(i); + if (tmp.activityInfo.name.equals(resolved.activityInfo.name) + && tmp.activityInfo.packageName.equals(resolved.activityInfo.packageName)) { + return false; } - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Unable to add favorite: " + packageName + - "/" + className, e); } - return id; + return true; + } + + private long addAppShortcut(SQLiteDatabase db, ContentValues values, + XmlResourceParser parser) { + final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); + final String className = getAttributeValue(parser, ATTR_CLASS_NAME); + final String uri = getAttributeValue(parser, ATTR_URI); + + if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) { + ActivityInfo info; + try { + ComponentName cn; + try { + cn = new ComponentName(packageName, className); + info = mPackageManager.getActivityInfo(cn, 0); + } catch (PackageManager.NameNotFoundException nnfe) { + String[] packages = mPackageManager.currentToCanonicalPackageNames( + new String[] { packageName }); + cn = new ComponentName(packages[0], className); + info = mPackageManager.getActivityInfo(cn, 0); + } + final Intent intent = buildMainIntent(); + intent.setComponent(cn); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + + return addAppShortcut(db, values, info.loadLabel(mPackageManager).toString(), + intent); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Unable to add favorite: " + packageName + + "/" + className, e); + } + return -1; + } else if (!TextUtils.isEmpty(uri)) { + // If no component specified try to find a shortcut to add from the URI. + return addAppShortcutByUri(db, values, uri); + } else { + Log.e(TAG, "Skipping invalid <favorite> with no component or uri"); + return -1; + } + } + + private long addAppShortcut(SQLiteDatabase db, ContentValues values, String title, + Intent intent) { + long id = generateNewItemId(); + values.put(Favorites.INTENT, intent.toUri(0)); + values.put(Favorites.TITLE, title); + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION); + values.put(Favorites.SPANX, 1); + values.put(Favorites.SPANY, 1); + values.put(Favorites._ID, id); + if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) { + return -1; + } else { + return id; + } } private long addFolder(SQLiteDatabase db, ContentValues values) { @@ -1374,23 +1817,12 @@ public class LauncherProvider extends ContentProvider { 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 { + private boolean addAppWidget(XmlResourceParser parser, int type, + SQLiteDatabase db, ContentValues values) + throws XmlPullParserException, IOException { - String packageName = a.getString(R.styleable.Favorite_packageName); - String className = a.getString(R.styleable.Favorite_className); + String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); + String className = getAttributeValue(parser, ATTR_CLASS_NAME); if (packageName == null || className == null) { return false; @@ -1399,21 +1831,25 @@ public class LauncherProvider extends ContentProvider { boolean hasPackage = true; ComponentName cn = new ComponentName(packageName, className); try { - packageManager.getReceiverInfo(cn, 0); + mPackageManager.getReceiverInfo(cn, 0); } catch (Exception e) { - String[] packages = packageManager.currentToCanonicalPackageNames( + String[] packages = mPackageManager.currentToCanonicalPackageNames( new String[] { packageName }); cn = new ComponentName(packages[0], className); try { - packageManager.getReceiverInfo(cn, 0); + mPackageManager.getReceiverInfo(cn, 0); } catch (Exception e1) { + System.out.println("Can't find widget provider: " + className); hasPackage = false; } } if (hasPackage) { - int spanX = a.getInt(R.styleable.Favorite_spanX, 0); - int spanY = a.getInt(R.styleable.Favorite_spanY, 0); + String spanX = getAttributeValue(parser, ATTR_SPAN_X); + String spanY = getAttributeValue(parser, ATTR_SPAN_Y); + + values.put(Favorites.SPANX, spanX); + values.put(Favorites.SPANY, spanY); // Read the extras Bundle extras = new Bundle(); @@ -1424,10 +1860,9 @@ public class LauncherProvider extends ContentProvider { 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); + String key = getAttributeValue(parser, ATTR_KEY); + String value = getAttributeValue(parser, ATTR_VALUE); if (key != null && value != null) { extras.putString(key, value); } else { @@ -1436,16 +1871,16 @@ public class LauncherProvider extends ContentProvider { } else { throw new RuntimeException("Widgets can contain only extras"); } - ar.recycle(); } - return addAppWidget(db, values, cn, spanX, spanY, extras); + return addAppWidget(db, values, cn, extras); } return false; } + private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn, - int spanX, int spanY, Bundle extras) { + Bundle extras) { boolean allocatedAppWidgets = false; final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); @@ -1453,8 +1888,6 @@ public class LauncherProvider extends ContentProvider { 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.APPWIDGET_PROVIDER, cn.flattenToString()); values.put(Favorites._ID, generateNewItemId()); @@ -1480,17 +1913,15 @@ public class LauncherProvider extends ContentProvider { 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); + private long addUriShortcut(SQLiteDatabase db, ContentValues values, Resources res, + XmlResourceParser parser) { + final int iconResId = getAttributeResourceValue(parser, ATTR_ICON, 0); + final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0); Intent intent; String uri = null; try { - uri = a.getString(R.styleable.Favorite_uri); + uri = getAttributeValue(parser, ATTR_URI); intent = Intent.parseUri(uri, 0); } catch (URISyntaxException e) { Log.w(TAG, "Shortcut has malformed uri: " + uri); @@ -1505,13 +1936,13 @@ public class LauncherProvider extends ContentProvider { long id = generateNewItemId(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); values.put(Favorites.INTENT, intent.toUri(0)); - values.put(Favorites.TITLE, r.getString(titleResId)); + values.put(Favorites.TITLE, res.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.ICON_PACKAGE, res.getResourcePackageName(iconResId)); + values.put(Favorites.ICON_RESOURCE, res.getResourceName(iconResId)); values.put(Favorites._ID, id); if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) { @@ -1520,7 +1951,7 @@ public class LauncherProvider extends ContentProvider { return id; } - public void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { + private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { final ContentResolver resolver = mContext.getContentResolver(); Cursor c = null; int count = 0; @@ -1563,6 +1994,8 @@ public class LauncherProvider extends ContentProvider { = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE); + final int profileIndex + = c.getColumnIndex(LauncherSettings.Favorites.PROFILE_ID); int i = 0; int curX = 0; @@ -1573,7 +2006,6 @@ public class LauncherProvider extends ContentProvider { final int width = (int) grid.numColumns; final int height = (int) grid.numRows; final int hotseatWidth = (int) grid.numHotseatIcons; - PackageManager pm = mContext.getPackageManager(); final HashSet<String> seenIntents = new HashSet<String>(c.getCount()); @@ -1594,6 +2026,19 @@ public class LauncherProvider extends ContentProvider { final int screen = c.getInt(screenIndex); int container = c.getInt(containerIndex); final String intentStr = c.getString(intentIndex); + + UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); + UserHandleCompat userHandle; + final long userSerialNumber; + if (profileIndex != -1 && !c.isNull(profileIndex)) { + userSerialNumber = c.getInt(profileIndex); + userHandle = userManager.getUserForSerialNumber(userSerialNumber); + } else { + // Default to the serial number of this user, for older + // shortcuts. + userHandle = UserHandleCompat.myUserHandle(); + userSerialNumber = userManager.getSerialNumberForUser(userHandle); + } Launcher.addDumpLog(TAG, "migrating \"" + c.getString(titleIndex) + "\" (" + cellX + "," + cellY + "@" @@ -1620,7 +2065,8 @@ public class LauncherProvider extends ContentProvider { Launcher.addDumpLog(TAG, "skipping empty intent", true); continue; } else if (cn != null && - !LauncherModel.isValidPackageComponent(pm, cn)) { + !LauncherModel.isValidPackageActivity(mContext, cn, + userHandle)) { // component no longer exists. Launcher.addDumpLog(TAG, "skipping item whose component " + "no longer exists.", true); @@ -1659,6 +2105,7 @@ public class LauncherProvider extends ContentProvider { values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex)); values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex)); + values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber); if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { hotseat.put(screen, values); @@ -1792,7 +2239,7 @@ public class LauncherProvider extends ContentProvider { * 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) { + private 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]); @@ -1803,6 +2250,38 @@ public class LauncherProvider extends ContentProvider { return selectWhere.toString(); } + /** + * Return attribute value, attempting launcher-specific namespace first + * before falling back to anonymous attribute. + */ + private static String getAttributeValue(XmlResourceParser parser, String attribute) { + String value = parser.getAttributeValue( + "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute); + if (value == null) { + value = parser.getAttributeValue(null, attribute); + } + return value; + } + + /** + * Return attribute resource value, attempting launcher-specific namespace + * first before falling back to anonymous attribute. + */ + private static int getAttributeResourceValue(XmlResourceParser parser, String attribute, + int defaultValue) { + int value = parser.getAttributeResourceValue( + "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute, + defaultValue); + if (value == defaultValue) { + value = parser.getAttributeResourceValue(null, attribute, defaultValue); + } + return value; + } + + private static void copyInteger(ContentValues from, ContentValues to, String key) { + to.put(key, from.getAsInteger(key)); + } + static class SqlArguments { public final String table; public final String where; @@ -1834,4 +2313,29 @@ public class LauncherProvider extends ContentProvider { } } } + + static interface WorkspaceLoader { + /** + * @param screenIds A mutable list of screen its + * @return the number of workspace items added. + */ + int loadLayout(SQLiteDatabase db, ArrayList<Long> screenIds); + } + + private static class SimpleWorkspaceLoader implements WorkspaceLoader { + private final Resources mRes; + private final int mWorkspaceId; + private final DatabaseHelper mHelper; + + SimpleWorkspaceLoader(DatabaseHelper helper, Resources res, int workspaceId) { + mHelper = helper; + mRes = res; + mWorkspaceId = workspaceId; + } + + @Override + public int loadLayout(SQLiteDatabase db, ArrayList<Long> screenIds) { + return mHelper.loadFavoritesRecursive(db, mRes, mWorkspaceId, screenIds); + } + } } diff --git a/src/com/android/launcher3/LauncherProviderChangeListener.java b/src/com/android/launcher3/LauncherProviderChangeListener.java new file mode 100644 index 000000000..0de96fbc4 --- /dev/null +++ b/src/com/android/launcher3/LauncherProviderChangeListener.java @@ -0,0 +1,11 @@ +package com.android.launcher3; + +/** + * This class is a listener for {@link LauncherProvider} changes. It gets notified in the + * sendNotify method. This listener is needed because by default the Launcher suppresses + * standard data change callbacks. + */ +public interface LauncherProviderChangeListener { + + public void onLauncherProviderChange(); +} diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 2a768a278..355370283 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -212,6 +212,14 @@ class LauncherSettings { static final String SPANY = "spanY"; /** + * The profile id of the item in the cell. + * <P> + * Type: INTEGER + * </P> + */ + static final String PROFILE_ID = "profileId"; + + /** * The favorite is a user created folder */ static final int ITEM_TYPE_FOLDER = 2; diff --git a/src/com/android/launcher3/LogAccelerateInterpolator.java b/src/com/android/launcher3/LogAccelerateInterpolator.java new file mode 100644 index 000000000..c3bbfa536 --- /dev/null +++ b/src/com/android/launcher3/LogAccelerateInterpolator.java @@ -0,0 +1,25 @@ +package com.android.launcher3; + +import android.animation.TimeInterpolator; + +public class LogAccelerateInterpolator implements TimeInterpolator { + + int mBase; + int mDrift; + final float mLogScale; + + public LogAccelerateInterpolator(int base, int drift) { + mBase = base; + mDrift = drift; + mLogScale = 1f / computeLog(1, mBase, mDrift); + } + + static float computeLog(float t, int base, int drift) { + return (float) -Math.pow(base, -t) + 1 + (drift * t); + } + + @Override + public float getInterpolation(float t) { + return 1 - computeLog(1 - t, mBase, mDrift) * mLogScale; + } +} diff --git a/src/com/android/launcher3/LogDecelerateInterpolator.java b/src/com/android/launcher3/LogDecelerateInterpolator.java new file mode 100644 index 000000000..4c5f6f08c --- /dev/null +++ b/src/com/android/launcher3/LogDecelerateInterpolator.java @@ -0,0 +1,26 @@ +package com.android.launcher3; + +import android.animation.TimeInterpolator; + +public class LogDecelerateInterpolator implements TimeInterpolator { + + int mBase; + int mDrift; + final float mLogScale; + + public LogDecelerateInterpolator(int base, int drift) { + mBase = base; + mDrift = drift; + + mLogScale = 1f / computeLog(1, mBase, mDrift); + } + + static float computeLog(float t, int base, int drift) { + return (float) -Math.pow(base, -t) + 1 + (drift * t); + } + + @Override + public float getInterpolation(float t) { + return computeLog(t, mBase, mDrift) * mLogScale; + } +} diff --git a/src/com/android/launcher3/MainThreadExecutor.java b/src/com/android/launcher3/MainThreadExecutor.java new file mode 100644 index 000000000..866b17c71 --- /dev/null +++ b/src/com/android/launcher3/MainThreadExecutor.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2014 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 java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * An executor service that executes its tasks on the main thread. + * + * Shutting down this executor is not supported. + */ +public class MainThreadExecutor extends AbstractExecutorService { + + private Handler mHandler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable runnable) { + if (Looper.getMainLooper() == Looper.myLooper()) { + runnable.run(); + } else { + mHandler.post(runnable); + } + } + + /** + * Not supported and throws an exception when used. + */ + @Override + @Deprecated + public void shutdown() { + throw new UnsupportedOperationException(); + } + + /** + * Not supported and throws an exception when used. + */ + @Override + @Deprecated + public List<Runnable> shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + /** + * Not supported and throws an exception when used. + */ + @Override + @Deprecated + public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException { + throw new UnsupportedOperationException(); + } +} diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 8d5d8dd4d..48fc0c98f 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -74,11 +74,12 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private static final int MIN_LENGTH_FOR_FLING = 25; protected static final int PAGE_SNAP_ANIMATION_DURATION = 750; + protected static final int OVER_SCROLL_PAGE_SNAP_ANIMATION_DURATION = 350; 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 OVERSCROLL_DAMP_FACTOR = 0.07f; 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. @@ -152,16 +153,11 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected int mTouchState = TOUCH_STATE_REST; protected boolean mForceScreenScrolled = false; - protected OnLongClickListener mLongClickListener; protected int mTouchSlop; private int mPagingTouchSlop; private int mMaximumVelocity; - protected int mPageLayoutPaddingTop; - protected int mPageLayoutPaddingBottom; - protected int mPageLayoutPaddingLeft; - protected int mPageLayoutPaddingRight; protected int mPageLayoutWidthGap; protected int mPageLayoutHeightGap; protected int mCellCountX = 0; @@ -171,6 +167,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected int mUnboundedScrollX; protected int[] mTempVisiblePagesRange = new int[2]; protected boolean mForceDrawAllChildrenNextFrame; + private boolean mSpacePagesAutomatically = false; // 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 @@ -283,14 +280,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PagedView, defStyle, 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( @@ -339,8 +328,9 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Hook up the page indicator ViewGroup parent = (ViewGroup) getParent(); + ViewGroup grandParent = (ViewGroup) parent.getParent(); if (mPageIndicator == null && mPageIndicatorViewId > -1) { - mPageIndicator = (PageIndicator) parent.findViewById(mPageIndicatorViewId); + mPageIndicator = (PageIndicator) grandParent.findViewById(mPageIndicatorViewId); mPageIndicator.removeAllMarkers(mAllowPagedViewAnimations); ArrayList<PageIndicator.PageMarkerResources> markers = @@ -547,6 +537,19 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc mNextPage = INVALID_PAGE; } + private int validateNewPage(int newPage) { + int validatedPage = newPage; + // When in free scroll mode, we need to clamp to the free scroll page range. + if (mFreeScroll) { + getFreeScrollPageRange(mTempVisiblePagesRange); + validatedPage = Math.max(mTempVisiblePagesRange[0], + Math.min(newPage, mTempVisiblePagesRange[1])); + } + // Ensure that it is clamped by the actual set of children in all cases + validatedPage = Math.max(0, Math.min(validatedPage, getPageCount() - 1)); + return validatedPage; + } + /** * Sets the current page. */ @@ -560,7 +563,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return; } mForceScreenScrolled = true; - mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1)); + mCurrentPage = validateNewPage(currentPage); updateCurrentPageScroll(); notifyPageSwitchListener(); invalidate(); @@ -591,8 +594,11 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private void updatePageIndicator() { // Update the page indicator (when we aren't reordering) - if (mPageIndicator != null && !isReordering(false)) { - mPageIndicator.setActiveMarker(getNextPage()); + if (mPageIndicator != null) { + mPageIndicator.setContentDescription(getPageIndicatorDescription()); + if (!isReordering(false)) { + mPageIndicator.setActiveMarker(getNextPage()); + } } } protected void pageBeginMoving() { @@ -727,7 +733,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } else if (mNextPage != INVALID_PAGE) { sendScrollAccessibilityEvent(); - mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1)); + mCurrentPage = validateNewPage(mNextPage); mNextPage = INVALID_PAGE; notifyPageSwitchListener(); @@ -843,6 +849,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc final int verticalPadding = getPaddingTop() + getPaddingBottom(); final int horizontalPadding = getPaddingLeft() + getPaddingRight(); + int referenceChildWidth = 0; // 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); @@ -887,6 +894,9 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc childWidth = getViewportWidth() - mInsets.left - mInsets.right; childHeight = getViewportHeight(); } + if (referenceChildWidth == 0) { + referenceChildWidth = childWidth; + } final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, childWidthMode); @@ -895,9 +905,24 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } + if (mSpacePagesAutomatically) { + int spacing = (getViewportWidth() - mInsets.left - mInsets.right + - referenceChildWidth) / 2; + if (spacing >= 0) { + setPageSpacing(spacing); + } + mSpacePagesAutomatically = false; + } setMeasuredDimension(scaledWidthSize, scaledHeightSize); } + /** + * This method should be called once before first layout / measure pass. + */ + protected void setSinglePageInViewport() { + mSpacePagesAutomatically = true; + } + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (!mIsDataReady || getChildCount() == 0) { @@ -974,9 +999,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { - setHorizontalScrollBarEnabled(false); updateCurrentPageScroll(); - setHorizontalScrollBarEnabled(true); mFirstLayout = false; } @@ -1105,7 +1128,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return offset; } - protected void getOverviewModePages(int[] range) { + protected void getFreeScrollPageRange(int[] range) { range[0] = 0; range[1] = Math.max(0, getChildCount() - 1); } @@ -1158,7 +1181,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } protected boolean shouldDrawChild(View child) { - return child.getAlpha() > 0 && child.getVisibility() == VISIBLE; + return child.getVisibility() == VISIBLE; } @Override @@ -1580,29 +1603,20 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return f * f * f + 1.0f; } - protected void acceleratedOverScroll(float amount) { + protected float acceleratedOverFactor(float amount) { int screenSize = getViewportWidth(); // 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; + if (f == 0) return 0; // 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(); + return f; } protected void dampedOverScroll(float amount) { @@ -1621,10 +1635,10 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize); if (amount < 0) { mOverScrollX = overScrollAmount; - super.scrollTo(0, getScrollY()); + super.scrollTo(mOverScrollX, getScrollY()); } else { mOverScrollX = mMaxScrollX + overScrollAmount; - super.scrollTo(mMaxScrollX, getScrollY()); + super.scrollTo(mOverScrollX, getScrollY()); } invalidate(); } @@ -1650,7 +1664,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } void updateFreescrollBounds() { - getOverviewModePages(mTempVisiblePagesRange); + getFreeScrollPageRange(mTempVisiblePagesRange); if (isLayoutRtl()) { mFreeScrollMinScrollX = getScrollForPage(mTempVisiblePagesRange[1]); mFreeScrollMaxScrollX = getScrollForPage(mTempVisiblePagesRange[0]); @@ -1665,7 +1679,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (mFreeScroll) { updateFreescrollBounds(); - getOverviewModePages(mTempVisiblePagesRange); + getFreeScrollPageRange(mTempVisiblePagesRange); if (getCurrentPage() < mTempVisiblePagesRange[0]) { setCurrentPage(mTempVisiblePagesRange[0]); } else if (getCurrentPage() > mTempVisiblePagesRange[1]) { @@ -1684,7 +1698,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (mDragView != null) { int dragX = (int) (mDragView.getLeft() + (mDragView.getMeasuredWidth() / 2) + mDragView.getTranslationX()); - getOverviewModePages(mTempVisiblePagesRange); + getFreeScrollPageRange(mTempVisiblePagesRange); int minDistance = Integer.MAX_VALUE; int minIndex = indexOfChild(mDragView); for (int i = mTempVisiblePagesRange[0]; i <= mTempVisiblePagesRange[1]; i++) { @@ -1801,7 +1815,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc !isHoveringOverDelete) { mTempVisiblePagesRange[0] = 0; mTempVisiblePagesRange[1] = getPageCount() - 1; - getOverviewModePages(mTempVisiblePagesRange); + getFreeScrollPageRange(mTempVisiblePagesRange); if (mTempVisiblePagesRange[0] <= pageUnderPointIndex && pageUnderPointIndex <= mTempVisiblePagesRange[1] && pageUnderPointIndex != mSidePageHoverIndex && mScroller.isFinished()) { @@ -2124,8 +2138,20 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return minDistanceFromScreenCenterIndex; } + protected boolean isInOverScroll() { + return (mOverScrollX > mMaxScrollX || mOverScrollX < 0); + } + + protected int getPageSnapDuration() { + if (isInOverScroll()) { + return OVER_SCROLL_PAGE_SNAP_ANIMATION_DURATION; + } + return PAGE_SNAP_ANIMATION_DURATION; + + } + protected void snapToDestination() { - snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); + snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration()); } private static class ScrollInterpolator implements Interpolator { @@ -2149,17 +2175,17 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } protected void snapToPageWithVelocity(int whichPage, int velocity) { - whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); + whichPage = validateNewPage(whichPage); int halfScreenSize = getViewportWidth() / 2; final int newX = getScrollForPage(whichPage); int delta = newX - mUnboundedScrollX; int duration = 0; - if (Math.abs(velocity) < mMinFlingVelocity) { + if (Math.abs(velocity) < mMinFlingVelocity || isInOverScroll()) { // 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); + snapToPage(whichPage, getPageSnapDuration()); return; } @@ -2183,11 +2209,11 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } protected void snapToPage(int whichPage) { - snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); + snapToPage(whichPage, getPageSnapDuration()); } protected void snapToPageImmediately(int whichPage) { - snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null); + snapToPage(whichPage, getPageSnapDuration(), true, null); } protected void snapToPage(int whichPage, int duration) { @@ -2200,7 +2226,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected void snapToPage(int whichPage, int duration, boolean immediate, TimeInterpolator interpolator) { - whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1)); + whichPage = validateNewPage(whichPage); int newX = getScrollForPage(whichPage); final int sX = mUnboundedScrollX; @@ -2214,6 +2240,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected void snapToPage(int whichPage, int delta, int duration, boolean immediate, TimeInterpolator interpolator) { + whichPage = validateNewPage(whichPage); + mNextPage = whichPage; View focusedChild = getFocusedChild(); if (focusedChild != null && whichPage != mCurrentPage && @@ -2482,11 +2510,11 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc public boolean startReordering(View v) { int dragViewIndex = indexOfChild(v); - if (mTouchState != TOUCH_STATE_REST) return false; + if (mTouchState != TOUCH_STATE_REST || dragViewIndex == -1) return false; mTempVisiblePagesRange[0] = 0; mTempVisiblePagesRange[1] = getPageCount() - 1; - getOverviewModePages(mTempVisiblePagesRange); + getFreeScrollPageRange(mTempVisiblePagesRange); mReorderingStarted = true; // Check if we are within the reordering range @@ -2619,7 +2647,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // in the layout) // NOTE: We can make an assumption here because we have side-bound pages that we // will always have pages to animate in from the left - getOverviewModePages(mTempVisiblePagesRange); + getFreeScrollPageRange(mTempVisiblePagesRange); boolean isLastWidgetPage = (mTempVisiblePagesRange[0] == mTempVisiblePagesRange[1]); boolean slideFromLeft = (isLastWidgetPage || dragViewIndex > mTempVisiblePagesRange[0]); diff --git a/src/com/android/launcher3/PagedViewGridLayout.java b/src/com/android/launcher3/PagedViewGridLayout.java index b28686113..f69fa562d 100644 --- a/src/com/android/launcher3/PagedViewGridLayout.java +++ b/src/com/android/launcher3/PagedViewGridLayout.java @@ -56,18 +56,6 @@ public class PagedViewGridLayout extends GridLayout implements Page { } } - 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(); diff --git a/src/com/android/launcher3/PagedViewIcon.java b/src/com/android/launcher3/PagedViewIcon.java deleted file mode 100644 index f7cb997cd..000000000 --- a/src/com/android/launcher3/PagedViewIcon.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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.graphics.Canvas; -import android.graphics.Region; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.TypedValue; -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 onFinishInflate() { - super.onFinishInflate(); - - // Ensure we are using the right text size - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); - } - - public void applyFromApplicationInfo(AppInfo info, boolean scaleUp, - PagedViewIcon.PressedCallback cb) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - - mIcon = info.iconBitmap; - mPressedCallback = cb; - Drawable icon = Utilities.createIconDrawable(mIcon); - icon.setBounds(0, 0, grid.allAppsIconSizePx, grid.allAppsIconSizePx); - setCompoundDrawables(null, icon, null, null); - setCompoundDrawablePadding(grid.iconDrawablePaddingPx); - 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); - } - } - - @Override - public void draw(Canvas canvas) { - // 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(BubbleTextView.SHADOW_LARGE_RADIUS, 0.0f, - BubbleTextView.SHADOW_Y_OFFSET, BubbleTextView.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(BubbleTextView.SHADOW_SMALL_RADIUS, 0.0f, 0.0f, - BubbleTextView.SHADOW_SMALL_COLOUR); - super.draw(canvas); - canvas.restore(); - } -} diff --git a/src/com/android/launcher3/PagedViewIconCache.java b/src/com/android/launcher3/PagedViewIconCache.java deleted file mode 100644 index 93887ea23..000000000 --- a/src/com/android/launcher3/PagedViewIconCache.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; -import android.content.pm.ComponentInfo; -import android.content.pm.ResolveInfo; -import android.graphics.Bitmap; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; - -/** - * 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(AppInfo 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<AppInfo> keys) { - HashSet<Key> keysSet = new HashSet<Key>(); - for (AppInfo 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 index db4aeb940..e6e11a312 100644 --- a/src/com/android/launcher3/PagedViewWidget.java +++ b/src/com/android/launcher3/PagedViewWidget.java @@ -30,6 +30,8 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.launcher3.compat.AppWidgetManagerCompat; + /** * The linear layout used strictly for the widget/wallpaper tab of the customization tray */ @@ -127,7 +129,7 @@ public class PagedViewWidget extends LinearLayout { image.setMaxWidth(maxWidth); } final TextView name = (TextView) findViewById(R.id.widget_name); - name.setText(info.label); + name.setText(AppWidgetManagerCompat.getInstance(getContext()).loadLabel(info)); final TextView dims = (TextView) findViewById(R.id.widget_dims); if (dims != null) { int hSpan = Math.min(cellSpan[0], (int) grid.numColumns); diff --git a/src/com/android/launcher3/Partner.java b/src/com/android/launcher3/Partner.java new file mode 100644 index 000000000..e1913193b --- /dev/null +++ b/src/com/android/launcher3/Partner.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2014 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.pm.PackageManager; +import android.content.res.Resources; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.Pair; + +import java.io.File; + +/** + * Utilities to discover and interact with partner customizations. There can + * only be one set of customizations on a device, and it must be bundled with + * the system. + */ +public class Partner { + + static final String TAG = "Launcher.Partner"; + + /** Marker action used to discover partner */ + private static final String + ACTION_PARTNER_CUSTOMIZATION = "com.android.launcher3.action.PARTNER_CUSTOMIZATION"; + + public static final String RES_FOLDER = "partner_folder"; + public static final String RES_WALLPAPERS = "partner_wallpapers"; + public static final String RES_DEFAULT_LAYOUT = "partner_default_layout"; + + public static final String RES_DEFAULT_WALLPAPER_HIDDEN = "default_wallpapper_hidden"; + public static final String RES_SYSTEM_WALLPAPER_DIR = "system_wallpaper_directory"; + + public static final String RES_REQUIRE_FIRST_RUN_FLOW = "requires_first_run_flow"; + + /** These resources are used to override the device profile */ + public static final String RES_GRID_AA_SHORT_EDGE_COUNT = "grid_aa_short_edge_count"; + public static final String RES_GRID_AA_LONG_EDGE_COUNT = "grid_aa_long_edge_count"; + public static final String RES_GRID_NUM_ROWS = "grid_num_rows"; + public static final String RES_GRID_NUM_COLUMNS = "grid_num_columns"; + public static final String RES_GRID_ICON_SIZE_DP = "grid_icon_size_dp"; + + private static boolean sSearched = false; + private static Partner sPartner; + + /** + * Find and return partner details, or {@code null} if none exists. + */ + public static synchronized Partner get(PackageManager pm) { + if (!sSearched) { + Pair<String, Resources> apkInfo = Utilities.findSystemApk(ACTION_PARTNER_CUSTOMIZATION, pm); + if (apkInfo != null) { + sPartner = new Partner(apkInfo.first, apkInfo.second); + } + sSearched = true; + } + return sPartner; + } + + private final String mPackageName; + private final Resources mResources; + + private Partner(String packageName, Resources res) { + mPackageName = packageName; + mResources = res; + } + + public String getPackageName() { + return mPackageName; + } + + public Resources getResources() { + return mResources; + } + + public boolean hasDefaultLayout() { + int defaultLayout = getResources().getIdentifier(Partner.RES_DEFAULT_LAYOUT, + "xml", getPackageName()); + return defaultLayout != 0; + } + + public boolean hasFolder() { + int folder = getResources().getIdentifier(Partner.RES_FOLDER, + "xml", getPackageName()); + return folder != 0; + } + + public boolean hideDefaultWallpaper() { + int resId = getResources().getIdentifier(RES_DEFAULT_WALLPAPER_HIDDEN, "bool", + getPackageName()); + return resId != 0 && getResources().getBoolean(resId); + } + + public File getWallpaperDirectory() { + int resId = getResources().getIdentifier(RES_SYSTEM_WALLPAPER_DIR, "string", + getPackageName()); + return (resId != 0) ? new File(getResources().getString(resId)) : null; + } + + public boolean requiresFirstRunFlow() { + int resId = getResources().getIdentifier(RES_REQUIRE_FIRST_RUN_FLOW, "bool", + getPackageName()); + return resId != 0 && getResources().getBoolean(resId); + } + + public DeviceProfile getDeviceProfileOverride(DisplayMetrics dm) { + boolean containsProfileOverrides = false; + + DeviceProfile dp = new DeviceProfile(); + + // We initialize customizable fields to be invalid + dp.numRows = -1; + dp.numColumns = -1; + dp.allAppsShortEdgeCount = -1; + dp.allAppsLongEdgeCount = -1; + + try { + int resId = getResources().getIdentifier(RES_GRID_NUM_ROWS, + "integer", getPackageName()); + if (resId > 0) { + containsProfileOverrides = true; + dp.numRows = getResources().getInteger(resId); + } + + resId = getResources().getIdentifier(RES_GRID_NUM_COLUMNS, + "integer", getPackageName()); + if (resId > 0) { + containsProfileOverrides = true; + dp.numColumns = getResources().getInteger(resId); + } + + resId = getResources().getIdentifier(RES_GRID_AA_SHORT_EDGE_COUNT, + "integer", getPackageName()); + if (resId > 0) { + containsProfileOverrides = true; + dp.allAppsShortEdgeCount = getResources().getInteger(resId); + } + + resId = getResources().getIdentifier(RES_GRID_AA_LONG_EDGE_COUNT, + "integer", getPackageName()); + if (resId > 0) { + containsProfileOverrides = true; + dp.allAppsLongEdgeCount = getResources().getInteger(resId); + } + + resId = getResources().getIdentifier(RES_GRID_ICON_SIZE_DP, + "dimen", getPackageName()); + if (resId > 0) { + containsProfileOverrides = true; + int px = getResources().getDimensionPixelSize(resId); + dp.iconSize = DynamicGrid.dpiFromPx(px, dm); + } + } catch (Resources.NotFoundException ex) { + Log.e(TAG, "Invalid Partner grid resource!", ex); + } + return containsProfileOverrides ? dp : null; + } +} diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java new file mode 100644 index 000000000..d23a33033 --- /dev/null +++ b/src/com/android/launcher3/PendingAppWidgetHostView.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2014 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.Intent; +import android.content.res.Resources.Theme; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.TypedValue; +import android.view.View; +import android.view.View.OnClickListener; + +public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implements OnClickListener { + + private static Theme sPreloaderTheme; + + private final Rect mRect = new Rect(); + private View mDefaultView; + private OnClickListener mClickListener; + private final LauncherAppWidgetInfo mInfo; + private final int mStartState; + private final Intent mIconLookupIntent; + + private Bitmap mIcon; + private PreloadIconDrawable mDrawable; + + private Drawable mCenterDrawable; + private Drawable mTopCornerDrawable; + + private boolean mDrawableSizeChanged; + + private final TextPaint mPaint; + private Layout mSetupTextLayout; + + public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info) { + super(context); + mInfo = info; + mStartState = info.restoreStatus; + mIconLookupIntent = new Intent().setComponent(info.providerName); + + mPaint = new TextPaint(); + mPaint.setColor(0xFFFFFFFF); + mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, + getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics())); + setBackgroundResource(R.drawable.quantum_panel_dark); + setWillNotDraw(false); + } + + @Override + public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth, + int maxHeight) { + // No-op + } + + @Override + protected View getDefaultView() { + if (mDefaultView == null) { + mDefaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false); + mDefaultView.setOnClickListener(this); + applyState(); + } + return mDefaultView; + } + + @Override + public void setOnClickListener(OnClickListener l) { + mClickListener = l; + } + + @Override + public boolean isReinflateRequired() { + // Re inflate is required any time the widget restore status changes + return mStartState != mInfo.restoreStatus; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mDrawableSizeChanged = true; + } + + public void updateIcon(IconCache cache) { + Bitmap icon = cache.getIcon(mIconLookupIntent, mInfo.user); + if (mIcon == icon) { + return; + } + mIcon = icon; + if (mDrawable != null) { + mDrawable.setCallback(null); + mDrawable = null; + } + if (mIcon != null) { + // The view displays two modes, one with a setup icon and another with a preload icon + // in the center. + if (isReadyForClickSetup()) { + mCenterDrawable = getResources().getDrawable(R.drawable.ic_setting); + mTopCornerDrawable = new FastBitmapDrawable(mIcon); + } else { + if (sPreloaderTheme == null) { + sPreloaderTheme = getResources().newTheme(); + sPreloaderTheme.applyStyle(R.style.PreloadIcon, true); + } + + FastBitmapDrawable drawable = Utilities.createIconDrawable(mIcon); + mDrawable = new PreloadIconDrawable(drawable, sPreloaderTheme); + mDrawable.setCallback(this); + applyState(); + } + mDrawableSizeChanged = true; + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return (who == mDrawable) || super.verifyDrawable(who); + } + + public void applyState() { + if (mDrawable != null) { + mDrawable.setLevel(Math.max(mInfo.installProgress, 0)); + } + } + + @Override + public void onClick(View v) { + // AppWidgetHostView blocks all click events on the root view. Instead handle click events + // on the content and pass it along. + if (mClickListener != null) { + mClickListener.onClick(this); + } + } + + public boolean isReadyForClickSetup() { + return (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0 + && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDrawable != null) { + if (mDrawableSizeChanged) { + int maxSize = LauncherAppState.getInstance().getDynamicGrid() + .getDeviceProfile().iconSizePx + 2 * mDrawable.getOutset(); + int size = Math.min(maxSize, Math.min( + getWidth() - getPaddingLeft() - getPaddingRight(), + getHeight() - getPaddingTop() - getPaddingBottom())); + + mRect.set(0, 0, size, size); + mRect.inset(mDrawable.getOutset(), mDrawable.getOutset()); + mRect.offsetTo((getWidth() - mRect.width()) / 2, (getHeight() - mRect.height()) / 2); + mDrawable.setBounds(mRect); + mDrawableSizeChanged = false; + } + + mDrawable.draw(canvas); + } else if ((mCenterDrawable != null) && (mTopCornerDrawable != null)) { + if (mDrawableSizeChanged) { + DeviceProfile grid = getDeviceProfile(); + int iconSize = grid.iconSizePx; + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + + int availableWidth = getWidth() - paddingLeft - paddingRight; + int availableHeight = getHeight() - paddingTop - paddingBottom; + + // Recreate the setup text. + mSetupTextLayout = new StaticLayout( + getResources().getText(R.string.gadget_setup_text), mPaint, availableWidth, + Layout.Alignment.ALIGN_CENTER, 1, 0, true); + if (mSetupTextLayout.getLineCount() == 1) { + // The text fits in a single line. No need to draw the setup icon. + int size = Math.min(iconSize, Math.min(availableWidth, + availableHeight - mSetupTextLayout.getHeight())); + mRect.set(0, 0, size, size); + mRect.offsetTo((getWidth() - mRect.width()) / 2, + (getHeight() - mRect.height() - mSetupTextLayout.getHeight() + - grid.iconDrawablePaddingPx) / 2); + + mTopCornerDrawable.setBounds(mRect); + + // Update left and top to indicate the position where the text will be drawn. + mRect.left = paddingLeft; + mRect.top = mRect.bottom + grid.iconDrawablePaddingPx; + } else { + // The text can't be drawn in a single line. Draw a setup icon instead. + mSetupTextLayout = null; + int size = Math.min(iconSize, Math.min( + getWidth() - paddingLeft - paddingRight, + getHeight() - paddingTop - paddingBottom)); + mRect.set(0, 0, size, size); + mRect.offsetTo((getWidth() - mRect.width()) / 2, (getHeight() - mRect.height()) / 2); + mCenterDrawable.setBounds(mRect); + + size = Math.min(size / 2, + Math.max(mRect.top - paddingTop, mRect.left - paddingLeft)); + mTopCornerDrawable.setBounds(paddingLeft, paddingTop, + paddingLeft + size, paddingTop + size); + } + mDrawableSizeChanged = false; + } + + if (mSetupTextLayout == null) { + mCenterDrawable.draw(canvas); + mTopCornerDrawable.draw(canvas); + } else { + canvas.save(); + canvas.translate(mRect.left, mRect.top); + mSetupTextLayout.draw(canvas); + canvas.restore(); + mTopCornerDrawable.draw(canvas); + } + } + } + + private DeviceProfile getDeviceProfile() { + return LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); + } +} diff --git a/src/com/android/launcher3/PreloadIconDrawable.java b/src/com/android/launcher3/PreloadIconDrawable.java new file mode 100644 index 000000000..2972c4f9b --- /dev/null +++ b/src/com/android/launcher3/PreloadIconDrawable.java @@ -0,0 +1,249 @@ +package com.android.launcher3; + +import android.animation.ObjectAnimator; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +class PreloadIconDrawable extends Drawable { + + private static final float ANIMATION_PROGRESS_STOPPED = -1.0f; + private static final float ANIMATION_PROGRESS_STARTED = 0f; + private static final float ANIMATION_PROGRESS_COMPLETED = 1.0f; + + private static final float MIN_SATUNATION = 0.2f; + private static final float MIN_LIGHTNESS = 0.6f; + + private static final float ICON_SCALE_FACTOR = 0.5f; + private static final int DEFAULT_COLOR = 0xFF009688; + + private static final Rect sTempRect = new Rect(); + + private final RectF mIndicatorRect = new RectF(); + private boolean mIndicatorRectDirty; + + private final Paint mPaint; + final Drawable mIcon; + + private Drawable mBgDrawable; + private int mRingOutset; + + private int mIndicatorColor = 0; + + /** + * Indicates the progress of the preloader [0-100]. If it goes above 100, only the icon + * is shown with no progress bar. + */ + private int mProgress = 0; + + private float mAnimationProgress = ANIMATION_PROGRESS_STOPPED; + private ObjectAnimator mAnimator; + + public PreloadIconDrawable(Drawable icon, Theme theme) { + mIcon = icon; + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeCap(Paint.Cap.ROUND); + + setBounds(icon.getBounds()); + applyTheme(theme); + onLevelChange(0); + } + + @Override + public void applyTheme(Theme t) { + TypedArray ta = t.obtainStyledAttributes(R.styleable.PreloadIconDrawable); + mBgDrawable = ta.getDrawable(R.styleable.PreloadIconDrawable_background); + mBgDrawable.setFilterBitmap(true); + mPaint.setStrokeWidth(ta.getDimension(R.styleable.PreloadIconDrawable_indicatorSize, 0)); + mRingOutset = ta.getDimensionPixelSize(R.styleable.PreloadIconDrawable_ringOutset, 0); + ta.recycle(); + onBoundsChange(getBounds()); + invalidateSelf(); + } + + @Override + protected void onBoundsChange(Rect bounds) { + mIcon.setBounds(bounds); + if (mBgDrawable != null) { + sTempRect.set(bounds); + sTempRect.inset(-mRingOutset, -mRingOutset); + mBgDrawable.setBounds(sTempRect); + } + mIndicatorRectDirty = true; + } + + public int getOutset() { + return mRingOutset; + } + + /** + * The size of the indicator is same as the content region of the {@link #mBgDrawable} minus + * half the stroke size to accommodate the indicator. + */ + private void initIndicatorRect() { + Drawable d = mBgDrawable; + Rect bounds = d.getBounds(); + + d.getPadding(sTempRect); + // Amount by which padding has to be scaled + float paddingScaleX = ((float) bounds.width()) / d.getIntrinsicWidth(); + float paddingScaleY = ((float) bounds.height()) / d.getIntrinsicHeight(); + mIndicatorRect.set( + bounds.left + sTempRect.left * paddingScaleX, + bounds.top + sTempRect.top * paddingScaleY, + bounds.right - sTempRect.right * paddingScaleX, + bounds.bottom - sTempRect.bottom * paddingScaleY); + + float inset = mPaint.getStrokeWidth() / 2; + mIndicatorRect.inset(inset, inset); + mIndicatorRectDirty = false; + } + + @Override + public void draw(Canvas canvas) { + final Rect r = new Rect(getBounds()); + if (canvas.getClipBounds(sTempRect) && !Rect.intersects(sTempRect, r)) { + // The draw region has been clipped. + return; + } + if (mIndicatorRectDirty) { + initIndicatorRect(); + } + final float iconScale; + + if ((mAnimationProgress >= ANIMATION_PROGRESS_STARTED) + && (mAnimationProgress < ANIMATION_PROGRESS_COMPLETED)) { + mPaint.setAlpha((int) ((1 - mAnimationProgress) * 255)); + mBgDrawable.setAlpha(mPaint.getAlpha()); + mBgDrawable.draw(canvas); + canvas.drawOval(mIndicatorRect, mPaint); + + iconScale = ICON_SCALE_FACTOR + (1 - ICON_SCALE_FACTOR) * mAnimationProgress; + } else if (mAnimationProgress == ANIMATION_PROGRESS_STOPPED) { + mPaint.setAlpha(255); + iconScale = ICON_SCALE_FACTOR; + mBgDrawable.setAlpha(255); + mBgDrawable.draw(canvas); + + if (mProgress >= 100) { + canvas.drawOval(mIndicatorRect, mPaint); + } else if (mProgress > 0) { + canvas.drawArc(mIndicatorRect, -90, mProgress * 3.6f, false, mPaint); + } + } else { + iconScale = 1; + } + + canvas.save(); + canvas.scale(iconScale, iconScale, r.exactCenterX(), r.exactCenterY()); + mIcon.draw(canvas); + canvas.restore(); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + mIcon.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mIcon.setColorFilter(cf); + } + + @Override + protected boolean onLevelChange(int level) { + mProgress = level; + + // Stop Animation + if (mAnimator != null) { + mAnimator.cancel(); + mAnimator = null; + } + mAnimationProgress = ANIMATION_PROGRESS_STOPPED; + if (level > 0) { + // Set the paint color only when the level changes, so that the dominant color + // is only calculated when needed. + mPaint.setColor(getIndicatorColor()); + } + if (mIcon instanceof FastBitmapDrawable) { + ((FastBitmapDrawable) mIcon).setGhostModeEnabled(level <= 0); + } + + invalidateSelf(); + return true; + } + + /** + * Runs the finish animation if it is has not been run after last level change. + */ + public void maybePerformFinishedAnimation() { + if (mAnimationProgress > ANIMATION_PROGRESS_STOPPED) { + return; + } + if (mAnimator != null) { + mAnimator.cancel(); + } + setAnimationProgress(ANIMATION_PROGRESS_STARTED); + mAnimator = ObjectAnimator.ofFloat(this, "animationProgress", + ANIMATION_PROGRESS_STARTED, ANIMATION_PROGRESS_COMPLETED); + mAnimator.start(); + } + + public void setAnimationProgress(float progress) { + if (progress != mAnimationProgress) { + mAnimationProgress = progress; + invalidateSelf(); + } + } + + public float getAnimationProgress() { + return mAnimationProgress; + } + + @Override + public int getIntrinsicHeight() { + return mIcon.getIntrinsicHeight(); + } + + @Override + public int getIntrinsicWidth() { + return mIcon.getIntrinsicWidth(); + } + + private int getIndicatorColor() { + if (mIndicatorColor != 0) { + return mIndicatorColor; + } + if (!(mIcon instanceof FastBitmapDrawable)) { + mIndicatorColor = DEFAULT_COLOR; + return mIndicatorColor; + } + mIndicatorColor = Utilities.findDominantColorByHue( + ((FastBitmapDrawable) mIcon).getBitmap(), 20); + + // Make sure that the dominant color has enough saturation to be visible properly. + float[] hsv = new float[3]; + Color.colorToHSV(mIndicatorColor, hsv); + if (hsv[1] < MIN_SATUNATION) { + mIndicatorColor = DEFAULT_COLOR; + return mIndicatorColor; + } + hsv[2] = Math.max(MIN_LIGHTNESS, hsv[2]); + mIndicatorColor = Color.HSVToColor(hsv); + return mIndicatorColor; + } +} diff --git a/src/com/android/launcher3/PreloadReceiver.java b/src/com/android/launcher3/PreloadReceiver.java deleted file mode 100644 index ca25746eb..000000000 --- a/src/com/android/launcher3/PreloadReceiver.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.os.AsyncTask; -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 LauncherProvider provider = LauncherAppState.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 AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - provider.loadDefaultFavoritesIfNecessary(workspaceResId); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - } - } -} diff --git a/src/com/android/launcher3/ScrimView.java b/src/com/android/launcher3/ScrimView.java deleted file mode 100644 index 68200fe64..000000000 --- a/src/com/android/launcher3/ScrimView.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.Rect; -import android.util.AttributeSet; -import android.widget.FrameLayout; - -public class ScrimView extends FrameLayout implements Insettable { - - public ScrimView(Context context) { - this(context, null, 0); - } - - public ScrimView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ScrimView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public void setInsets(Rect insets) { - // Do nothing - } -} diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 79d114c06..daf343460 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -20,18 +20,44 @@ import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Bitmap; import android.util.Log; +import com.android.launcher3.compat.UserHandleCompat; + import java.util.ArrayList; +import java.util.Arrays; /** * Represents a launchable icon on the workspaces and in folders. */ -class ShortcutInfo extends ItemInfo { +public class ShortcutInfo extends ItemInfo { + + public static final int DEFAULT = 0; + + /** + * The shortcut was restored from a backup and it not ready to be used. This is automatically + * set during backup/restore + */ + public static final int FLAG_RESTORED_ICON = 1; + + /** + * The icon was added as an auto-install app, and is not ready to be used. This flag can't + * be present along with {@link #FLAG_RESTORED_ICON}, and is set during default layout + * parsing. + */ + public static final int FLAG_AUTOINTALL_ICON = 2; + + /** + * The icon is being installed. If {@link FLAG_RESTORED_ICON} or {@link FLAG_AUTOINTALL_ICON} + * is set, then the icon is either being installed or is in a broken state. + */ + public static final int FLAG_INSTALL_SESSION_ACTIVE = 4; + + /** + * Indicates that the widget restore has started. + */ + public static final int FLAG_RESTORE_STARTED = 8; /** * The intent used to start the application. @@ -61,43 +87,52 @@ class ShortcutInfo extends ItemInfo { */ private Bitmap mIcon; + /** + * Could be disabled, if the the app is installed but unavailable (eg. in safe mode or when + * sd-card is not available). + */ + boolean isDisabled = false; + + int status; + + /** + * The installation progress [0-100] of the package that this shortcut represents. + */ + private int mInstallProgress; + + /** + * Refer {@link AppInfo#firstInstallTime}. + */ long firstInstallTime; + + /** + * TODO move this to {@link status} + */ int flags = 0; /** * If this shortcut is a placeholder, then intent will be a market intent for the package, and * this will hold the original intent from the database. Otherwise, null. + * Refer {@link #FLAG_RESTORE_PENDING}, {@link #FLAG_INSTALL_PENDING} */ - Intent restoredIntent; + Intent promisedIntent; ShortcutInfo() { itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; } - protected Intent getIntent() { + public Intent getIntent() { return intent; } - protected Intent getRestoredIntent() { - return restoredIntent; - } - - /** - * Overwrite placeholder data with restored data, or do nothing if this is not a placeholder. - */ - public void restore() { - if (restoredIntent != null) { - intent = restoredIntent; - restoredIntent = null; - } - } - - - ShortcutInfo(Intent intent, CharSequence title, Bitmap icon) { + ShortcutInfo(Intent intent, CharSequence title, CharSequence contentDescription, + Bitmap icon, UserHandleCompat user) { this(); this.intent = intent; this.title = title; + this.contentDescription = contentDescription; mIcon = icon; + this.user = user; } public ShortcutInfo(Context context, ShortcutInfo info) { @@ -111,8 +146,10 @@ class ShortcutInfo extends ItemInfo { } mIcon = info.mIcon; // TODO: should make a copy here. maybe we don't need this ctor at all customIcon = info.customIcon; - initFlagsAndFirstInstallTime( - getPackageInfo(context, intent.getComponent().getPackageName())); + flags = info.flags; + firstInstallTime = info.firstInstallTime; + user = info.user; + status = info.status; } /** TODO: Remove this. It's only called by ApplicationInfo.makeShortcut. */ @@ -125,22 +162,6 @@ class ShortcutInfo extends ItemInfo { firstInstallTime = info.firstInstallTime; } - public static PackageInfo getPackageInfo(Context context, String packageName) { - PackageInfo pi = null; - try { - PackageManager pm = context.getPackageManager(); - pi = pm.getPackageInfo(packageName, 0); - } catch (NameNotFoundException e) { - Log.d("ShortcutInfo", "PackageManager.getPackageInfo failed for " + packageName); - } - return pi; - } - - void initFlagsAndFirstInstallTime(PackageInfo pi) { - flags = AppInfo.initFlags(pi); - firstInstallTime = AppInfo.initFirstInstallTime(pi); - } - public void setIcon(Bitmap b) { mIcon = b; } @@ -153,35 +174,19 @@ class ShortcutInfo extends ItemInfo { } 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(Context context, 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; - initFlagsAndFirstInstallTime( - getPackageInfo(context, intent.getComponent().getPackageName())); + mIcon = iconCache.getIcon(promisedIntent != null ? promisedIntent : intent, user); + usingFallbackIcon = iconCache.isDefaultIcon(mIcon, user); } @Override - void onAddToDatabase(ContentValues values) { - super.onAddToDatabase(values); + void onAddToDatabase(Context context, ContentValues values) { + super.onAddToDatabase(context, values); String titleStr = title != null ? title.toString() : null; values.put(LauncherSettings.BaseLauncherColumns.TITLE, titleStr); - String uri = intent != null ? intent.toUri(0) : null; + String uri = promisedIntent != null ? promisedIntent.toUri(0) + : (intent != null ? intent.toUri(0) : null); values.put(LauncherSettings.BaseLauncherColumns.INTENT, uri); if (customIcon) { @@ -205,10 +210,10 @@ class ShortcutInfo extends ItemInfo { @Override public String toString() { - return "ShortcutInfo(title=" + title.toString() + "intent=" + intent + "id=" + this.id + return "ShortcutInfo(title=" + title + "intent=" + intent + "id=" + this.id + " type=" + this.itemType + " container=" + this.container + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY - + " dropPos=" + dropPos + ")"; + + " dropPos=" + Arrays.toString(dropPos) + " user=" + user + ")"; } public static void dumpShortcutInfoList(String tag, String label, @@ -219,5 +224,27 @@ class ShortcutInfo extends ItemInfo { + " customIcon=" + info.customIcon); } } + + public ComponentName getTargetComponent() { + return promisedIntent != null ? promisedIntent.getComponent() : intent.getComponent(); + } + + public boolean hasStatusFlag(int flag) { + return (status & flag) != 0; + } + + + public final boolean isPromise() { + return hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINTALL_ICON); + } + + public int getInstallProgress() { + return mInstallProgress; + } + + public void setInstallProgress(int progress) { + mInstallProgress = progress; + status |= FLAG_INSTALL_SESSION_ACTIVE; + } } diff --git a/src/com/android/launcher3/StartupReceiver.java b/src/com/android/launcher3/StartupReceiver.java new file mode 100644 index 000000000..65f913fdf --- /dev/null +++ b/src/com/android/launcher3/StartupReceiver.java @@ -0,0 +1,15 @@ +package com.android.launcher3; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class StartupReceiver extends BroadcastReceiver { + + static final String SYSTEM_READY = "com.android.launcher3.SYSTEM_READY"; + + @Override + public void onReceive(Context context, Intent intent) { + context.sendStickyBroadcast(new Intent(SYSTEM_READY)); + } +} diff --git a/src/com/android/launcher3/Stats.java b/src/com/android/launcher3/Stats.java index 882fb04a3..f3977e456 100644 --- a/src/com/android/launcher3/Stats.java +++ b/src/com/android/launcher3/Stats.java @@ -32,7 +32,6 @@ public class Stats { private static final boolean LOCAL_LAUNCH_LOG = true; public static final String ACTION_LAUNCH = "com.android.launcher3.action.LAUNCH"; - public static final String PERM_LAUNCH = "com.android.launcher3.permission.RECEIVE_LAUNCH_BROADCASTS"; public static final String EXTRA_INTENT = "intent"; public static final String EXTRA_CONTAINER = "container"; public static final String EXTRA_SCREEN = "screen"; @@ -53,6 +52,8 @@ public class Stats { private final Launcher mLauncher; + private final String mLaunchBroadcastPermission; + DataOutputStream mLog; ArrayList<String> mIntents; @@ -61,6 +62,9 @@ public class Stats { public Stats(Launcher launcher) { mLauncher = launcher; + mLaunchBroadcastPermission = + launcher.getResources().getString(R.string.receive_launch_broadcasts_permission); + loadStats(); if (LOCAL_LAUNCH_LOG) { @@ -87,7 +91,7 @@ public class Stats { } }, new IntentFilter(ACTION_LAUNCH), - PERM_LAUNCH, + mLaunchBroadcastPermission, null ); } @@ -120,7 +124,7 @@ public class Stats { .putExtra(EXTRA_CELLX, shortcut.cellX) .putExtra(EXTRA_CELLY, shortcut.cellY); } - mLauncher.sendBroadcast(broadcastIntent, PERM_LAUNCH); + mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission); incrementLaunch(flat); diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index cbc978585..80d4b22ce 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -18,14 +18,18 @@ package com.android.launcher3; import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +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.Resources; import android.graphics.Bitmap; -import android.graphics.BlurMaskFilter; import android.graphics.Canvas; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; @@ -33,8 +37,10 @@ import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; -import android.util.DisplayMetrics; +import android.os.Build; import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; import android.view.View; import android.widget.Toast; @@ -51,10 +57,6 @@ public final class Utilities { public static int sIconTextureWidth = -1; public 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(); @@ -65,6 +67,8 @@ public final class Utilities { static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff }; static int sColorIndex = 0; + static int[] sLoc0 = new int[2]; + static int[] sLoc1 = new int[2]; // To turn on these properties, type // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] @@ -74,7 +78,7 @@ public final class Utilities { /** * Returns a FastBitmapDrawable with the icon, accurately sized. */ - static Drawable createIconDrawable(Bitmap icon) { + public static FastBitmapDrawable createIconDrawable(Bitmap icon) { FastBitmapDrawable d = new FastBitmapDrawable(icon); d.setFilterBitmap(true); resizeIconDrawable(d); @@ -99,6 +103,13 @@ public final class Utilities { } /** + * Indicates if the device is running LMP or higher. + */ + public static boolean isLmpOrAbove() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.L; + } + + /** * 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) @@ -305,22 +316,21 @@ public final class Utilities { return scale; } + /** + * Utility method to determine whether the given point, in local coordinates, + * is inside the view, where the area of the view is expanded by the slop factor. + * This method is called while processing touch-move events to determine if the event + * is still within the view. + */ + public static boolean pointInView(View v, float localX, float localY, float slop) { + return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) && + localY < (v.getHeight() + slop); + } + 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); } public static void setIconSize(int widthPx) { @@ -337,6 +347,25 @@ public final class Utilities { } } + public static int[] getCenterDeltaInScreenSpace(View v0, View v1, int[] delta) { + v0.getLocationInWindow(sLoc0); + v1.getLocationInWindow(sLoc1); + + sLoc0[0] += (v0.getMeasuredWidth() * v0.getScaleX()) / 2; + sLoc0[1] += (v0.getMeasuredHeight() * v0.getScaleY()) / 2; + sLoc1[0] += (v1.getMeasuredWidth() * v1.getScaleX()) / 2; + sLoc1[1] += (v1.getMeasuredHeight() * v1.getScaleY()) / 2; + + if (delta == null) { + delta = new int[2]; + } + + delta[0] = sLoc1[0] - sLoc0[0]; + delta[1] = sLoc1[1] - sLoc0[1]; + + return delta; + } + public static void scaleRectAboutCenter(Rect r, float scale) { int cx = r.centerX(); int cy = r.centerY(); @@ -358,4 +387,130 @@ public final class Utilities { "or use the exported attribute for this activity.", e); } } + + static boolean isSystemApp(Context context, Intent intent) { + PackageManager pm = context.getPackageManager(); + ComponentName cn = intent.getComponent(); + String packageName = null; + if (cn == null) { + ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + if ((info != null) && (info.activityInfo != null)) { + packageName = info.activityInfo.packageName; + } + } else { + packageName = cn.getPackageName(); + } + if (packageName != null) { + try { + PackageInfo info = pm.getPackageInfo(packageName, 0); + return (info != null) && (info.applicationInfo != null) && + ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); + } catch (NameNotFoundException e) { + return false; + } + } else { + return false; + } + } + + /** + * This picks a dominant color, looking for high-saturation, high-value, repeated hues. + * @param bitmap The bitmap to scan + * @param samples The approximate max number of samples to use. + */ + static int findDominantColorByHue(Bitmap bitmap, int samples) { + final int height = bitmap.getHeight(); + final int width = bitmap.getWidth(); + int sampleStride = (int) Math.sqrt((height * width) / samples); + if (sampleStride < 1) { + sampleStride = 1; + } + + // This is an out-param, for getting the hsv values for an rgb + float[] hsv = new float[3]; + + // First get the best hue, by creating a histogram over 360 hue buckets, + // where each pixel contributes a score weighted by saturation, value, and alpha. + float[] hueScoreHistogram = new float[360]; + float highScore = -1; + int bestHue = -1; + + for (int y = 0; y < height; y += sampleStride) { + for (int x = 0; x < width; x += sampleStride) { + int argb = bitmap.getPixel(x, y); + int alpha = 0xFF & (argb >> 24); + if (alpha < 0x80) { + // Drop mostly-transparent pixels. + continue; + } + // Remove the alpha channel. + int rgb = argb | 0xFF000000; + Color.colorToHSV(rgb, hsv); + // Bucket colors by the 360 integer hues. + int hue = (int) hsv[0]; + if (hue < 0 || hue >= hueScoreHistogram.length) { + // Defensively avoid array bounds violations. + continue; + } + float score = hsv[1] * hsv[2]; + hueScoreHistogram[hue] += score; + if (hueScoreHistogram[hue] > highScore) { + highScore = hueScoreHistogram[hue]; + bestHue = hue; + } + } + } + + SparseArray<Float> rgbScores = new SparseArray<Float>(); + int bestColor = 0xff000000; + highScore = -1; + // Go back over the RGB colors that match the winning hue, + // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. + // The highest-scoring RGB color wins. + for (int y = 0; y < height; y += sampleStride) { + for (int x = 0; x < width; x += sampleStride) { + int rgb = bitmap.getPixel(x, y) | 0xff000000; + Color.colorToHSV(rgb, hsv); + int hue = (int) hsv[0]; + if (hue == bestHue) { + float s = hsv[1]; + float v = hsv[2]; + int bucket = (int) (s * 100) + (int) (v * 10000); + // Score by cumulative saturation * value. + float score = s * v; + Float oldTotal = rgbScores.get(bucket); + float newTotal = oldTotal == null ? score : oldTotal + score; + rgbScores.put(bucket, newTotal); + if (newTotal > highScore) { + highScore = newTotal; + // All the colors in the winning bucket are very similar. Last in wins. + bestColor = rgb; + } + } + } + } + return bestColor; + } + + /* + * Finds a system apk which had a broadcast receiver listening to a particular action. + * @param action intent action used to find the apk + * @return a pair of apk package name and the resources. + */ + static Pair<String, Resources> findSystemApk(String action, PackageManager pm) { + final Intent intent = new Intent(action); + for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { + if (info.activityInfo != null && + (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + final String packageName = info.activityInfo.packageName; + try { + final Resources res = pm.getResourcesForApplication(packageName); + return Pair.create(packageName, res); + } catch (NameNotFoundException e) { + Log.w(TAG, "Failed to find resources for " + packageName); + } + } + } + return null; + } } diff --git a/src/com/android/launcher3/WidgetAdder.java b/src/com/android/launcher3/WidgetAdder.java deleted file mode 100644 index 79ac50492..000000000 --- a/src/com/android/launcher3/WidgetAdder.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.android.launcher3; - -import android.app.Activity; - -public class WidgetAdder extends Activity { - -} diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index 3db0b51ad..5aa719027 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -5,16 +5,17 @@ import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.database.Cursor; +import android.database.sqlite.SQLiteCantOpenDatabaseException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDiskIOException; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; +import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; @@ -27,129 +28,138 @@ import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.util.Log; +import com.android.launcher3.compat.AppWidgetManagerCompat; + import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; -abstract class SoftReferenceThreadLocal<T> { - private ThreadLocal<SoftReference<T>> mThreadLocal; - public SoftReferenceThreadLocal() { - mThreadLocal = new ThreadLocal<SoftReference<T>>(); - } +public class WidgetPreviewLoader { - abstract T initialValue(); + private static abstract class SoftReferenceThreadLocal<T> { + private ThreadLocal<SoftReference<T>> mThreadLocal; + public SoftReferenceThreadLocal() { + mThreadLocal = new ThreadLocal<SoftReference<T>>(); + } - public void set(T t) { - mThreadLocal.set(new SoftReference<T>(t)); - } + abstract T initialValue(); - 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) { + 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; } - return obj; } } -} -class CanvasCache extends SoftReferenceThreadLocal<Canvas> { - @Override - protected Canvas initialValue() { - return new Canvas(); + private static class CanvasCache extends SoftReferenceThreadLocal<Canvas> { + @Override + protected Canvas initialValue() { + return new Canvas(); + } } -} -class PaintCache extends SoftReferenceThreadLocal<Paint> { - @Override - protected Paint initialValue() { - return null; + private static class PaintCache extends SoftReferenceThreadLocal<Paint> { + @Override + protected Paint initialValue() { + return null; + } } -} -class BitmapCache extends SoftReferenceThreadLocal<Bitmap> { - @Override - protected Bitmap initialValue() { - return null; + private static class BitmapCache extends SoftReferenceThreadLocal<Bitmap> { + @Override + protected Bitmap initialValue() { + return null; + } } -} -class RectCache extends SoftReferenceThreadLocal<Rect> { - @Override - protected Rect initialValue() { - return new Rect(); + private static 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(); + private static class BitmapFactoryOptionsCache extends + SoftReferenceThreadLocal<BitmapFactory.Options> { + @Override + protected BitmapFactory.Options initialValue() { + return new BitmapFactory.Options(); + } } -} -public class WidgetPreviewLoader { - static final String TAG = "WidgetPreviewLoader"; - static final String ANDROID_INCREMENTAL_VERSION_NAME_KEY = "android.incremental.version"; + private static final String TAG = "WidgetPreviewLoader"; + private static final String ANDROID_INCREMENTAL_VERSION_NAME_KEY = "android.incremental.version"; + + private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f; + private static final HashSet<String> sInvalidPackages = new HashSet<String>(); + + // Used for drawing shortcut previews + private final BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); + private final PaintCache mCachedShortcutPreviewPaint = new PaintCache(); + private final CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); + + // Used for drawing widget previews + private final CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); + private final RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); + private final RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); + private final PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); + private final PaintCache mDefaultAppWidgetPreviewPaint = new PaintCache(); + private final BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache(); + + private final HashMap<String, WeakReference<Bitmap>> mLoadedPreviews = new HashMap<>(); + private final ArrayList<SoftReference<Bitmap>> mUnusedBitmaps = new ArrayList<>(); + + private final Context mContext; + private final int mAppIconSize; + private final IconCache mIconCache; + private final AppWidgetManagerCompat mManager; private int mPreviewBitmapWidth; private int mPreviewBitmapHeight; private String mSize; - private Context mContext; - 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>(); - } + private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); public WidgetPreviewLoader(Context context) { LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); mContext = context; - mPackageManager = mContext.getPackageManager(); mAppIconSize = grid.iconSizePx; mIconCache = app.getIconCache(); + mManager = AppWidgetManagerCompat.getInstance(context); + mDb = app.getWidgetPreviewCacheDb(); - mLoadedPreviews = new HashMap<String, WeakReference<Bitmap>>(); - mUnusedBitmaps = new ArrayList<SoftReference<Bitmap>>(); SharedPreferences sp = context.getSharedPreferences( LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); @@ -165,7 +175,7 @@ public class WidgetPreviewLoader { editor.commit(); } } - + public void recreateDb() { LauncherAppState app = LauncherAppState.getInstance(); app.recreateWidgetPreviewDb(); @@ -184,18 +194,19 @@ public class WidgetPreviewLoader { final String name = getObjectName(o); final String packageName = getObjectPackage(o); // check if the package is valid - boolean packageValid = true; synchronized(sInvalidPackages) { - packageValid = !sInvalidPackages.contains(packageName); - } - if (!packageValid) { - return null; + boolean packageValid = !sInvalidPackages.contains(packageName); + 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(); + synchronized(mLoadedPreviews) { + // check if it exists in our existing cache + if (mLoadedPreviews.containsKey(name)) { + WeakReference<Bitmap> bitmapReference = mLoadedPreviews.get(name); + Bitmap bitmap = bitmapReference.get(); + if (bitmap != null) { + return bitmap; } } } @@ -203,11 +214,13 @@ public class WidgetPreviewLoader { 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(); + while (unusedBitmap == null && mUnusedBitmaps.size() > 0) { + Bitmap candidate = mUnusedBitmaps.remove(0).get(); + if (candidate != null && candidate.isMutable() && + candidate.getWidth() == mPreviewBitmapWidth && + candidate.getHeight() == mPreviewBitmapHeight) { + unusedBitmap = candidate; + } } if (unusedBitmap != null) { final Canvas c = mCachedAppWidgetPreviewCanvas.get(); @@ -221,12 +234,7 @@ public class WidgetPreviewLoader { unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight, Bitmap.Config.ARGB_8888); } - - Bitmap preview = null; - - if (packageValid) { - preview = readFromDb(name, unusedBitmap); - } + Bitmap preview = readFromDb(name, unusedBitmap); if (preview != null) { synchronized(mLoadedPreviews) { @@ -320,7 +328,7 @@ public class WidgetPreviewLoader { String output; if (o instanceof AppWidgetProviderInfo) { sb.append(WIDGET_PREFIX); - sb.append(((AppWidgetProviderInfo) o).provider.flattenToString()); + sb.append(((AppWidgetProviderInfo) o).toString()); output = sb.toString(); sb.setLength(0); } else { @@ -358,6 +366,9 @@ public class WidgetPreviewLoader { db.insert(CacheDb.TABLE_NAME, null, values); } catch (SQLiteDiskIOException e) { recreateDb(); + } catch (SQLiteCantOpenDatabaseException e) { + dumpOpenFiles(); + throw e; } } @@ -367,6 +378,9 @@ public class WidgetPreviewLoader { try { db.delete(CacheDb.TABLE_NAME, null, null); } catch (SQLiteDiskIOException e) { + } catch (SQLiteCantOpenDatabaseException e) { + dumpOpenFiles(); + throw e; } } @@ -387,6 +401,9 @@ public class WidgetPreviewLoader { } // args to SELECT query ); } catch (SQLiteDiskIOException e) { + } catch (SQLiteCantOpenDatabaseException e) { + dumpOpenFiles(); + throw e; } synchronized(sInvalidPackages) { sInvalidPackages.remove(packageName); @@ -396,7 +413,7 @@ public class WidgetPreviewLoader { }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } - public static void removeItemFromDb(final CacheDb cacheDb, final String objectName) { + private static void removeItemFromDb(final CacheDb cacheDb, final String objectName) { new AsyncTask<Void, Void, Void>() { public Void doInBackground(Void ... args) { SQLiteDatabase db = cacheDb.getWritableDatabase(); @@ -405,6 +422,9 @@ public class WidgetPreviewLoader { CacheDb.COLUMN_NAME + " = ? ", // SELECT query new String[] { objectName }); // args to SELECT query } catch (SQLiteDiskIOException e) { + } catch (SQLiteCantOpenDatabaseException e) { + dumpOpenFiles(); + throw e; } return null; } @@ -430,6 +450,9 @@ public class WidgetPreviewLoader { } catch (SQLiteDiskIOException e) { recreateDb(); return null; + } catch (SQLiteCantOpenDatabaseException e) { + dumpOpenFiles(); + throw e; } if (result.getCount() > 0) { result.moveToFirst(); @@ -450,7 +473,7 @@ public class WidgetPreviewLoader { } } - public Bitmap generatePreview(Object info, Bitmap preview) { + private Bitmap generatePreview(Object info, Bitmap preview) { if (preview != null && (preview.getWidth() != mPreviewBitmapWidth || preview.getHeight() != mPreviewBitmapHeight)) { @@ -468,8 +491,8 @@ public class WidgetPreviewLoader { int[] cellSpans = Launcher.getSpanForWidget(mContext, 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); + return generateWidgetPreview(info, cellSpans[0], cellSpans[1], + maxWidth, maxHeight, preview, null); } public int maxWidthForWidgetPreview(int spanX) { @@ -482,20 +505,20 @@ public class WidgetPreviewLoader { mWidgetSpacingLayout.estimateCellHeight(spanY)); } - public Bitmap generateWidgetPreview(ComponentName provider, int previewImage, - int iconId, int cellHSpan, int cellVSpan, int maxPreviewWidth, int maxPreviewHeight, - Bitmap preview, int[] preScaledWidthOut) { + public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, 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) { + if (info.previewImage != 0) { + drawable = mManager.loadPreview(info); + if (drawable != null) { + drawable = mutateOnMainThread(drawable); + } else { Log.w(TAG, "Can't load widget preview drawable 0x" + - Integer.toHexString(previewImage) + " for provider: " + provider); + Integer.toHexString(info.previewImage) + " for provider: " + info.provider); } } @@ -511,6 +534,7 @@ public class WidgetPreviewLoader { if (cellHSpan < 1) cellHSpan = 1; if (cellVSpan < 1) cellVSpan = 1; + // This Drawable is not directly drawn, so there's no need to mutate it. BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources() .getDrawable(R.drawable.widget_tile); final int previewDrawableWidth = previewDrawable @@ -520,31 +544,33 @@ public class WidgetPreviewLoader { previewWidth = previewDrawableWidth * cellHSpan; previewHeight = previewDrawableHeight * cellVSpan; - defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, - Config.ARGB_8888); + 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); + Paint p = mDefaultAppWidgetPreviewPaint.get(); + if (p == null) { + p = new Paint(); + p.setShader(new BitmapShader(previewDrawable.getBitmap(), + Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); + mDefaultAppWidgetPreviewPaint.set(p); + } + final Rect dest = mCachedAppWidgetPreviewDestRect.get(); + dest.set(0, 0, previewWidth, previewHeight); + c.drawRect(dest, p); c.setBitmap(null); // Draw the icon in the top left corner - int minOffset = (int) (mAppIconSize * sWidgetPreviewIconPaddingPercentage); + int minOffset = (int) (mAppIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); 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); + Drawable icon = mManager.loadIcon(info, mIconCache); if (icon != null) { + int hoffset = (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2); + int yoffset = (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2); + icon = mutateOnMainThread(icon); renderDrawableToBitmap(icon, defaultPreview, hoffset, yoffset, (int) (mAppIconSize * iconScale), (int) (mAppIconSize * iconScale)); @@ -594,7 +620,7 @@ public class WidgetPreviewLoader { c.drawBitmap(defaultPreview, src, dest, p); c.setBitmap(null); } - return preview; + return mManager.getBadgeBitmap(info, preview); } private Bitmap generateShortcutPreview( @@ -612,7 +638,7 @@ public class WidgetPreviewLoader { c.setBitmap(null); } // Render the icon - Drawable icon = mIconCache.getFullResIcon(info); + Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info)); int paddingTop = mContext. getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); @@ -652,18 +678,10 @@ public class WidgetPreviewLoader { 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) { + Drawable d, Bitmap bitmap, int x, int y, int w, int h) { 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); @@ -672,4 +690,98 @@ public class WidgetPreviewLoader { } } + private Drawable mutateOnMainThread(final Drawable drawable) { + try { + return mMainThreadExecutor.submit(new Callable<Drawable>() { + @Override + public Drawable call() throws Exception { + return drawable.mutate(); + } + }).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static final int MAX_OPEN_FILES = 1024; + private static final int SAMPLE_RATE = 23; + /** + * Dumps all files that are open in this process without allocating a file descriptor. + */ + private static void dumpOpenFiles() { + try { + Log.i(TAG, "DUMP OF OPEN FILES (sample rate: 1 every " + SAMPLE_RATE + "):"); + final String TYPE_APK = "apk"; + final String TYPE_JAR = "jar"; + final String TYPE_PIPE = "pipe"; + final String TYPE_SOCKET = "socket"; + final String TYPE_DB = "db"; + final String TYPE_ANON_INODE = "anon_inode"; + final String TYPE_DEV = "dev"; + final String TYPE_NON_FS = "non-fs"; + final String TYPE_OTHER = "other"; + List<String> types = Arrays.asList(TYPE_APK, TYPE_JAR, TYPE_PIPE, TYPE_SOCKET, TYPE_DB, + TYPE_ANON_INODE, TYPE_DEV, TYPE_NON_FS, TYPE_OTHER); + int[] count = new int[types.size()]; + int[] duplicates = new int[types.size()]; + HashSet<String> files = new HashSet<String>(); + int total = 0; + for (int i = 0; i < MAX_OPEN_FILES; i++) { + // This is a gigantic hack but unfortunately the only way to resolve an fd + // to a file name. Note that we have to loop over all possible fds because + // reading the directory would require allocating a new fd. The kernel is + // currently implemented such that no fd is larger then the current rlimit, + // which is why it's safe to loop over them in such a way. + String fd = "/proc/self/fd/" + i; + try { + // getCanonicalPath() uses readlink behind the scene which doesn't require + // a file descriptor. + String resolved = new File(fd).getCanonicalPath(); + int type = types.indexOf(TYPE_OTHER); + if (resolved.startsWith("/dev/")) { + type = types.indexOf(TYPE_DEV); + } else if (resolved.endsWith(".apk")) { + type = types.indexOf(TYPE_APK); + } else if (resolved.endsWith(".jar")) { + type = types.indexOf(TYPE_JAR); + } else if (resolved.contains("/fd/pipe:")) { + type = types.indexOf(TYPE_PIPE); + } else if (resolved.contains("/fd/socket:")) { + type = types.indexOf(TYPE_SOCKET); + } else if (resolved.contains("/fd/anon_inode:")) { + type = types.indexOf(TYPE_ANON_INODE); + } else if (resolved.endsWith(".db") || resolved.contains("/databases/")) { + type = types.indexOf(TYPE_DB); + } else if (resolved.startsWith("/proc/") && resolved.contains("/fd/")) { + // Those are the files that don't point anywhere on the file system. + // getCanonicalPath() wrongly interprets these as relative symlinks and + // resolves them within /proc/<pid>/fd/. + type = types.indexOf(TYPE_NON_FS); + } + count[type]++; + total++; + if (files.contains(resolved)) { + duplicates[type]++; + } + files.add(resolved); + if (total % SAMPLE_RATE == 0) { + Log.i(TAG, " fd " + i + ": " + resolved + + " (" + types.get(type) + ")"); + } + } catch (IOException e) { + // Ignoring exceptions for non-existing file descriptors. + } + } + for (int i = 0; i < types.size(); i++) { + Log.i(TAG, String.format("Open %10s files: %4d total, %4d duplicates", + types.get(i), count[i], duplicates[i])); + } + } catch (Throwable t) { + // Catch everything. This is called from an exception handler that we shouldn't upset. + Log.e(TAG, "Unable to log open files.", t); + } + } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 567abfa47..774996e56 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -31,12 +31,16 @@ 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.pm.PackageManager; +import android.content.pm.ResolveInfo; 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.Paint; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; @@ -44,6 +48,7 @@ import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; +import android.os.Handler; import android.os.IBinder; import android.os.Parcelable; import android.support.v4.view.ViewCompat; @@ -63,11 +68,17 @@ import android.widget.TextView; import com.android.launcher3.FolderIcon.FolderRingAnimator; import com.android.launcher3.Launcher.CustomContentCallbacks; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.compat.PackageInstallerCompat; +import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; +import com.android.launcher3.compat.UserHandleCompat; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; /** * The workspace is a wide area with a wallpaper and a finite number of pages. @@ -96,6 +107,9 @@ public class Workspace extends SmoothPagedView private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f; + static final boolean MAP_NO_RECURSE = false; + static final boolean MAP_RECURSE = true; + // These animators are used to fade the children's outlines private ObjectAnimator mChildrenOutlineFadeInAnimation; private ObjectAnimator mChildrenOutlineFadeOutAnimation; @@ -104,9 +118,6 @@ public class Workspace extends SmoothPagedView // 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 static final long CUSTOM_CONTENT_GESTURE_DELAY = 200; private long mTouchDownTime = -1; @@ -123,13 +134,14 @@ public class Workspace extends SmoothPagedView private static boolean sAccessibilityEnabled; // The screen id used for the empty screen always present to the right. - private final static long EXTRA_EMPTY_SCREEN_ID = -201; + final static long EXTRA_EMPTY_SCREEN_ID = -201; private final static long CUSTOM_CONTENT_SCREEN_ID = -301; private HashMap<Long, CellLayout> mWorkspaceScreens = new HashMap<Long, CellLayout>(); private ArrayList<Long> mScreenOrder = new ArrayList<Long>(); private Runnable mRemoveEmptyScreenRunnable; + private boolean mDeferRemoveExtraEmptyScreen = false; /** * CellInfo for the cell that is currently being dragged @@ -185,7 +197,7 @@ public class Workspace extends SmoothPagedView // 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, OVERVIEW}; + enum State { NORMAL, NORMAL_HIDDEN, SPRING_LOADED, OVERVIEW, OVERVIEW_HIDDEN}; private State mState = State.NORMAL; private boolean mIsSwitchingState = false; @@ -200,11 +212,10 @@ public class Workspace extends SmoothPagedView private HolographicOutlineHelper mOutlineHelper; private Bitmap mDragOutline = null; - private final Rect mTempRect = new Rect(); + private static final Rect sTempRect = new Rect(); private final int[] mTempXY = new int[2]; private int[] mTempVisiblePagesRange = new int[2]; - private boolean mOverscrollTransformsSet; - private float mLastOverscrollPivotX; + private boolean mOverscrollEffectSet; public static final int DRAG_BITMAP_PADDING = 2; private boolean mWorkspaceFadeInAdjacentScreens; @@ -230,6 +241,8 @@ public class Workspace extends SmoothPagedView private DropTarget.DragEnforcer mDragEnforcer; private float mMaxDistanceForFolderCreation; + private final Canvas mCanvas = new Canvas(); + // Variables relating to touch disambiguation (scrolling workspace vs. scrolling a widget) private float mXDown; private float mYDown; @@ -270,6 +283,8 @@ public class Workspace extends SmoothPagedView private int mLastChildCount = -1; private float mTransitionProgress; + float mOverScrollEffect = 0f; + private Runnable mDeferredAction; private boolean mDeferDropAfterUninstall; private boolean mUninstallSuccessful; @@ -392,13 +407,23 @@ public class Workspace extends SmoothPagedView @Override public void run() { if (mIsDragOccuring) { + mDeferRemoveExtraEmptyScreen = false; addExtraEmptyScreenOnDrag(); } } }); } + + public void deferRemoveExtraEmptyScreen() { + mDeferRemoveExtraEmptyScreen = true; + } + public void onDragEnd() { + if (!mDeferRemoveExtraEmptyScreen) { + removeExtraEmptyScreen(true, mDragSourceInternal != null); + } + mIsDragOccuring = false; updateChildrenLayersEnabled(false); mLauncher.unlockScreenOrientation(false); @@ -415,7 +440,6 @@ public class Workspace extends SmoothPagedView * Initializes various states for this workspace. */ protected void initWorkspace() { - Context context = getContext(); mCurrentPage = mDefaultPage; Launcher.setScreen(mCurrentPage); LauncherAppState app = LauncherAppState.getInstance(); @@ -429,13 +453,6 @@ public class Workspace extends SmoothPagedView setMinScale(mOverviewModeShrinkFactor); setupLayoutTransition(); - 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); @@ -570,6 +587,7 @@ public class Workspace extends SmoothPagedView CellLayout customScreen = (CellLayout) mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, null); customScreen.disableBackground(); + customScreen.disableDragTarget(); mWorkspaceScreens.put(CUSTOM_CONTENT_SCREEN_ID, customScreen); mScreenOrder.add(0, CUSTOM_CONTENT_SCREEN_ID); @@ -583,7 +601,6 @@ public class Workspace extends SmoothPagedView mDefaultPage = mOriginalDefaultPage + 1; // Update the custom content hint - mLauncher.getLauncherClings().updateCustomContentHintVisibility(); if (mRestorePage != INVALID_RESTORE_PAGE) { mRestorePage = mRestorePage + 1; } else { @@ -612,7 +629,6 @@ public class Workspace extends SmoothPagedView mDefaultPage = mOriginalDefaultPage - 1; // Update the custom content hint - mLauncher.getLauncherClings().updateCustomContentHintVisibility(); if (mRestorePage != INVALID_RESTORE_PAGE) { mRestorePage = mRestorePage - 1; } else { @@ -693,6 +709,12 @@ public class Workspace extends SmoothPagedView // Log to disk Launcher.addDumpLog(TAG, "11683562 - convertFinalScreenToEmptyScreenIfNecessary()", true); + if (mLauncher.isWorkspaceLoading()) { + // Invalid and dangerous operation if workspace is loading + Launcher.addDumpLog(TAG, " - workspace loading, skip", true); + return; + } + if (hasExtraEmptyScreen() || mScreenOrder.size() == 0) return; long finalScreenId = mScreenOrder.get(mScreenOrder.size() - 1); @@ -715,21 +737,26 @@ public class Workspace extends SmoothPagedView } } - public void removeExtraEmptyScreen(final boolean animate, final Runnable onComplete) { - removeExtraEmptyScreen(animate, onComplete, 0, false); + public void removeExtraEmptyScreen(final boolean animate, boolean stripEmptyScreens) { + removeExtraEmptyScreenDelayed(animate, null, 0, stripEmptyScreens); } - public void removeExtraEmptyScreen(final boolean animate, final Runnable onComplete, + public void removeExtraEmptyScreenDelayed(final boolean animate, final Runnable onComplete, final int delay, final boolean stripEmptyScreens) { // Log to disk Launcher.addDumpLog(TAG, "11683562 - removeExtraEmptyScreen()", true); + if (mLauncher.isWorkspaceLoading()) { + // Don't strip empty screens if the workspace is still loading + Launcher.addDumpLog(TAG, " - workspace loading, skip", true); + return; + } + if (delay > 0) { postDelayed(new Runnable() { @Override public void run() { - removeExtraEmptyScreen(animate, onComplete, 0, stripEmptyScreens); + removeExtraEmptyScreenDelayed(animate, onComplete, 0, stripEmptyScreens); } - }, delay); return; } @@ -807,6 +834,11 @@ public class Workspace extends SmoothPagedView public long commitExtraEmptyScreen() { // Log to disk Launcher.addDumpLog(TAG, "11683562 - commitExtraEmptyScreen()", true); + if (mLauncher.isWorkspaceLoading()) { + // Invalid and dangerous operation if workspace is loading + Launcher.addDumpLog(TAG, " - workspace loading, skip", true); + return -1; + } int index = getPageIndexForScreenId(EXTRA_EMPTY_SCREEN_ID); CellLayout cl = mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID); @@ -864,7 +896,8 @@ public class Workspace extends SmoothPagedView Launcher.addDumpLog(TAG, "11683562 - stripEmptyScreens()", true); if (mLauncher.isWorkspaceLoading()) { - // Don't strip empty screens if the workspace is still loading + // Don't strip empty screens if the workspace is still loading. + // This is dangerous and can result in data loss. Launcher.addDumpLog(TAG, " - workspace loading, skip", true); return; } @@ -969,7 +1002,7 @@ public class Workspace extends SmoothPagedView final CellLayout layout; if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { layout = mLauncher.getHotseat().getLayout(); - child.setOnKeyListener(null); + child.setOnKeyListener(new HotseatIconKeyEventListener()); // Hide folder title in the hotseat if (child instanceof FolderIcon) { @@ -1035,8 +1068,8 @@ public class Workspace extends SmoothPagedView */ @Override public boolean onTouch(View v, MotionEvent event) { - return (isSmall() || !isFinishedSwitchingState()) - || (!isSmall() && indexOfChild(v) != mCurrentPage); + return (workspaceInModalState() || !isFinishedSwitchingState()) + || (!workspaceInModalState() && indexOfChild(v) != mCurrentPage); } public boolean isSwitchingState() { @@ -1055,7 +1088,7 @@ public class Workspace extends SmoothPagedView @Override public boolean dispatchUnhandledMove(View focused, int direction) { - if (isSmall() || !isFinishedSwitchingState()) { + if (workspaceInModalState() || !isFinishedSwitchingState()) { // when the home screens are shrunken, shouldn't allow side-scrolling return false; } @@ -1074,7 +1107,7 @@ public class Workspace extends SmoothPagedView case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_REST) { final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage); - if (!currentPage.lastDownOnOccupiedCell()) { + if (currentPage != null && !currentPage.lastDownOnOccupiedCell()) { onWallpaperTap(ev); } } @@ -1082,6 +1115,17 @@ public class Workspace extends SmoothPagedView return super.onInterceptTouchEvent(ev); } + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + // Ignore pointer scroll events if the custom content doesn't allow scrolling. + if ((getScreenIdForPageIndex(getCurrentPage()) == CUSTOM_CONTENT_SCREEN_ID) + && (mCustomContentCallbacks != null) + && !mCustomContentCallbacks.isScrollingAllowed()) { + return false; + } + return super.onGenericMotionEvent(event); + } + protected void reinflateWidgetsIfNecessary() { final int clCount = getChildCount(); for (int i = 0; i < clCount; i++) { @@ -1091,10 +1135,10 @@ public class Workspace extends SmoothPagedView for (int j = 0; j < itemCount; j++) { View v = swc.getChildAt(j); - if (v.getTag() instanceof LauncherAppWidgetInfo) { + if (v != null && v.getTag() instanceof LauncherAppWidgetInfo) { LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag(); LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) info.hostView; - if (lahv != null && lahv.orientationChangedSincedInflation()) { + if (lahv != null && lahv.isReinflateRequired()) { mLauncher.removeAppWidget(info); // Remove the current widget which is inflated with the wrong orientation cl.removeView(lahv); @@ -1126,12 +1170,20 @@ public class Workspace extends SmoothPagedView (mTouchDownTime - mCustomContentShowTime) > CUSTOM_CONTENT_GESTURE_DELAY; boolean swipeInIgnoreDirection = isLayoutRtl() ? deltaX < 0 : deltaX > 0; - if (swipeInIgnoreDirection && getScreenIdForPageIndex(getCurrentPage()) == - CUSTOM_CONTENT_SCREEN_ID && passRightSwipesToCustomContent) { + boolean onCustomContentScreen = + getScreenIdForPageIndex(getCurrentPage()) == CUSTOM_CONTENT_SCREEN_ID; + if (swipeInIgnoreDirection && onCustomContentScreen && passRightSwipesToCustomContent) { // Pass swipes to the right to the custom content page. return; } + if (onCustomContentScreen && (mCustomContentCallbacks != null) + && !mCustomContentCallbacks.isScrollingAllowed()) { + // Don't allow workspace scrolling if the current custom content screen doesn't allow + // scrolling. + return; + } + if (theta > MAX_SWIPE_ANGLE) { // Above MAX_SWIPE_ANGLE, we don't want to ever start scrolling the workspace return; @@ -1165,14 +1217,6 @@ public class Workspace extends SmoothPagedView enableChildrenCache(mCurrentPage - 1, mCurrentPage + 1); } } - - // 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); - } - } } protected void onPageEndMoving() { @@ -1185,7 +1229,7 @@ public class Workspace extends SmoothPagedView } if (mDragController.isDragging()) { - if (isSmall()) { + if (workspaceInModalState()) { // 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(); @@ -1215,7 +1259,7 @@ public class Workspace extends SmoothPagedView if (hasCustomContent() && getNextPage() == 0 && !mCustomContentShowing) { mCustomContentShowing = true; if (mCustomContentCallbacks != null) { - mCustomContentCallbacks.onShow(); + mCustomContentCallbacks.onShow(false); mCustomContentShowTime = System.currentTimeMillis(); mLauncher.updateVoiceButtonProxyVisible(false); } @@ -1227,9 +1271,6 @@ public class Workspace extends SmoothPagedView mLauncher.updateVoiceButtonProxyVisible(false); } } - if (getPageIndicator() != null) { - getPageIndicator().setContentDescription(getPageIndicatorDescription()); - } } protected CustomContentCallbacks getCustomContentCallbacks() { @@ -1243,7 +1284,8 @@ public class Workspace extends SmoothPagedView SharedPreferences sp = mLauncher.getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); LauncherWallpaperPickerActivity.suggestWallpaperDimension(mLauncher.getResources(), - sp, mLauncher.getWindowManager(), mWallpaperManager); + sp, mLauncher.getWindowManager(), mWallpaperManager, + mLauncher.overrideWallpaperDimensions()); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); @@ -1261,6 +1303,10 @@ public class Workspace extends SmoothPagedView snapToPage(whichPage, duration); } + public void snapToScreenId(long screenId) { + snapToScreenId(screenId, null); + } + protected void snapToScreenId(long screenId, Runnable r) { snapToPage(getPageIndexForScreenId(screenId), r); } @@ -1445,8 +1491,16 @@ public class Workspace extends SmoothPagedView mWallpaperOffset.syncWithScroll(); } + @Override + public void announceForAccessibility(CharSequence text) { + // Don't announce if apps is on top of us. + if (!mLauncher.isAllAppsVisible()) { + super.announceForAccessibility(text); + } + } + void showOutlines() { - if (!isSmall() && !mIsSwitchingState) { + if (!workspaceInModalState() && !mIsSwitchingState) { if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); mChildrenOutlineFadeInAnimation = LauncherAnimUtils.ofFloat(this, "childrenOutlineAlpha", 1.0f); @@ -1456,7 +1510,7 @@ public class Workspace extends SmoothPagedView } void hideOutlines() { - if (!isSmall() && !mIsSwitchingState) { + if (!workspaceInModalState() && !mIsSwitchingState) { if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); mChildrenOutlineFadeOutAnimation = LauncherAnimUtils.ofFloat(this, "childrenOutlineAlpha", 0.0f); @@ -1484,15 +1538,9 @@ public class Workspace extends SmoothPagedView return mChildrenOutlineAlpha; } - void disableBackground() { - mDrawBackground = false; - } - void enableBackground() { - mDrawBackground = true; - } - private void animateBackgroundGradient(float finalAlpha, boolean animated) { - if (mBackground == null) return; + final DragLayer dragLayer = mLauncher.getDragLayer(); + if (mBackgroundFadeInAnimation != null) { mBackgroundFadeInAnimation.cancel(); mBackgroundFadeInAnimation = null; @@ -1501,36 +1549,26 @@ public class Workspace extends SmoothPagedView mBackgroundFadeOutAnimation.cancel(); mBackgroundFadeOutAnimation = null; } - float startAlpha = getBackgroundAlpha(); + float startAlpha = dragLayer.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()); + dragLayer.setBackgroundAlpha( + ((Float)animation.getAnimatedValue()).floatValue()); } }); mBackgroundFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); mBackgroundFadeOutAnimation.setDuration(BACKGROUND_FADE_OUT_DURATION); mBackgroundFadeOutAnimation.start(); } else { - setBackgroundAlpha(finalAlpha); + dragLayer.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; @@ -1546,7 +1584,7 @@ public class Workspace extends SmoothPagedView private void updatePageAlphaValues(int screenCenter) { boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; if (mWorkspaceFadeInAdjacentScreens && - mState == State.NORMAL && + !workspaceInModalState() && !mIsSwitchingState && !isInOverscroll) { for (int i = numCustomPages(); i < getChildCount(); i++) { @@ -1555,6 +1593,7 @@ public class Workspace extends SmoothPagedView float scrollProgress = getScrollProgress(screenCenter, child, i); float alpha = 1 - Math.abs(scrollProgress); child.getShortcutsAndWidgets().setAlpha(alpha); + //child.setBackgroundAlphaMultiplier(1 - alpha); } } } @@ -1602,13 +1641,13 @@ public class Workspace extends SmoothPagedView if (Float.compare(progress, mLastCustomContentScrollProgress) == 0) return; CellLayout cc = mWorkspaceScreens.get(CUSTOM_CONTENT_SCREEN_ID); - if (progress > 0 && cc.getVisibility() != VISIBLE && !isSmall()) { + if (progress > 0 && cc.getVisibility() != VISIBLE && !workspaceInModalState()) { cc.setVisibility(VISIBLE); } mLastCustomContentScrollProgress = progress; - setBackgroundAlpha(progress * 0.8f); + mLauncher.getDragLayer().setBackgroundAlpha(progress * 0.8f); if (mLauncher.getHotseat() != null) { mLauncher.getHotseat().setTranslationX(translationX); @@ -1648,47 +1687,40 @@ public class Workspace extends SmoothPagedView updateStateForCustomContent(screenCenter); enableHwLayersOnVisiblePages(); - boolean shouldOverScroll = (mOverScrollX < 0 && (!hasCustomContent() || isLayoutRtl())) || - (mOverScrollX > mMaxScrollX && (!hasCustomContent() || !isLayoutRtl())); + boolean shouldOverScroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; if (shouldOverScroll) { 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; final boolean isLeftPage = mOverScrollX < 0; index = (!isRtl && isLeftPage) || (isRtl && !isLeftPage) ? lowerIndex : upperIndex; - pivotX = isLeftPage ? rightBiasedPivot : leftBiasedPivot; CellLayout cl = (CellLayout) getChildAt(index); - float scrollProgress = getScrollProgress(screenCenter, cl, index); - cl.setOverScrollAmount(Math.abs(scrollProgress), isLeftPage); - float rotation = -WORKSPACE_OVERSCROLL_ROTATION * scrollProgress; - cl.setRotationY(rotation); - - if (!mOverscrollTransformsSet || Float.compare(mLastOverscrollPivotX, pivotX) != 0) { - mOverscrollTransformsSet = true; - mLastOverscrollPivotX = pivotX; - cl.setCameraDistance(mDensity * mCameraDistance); - cl.setPivotX(cl.getMeasuredWidth() * pivotX); - cl.setPivotY(cl.getMeasuredHeight() * 0.5f); - cl.setOverscrollTransformsDirty(true); - } + float effect = Math.abs(mOverScrollEffect); + cl.setOverScrollAmount(Math.abs(effect), isLeftPage); + + mOverscrollEffectSet = true; } else { - if (mOverscrollTransformsSet && getChildCount() > 0) { - mOverscrollTransformsSet = false; - ((CellLayout) getChildAt(0)).resetOverscrollTransforms(); - ((CellLayout) getChildAt(getChildCount() - 1)).resetOverscrollTransforms(); + if (mOverscrollEffectSet && getChildCount() > 0) { + mOverscrollEffectSet = false; + ((CellLayout) getChildAt(0)).setOverScrollAmount(0, false); + ((CellLayout) getChildAt(getChildCount() - 1)).setOverScrollAmount(0, false); } } } @Override protected void overScroll(float amount) { - acceleratedOverScroll(amount); + boolean shouldOverScroll = (amount < 0 && (!hasCustomContent() || isLayoutRtl())) || + (amount > 0 && (!hasCustomContent() || !isLayoutRtl())); + if (shouldOverScroll) { + dampedOverScroll(amount); + mOverScrollEffect = acceleratedOverFactor(amount); + } else { + mOverScrollEffect = 0; + } } protected void onAttachedToWindow() { @@ -1738,25 +1770,12 @@ public class Workspace extends SmoothPagedView @Override protected void onDraw(Canvas canvas) { - // 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()) { @@ -1772,7 +1791,7 @@ public class Workspace extends SmoothPagedView @Override public int getDescendantFocusability() { - if (isSmall()) { + if (workspaceInModalState()) { return ViewGroup.FOCUS_BLOCK_DESCENDANTS; } return super.getDescendantFocusability(); @@ -1790,8 +1809,8 @@ public class Workspace extends SmoothPagedView } } - public boolean isSmall() { - return mState == State.SMALL || mState == State.SPRING_LOADED || mState == State.OVERVIEW; + public boolean workspaceInModalState() { + return mState != State.NORMAL; } void enableChildrenCache(int fromPage, int toPage) { @@ -1826,7 +1845,7 @@ public class Workspace extends SmoothPagedView } private void updateChildrenLayersEnabled(boolean force) { - boolean small = mState == State.SMALL || mState == State.OVERVIEW || mIsSwitchingState; + boolean small = mState == State.OVERVIEW || mIsSwitchingState; boolean enableChildrenLayers = force || small || mAnimatingViewIntoPlace || isPageMoving(); if (enableChildrenLayers != mChildrenLayersEnabled) { @@ -1964,21 +1983,54 @@ public class Workspace extends SmoothPagedView * appearance). * */ - public void onDragStartedWithItem(View v) { - final Canvas canvas = new Canvas(); + private static Rect getDrawableBounds(Drawable d) { + Rect bounds = new Rect(); + d.copyBounds(bounds); + if (bounds.width() == 0 || bounds.height() == 0) { + bounds.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); + } else { + bounds.offsetTo(0, 0); + } + if (d instanceof PreloadIconDrawable) { + int inset = -((PreloadIconDrawable) d).getOutset(); + bounds.inset(inset, inset); + } + return bounds; + } + + public void onExternalDragStartedWithItem(View v) { + // Compose a drag bitmap with the view scaled to the icon size + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + int iconSize = grid.iconSizePx; + int bmpWidth = v.getMeasuredWidth(); + int bmpHeight = v.getMeasuredHeight(); + + // If this is a text view, use its drawable instead + if (v instanceof TextView) { + TextView tv = (TextView) v; + Drawable d = tv.getCompoundDrawables()[1]; + Rect bounds = getDrawableBounds(d); + bmpWidth = bounds.width(); + bmpHeight = bounds.height(); + } + + // Compose the bitmap to create the icon from + Bitmap b = Bitmap.createBitmap(bmpWidth, bmpHeight, + Bitmap.Config.ARGB_8888); + mCanvas.setBitmap(b); + drawDragView(v, mCanvas, 0); + mCanvas.setBitmap(null); // The outline is used to visualize where the item will land if dropped - mDragOutline = createDragOutline(v, canvas, DRAG_BITMAP_PADDING); + mDragOutline = createDragOutline(b, DRAG_BITMAP_PADDING, iconSize, iconSize, true); } 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); + mDragOutline = createDragOutline(b, DRAG_BITMAP_PADDING, size[0], size[1], clipAlpha); } public void exitWidgetResizeMode() { @@ -1996,18 +2048,23 @@ public class Workspace extends SmoothPagedView mNewAlphas = new float[childCount]; } - Animator getChangeStateAnimation(final State state, boolean animated) { - return getChangeStateAnimation(state, animated, 0, -1); + Animator getChangeStateAnimation(final State state, boolean animated, + ArrayList<View> layerViews) { + return getChangeStateAnimation(state, animated, 0, -1, layerViews); } @Override - protected void getOverviewModePages(int[] range) { + protected void getFreeScrollPageRange(int[] range) { + getOverviewModePages(range); + } + + private void getOverviewModePages(int[] range) { int start = numCustomPages(); int end = getChildCount() - 1; range[0] = Math.max(0, Math.min(start, getChildCount() - 1)); range[1] = Math.max(0, end); - } + } protected void onStartReordering() { super.onStartReordering(); @@ -2019,6 +2076,11 @@ public class Workspace extends SmoothPagedView protected void onEndReordering() { super.onEndReordering(); + if (mLauncher.isWorkspaceLoading()) { + // Invalid and dangerous operation if workspace is loading + return; + } + hideOutlines(); mScreenOrder.clear(); int count = getChildCount(); @@ -2110,6 +2172,10 @@ public class Workspace extends SmoothPagedView updateAccessibilityFlags(); } + State getState() { + return mState; + } + private void updateAccessibilityFlags() { int accessible = mState == State.NORMAL ? ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES : @@ -2117,7 +2183,14 @@ public class Workspace extends SmoothPagedView setImportantForAccessibility(accessible); } + private static final int HIDE_WORKSPACE_DURATION = 100; + Animator getChangeStateAnimation(final State state, boolean animated, int delay, int snapPage) { + return getChangeStateAnimation(state, animated, delay, snapPage, null); + } + + Animator getChangeStateAnimation(final State state, boolean animated, int delay, int snapPage, + ArrayList<View> layerViews) { if (mState == state) { return null; } @@ -2130,21 +2203,25 @@ public class Workspace extends SmoothPagedView final State oldState = mState; final boolean oldStateIsNormal = (oldState == State.NORMAL); final boolean oldStateIsSpringLoaded = (oldState == State.SPRING_LOADED); - final boolean oldStateIsSmall = (oldState == State.SMALL); + final boolean oldStateIsNormalHidden = (oldState == State.NORMAL_HIDDEN); + final boolean oldStateIsOverviewHidden = (oldState == State.OVERVIEW_HIDDEN); final boolean oldStateIsOverview = (oldState == State.OVERVIEW); setState(state); final boolean stateIsNormal = (state == State.NORMAL); final boolean stateIsSpringLoaded = (state == State.SPRING_LOADED); - final boolean stateIsSmall = (state == State.SMALL); + final boolean stateIsNormalHidden = (state == State.NORMAL_HIDDEN); + final boolean stateIsOverviewHidden = (state == State.OVERVIEW_HIDDEN); final boolean stateIsOverview = (state == State.OVERVIEW); float finalBackgroundAlpha = (stateIsSpringLoaded || stateIsOverview) ? 1.0f : 0f; - float finalHotseatAndPageIndicatorAlpha = (stateIsOverview || stateIsSmall) ? 0f : 1f; + float finalHotseatAndPageIndicatorAlpha = (stateIsNormal || stateIsSpringLoaded) ? 1f : 0f; float finalOverviewPanelAlpha = stateIsOverview ? 1f : 0f; float finalSearchBarAlpha = !stateIsNormal ? 0f : 1f; - float finalWorkspaceTranslationY = stateIsOverview ? getOverviewModeTranslationY() : 0; + float finalWorkspaceTranslationY = stateIsOverview || stateIsOverviewHidden ? + getOverviewModeTranslationY() : 0; - boolean workspaceToAllApps = (oldStateIsNormal && stateIsSmall); - boolean allAppsToWorkspace = (oldStateIsSmall && stateIsNormal); + boolean workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden); + boolean overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden); + boolean allAppsToWorkspace = (stateIsNormalHidden && stateIsNormal); boolean workspaceToOverview = (oldStateIsNormal && stateIsOverview); boolean overviewToWorkspace = (oldStateIsOverview && stateIsNormal); @@ -2159,19 +2236,14 @@ public class Workspace extends SmoothPagedView if (state != State.NORMAL) { if (stateIsSpringLoaded) { mNewScale = mSpringLoadedShrinkFactor; - } else if (stateIsOverview) { + } else if (stateIsOverview || stateIsOverviewHidden) { mNewScale = mOverviewModeShrinkFactor; - } else if (stateIsSmall){ - mNewScale = mOverviewModeShrinkFactor - 0.3f; - } - if (workspaceToAllApps) { - updateChildrenLayersEnabled(false); } } final int duration; - if (workspaceToAllApps) { - duration = getResources().getInteger(R.integer.config_workspaceUnshrinkTime); + if (workspaceToAllApps || overviewToAllApps) { + duration = HIDE_WORKSPACE_DURATION; //getResources().getInteger(R.integer.config_workspaceUnshrinkTime); } else if (workspaceToOverview || overviewToWorkspace) { duration = getResources().getInteger(R.integer.config_overviewTransitionTime); } else { @@ -2188,7 +2260,7 @@ public class Workspace extends SmoothPagedView boolean isCurrentPage = (i == snapPage); float initialAlpha = cl.getShortcutsAndWidgets().getAlpha(); float finalAlpha; - if (stateIsSmall) { + if (stateIsNormalHidden || stateIsOverviewHidden) { finalAlpha = 0f; } else if (stateIsNormal && mWorkspaceFadeInAdjacentScreens) { finalAlpha = (i == snapPage || i < numCustomPages()) ? 1f : 0f; @@ -2225,11 +2297,11 @@ public class Workspace extends SmoothPagedView final View hotseat = mLauncher.getHotseat(); final View pageIndicator = getPageIndicator(); if (animated) { - anim.setDuration(duration); LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(this); scale.scaleX(mNewScale) .scaleY(mNewScale) .translationY(finalWorkspaceTranslationY) + .setDuration(duration) .setInterpolator(mZoomInInterpolator); anim.play(scale); for (int index = 0; index < getChildCount(); index++) { @@ -2240,10 +2312,14 @@ public class Workspace extends SmoothPagedView cl.setBackgroundAlpha(mNewBackgroundAlphas[i]); cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); } else { + if (layerViews != null) { + layerViews.add(cl); + } 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); } @@ -2252,6 +2328,7 @@ public class Workspace extends SmoothPagedView ValueAnimator bgAnim = LauncherAnimUtils.ofFloat(cl, 0f, 1f); bgAnim.setInterpolator(mZoomInInterpolator); + bgAnim.setDuration(duration); bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { public void onAnimationUpdate(float a, float b) { cl.setBackgroundAlpha( @@ -2285,6 +2362,17 @@ public class Workspace extends SmoothPagedView .alpha(finalOverviewPanelAlpha).withLayer(); overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel)); + // For animation optimations, we may need to provide the Launcher transition + // with a set of views on which to force build layers in certain scenarios. + hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null); + searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null); + overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null); + if (layerViews != null) { + layerViews.add(hotseat); + layerViews.add(searchBar); + layerViews.add(overviewPanel); + } + if (workspaceToOverview) { pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2)); hotseatAlpha.setInterpolator(new DecelerateInterpolator(2)); @@ -2294,7 +2382,11 @@ public class Workspace extends SmoothPagedView hotseatAlpha.setInterpolator(null); overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2)); } - searchBarAlpha.setInterpolator(null); + + overviewPanelAlpha.setDuration(duration); + pageIndicatorAlpha.setDuration(duration); + hotseatAlpha.setDuration(duration); + searchBarAlpha.setDuration(duration); anim.play(overviewPanelAlpha); anim.play(hotseatAlpha); @@ -2319,18 +2411,11 @@ public class Workspace extends SmoothPagedView } mLauncher.updateVoiceButtonProxyVisible(false); - 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 if (stateIsOverview) { - animateBackgroundGradient(getResources().getInteger( - R.integer.config_appsCustomizeSpringLoadedBgAlpha) / 100f, true); - } else { - // Fade the background gradient away + if (stateIsNormal) { animateBackgroundGradient(0f, animated); + } else { + animateBackgroundGradient(getResources().getInteger( + R.integer.config_workspaceScrimAlpha) / 100f, animated); } return anim; } @@ -2433,21 +2518,6 @@ public class Workspace extends SmoothPagedView private void onTransitionEnd() { mIsSwitchingState = 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); - } - } else { - for (int i = 0; i < numCustomPages(); i++) { - final CellLayout cl = (CellLayout) getChildAt(i); - cl.setShortcutAndWidgetAlpha(1f); - } - } showCustomContentIfNecessary(); } @@ -2463,17 +2533,18 @@ public class Workspace extends SmoothPagedView * @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; + private static void drawDragView(View v, Canvas destCanvas, int padding) { + final Rect clipRect = sTempRect; v.getDrawingRect(clipRect); boolean textVisible = false; destCanvas.save(); - if (v instanceof TextView && pruneToDrawable) { + if (v instanceof TextView) { Drawable d = ((TextView) v).getCompoundDrawables()[1]; - clipRect.set(0, 0, d.getIntrinsicWidth() + padding, d.getIntrinsicHeight() + padding); - destCanvas.translate(padding / 2, padding / 2); + Rect bounds = getDrawableBounds(d); + clipRect.set(0, 0, bounds.width() + padding, bounds.height() + padding); + destCanvas.translate(padding / 2 - bounds.left, padding / 2 - bounds.top); d.draw(destCanvas); } else { if (v instanceof FolderIcon) { @@ -2483,14 +2554,6 @@ public class Workspace extends SmoothPagedView ((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); @@ -2507,22 +2570,27 @@ public class Workspace extends SmoothPagedView /** * Returns a new bitmap to show when the given View is being dragged around. * Responsibility for the bitmap is transferred to the caller. + * @param expectedPadding padding to add to the drag view. If a different padding was used + * its value will be changed */ - public Bitmap createDragBitmap(View v, Canvas canvas, int padding) { + public Bitmap createDragBitmap(View v, AtomicInteger expectedPadding) { Bitmap b; + int padding = expectedPadding.get(); if (v instanceof TextView) { Drawable d = ((TextView) v).getCompoundDrawables()[1]; - b = Bitmap.createBitmap(d.getIntrinsicWidth() + padding, - d.getIntrinsicHeight() + padding, Bitmap.Config.ARGB_8888); + Rect bounds = getDrawableBounds(d); + b = Bitmap.createBitmap(bounds.width() + padding, + bounds.height() + padding, Bitmap.Config.ARGB_8888); + expectedPadding.set(padding - bounds.left - bounds.top); } 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); + mCanvas.setBitmap(b); + drawDragView(v, mCanvas, padding); + mCanvas.setBitmap(null); return b; } @@ -2531,15 +2599,15 @@ public class Workspace extends SmoothPagedView * 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) { + private Bitmap createDragOutline(View v, int padding) { final int outlineColor = getResources().getColor(R.color.outline_color); 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); + mCanvas.setBitmap(b); + drawDragView(v, mCanvas, padding); + mOutlineHelper.applyExpensiveOutlineWithBlur(b, mCanvas, outlineColor, outlineColor); + mCanvas.setBitmap(null); return b; } @@ -2547,11 +2615,11 @@ public class Workspace extends SmoothPagedView * 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, + private Bitmap createDragOutline(Bitmap orig, int padding, int w, int h, boolean clipAlpha) { final int outlineColor = getResources().getColor(R.color.outline_color); final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - canvas.setBitmap(b); + mCanvas.setBitmap(b); Rect src = new Rect(0, 0, orig.getWidth(), orig.getHeight()); float scaleFactor = Math.min((w - padding) / (float) orig.getWidth(), @@ -2563,10 +2631,10 @@ public class Workspace extends SmoothPagedView // center the image dst.offset((w - scaledWidth) / 2, (h - scaledHeight) / 2); - canvas.drawBitmap(orig, src, dst, null); - mOutlineHelper.applyMediumExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor, + mCanvas.drawBitmap(orig, src, dst, null); + mOutlineHelper.applyExpensiveOutlineWithBlur(b, mCanvas, outlineColor, outlineColor, clipAlpha); - canvas.setBitmap(null); + mCanvas.setBitmap(null); return b; } @@ -2584,35 +2652,34 @@ public class Workspace extends SmoothPagedView CellLayout layout = (CellLayout) child.getParent().getParent(); layout.prepareChildForDrag(child); + beginDragShared(child, this); + } + + public void beginDragShared(View child, DragSource source) { 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); - } + mDragOutline = createDragOutline(child, DRAG_BITMAP_PADDING); - public void beginDragShared(View child, DragSource source) { + mLauncher.onDragStarted(child); // The drag bitmap follows the touch point around on the screen - final Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING); + AtomicInteger padding = new AtomicInteger(DRAG_BITMAP_PADDING); + final Bitmap b = createDragBitmap(child, 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); + int dragLayerX = Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2); + int dragLayerY = Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2 + - padding.get() / 2); LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); Point dragVisualizeOffset = null; Rect dragRect = null; - if (child instanceof BubbleTextView || child instanceof PagedViewIcon) { + if (child instanceof BubbleTextView) { int iconSize = grid.iconSizePx; int top = child.getPaddingTop(); int left = (bmpWidth - iconSize) / 2; @@ -2621,7 +2688,7 @@ public class Workspace extends SmoothPagedView 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, DRAG_BITMAP_PADDING / 2); + dragVisualizeOffset = new Point(-padding.get() / 2, padding.get() / 2); dragRect = new Rect(left, top, right, bottom); } else if (child instanceof FolderIcon) { int previewSize = grid.folderIconSizePx; @@ -2631,10 +2698,7 @@ public class Workspace extends SmoothPagedView // Clear the pressed state if necessary if (child instanceof BubbleTextView) { BubbleTextView icon = (BubbleTextView) child; - icon.clearPressedOrFocusedBackground(); - } else if (child instanceof FolderIcon) { - // Dismiss the folder cling if we haven't already - mLauncher.getLauncherClings().markFolderClingDismissed(); + icon.clearPressedBackground(); } if (child.getTag() == null || !(child.getTag() instanceof ItemInfo)) { @@ -2655,6 +2719,53 @@ public class Workspace extends SmoothPagedView b.recycle(); } + public void beginExternalDragShared(View child, DragSource source) { + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + int iconSize = grid.iconSizePx; + + // Notify launcher of drag start + mLauncher.onDragStarted(child); + + // Compose a new drag bitmap that is of the icon size + AtomicInteger padding = new AtomicInteger(DRAG_BITMAP_PADDING); + final Bitmap tmpB = createDragBitmap(child, padding); + Bitmap b = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); + Paint p = new Paint(); + p.setFilterBitmap(true); + mCanvas.setBitmap(b); + mCanvas.drawBitmap(tmpB, new Rect(0, 0, tmpB.getWidth(), tmpB.getHeight()), + new Rect(0, 0, iconSize, iconSize), p); + mCanvas.setBitmap(null); + + // Find the child's location on the screen + int bmpWidth = tmpB.getWidth(); + float iconScale = (float) bmpWidth / iconSize; + float scale = mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY) * iconScale; + int dragLayerX = Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2); + int dragLayerY = Math.round(mTempXY[1]); + + // 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. + Point dragVisualizeOffset = new Point(-padding.get() / 2, padding.get() / 2); + Rect dragRect = new Rect(0, 0, iconSize, iconSize); + + if (child.getTag() == null || !(child.getTag() instanceof ItemInfo)) { + String msg = "Drag started with a view that has no tag set. This " + + "will cause a crash (issue 11627249) down the line. " + + "View: " + child + " tag: " + child.getTag(); + throw new IllegalStateException(msg); + } + + // Start the drag + DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); + dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor()); + + // Recycle temporary bitmaps + tmpB.recycle(); + } + void addApplicationShortcut(ShortcutInfo info, CellLayout target, long container, long screenId, int cellX, int cellY, boolean insertAtFirst, int intersectX, int intersectY) { View view = mLauncher.createShortcut(R.layout.application, target, (ShortcutInfo) info); @@ -2668,7 +2779,8 @@ public class Workspace extends SmoothPagedView } public boolean transitionStateShouldAllowDrop() { - return ((!isSwitchingState() || mTransitionProgress > 0.5f) && mState != State.SMALL); + return ((!isSwitchingState() || mTransitionProgress > 0.5f) && + (mState == State.NORMAL || mState == State.SPRING_LOADED)); } /** @@ -2934,13 +3046,11 @@ public class Workspace extends SmoothPagedView // 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)) { - removeExtraEmptyScreen(true, null, 0, true); return; } if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell, distance, d, false)) { - removeExtraEmptyScreen(true, null, 0, true); return; } @@ -2981,7 +3091,12 @@ public class Workspace extends SmoothPagedView final ItemInfo info = (ItemInfo) cell.getTag(); if (hasMovedLayouts) { // Reparent the view - getParentCellLayoutForView(cell).removeView(cell); + CellLayout parentCell = getParentCellLayoutForView(cell); + if (parentCell != null) { + parentCell.removeView(cell); + } else if (LauncherAppState.isDogfoodBuild()) { + throw new NullPointerException("mDragInfo.cell has null parent"); + } addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1], info.spanX, info.spanY); } @@ -3046,7 +3161,6 @@ public class Workspace extends SmoothPagedView if (finalResizeRunnable != null) { finalResizeRunnable.run(); } - removeExtraEmptyScreen(true, null, 0, true); } }; mAnimatingViewIntoPlace = true; @@ -3115,10 +3229,8 @@ public class Workspace extends SmoothPagedView 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 (LauncherAppState.getInstance().isScreenLarge()) { - showOutlines(); + if (!workspaceInModalState()) { + mLauncher.getDragLayer().showPageHints(); } } @@ -3128,7 +3240,6 @@ public class Workspace extends SmoothPagedView LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - Resources res = launcher.getResources(); Display display = launcher.getWindowManager().getDefaultDisplay(); Point smallestSize = new Point(); Point largestSize = new Point(); @@ -3194,6 +3305,7 @@ public class Workspace extends SmoothPagedView if (!mIsPageMoving) { hideOutlines(); } + mLauncher.getDragLayer().hidePageHints(); } void setCurrentDropLayout(CellLayout layout) { @@ -3436,11 +3548,17 @@ public class Workspace extends SmoothPagedView public void onDragOver(DragObject d) { // Skip drag over events while we are dragging over side pages - if (mInScrollArea || mIsSwitchingState || mState == State.SMALL) return; + if (mInScrollArea || !transitionStateShouldAllowDrop()) return; Rect r = new Rect(); CellLayout layout = null; ItemInfo item = (ItemInfo) d.dragInfo; + if (item == null) { + if (LauncherAppState.isDogfoodBuild()) { + throw new NullPointerException("DragObject has null info"); + } + return; + } // 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"); @@ -3449,7 +3567,7 @@ public class Workspace extends SmoothPagedView final View child = (mDragInfo == null) ? null : mDragInfo.cell; // Identify whether we have dragged over a side page - if (isSmall()) { + if (workspaceInModalState()) { if (mLauncher.getHotseat() != null && !isExternalDragWidget(d)) { if (isPointInSelfOverHotseat(d.x, d.y, r)) { layout = mLauncher.getHotseat().getLayout(); @@ -3700,13 +3818,8 @@ public class Workspace extends SmoothPagedView final Runnable exitSpringLoadedRunnable = new Runnable() { @Override public void run() { - removeExtraEmptyScreen(false, new Runnable() { - @Override - public void run() { - mLauncher.exitSpringLoadedDragModeDelayed(true, - Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); - } - }); + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); } }; @@ -3768,6 +3881,11 @@ public class Workspace extends SmoothPagedView Runnable onAnimationCompleteRunnable = new Runnable() { @Override public void run() { + // Normally removeExtraEmptyScreen is called in Workspace#onDragEnd, but when + // adding an item that may not be dropped right away (due to a config activity) + // we defer the removal until the activity returns. + deferRemoveExtraEmptyScreen(); + // When dragging and dropping from customization tray, we deal with creating // widgets/shortcuts/folders in a slightly different way switch (pendingInfo.itemType) { @@ -3852,15 +3970,16 @@ public class Workspace extends SmoothPagedView } else { cellLayout.findCellForSpan(mTargetCell, 1, 1); } + // Add the item to DB before adding to screen ensures that the container and other + // values of the info is properly updated. + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screenId, + mTargetCell[0], mTargetCell[1]); + addInScreen(view, container, screenId, 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, screenId, - 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 @@ -3883,12 +4002,12 @@ public class Workspace extends SmoothPagedView 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); + mCanvas.setBitmap(b); layout.measure(width, height); layout.layout(0, 0, unScaledSize[0], unScaledSize[1]); - layout.draw(c); - c.setBitmap(null); + layout.draw(mCanvas); + mCanvas.setBitmap(null); layout.setVisibility(visibility); return b; } @@ -4064,14 +4183,12 @@ public class Workspace extends SmoothPagedView CellLayout parentCell = getParentCellLayoutForView(mDragInfo.cell); if (parentCell != null) { parentCell.removeView(mDragInfo.cell); + } else if (LauncherAppState.isDogfoodBuild()) { + throw new NullPointerException("mDragInfo.cell has null parent"); } if (mDragInfo.cell instanceof DropTarget) { mDragController.removeDropTarget((DropTarget) mDragInfo.cell); } - // If we move the item to anything not on the Workspace, check if any empty - // screens need to be removed. If we dropped back on the workspace, this will - // be done post drop animation. - removeExtraEmptyScreen(true, null, 0, true); } } else if (mDragInfo != null) { CellLayout cellLayout; @@ -4327,7 +4444,7 @@ public class Workspace extends SmoothPagedView @Override public void scrollLeft() { - if (!isSmall() && !mIsSwitchingState) { + if (!workspaceInModalState() && !mIsSwitchingState) { super.scrollLeft(); } Folder openFolder = getOpenFolder(); @@ -4338,7 +4455,7 @@ public class Workspace extends SmoothPagedView @Override public void scrollRight() { - if (!isSmall() && !mIsSwitchingState) { + if (!workspaceInModalState() && !mIsSwitchingState) { super.scrollRight(); } Folder openFolder = getOpenFolder(); @@ -4360,7 +4477,7 @@ public class Workspace extends SmoothPagedView } boolean result = false; - if (!isSmall() && !mIsSwitchingState && getOpenFolder() == null) { + if (!workspaceInModalState() && !mIsSwitchingState && getOpenFolder() == null) { mInScrollArea = true; final int page = getNextPage() + @@ -4452,57 +4569,70 @@ public class Workspace extends SmoothPagedView 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; - } - } + public Folder getFolderForTag(final Object tag) { + return (Folder) getFirstMatch(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + return (v instanceof Folder) && (((Folder) v).getInfo() == tag) + && ((Folder) v).getInfo().opened; } - } - 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; + public View getViewForTag(final Object tag) { + return getFirstMatch(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + return info == tag; + } + }); + } + + public LauncherAppWidgetHostView getWidgetForAppWidgetId(final int appWidgetId) { + return (LauncherAppWidgetHostView) getFirstMatch(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + return (info instanceof LauncherAppWidgetInfo) && + ((LauncherAppWidgetInfo) info).appWidgetId == appWidgetId; + } + }); + } + + private View getFirstMatch(final ItemOperator operator) { + final View[] value = new View[1]; + mapOverItems(MAP_NO_RECURSE, new ItemOperator() { + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + if (operator.evaluate(info, v, parent)) { + value[0] = v; + return true; } + return false; } - } - return null; + }); + return value[0]; } 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); + mapOverItems(MAP_NO_RECURSE, new ItemOperator() { + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { if (v instanceof DropTarget) { mDragController.removeDropTarget((DropTarget) v); } + // not done, process all the shortcuts + return false; } - } + }); } // 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) { + void removeItemsByPackageName(final ArrayList<String> packages, final UserHandleCompat user) { final HashSet<String> packageNames = new HashSet<String>(); packageNames.addAll(packages); @@ -4522,7 +4652,8 @@ public class Workspace extends SmoothPagedView @Override public boolean filterItem(ItemInfo parent, ItemInfo info, ComponentName cn) { - if (packageNames.contains(cn.getPackageName())) { + if (packageNames.contains(cn.getPackageName()) + && info.user.equals(user)) { cns.add(cn); return true; } @@ -4532,13 +4663,13 @@ public class Workspace extends SmoothPagedView LauncherModel.filterItemInfos(infos, filter); // Remove the affected components - removeItemsByComponentName(cns); + removeItemsByComponentName(cns, user); } // 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<AppInfo> appInfos) { + void removeItemsByApplicationInfo(final ArrayList<AppInfo> appInfos, UserHandleCompat user) { // Just create a hash table of all the specific components that this will affect HashSet<ComponentName> cns = new HashSet<ComponentName>(); for (AppInfo info : appInfos) { @@ -4546,10 +4677,11 @@ public class Workspace extends SmoothPagedView } // Remove all the things - removeItemsByComponentName(cns); + removeItemsByComponentName(cns, user); } - void removeItemsByComponentName(final HashSet<ComponentName> componentNames) { + void removeItemsByComponentName(final HashSet<ComponentName> componentNames, + final UserHandleCompat user) { ArrayList<CellLayout> cellLayouts = getWorkspaceAndHotseatCellLayouts(); for (final CellLayout layoutParent: cellLayouts) { final ViewGroup layout = layoutParent.getShortcutsAndWidgets(); @@ -4568,7 +4700,7 @@ public class Workspace extends SmoothPagedView public boolean filterItem(ItemInfo parent, ItemInfo info, ComponentName cn) { if (parent instanceof FolderInfo) { - if (componentNames.contains(cn)) { + if (componentNames.contains(cn) && info.user.equals(user)) { FolderInfo folder = (FolderInfo) parent; ArrayList<ShortcutInfo> appsToRemove; if (folderAppsToRemove.containsKey(folder)) { @@ -4581,7 +4713,7 @@ public class Workspace extends SmoothPagedView return true; } } else { - if (componentNames.contains(cn)) { + if (componentNames.contains(cn) && info.user.equals(user)) { childrenToRemove.add(children.get(info)); return true; } @@ -4619,56 +4751,290 @@ public class Workspace extends SmoothPagedView stripEmptyScreens(); } - private void updateShortcut(HashMap<ComponentName, AppInfo> appsMap, ItemInfo info, - View child) { - ComponentName cn = info.getIntent().getComponent(); - if (info.getRestoredIntent() != null) { - cn = info.getRestoredIntent().getComponent(); + interface ItemOperator { + /** + * Process the next itemInfo, possibly with side-effect on {@link ItemOperator#value}. + * + * @param info info for the shortcut + * @param view view for the shortcut + * @param parent containing folder, or null + * @return true if done, false to continue the map + */ + public boolean evaluate(ItemInfo info, View view, View parent); + } + + /** + * Map the operator over the shortcuts and widgets, return the first-non-null value. + * + * @param recurse true: iterate over folder children. false: op get the folders themselves. + * @param op the operator to map over the shortcuts + */ + void mapOverItems(boolean recurse, ItemOperator op) { + ArrayList<ShortcutAndWidgetContainer> containers = getAllShortcutAndWidgetContainers(); + final int containerCount = containers.size(); + for (int containerIdx = 0; containerIdx < containerCount; containerIdx++) { + ShortcutAndWidgetContainer container = containers.get(containerIdx); + // map over all the shortcuts on the workspace + final int itemCount = container.getChildCount(); + for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) { + View item = container.getChildAt(itemIdx); + ItemInfo info = (ItemInfo) item.getTag(); + if (recurse && info instanceof FolderInfo && item instanceof FolderIcon) { + FolderIcon folder = (FolderIcon) item; + ArrayList<View> folderChildren = folder.getFolder().getItemsInReadingOrder(); + // map over all the children in the folder + final int childCount = folderChildren.size(); + for (int childIdx = 0; childIdx < childCount; childIdx++) { + View child = folderChildren.get(childIdx); + info = (ItemInfo) child.getTag(); + if (op.evaluate(info, child, folder)) { + return; + } + } + } else { + if (op.evaluate(info, item, null)) { + return; + } + } + } } - if (cn != null) { - AppInfo appInfo = appsMap.get(cn); - if ((appInfo != null) && LauncherModel.isShortcutInfoUpdateable(info)) { - ShortcutInfo shortcutInfo = (ShortcutInfo) info; - BubbleTextView shortcut = (BubbleTextView) child; - shortcutInfo.restore(); - shortcutInfo.updateIcon(mIconCache); - shortcutInfo.title = appInfo.title.toString(); - shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache); + } + + void updateShortcutsAndWidgets(ArrayList<AppInfo> apps) { + // Break the appinfo list per user + final HashMap<UserHandleCompat, ArrayList<AppInfo>> appsPerUser = + new HashMap<UserHandleCompat, ArrayList<AppInfo>>(); + for (AppInfo info : apps) { + ArrayList<AppInfo> filtered = appsPerUser.get(info.user); + if (filtered == null) { + filtered = new ArrayList<AppInfo>(); + appsPerUser.put(info.user, filtered); } + filtered.add(info); + } + + for (Map.Entry<UserHandleCompat, ArrayList<AppInfo>> entry : appsPerUser.entrySet()) { + updateShortcutsAndWidgetsPerUser(entry.getValue(), entry.getKey()); } } - void updateShortcuts(ArrayList<AppInfo> apps) { + private void updateShortcutsAndWidgetsPerUser(ArrayList<AppInfo> apps, + final UserHandleCompat user) { // Create a map of the apps to test against final HashMap<ComponentName, AppInfo> appsMap = new HashMap<ComponentName, AppInfo>(); + final HashSet<String> pkgNames = new HashSet<String>(); for (AppInfo ai : apps) { appsMap.put(ai.componentName, ai); + pkgNames.add(ai.componentName.getPackageName()); } + final HashSet<ComponentName> iconsToRemove = new HashSet<ComponentName>(); - ArrayList<ShortcutAndWidgetContainer> childrenLayouts = getAllShortcutAndWidgetContainers(); - for (ShortcutAndWidgetContainer layout: childrenLayouts) { - // Update all the children shortcuts - final HashMap<ItemInfo, View> children = new HashMap<ItemInfo, View>(); - for (int j = 0; j < layout.getChildCount(); j++) { - View v = layout.getChildAt(j); - ItemInfo info = (ItemInfo) v.getTag(); - if (info instanceof FolderInfo && v instanceof FolderIcon) { - FolderIcon folder = (FolderIcon) v; - ArrayList<View> folderChildren = folder.getFolder().getItemsInReadingOrder(); - for (View fv : folderChildren) { - info = (ItemInfo) fv.getTag(); - updateShortcut(appsMap, info, fv); + mapOverItems(MAP_RECURSE, new ItemOperator() { + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + if (info instanceof ShortcutInfo && v instanceof BubbleTextView) { + ShortcutInfo shortcutInfo = (ShortcutInfo) info; + ComponentName cn = shortcutInfo.getTargetComponent(); + AppInfo appInfo = appsMap.get(cn); + if (user.equals(shortcutInfo.user) && cn != null + && LauncherModel.isShortcutInfoUpdateable(info) + && pkgNames.contains(cn.getPackageName())) { + boolean promiseStateChanged = false; + boolean infoUpdated = false; + if (shortcutInfo.isPromise()) { + if (shortcutInfo.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) { + // Auto install icon + PackageManager pm = getContext().getPackageManager(); + ResolveInfo matched = pm.resolveActivity( + new Intent(Intent.ACTION_MAIN) + .setComponent(cn).addCategory(Intent.CATEGORY_LAUNCHER), + PackageManager.MATCH_DEFAULT_ONLY); + if (matched == null) { + // Try to find the best match activity. + Intent intent = pm.getLaunchIntentForPackage( + cn.getPackageName()); + if (intent != null) { + cn = intent.getComponent(); + appInfo = appsMap.get(cn); + } + + if ((intent == null) || (appsMap == null)) { + // Could not find a default activity. Remove this item. + iconsToRemove.add(shortcutInfo.getTargetComponent()); + + // process next shortcut. + return false; + } + shortcutInfo.promisedIntent = intent; + } + } + + // Restore the shortcut. + shortcutInfo.intent = shortcutInfo.promisedIntent; + shortcutInfo.promisedIntent = null; + shortcutInfo.status &= ~ShortcutInfo.FLAG_RESTORED_ICON + & ~ShortcutInfo.FLAG_AUTOINTALL_ICON + & ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; + + promiseStateChanged = true; + infoUpdated = true; + shortcutInfo.updateIcon(mIconCache); + LauncherModel.updateItemInDatabase(getContext(), shortcutInfo); + } + + + if (appInfo != null) { + shortcutInfo.updateIcon(mIconCache); + shortcutInfo.title = appInfo.title.toString(); + shortcutInfo.contentDescription = appInfo.contentDescription; + infoUpdated = true; + } + + if (infoUpdated) { + BubbleTextView shortcut = (BubbleTextView) v; + shortcut.applyFromShortcutInfo(shortcutInfo, + mIconCache, true, promiseStateChanged); + + if (parent != null) { + parent.invalidate(); + } + } + } + } + // process all the shortcuts + return false; + } + }); + + if (!iconsToRemove.isEmpty()) { + removeItemsByComponentName(iconsToRemove, user); + } + if (user.equals(UserHandleCompat.myUserHandle())) { + restorePendingWidgets(pkgNames); + } + } + + public void removeAbandonedPromise(String packageName, UserHandleCompat user) { + ArrayList<String> packages = new ArrayList<String>(1); + packages.add(packageName); + LauncherModel.deletePackageFromDatabase(mLauncher, packageName, user); + removeItemsByPackageName(packages, user); + } + + public void updatePackageBadge(final String packageName, final UserHandleCompat user) { + mapOverItems(MAP_RECURSE, new ItemOperator() { + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + if (info instanceof ShortcutInfo && v instanceof BubbleTextView) { + ShortcutInfo shortcutInfo = (ShortcutInfo) info; + ComponentName cn = shortcutInfo.getTargetComponent(); + if (user.equals(shortcutInfo.user) && cn != null + && shortcutInfo.isPromise() + && packageName.equals(cn.getPackageName())) { + if (shortcutInfo.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) { + // For auto install apps update the icon as well as label. + mIconCache.getTitleAndIcon(shortcutInfo, + shortcutInfo.promisedIntent, user, true); + } else { + // Only update the icon for restored apps. + shortcutInfo.updateIcon(mIconCache); + } + BubbleTextView shortcut = (BubbleTextView) v; + shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache, true, false); + + if (parent != null) { + parent.invalidate(); + } + } + } + // process all the shortcuts + return false; + } + }); + } + + public void updatePackageState(ArrayList<PackageInstallInfo> installInfos) { + HashSet<String> completedPackages = new HashSet<String>(); + + for (final PackageInstallInfo installInfo : installInfos) { + mapOverItems(MAP_RECURSE, new ItemOperator() { + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + if (info instanceof ShortcutInfo && v instanceof BubbleTextView) { + ShortcutInfo si = (ShortcutInfo) info; + ComponentName cn = si.getTargetComponent(); + if (si.isPromise() && (cn != null) + && installInfo.packageName.equals(cn.getPackageName())) { + si.setInstallProgress(installInfo.progress); + if (installInfo.state == PackageInstallerCompat.STATUS_FAILED) { + // Mark this info as broken. + si.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; + } + ((BubbleTextView)v).applyState(false); + } + } else if (v instanceof PendingAppWidgetHostView + && info instanceof LauncherAppWidgetInfo + && ((LauncherAppWidgetInfo) info).providerName.getPackageName() + .equals(installInfo.packageName)) { + ((LauncherAppWidgetInfo) info).installProgress = installInfo.progress; + ((PendingAppWidgetHostView) v).applyState(); + } + + // process all the shortcuts + return false; + } + }); + + if (installInfo.state == PackageInstallerCompat.STATUS_INSTALLED) { + completedPackages.add(installInfo.packageName); + } + } + + // Note that package states are sent only for myUser + if (!completedPackages.isEmpty()) { + restorePendingWidgets(completedPackages); + } + } + + private void restorePendingWidgets(final Set<String> installedPackaged) { + final ArrayList<LauncherAppWidgetInfo> changedInfo = new ArrayList<LauncherAppWidgetInfo>(); + + // Iterate non recursively as widgets can't be inside a folder. + mapOverItems(MAP_NO_RECURSE, new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + if (info instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info; + if (widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) + && installedPackaged.contains(widgetInfo.providerName.getPackageName())) { + + changedInfo.add(widgetInfo); + + // Remove the provider not ready flag + widgetInfo.restoreStatus &= ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; + LauncherModel.updateItemInDatabase(getContext(), widgetInfo); } - folder.invalidate(); - } else if (info instanceof ShortcutInfo) { - updateShortcut(appsMap, info, v); } + // process all the widget + return false; + } + }); + if (!changedInfo.isEmpty()) { + DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo, + mLauncher.getAppWidgetHost()); + if (LauncherModel.findAppWidgetProviderInfoWithComponent(getContext(), + changedInfo.get(0).providerName) != null) { + // Re-inflate the widgets which have changed status + widgetRefresh.run(); + } else { + // widgetRefresh will automatically run when the packages are updated. } } } private void moveToScreen(int page, boolean animate) { - if (!isSmall()) { + if (!workspaceInModalState()) { if (animate) { snapToPage(page); } else { @@ -4741,4 +5107,53 @@ public class Workspace extends SmoothPagedView public void getLocationInDragLayer(int[] loc) { mLauncher.getDragLayer().getLocationInDragLayer(this, loc); } + + /** + * Used as a workaround to ensure that the AppWidgetService receives the + * PACKAGE_ADDED broadcast before updating widgets. + */ + private class DeferredWidgetRefresh implements Runnable { + private final ArrayList<LauncherAppWidgetInfo> mInfos; + private final LauncherAppWidgetHost mHost; + private final Handler mHandler; + + private boolean mRefreshPending; + + public DeferredWidgetRefresh(ArrayList<LauncherAppWidgetInfo> infos, + LauncherAppWidgetHost host) { + mInfos = infos; + mHost = host; + mHandler = new Handler(); + mRefreshPending = true; + + mHost.addProviderChangeListener(this); + // Force refresh after 10 seconds, if we don't get the provider changed event. + // This could happen when the provider is no longer available in the app. + mHandler.postDelayed(this, 10000); + } + + @Override + public void run() { + mHost.removeProviderChangeListener(this); + mHandler.removeCallbacks(this); + + if (!mRefreshPending) { + return; + } + + mRefreshPending = false; + + for (LauncherAppWidgetInfo info : mInfos) { + if (info.hostView instanceof PendingAppWidgetHostView) { + PendingAppWidgetHostView view = (PendingAppWidgetHostView) info.hostView; + mLauncher.removeAppWidget(info); + + CellLayout cl = (CellLayout) view.getParent().getParent(); + // Remove the current widget + cl.removeView(view); + mLauncher.bindAppWidget(info); + } + } + } + } } diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java new file mode 100644 index 000000000..6512d427e --- /dev/null +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.app.Activity; +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Bundle; + +import com.android.launcher3.IconCache; +import com.android.launcher3.Utilities; + +import java.util.List; + +public abstract class AppWidgetManagerCompat { + + private static final Object sInstanceLock = new Object(); + private static AppWidgetManagerCompat sInstance; + + + public static AppWidgetManagerCompat getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + if (Utilities.isLmpOrAbove()) { + sInstance = new AppWidgetManagerCompatVL(context.getApplicationContext()); + } else { + sInstance = new AppWidgetManagerCompatV16(context.getApplicationContext()); + } + } + return sInstance; + } + } + + final AppWidgetManager mAppWidgetManager; + final Context mContext; + + AppWidgetManagerCompat(Context context) { + mContext = context; + mAppWidgetManager = AppWidgetManager.getInstance(context); + } + + public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) { + return mAppWidgetManager.getAppWidgetInfo(appWidgetId); + } + + public abstract List<AppWidgetProviderInfo> getAllProviders(); + + public abstract String loadLabel(AppWidgetProviderInfo info); + + public abstract boolean bindAppWidgetIdIfAllowed( + int appWidgetId, AppWidgetProviderInfo info, Bundle options); + + public abstract UserHandleCompat getUser(AppWidgetProviderInfo info); + + public abstract void startConfigActivity(AppWidgetProviderInfo info, int widgetId, + Activity activity, AppWidgetHost host, int requestCode); + + public abstract Drawable loadPreview(AppWidgetProviderInfo info); + + public abstract Drawable loadIcon(AppWidgetProviderInfo info, IconCache cache); + + public abstract Bitmap getBadgeBitmap(AppWidgetProviderInfo info, Bitmap bitmap); + +} diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java new file mode 100644 index 000000000..f599f4303 --- /dev/null +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.app.Activity; +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; + +import com.android.launcher3.IconCache; +import com.android.launcher3.Utilities; + +import java.util.List; + +class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { + + AppWidgetManagerCompatV16(Context context) { + super(context); + } + + @Override + public List<AppWidgetProviderInfo> getAllProviders() { + return mAppWidgetManager.getInstalledProviders(); + } + + @Override + public String loadLabel(AppWidgetProviderInfo info) { + return info.label.trim(); + } + + @Override + public boolean bindAppWidgetIdIfAllowed(int appWidgetId, AppWidgetProviderInfo info, + Bundle options) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, info.provider); + } else { + return mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, info.provider, options); + } + } + + @Override + public UserHandleCompat getUser(AppWidgetProviderInfo info) { + return UserHandleCompat.myUserHandle(); + } + + @Override + public void startConfigActivity(AppWidgetProviderInfo info, int widgetId, Activity activity, + AppWidgetHost host, int requestCode) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); + intent.setComponent(info.configure); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); + Utilities.startActivityForResultSafely(activity, intent, requestCode); + } + + @Override + public Drawable loadPreview(AppWidgetProviderInfo info) { + return mContext.getPackageManager().getDrawable( + info.provider.getPackageName(), info.previewImage, null); + } + + @Override + public Drawable loadIcon(AppWidgetProviderInfo info, IconCache cache) { + return cache.getFullResIcon(info.provider.getPackageName(), info.icon); + } + + @Override + public Bitmap getBadgeBitmap(AppWidgetProviderInfo info, Bitmap bitmap) { + return bitmap; + } +} diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java new file mode 100644 index 000000000..c3853ab62 --- /dev/null +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.view.View; +import android.widget.Toast; + +import com.android.launcher3.IconCache; +import com.android.launcher3.R; + +import java.util.ArrayList; +import java.util.List; + +@TargetApi(Build.VERSION_CODES.L) +class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { + + private final UserManager mUserManager; + private final PackageManager mPm; + + AppWidgetManagerCompatVL(Context context) { + super(context); + mPm = context.getPackageManager(); + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + } + + @Override + public List<AppWidgetProviderInfo> getAllProviders() { + ArrayList<AppWidgetProviderInfo> providers = new ArrayList<AppWidgetProviderInfo>(); + for (UserHandle user : mUserManager.getUserProfiles()) { + providers.addAll(mAppWidgetManager.getInstalledProvidersForProfile(user)); + } + return providers; + } + + @Override + public String loadLabel(AppWidgetProviderInfo info) { + return info.loadLabel(mPm); + } + + @Override + public boolean bindAppWidgetIdIfAllowed(int appWidgetId, AppWidgetProviderInfo info, + Bundle options) { + return mAppWidgetManager.bindAppWidgetIdIfAllowed( + appWidgetId, info.getProfile(), info.provider, options); + } + + @Override + public UserHandleCompat getUser(AppWidgetProviderInfo info) { + return UserHandleCompat.fromUser(info.getProfile()); + } + + @Override + public void startConfigActivity(AppWidgetProviderInfo info, int widgetId, Activity activity, + AppWidgetHost host, int requestCode) { + try { + host.startAppWidgetConfigureActivityForResult(activity, widgetId, 0, requestCode, null); + } catch (ActivityNotFoundException e) { + Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + } catch (SecurityException e) { + Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public Drawable loadPreview(AppWidgetProviderInfo info) { + return info.loadPreviewImage(mContext, 0); + } + + @Override + public Drawable loadIcon(AppWidgetProviderInfo info, IconCache cache) { + return info.loadIcon(mContext, cache.getFullResIconDpi()); + } + + @Override + public Bitmap getBadgeBitmap(AppWidgetProviderInfo info, Bitmap bitmap) { + if (info.getProfile().equals(android.os.Process.myUserHandle())) { + return bitmap; + } + + // Add a user badge in the bottom right of the image. + final Resources res = mContext.getResources(); + final int badgeSize = res.getDimensionPixelSize(R.dimen.profile_badge_size); + final int badgeMargin = res.getDimensionPixelSize(R.dimen.profile_badge_margin); + final Rect badgeLocation = new Rect(0, 0, badgeSize, badgeSize); + + final int top = bitmap.getHeight() - badgeSize - badgeMargin; + if (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + badgeLocation.offset(badgeMargin, top); + } else { + badgeLocation.offset(bitmap.getWidth() - badgeSize - badgeMargin, top); + } + + Drawable drawable = mPm.getUserBadgedDrawableForDensity( + new BitmapDrawable(res, bitmap), info.getProfile(), badgeLocation, 0); + + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + bitmap.eraseColor(Color.TRANSPARENT); + Canvas c = new Canvas(bitmap); + drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); + drawable.draw(c); + c.setBitmap(null); + return bitmap; + } +} diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java new file mode 100644 index 000000000..90a4d1a1f --- /dev/null +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.ComponentName; +import android.content.pm.ApplicationInfo; +import android.graphics.drawable.Drawable; + +public abstract class LauncherActivityInfoCompat { + + LauncherActivityInfoCompat() { + } + + public abstract ComponentName getComponentName(); + public abstract UserHandleCompat getUser(); + public abstract CharSequence getLabel(); + public abstract Drawable getIcon(int density); + public abstract ApplicationInfo getApplicationInfo(); + public abstract long getFirstInstallTime(); + public abstract Drawable getBadgedIcon(int density); +} diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java new file mode 100644 index 000000000..1d41a6ff6 --- /dev/null +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; + + +public class LauncherActivityInfoCompatV16 extends LauncherActivityInfoCompat { + private ActivityInfo mActivityInfo; + private ComponentName mComponentName; + private PackageManager mPm; + + LauncherActivityInfoCompatV16(Context context, ResolveInfo info) { + super(); + this.mActivityInfo = info.activityInfo; + mComponentName = new ComponentName(mActivityInfo.packageName, mActivityInfo.name); + mPm = context.getPackageManager(); + } + + public ComponentName getComponentName() { + return mComponentName; + } + + public UserHandleCompat getUser() { + return UserHandleCompat.myUserHandle(); + } + + public CharSequence getLabel() { + return mActivityInfo.loadLabel(mPm); + } + + public Drawable getIcon(int density) { + Drawable d = null; + if (mActivityInfo.getIconResource() != 0) { + Resources resources; + try { + resources = mPm.getResourcesForApplication(mActivityInfo.packageName); + } catch (PackageManager.NameNotFoundException e) { + resources = null; + } + if (resources != null) { + try { + d = resources.getDrawableForDensity(mActivityInfo.getIconResource(), density); + } catch (Resources.NotFoundException e) { + // Return default icon below. + } + } + } + if (d == null) { + Resources resources = Resources.getSystem(); + d = resources.getDrawableForDensity(android.R.mipmap.sym_def_app_icon, density); + } + return d; + } + + public ApplicationInfo getApplicationInfo() { + return mActivityInfo.applicationInfo; + } + + public long getFirstInstallTime() { + try { + PackageInfo info = mPm.getPackageInfo(mActivityInfo.packageName, 0); + return info != null ? info.firstInstallTime : 0; + } catch (NameNotFoundException e) { + return 0; + } + } + + public String getName() { + return mActivityInfo.name; + } + + public Drawable getBadgedIcon(int density) { + return getIcon(density); + } +} diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java new file mode 100644 index 000000000..b52cf1de2 --- /dev/null +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.ComponentName; +import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherActivityInfo; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; + +public class LauncherActivityInfoCompatVL extends LauncherActivityInfoCompat { + private LauncherActivityInfo mLauncherActivityInfo; + + LauncherActivityInfoCompatVL(LauncherActivityInfo launcherActivityInfo) { + super(); + mLauncherActivityInfo = launcherActivityInfo; + } + + public ComponentName getComponentName() { + return mLauncherActivityInfo.getComponentName(); + } + + public UserHandleCompat getUser() { + return UserHandleCompat.fromUser(mLauncherActivityInfo.getUser()); + } + + public CharSequence getLabel() { + return mLauncherActivityInfo.getLabel(); + } + + public Drawable getIcon(int density) { + return mLauncherActivityInfo.getIcon(density); + } + + public ApplicationInfo getApplicationInfo() { + return mLauncherActivityInfo.getApplicationInfo(); + } + + public long getFirstInstallTime() { + return mLauncherActivityInfo.getFirstInstallTime(); + } + + public Drawable getBadgedIcon(int density) { + return mLauncherActivityInfo.getBadgedIcon(density); + } +} diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java new file mode 100644 index 000000000..6efcc00fd --- /dev/null +++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; + +import com.android.launcher3.Utilities; + +import java.util.List; + +public abstract class LauncherAppsCompat { + + public static final String ACTION_MANAGED_PROFILE_ADDED = + "android.intent.action.MANAGED_PROFILE_ADDED"; + public static final String ACTION_MANAGED_PROFILE_REMOVED = + "android.intent.action.MANAGED_PROFILE_REMOVED"; + + public interface OnAppsChangedCallbackCompat { + void onPackageRemoved(String packageName, UserHandleCompat user); + void onPackageAdded(String packageName, UserHandleCompat user); + void onPackageChanged(String packageName, UserHandleCompat user); + void onPackagesAvailable(String[] packageNames, UserHandleCompat user, boolean replacing); + void onPackagesUnavailable(String[] packageNames, UserHandleCompat user, boolean replacing); + } + + protected LauncherAppsCompat() { + } + + private static LauncherAppsCompat sInstance; + private static Object sInstanceLock = new Object(); + + public static LauncherAppsCompat getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + if (Utilities.isLmpOrAbove()) { + sInstance = new LauncherAppsCompatVL(context.getApplicationContext()); + } else { + sInstance = new LauncherAppsCompatV16(context.getApplicationContext()); + } + } + return sInstance; + } + } + + public abstract List<LauncherActivityInfoCompat> getActivityList(String packageName, + UserHandleCompat user); + public abstract LauncherActivityInfoCompat resolveActivity(Intent intent, + UserHandleCompat user); + public abstract void startActivityForProfile(ComponentName component, UserHandleCompat user, + Rect sourceBounds, Bundle opts); + public abstract void showAppDetailsForProfile(ComponentName component, UserHandleCompat user); + public abstract void addOnAppsChangedCallback(OnAppsChangedCallbackCompat listener); + public abstract void removeOnAppsChangedCallback(OnAppsChangedCallbackCompat listener); + public abstract boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user); + public abstract boolean isActivityEnabledForProfile(ComponentName component, + UserHandleCompat user); +}
\ No newline at end of file diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java new file mode 100644 index 000000000..7e5e6bf2c --- /dev/null +++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +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.graphics.Rect; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; + +import java.util.ArrayList; +import java.util.List; + +/** + * Version of {@link LauncherAppsCompat} for devices with API level 16. + * Devices Pre-L don't support multiple profiles in one launcher so + * user parameters are ignored and all methods operate on the current user. + */ +public class LauncherAppsCompatV16 extends LauncherAppsCompat { + + private PackageManager mPm; + private Context mContext; + private List<OnAppsChangedCallbackCompat> mCallbacks + = new ArrayList<OnAppsChangedCallbackCompat>(); + private PackageMonitor mPackageMonitor; + + LauncherAppsCompatV16(Context context) { + mPm = context.getPackageManager(); + mContext = context; + mPackageMonitor = new PackageMonitor(); + } + + public List<LauncherActivityInfoCompat> getActivityList(String packageName, + UserHandleCompat user) { + final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + mainIntent.setPackage(packageName); + List<ResolveInfo> infos = mPm.queryIntentActivities(mainIntent, 0); + List<LauncherActivityInfoCompat> list = + new ArrayList<LauncherActivityInfoCompat>(infos.size()); + for (ResolveInfo info : infos) { + list.add(new LauncherActivityInfoCompatV16(mContext, info)); + } + return list; + } + + public LauncherActivityInfoCompat resolveActivity(Intent intent, UserHandleCompat user) { + ResolveInfo info = mPm.resolveActivity(intent, 0); + if (info != null) { + return new LauncherActivityInfoCompatV16(mContext, info); + } + return null; + } + + public void startActivityForProfile(ComponentName component, UserHandleCompat user, + Rect sourceBounds, Bundle opts) { + Intent launchIntent = new Intent(Intent.ACTION_MAIN); + launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); + launchIntent.setComponent(component); + launchIntent.setSourceBounds(sourceBounds); + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(launchIntent, opts); + } + + public void showAppDetailsForProfile(ComponentName component, UserHandleCompat user) { + String packageName = component.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_CLEAR_TASK | + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + mContext.startActivity(intent, null); + } + + public synchronized void addOnAppsChangedCallback(OnAppsChangedCallbackCompat callback) { + if (callback != null && !mCallbacks.contains(callback)) { + mCallbacks.add(callback); + if (mCallbacks.size() == 1) { + registerForPackageIntents(); + } + } + } + + public synchronized void removeOnAppsChangedCallback(OnAppsChangedCallbackCompat callback) { + mCallbacks.remove(callback); + if (mCallbacks.size() == 0) { + unregisterForPackageIntents(); + } + } + + public boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user) { + try { + PackageInfo info = mPm.getPackageInfo(packageName, 0); + return info != null && info.applicationInfo.enabled; + } catch (NameNotFoundException e) { + return false; + } + } + + public boolean isActivityEnabledForProfile(ComponentName component, UserHandleCompat user) { + try { + ActivityInfo info = mPm.getActivityInfo(component, 0); + return info != null && info.isEnabled(); + } catch (NameNotFoundException e) { + return false; + } + } + + private void unregisterForPackageIntents() { + mContext.unregisterReceiver(mPackageMonitor); + } + + private void registerForPackageIntents() { + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + mContext.registerReceiver(mPackageMonitor, filter); + filter = new IntentFilter(); + filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + mContext.registerReceiver(mPackageMonitor, filter); + } + + private synchronized List<OnAppsChangedCallbackCompat> getCallbacks() { + return new ArrayList<OnAppsChangedCallbackCompat>(mCallbacks); + } + + private class PackageMonitor extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + final UserHandleCompat user = UserHandleCompat.myUserHandle(); + + 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); + + if (packageName == null || packageName.length() == 0) { + // they sent us a bad intent + return; + } + if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + for (OnAppsChangedCallbackCompat callback : getCallbacks()) { + callback.onPackageChanged(packageName, user); + } + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + if (!replacing) { + for (OnAppsChangedCallbackCompat callback : getCallbacks()) { + callback.onPackageRemoved(packageName, user); + } + } + // 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) { + for (OnAppsChangedCallbackCompat callback : getCallbacks()) { + callback.onPackageAdded(packageName, user); + } + } else { + for (OnAppsChangedCallbackCompat callback : getCallbacks()) { + callback.onPackageChanged(packageName, user); + } + } + } + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { + // EXTRA_REPLACING is available Kitkat onwards. For lower devices, it is broadcasted + // when moving a package or mounting/un-mounting external storage. Assume that + // it is a replacing operation. + final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, + Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT); + String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + for (OnAppsChangedCallbackCompat callback : getCallbacks()) { + callback.onPackagesAvailable(packages, user, replacing); + } + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { + final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, + Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT); + String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + for (OnAppsChangedCallbackCompat callback : getCallbacks()) { + callback.onPackagesUnavailable(packages, user, replacing); + } + } + } + } +} diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java new file mode 100644 index 000000000..e0d28b566 --- /dev/null +++ b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.LauncherApps; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.UserHandle; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LauncherAppsCompatVL extends LauncherAppsCompat { + + private LauncherApps mLauncherApps; + + private Map<OnAppsChangedCallbackCompat, WrappedCallback> mCallbacks + = new HashMap<OnAppsChangedCallbackCompat, WrappedCallback>(); + + LauncherAppsCompatVL(Context context) { + super(); + mLauncherApps = (LauncherApps) context.getSystemService("launcherapps"); + } + + public List<LauncherActivityInfoCompat> getActivityList(String packageName, + UserHandleCompat user) { + List<LauncherActivityInfo> list = mLauncherApps.getActivityList(packageName, + user.getUser()); + if (list.size() == 0) { + return Collections.EMPTY_LIST; + } + ArrayList<LauncherActivityInfoCompat> compatList = + new ArrayList<LauncherActivityInfoCompat>(list.size()); + for (LauncherActivityInfo info : list) { + compatList.add(new LauncherActivityInfoCompatVL(info)); + } + return compatList; + } + + public LauncherActivityInfoCompat resolveActivity(Intent intent, UserHandleCompat user) { + LauncherActivityInfo activity = mLauncherApps.resolveActivity(intent, user.getUser()); + if (activity != null) { + return new LauncherActivityInfoCompatVL(activity); + } else { + return null; + } + } + + public void startActivityForProfile(ComponentName component, UserHandleCompat user, + Rect sourceBounds, Bundle opts) { + mLauncherApps.startMainActivity(component, user.getUser(), sourceBounds, opts); + } + + public void showAppDetailsForProfile(ComponentName component, UserHandleCompat user) { + mLauncherApps.startAppDetailsActivity(component, user.getUser(), null, null); + } + + public void addOnAppsChangedCallback(LauncherAppsCompat.OnAppsChangedCallbackCompat callback) { + WrappedCallback wrappedCallback = new WrappedCallback(callback); + synchronized (mCallbacks) { + mCallbacks.put(callback, wrappedCallback); + } + mLauncherApps.registerCallback(wrappedCallback); + } + + public void removeOnAppsChangedCallback( + LauncherAppsCompat.OnAppsChangedCallbackCompat callback) { + WrappedCallback wrappedCallback = null; + synchronized (mCallbacks) { + wrappedCallback = mCallbacks.remove(callback); + } + if (wrappedCallback != null) { + mLauncherApps.unregisterCallback(wrappedCallback); + } + } + + public boolean isPackageEnabledForProfile(String packageName, UserHandleCompat user) { + return mLauncherApps.isPackageEnabled(packageName, user.getUser()); + } + + public boolean isActivityEnabledForProfile(ComponentName component, UserHandleCompat user) { + return mLauncherApps.isActivityEnabled(component, user.getUser()); + } + + private static class WrappedCallback extends LauncherApps.Callback { + private LauncherAppsCompat.OnAppsChangedCallbackCompat mCallback; + + public WrappedCallback(LauncherAppsCompat.OnAppsChangedCallbackCompat callback) { + mCallback = callback; + } + + public void onPackageRemoved(String packageName, UserHandle user) { + mCallback.onPackageRemoved(packageName, UserHandleCompat.fromUser(user)); + } + + public void onPackageAdded(String packageName, UserHandle user) { + mCallback.onPackageAdded(packageName, UserHandleCompat.fromUser(user)); + } + + public void onPackageChanged(String packageName, UserHandle user) { + mCallback.onPackageChanged(packageName, UserHandleCompat.fromUser(user)); + } + + public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) { + mCallback.onPackagesAvailable(packageNames, UserHandleCompat.fromUser(user), replacing); + } + + public void onPackagesUnavailable(String[] packageNames, UserHandle user, + boolean replacing) { + mCallback.onPackagesUnavailable(packageNames, UserHandleCompat.fromUser(user), + replacing); + } + } +} + diff --git a/src/com/android/launcher3/compat/PackageInstallerCompat.java b/src/com/android/launcher3/compat/PackageInstallerCompat.java new file mode 100644 index 000000000..0eb8754e8 --- /dev/null +++ b/src/com/android/launcher3/compat/PackageInstallerCompat.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.Context; + +import com.android.launcher3.Utilities; + +import java.util.HashSet; + +public abstract class PackageInstallerCompat { + + public static final int STATUS_INSTALLED = 0; + public static final int STATUS_INSTALLING = 1; + public static final int STATUS_FAILED = 2; + + private static final Object sInstanceLock = new Object(); + private static PackageInstallerCompat sInstance; + + public static PackageInstallerCompat getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + if (Utilities.isLmpOrAbove()) { + sInstance = new PackageInstallerCompatVL(context); + } else { + sInstance = new PackageInstallerCompatV16(context) { }; + } + } + return sInstance; + } + } + + public abstract HashSet<String> updateAndGetActiveSessionCache(); + + public abstract void onPause(); + + public abstract void onResume(); + + public abstract void onFinishBind(); + + public abstract void onStop(); + + public abstract void recordPackageUpdate(String packageName, int state, int progress); + + public static final class PackageInstallInfo { + public final String packageName; + + public int state; + public int progress; + + public PackageInstallInfo(String packageName) { + this.packageName = packageName; + } + + public PackageInstallInfo(String packageName, int state, int progress) { + this.packageName = packageName; + this.state = state; + this.progress = progress; + } + } +} diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatV16.java b/src/com/android/launcher3/compat/PackageInstallerCompatV16.java new file mode 100644 index 000000000..1910d22ae --- /dev/null +++ b/src/com/android/launcher3/compat/PackageInstallerCompatV16.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; +import android.util.Log; + +import com.android.launcher3.LauncherAppState; + +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONStringer; +import org.json.JSONTokener; + +import java.util.ArrayList; +import java.util.HashSet; + +public class PackageInstallerCompatV16 extends PackageInstallerCompat { + + private static final String TAG = "PackageInstallerCompatV16"; + private static final boolean DEBUG = false; + + private static final String KEY_PROGRESS = "progress"; + private static final String KEY_STATE = "state"; + + private static final String PREFS = + "com.android.launcher3.compat.PackageInstallerCompatV16.queue"; + + protected final SharedPreferences mPrefs; + + boolean mUseQueue; + boolean mFinishedBind; + boolean mReplayPending; + + PackageInstallerCompatV16(Context context) { + mPrefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + } + + @Override + public void onPause() { + mUseQueue = true; + if (DEBUG) Log.d(TAG, "updates paused"); + } + + @Override + public void onResume() { + mUseQueue = false; + if (mFinishedBind) { + replayUpdates(); + } + } + + @Override + public void onFinishBind() { + mFinishedBind = true; + if (!mUseQueue) { + replayUpdates(); + } + } + + @Override + public void onStop() { } + + private void replayUpdates() { + if (DEBUG) Log.d(TAG, "updates resumed"); + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app == null) { + mReplayPending = true; // try again later + if (DEBUG) Log.d(TAG, "app is null, delaying send"); + return; + } + mReplayPending = false; + ArrayList<PackageInstallInfo> updates = new ArrayList<PackageInstallInfo>(); + for (String packageName: mPrefs.getAll().keySet()) { + final String json = mPrefs.getString(packageName, null); + if (!TextUtils.isEmpty(json)) { + updates.add(infoFromJson(packageName, json)); + } + } + if (!updates.isEmpty()) { + sendUpdate(app, updates); + } + } + + /** + * This should be called by the implementations to register a package update. + */ + @Override + public synchronized void recordPackageUpdate(String packageName, int state, int progress) { + SharedPreferences.Editor editor = mPrefs.edit(); + PackageInstallInfo installInfo = new PackageInstallInfo(packageName); + installInfo.progress = progress; + installInfo.state = state; + if (state == STATUS_INSTALLED) { + // no longer necessary to track this package + editor.remove(packageName); + if (DEBUG) Log.d(TAG, "no longer tracking " + packageName); + } else { + editor.putString(packageName, infoToJson(installInfo)); + if (DEBUG) + Log.d(TAG, "saved state: " + infoToJson(installInfo) + + " for package: " + packageName); + + } + editor.commit(); + + if (!mUseQueue) { + if (mReplayPending) { + replayUpdates(); + } else if (state != STATUS_INSTALLED) { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + ArrayList<PackageInstallInfo> update = new ArrayList<PackageInstallInfo>(); + update.add(installInfo); + sendUpdate(app, update); + } + } + } + + private void sendUpdate(LauncherAppState app, ArrayList<PackageInstallInfo> updates) { + if (app == null) { + mReplayPending = true; // try again later + if (DEBUG) Log.d(TAG, "app is null, delaying send"); + } else { + app.setPackageState(updates); + } + } + + private static PackageInstallInfo infoFromJson(String packageName, String json) { + PackageInstallInfo info = new PackageInstallInfo(packageName); + try { + JSONObject object = (JSONObject) new JSONTokener(json).nextValue(); + info.state = object.getInt(KEY_STATE); + info.progress = object.getInt(KEY_PROGRESS); + } catch (JSONException e) { + Log.e(TAG, "failed to deserialize app state update", e); + } + return info; + } + + private static String infoToJson(PackageInstallInfo info) { + String value = null; + try { + JSONStringer json = new JSONStringer() + .object() + .key(KEY_STATE).value(info.state) + .key(KEY_PROGRESS).value(info.progress) + .endObject(); + value = json.toString(); + } catch (JSONException e) { + Log.e(TAG, "failed to serialize app state update", e); + } + return value; + } + + @Override + public HashSet<String> updateAndGetActiveSessionCache() { + return new HashSet<String>(); + } +} diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java new file mode 100644 index 000000000..16ad3792a --- /dev/null +++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.Context; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageInstaller.SessionCallback; +import android.content.pm.PackageInstaller.SessionInfo; +import android.util.Log; +import android.util.SparseArray; + +import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppState; + +import java.util.ArrayList; +import java.util.HashSet; + +public class PackageInstallerCompatVL extends PackageInstallerCompat { + + private static final String TAG = "PackageInstallerCompatVL"; + private static final boolean DEBUG = false; + + private final SparseArray<SessionInfo> mPendingReplays = new SparseArray<SessionInfo>(); + private final HashSet<String> mPendingBadgeUpdates = new HashSet<String>(); + private final PackageInstaller mInstaller; + private final IconCache mCache; + + private boolean mResumed; + private boolean mBound; + + PackageInstallerCompatVL(Context context) { + mInstaller = context.getPackageManager().getPackageInstaller(); + LauncherAppState.setApplicationContext(context.getApplicationContext()); + mCache = LauncherAppState.getInstance().getIconCache(); + + mResumed = false; + mBound = false; + + mInstaller.registerSessionCallback(mCallback); + + // On start, send updates for all active sessions + for (SessionInfo info : mInstaller.getAllSessions()) { + mPendingReplays.append(info.getSessionId(), info); + } + } + + @Override + public HashSet<String> updateAndGetActiveSessionCache() { + HashSet<String> activePackages = new HashSet<String>(); + UserHandleCompat user = UserHandleCompat.myUserHandle(); + for (SessionInfo info : mInstaller.getAllSessions()) { + addSessionInfoToCahce(info, user); + if (info.getAppPackageName() != null) { + activePackages.add(info.getAppPackageName()); + } + } + return activePackages; + } + + private void addSessionInfoToCahce(SessionInfo info, UserHandleCompat user) { + String packageName = info.getAppPackageName(); + if (packageName != null) { + mCache.cachePackageInstallInfo(packageName, user, info.getAppIcon(), + info.getAppLabel()); + } + } + + @Override + public void onStop() { + } + + @Override + public void onFinishBind() { + mBound = true; + replayUpdates(null); + } + + @Override + public void onPause() { + mResumed = false; + } + + @Override + public void onResume() { + mResumed = true; + replayUpdates(null); + } + + @Override + public void recordPackageUpdate(String packageName, int state, int progress) { + // No op + } + + private void replayUpdates(PackageInstallInfo newInfo) { + if (DEBUG) Log.d(TAG, "updates resumed"); + if (!mResumed || !mBound) { + // Not yet ready + return; + } + if ((mPendingReplays.size() == 0) && (newInfo == null) && mPendingBadgeUpdates.isEmpty()) { + // Nothing to update + return; + } + + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app == null) { + // Try again later + if (DEBUG) Log.d(TAG, "app is null, delaying send"); + return; + } + + ArrayList<PackageInstallInfo> updates = new ArrayList<PackageInstallInfo>(); + if ((newInfo != null) && (newInfo.state != STATUS_INSTALLED)) { + updates.add(newInfo); + } + for (int i = mPendingReplays.size() - 1; i >= 0; i--) { + SessionInfo session = mPendingReplays.valueAt(i); + if (session.getAppPackageName() != null) { + updates.add(new PackageInstallInfo(session.getAppPackageName(), + STATUS_INSTALLING, + (int) (session.getProgress() * 100))); + } + } + mPendingReplays.clear(); + if (!updates.isEmpty()) { + app.setPackageState(updates); + } + + if (!mPendingBadgeUpdates.isEmpty()) { + for (String pkg : mPendingBadgeUpdates) { + app.updatePackageBadge(pkg); + } + mPendingBadgeUpdates.clear(); + } + } + + private final SessionCallback mCallback = new SessionCallback() { + + @Override + public void onCreated(int sessionId) { + pushSessionBadgeToLauncher(sessionId); + } + + @Override + public void onFinished(int sessionId, boolean success) { + mPendingReplays.remove(sessionId); + SessionInfo session = mInstaller.getSessionInfo(sessionId); + if ((session != null) && (session.getAppPackageName() != null)) { + mPendingBadgeUpdates.remove(session.getAppPackageName()); + // Replay all updates with a one time update for this installed package. No + // need to store this record for future updates, as the app list will get + // refreshed on resume. + replayUpdates(new PackageInstallInfo(session.getAppPackageName(), + success ? STATUS_INSTALLED : STATUS_FAILED, 0)); + } + } + + @Override + public void onProgressChanged(int sessionId, float progress) { + SessionInfo session = mInstaller.getSessionInfo(sessionId); + if (session != null) { + mPendingReplays.put(sessionId, session); + replayUpdates(null); + } + } + + @Override + public void onActiveChanged(int sessionId, boolean active) { } + + @Override + public void onBadgingChanged(int sessionId) { + pushSessionBadgeToLauncher(sessionId); + } + + private void pushSessionBadgeToLauncher(int sessionId) { + SessionInfo session = mInstaller.getSessionInfo(sessionId); + if (session != null) { + addSessionInfoToCahce(session, UserHandleCompat.myUserHandle()); + if (session.getAppPackageName() != null) { + mPendingBadgeUpdates.add(session.getAppPackageName()); + } + mPendingReplays.put(sessionId, session); + replayUpdates(null); + } + } + }; +} diff --git a/src/com/android/launcher3/compat/UserHandleCompat.java b/src/com/android/launcher3/compat/UserHandleCompat.java new file mode 100644 index 000000000..2ae673171 --- /dev/null +++ b/src/com/android/launcher3/compat/UserHandleCompat.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.Intent; +import android.os.Build; +import android.os.UserHandle; + +import com.android.launcher3.Utilities; + +public class UserHandleCompat { + private UserHandle mUser; + + private UserHandleCompat(UserHandle user) { + mUser = user; + } + + private UserHandleCompat() { + } + + public static UserHandleCompat myUserHandle() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return new UserHandleCompat(android.os.Process.myUserHandle()); + } else { + return new UserHandleCompat(); + } + } + + static UserHandleCompat fromUser(UserHandle user) { + if (user == null) { + return null; + } else { + return new UserHandleCompat(user); + } + } + + UserHandle getUser() { + return mUser; + } + + @Override + public String toString() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return mUser.toString(); + } else { + return ""; + } + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof UserHandleCompat)) { + return false; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return mUser.equals(((UserHandleCompat) other).mUser); + } else { + return true; + } + } + + @Override + public int hashCode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return mUser.hashCode(); + } else { + return 0; + } + } + + /** + * Adds {@link UserHandle} to the intent in for L or above. + * Pre-L the launcher doesn't support showing apps for multiple + * profiles so this is a no-op. + */ + public void addToIntent(Intent intent, String name) { + if (Utilities.isLmpOrAbove() && mUser != null) { + intent.putExtra(name, mUser); + } + } +} diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java new file mode 100644 index 000000000..1374b4e49 --- /dev/null +++ b/src/com/android/launcher3/compat/UserManagerCompat.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import com.android.launcher3.Utilities; + +import java.util.List; + +public abstract class UserManagerCompat { + protected UserManagerCompat() { + } + + public static UserManagerCompat getInstance(Context context) { + if (Utilities.isLmpOrAbove()) { + return new UserManagerCompatVL(context); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return new UserManagerCompatV17(context); + } else { + return new UserManagerCompatV16(); + } + } + + public abstract List<UserHandleCompat> getUserProfiles(); + public abstract long getSerialNumberForUser(UserHandleCompat user); + public abstract UserHandleCompat getUserForSerialNumber(long serialNumber); + public abstract Drawable getBadgedDrawableForUser(Drawable unbadged, UserHandleCompat user); + public abstract CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user); +} diff --git a/src/com/android/launcher3/compat/UserManagerCompatV16.java b/src/com/android/launcher3/compat/UserManagerCompatV16.java new file mode 100644 index 000000000..32f972e85 --- /dev/null +++ b/src/com/android/launcher3/compat/UserManagerCompatV16.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.graphics.drawable.Drawable; + +import java.util.ArrayList; +import java.util.List; + +public class UserManagerCompatV16 extends UserManagerCompat { + + UserManagerCompatV16() { + } + + public List<UserHandleCompat> getUserProfiles() { + List<UserHandleCompat> profiles = new ArrayList<UserHandleCompat>(1); + profiles.add(UserHandleCompat.myUserHandle()); + return profiles; + } + + public UserHandleCompat getUserForSerialNumber(long serialNumber) { + return UserHandleCompat.myUserHandle(); + } + + public Drawable getBadgedDrawableForUser(Drawable unbadged, + UserHandleCompat user) { + return unbadged; + } + + public long getSerialNumberForUser(UserHandleCompat user) { + return 0; + } + + public CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user) { + return label; + } +} diff --git a/src/com/android/launcher3/compat/UserManagerCompatV17.java b/src/com/android/launcher3/compat/UserManagerCompatV17.java new file mode 100644 index 000000000..055359afe --- /dev/null +++ b/src/com/android/launcher3/compat/UserManagerCompatV17.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 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.compat; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.os.UserManager; + +import java.util.ArrayList; +import java.util.List; + +public class UserManagerCompatV17 extends UserManagerCompatV16 { + protected UserManager mUserManager; + + UserManagerCompatV17(Context context) { + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + } + + public long getSerialNumberForUser(UserHandleCompat user) { + return mUserManager.getSerialNumberForUser(user.getUser()); + } + + public UserHandleCompat getUserForSerialNumber(long serialNumber) { + return UserHandleCompat.fromUser(mUserManager.getUserForSerialNumber(serialNumber)); + } +} + diff --git a/src/com/android/launcher3/compat/UserManagerCompatVL.java b/src/com/android/launcher3/compat/UserManagerCompatVL.java new file mode 100644 index 000000000..19eeabdcf --- /dev/null +++ b/src/com/android/launcher3/compat/UserManagerCompatVL.java @@ -0,0 +1,65 @@ + +/* + * Copyright (C) 2014 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.compat; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.os.UserManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class UserManagerCompatVL extends UserManagerCompatV17 { + private final PackageManager mPm; + + UserManagerCompatVL(Context context) { + super(context); + mPm = context.getPackageManager(); + } + + @Override + public List<UserHandleCompat> getUserProfiles() { + List<UserHandle> users = mUserManager.getUserProfiles(); + if (users == null) { + return Collections.EMPTY_LIST; + } + ArrayList<UserHandleCompat> compatUsers = new ArrayList<UserHandleCompat>( + users.size()); + for (UserHandle user : users) { + compatUsers.add(UserHandleCompat.fromUser(user)); + } + return compatUsers; + } + + @Override + public Drawable getBadgedDrawableForUser(Drawable unbadged, UserHandleCompat user) { + return mPm.getUserBadgedIcon(unbadged, user.getUser()); + } + + @Override + public CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user) { + if (user == null) { + return label; + } + return mPm.getUserBadgedLabel(label, user.getUser()); + } +} + |