diff options
201 files changed, 11961 insertions, 10401 deletions
diff --git a/.gitignore b/.gitignore index 4d5667e54..aea5d6102 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ db_files *.iml .project .classpath +.project.properties gen/ tests/stress/gen/ WallpaperPicker/gen/ +WallpaperPicker/.project.properties +bin/ diff --git a/Android.mk b/Android.mk index 340caca0b..d7bfdd464 100644 --- a/Android.mk +++ b/Android.mk @@ -23,7 +23,7 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional -LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13 +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 LOCAL_SRC_FILES := $(call all-java-files-under, src) \ $(call all-java-files-under, WallpaperPicker/src) \ @@ -37,6 +37,9 @@ LOCAL_PROTOC_OPTIMIZE_TYPE := nano LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ LOCAL_SDK_VERSION := 21 +LOCAL_STATIC_JAVA_LIBRARIES := \ + android-support-v4 \ + android-support-v7-recyclerview LOCAL_PACKAGE_NAME := Launcher3 #LOCAL_CERTIFICATE := shared diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fb7ac3fab..b61b90c5c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -29,12 +29,6 @@ android:label="@string/permlab_install_shortcut" android:description="@string/permdesc_install_shortcut" /> <permission - android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" - android:permissionGroup="android.permission-group.SYSTEM_TOOLS" - android:protectionLevel="dangerous" - android:label="@string/permlab_uninstall_shortcut" - android:description="@string/permdesc_uninstall_shortcut"/> - <permission android:name="com.android.launcher3.permission.READ_SETTINGS" android:permissionGroup="android.permission-group.SYSTEM_TOOLS" android:protectionLevel="normal" @@ -128,7 +122,7 @@ </activity> <activity - android:name="com.android.launcher3.LauncherWallpaperPickerActivity" + android:name="com.android.launcher3.WallpaperPickerActivity" android:theme="@style/Theme.WallpaperPicker" android:label="@string/pick_wallpaper" android:icon="@mipmap/ic_launcher_wallpaper" @@ -191,15 +185,6 @@ </intent-filter> </receiver> - <!-- Intent received used to uninstall shortcuts from other applications --> - <receiver - android:name="com.android.launcher3.UninstallShortcutReceiver" - android:permission="com.android.launcher.permission.UNINSTALL_SHORTCUT"> - <intent-filter> - <action android:name="com.android.launcher.action.UNINSTALL_SHORTCUT" /> - </intent-filter> - </receiver> - <!-- Intent received used to initialize a restored widget --> <receiver android:name="com.android.launcher3.AppWidgetsRestoredReceiver" > <intent-filter> @@ -207,24 +192,6 @@ </intent-filter> </receiver> - <!-- New user initialization; set up initial wallpaper --> - <receiver - android:name="com.android.launcher3.UserInitializeReceiver" - android:exported="false"> - <intent-filter> - <action android:name="android.intent.action.USER_INITIALIZE" /> - </intent-filter> - </receiver> - - <receiver android:name="com.android.launcher3.PackageChangedReceiver" > - <intent-filter> - <action android:name="android.intent.action.PACKAGE_CHANGED"/> - <action android:name="android.intent.action.PACKAGE_REPLACED"/> - <action android:name="android.intent.action.PACKAGE_REMOVED"/> - <data android:scheme="package"></data> - </intent-filter> - </receiver> - <receiver android:name="com.android.launcher3.StartupReceiver" > <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> diff --git a/WallpaperPicker/AndroidManifest.xml b/WallpaperPicker/AndroidManifest.xml index 5b6a0077d..cb1457bdc 100644 --- a/WallpaperPicker/AndroidManifest.xml +++ b/WallpaperPicker/AndroidManifest.xml @@ -4,7 +4,7 @@ android:versionCode="1" android:versionName="1.0" > - - <uses-sdk android:minSdkVersion="15" android:targetSdkVersion="19" /> + + <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" /> <application/> </manifest> diff --git a/WallpaperPicker/res/layout/wallpaper_cropper.xml b/WallpaperPicker/res/layout/wallpaper_cropper.xml index abb860898..ffe8df0fb 100644 --- a/WallpaperPicker/res/layout/wallpaper_cropper.xml +++ b/WallpaperPicker/res/layout/wallpaper_cropper.xml @@ -19,7 +19,6 @@ --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/wallpaper_root" android:layout_width="match_parent" android:layout_height="match_parent"> <com.android.launcher3.CropView @@ -28,7 +27,7 @@ android:layout_height="match_parent" /> <ProgressBar android:id="@+id/loading" - style="@android:style/Widget.Holo.ProgressBar.Large" + style="?android:attr/progressBarStyleLarge" android:visibility="invisible" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/WallpaperPicker/res/layout/wallpaper_picker.xml b/WallpaperPicker/res/layout/wallpaper_picker.xml index c36493d2f..0b970b09f 100644 --- a/WallpaperPicker/res/layout/wallpaper_picker.xml +++ b/WallpaperPicker/res/layout/wallpaper_picker.xml @@ -18,60 +18,74 @@ */ --> -<com.android.launcher3.WallpaperRootView - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/wallpaper_root" +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:launcher="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" > + <com.android.launcher3.CropView android:id="@+id/cropView" android:layout_width="match_parent" android:layout_height="match_parent" /> + <ProgressBar android:id="@+id/loading" - style="@android:style/Widget.Holo.ProgressBar.Large" - android:visibility="invisible" + style="?android:attr/progressBarStyleLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerInParent="true" + android:layout_gravity="center" android:indeterminate="true" android:indeterminateOnly="true" - android:background="@android:color/transparent" /> + android:visibility="invisible" /> + <LinearLayout android:id="@+id/wallpaper_strip" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentBottom="true" + android:layout_gravity="bottom" + android:fitsSystemWindows="true" android:orientation="vertical" > + <View android:layout_width="match_parent" android:layout_height="2dp" android:background="@drawable/tile_shadow_top" /> + <HorizontalScrollView android:id="@+id/wallpaper_scroll_container" android:layout_width="match_parent" android:layout_height="wrap_content" > - <LinearLayout android:id="@+id/master_wallpaper_list" + + <LinearLayout + android:id="@+id/master_wallpaper_list" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" > - <LinearLayout android:id="@+id/wallpaper_list" + + <LinearLayout + android:id="@+id/wallpaper_list" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" /> - <LinearLayout android:id="@+id/live_wallpaper_list" + + <LinearLayout + android:id="@+id/live_wallpaper_list" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" /> - <LinearLayout android:id="@+id/third_party_wallpaper_list" + + <LinearLayout + android:id="@+id/third_party_wallpaper_list" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" /> </LinearLayout> </HorizontalScrollView> + <View android:layout_width="match_parent" android:layout_height="2dp" android:background="@drawable/tile_shadow_bottom" /> </LinearLayout> -</com.android.launcher3.WallpaperRootView> + +</FrameLayout>
\ No newline at end of file diff --git a/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml b/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml index ae3c43d8e..dc6524486 100644 --- a/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml +++ b/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml @@ -20,7 +20,6 @@ android:layout_height="@dimen/wallpaperThumbnailHeight" android:focusable="true" android:clickable="true" - android:background="@drawable/wallpaper_tile_fg" android:foreground="@drawable/wallpaper_tile_fg"> <ImageView android:id="@+id/wallpaper_image" diff --git a/WallpaperPicker/res/layout/wallpaper_picker_item.xml b/WallpaperPicker/res/layout/wallpaper_picker_item.xml index 0ac8f97fb..3f57fcdbd 100644 --- a/WallpaperPicker/res/layout/wallpaper_picker_item.xml +++ b/WallpaperPicker/res/layout/wallpaper_picker_item.xml @@ -20,7 +20,6 @@ android:layout_height="@dimen/wallpaperThumbnailHeight" android:focusable="true" android:clickable="true" - android:background="@drawable/wallpaper_tile_fg" android:foreground="@drawable/wallpaper_tile_fg"> <ImageView android:id="@+id/wallpaper_image" diff --git a/WallpaperPicker/res/layout/wallpaper_picker_live_wallpaper_item.xml b/WallpaperPicker/res/layout/wallpaper_picker_live_wallpaper_item.xml index 29fdb1b82..2b152fce2 100644 --- a/WallpaperPicker/res/layout/wallpaper_picker_live_wallpaper_item.xml +++ b/WallpaperPicker/res/layout/wallpaper_picker_live_wallpaper_item.xml @@ -20,7 +20,6 @@ android:layout_height="@dimen/wallpaperThumbnailHeight" android:focusable="true" android:clickable="true" - android:background="@drawable/wallpaper_tile_fg" android:foreground="@drawable/wallpaper_tile_fg"> <ImageView android:id="@+id/wallpaper_image" diff --git a/WallpaperPicker/res/layout/wallpaper_picker_third_party_item.xml b/WallpaperPicker/res/layout/wallpaper_picker_third_party_item.xml index 68661bc00..a7e3a0c79 100644 --- a/WallpaperPicker/res/layout/wallpaper_picker_third_party_item.xml +++ b/WallpaperPicker/res/layout/wallpaper_picker_third_party_item.xml @@ -20,7 +20,6 @@ android:layout_height="@dimen/wallpaperThumbnailHeight" android:focusable="true" android:clickable="true" - android:background="@drawable/wallpaper_tile_fg" android:foreground="@drawable/wallpaper_tile_fg"> <ImageView android:id="@+id/wallpaper_image" diff --git a/WallpaperPicker/res/values-sw720dp-v19/styles.xml b/WallpaperPicker/res/values-sw720dp-v19/styles.xml index 91078517d..d8dab223a 100644 --- a/WallpaperPicker/res/values-sw720dp-v19/styles.xml +++ b/WallpaperPicker/res/values-sw720dp-v19/styles.xml @@ -18,7 +18,7 @@ --> <resources> - <style name="Theme" parent="android:Theme.Holo.Wallpaper.NoTitleBar"> + <style name="Theme" parent="@android:style/Theme.DeviceDefault.Wallpaper.NoTitleBar"> <item name="android:windowActionModeOverlay">true</item> <item name="android:windowTranslucentStatus">true</item> <item name="android:windowTranslucentNavigation">true</item> diff --git a/WallpaperPicker/res/values-sw720dp/styles.xml b/WallpaperPicker/res/values-sw720dp/styles.xml index b48909087..12f8884f1 100644 --- a/WallpaperPicker/res/values-sw720dp/styles.xml +++ b/WallpaperPicker/res/values-sw720dp/styles.xml @@ -18,7 +18,7 @@ --> <resources> - <style name="Theme" parent="android:Theme.Holo.Wallpaper.NoTitleBar"> + <style name="Theme" parent="@android:style/Theme.DeviceDefault.Wallpaper.NoTitleBar"> <item name="android:windowActionModeOverlay">true</item> </style> </resources> diff --git a/WallpaperPicker/src/android/util/Pools.java b/WallpaperPicker/src/android/util/Pools.java deleted file mode 100644 index 40bab1eae..000000000 --- a/WallpaperPicker/src/android/util/Pools.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -/** - * Helper class for crating pools of objects. An example use looks like this: - * <pre> - * public class MyPooledClass { - * - * private static final SynchronizedPool<MyPooledClass> sPool = - * new SynchronizedPool<MyPooledClass>(10); - * - * public static MyPooledClass obtain() { - * MyPooledClass instance = sPool.acquire(); - * return (instance != null) ? instance : new MyPooledClass(); - * } - * - * public void recycle() { - * // Clear state if needed. - * sPool.release(this); - * } - * - * . . . - * } - * </pre> - * - * @hide - */ -public final class Pools { - - /** - * Interface for managing a pool of objects. - * - * @param <T> The pooled type. - */ - public static interface Pool<T> { - - /** - * @return An instance from the pool if such, null otherwise. - */ - public T acquire(); - - /** - * Release an instance to the pool. - * - * @param instance The instance to release. - * @return Whether the instance was put in the pool. - * - * @throws IllegalStateException If the instance is already in the pool. - */ - public boolean release(T instance); - } - - private Pools() { - /* do nothing - hiding constructor */ - } - - /** - * Simple (non-synchronized) pool of objects. - * - * @param <T> The pooled type. - */ - public static class SimplePool<T> implements Pool<T> { - private final Object[] mPool; - - private int mPoolSize; - - /** - * Creates a new instance. - * - * @param maxPoolSize The max pool size. - * - * @throws IllegalArgumentException If the max pool size is less than zero. - */ - public SimplePool(int maxPoolSize) { - if (maxPoolSize <= 0) { - throw new IllegalArgumentException("The max pool size must be > 0"); - } - mPool = new Object[maxPoolSize]; - } - - @Override - @SuppressWarnings("unchecked") - public T acquire() { - if (mPoolSize > 0) { - final int lastPooledIndex = mPoolSize - 1; - T instance = (T) mPool[lastPooledIndex]; - mPool[lastPooledIndex] = null; - mPoolSize--; - return instance; - } - return null; - } - - @Override - public boolean release(T instance) { - if (isInPool(instance)) { - throw new IllegalStateException("Already in the pool!"); - } - if (mPoolSize < mPool.length) { - mPool[mPoolSize] = instance; - mPoolSize++; - return true; - } - return false; - } - - private boolean isInPool(T instance) { - for (int i = 0; i < mPoolSize; i++) { - if (mPool[i] == instance) { - return true; - } - } - return false; - } - } - - /** - * Synchronized) pool of objects. - * - * @param <T> The pooled type. - */ - public static class SynchronizedPool<T> extends SimplePool<T> { - private final Object mLock = new Object(); - - /** - * Creates a new instance. - * - * @param maxPoolSize The max pool size. - * - * @throws IllegalArgumentException If the max pool size is less than zero. - */ - public SynchronizedPool(int maxPoolSize) { - super(maxPoolSize); - } - - @Override - public T acquire() { - synchronized (mLock) { - return super.acquire(); - } - } - - @Override - public boolean release(T element) { - synchronized (mLock) { - return super.release(element); - } - } - } -}
\ No newline at end of file diff --git a/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java new file mode 100644 index 000000000..45118bf45 --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java @@ -0,0 +1,405 @@ +/** + * Copyright (C) 2015 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.gallery3d.common; + +import android.app.WallpaperManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class BitmapCropTask extends AsyncTask<Void, Void, Boolean> { + + public interface OnBitmapCroppedHandler { + public void onBitmapCropped(byte[] imageBytes); + } + + private static final int DEFAULT_COMPRESS_QUALITY = 90; + private static final String LOGTAG = "BitmapCropTask"; + + Uri mInUri = null; + Context mContext; + String mInFilePath; + byte[] mInImageBytes; + int mInResId = 0; + RectF mCropBounds = null; + int mOutWidth, mOutHeight; + int mRotation; + boolean mSetWallpaper; + boolean mSaveCroppedBitmap; + Bitmap mCroppedBitmap; + Runnable mOnEndRunnable; + Resources mResources; + BitmapCropTask.OnBitmapCroppedHandler mOnBitmapCroppedHandler; + boolean mNoCrop; + + public BitmapCropTask(Context c, String filePath, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mContext = c; + mInFilePath = filePath; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + public BitmapCropTask(byte[] imageBytes, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mInImageBytes = imageBytes; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + public BitmapCropTask(Context c, Uri inUri, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mContext = c; + mInUri = inUri; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + public BitmapCropTask(Context c, Resources res, int inResId, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mContext = c; + mInResId = inResId; + mResources = res; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + private void init(RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mCropBounds = cropBounds; + mRotation = rotation; + mOutWidth = outWidth; + mOutHeight = outHeight; + mSetWallpaper = setWallpaper; + mSaveCroppedBitmap = saveCroppedBitmap; + mOnEndRunnable = onEndRunnable; + } + + public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) { + mOnBitmapCroppedHandler = handler; + } + + public void setNoCrop(boolean value) { + mNoCrop = value; + } + + public void setOnEndRunnable(Runnable onEndRunnable) { + mOnEndRunnable = onEndRunnable; + } + + // Helper to setup input stream + private InputStream regenerateInputStream() { + if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) { + Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + + "image byte array given"); + } else { + try { + if (mInUri != null) { + return new BufferedInputStream( + mContext.getContentResolver().openInputStream(mInUri)); + } else if (mInFilePath != null) { + return mContext.openFileInput(mInFilePath); + } else if (mInImageBytes != null) { + return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes)); + } else { + return new BufferedInputStream(mResources.openRawResource(mInResId)); + } + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); + } + } + return null; + } + + public Point getImageBounds() { + InputStream is = regenerateInputStream(); + if (is != null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, options); + Utils.closeSilently(is); + if (options.outWidth != 0 && options.outHeight != 0) { + return new Point(options.outWidth, options.outHeight); + } + } + return null; + } + + public void setCropBounds(RectF cropBounds) { + mCropBounds = cropBounds; + } + + public Bitmap getCroppedBitmap() { + return mCroppedBitmap; + } + public boolean cropBitmap() { + boolean failure = false; + + + WallpaperManager wallpaperManager = null; + if (mSetWallpaper) { + wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext()); + } + + + if (mSetWallpaper && mNoCrop) { + try { + InputStream is = regenerateInputStream(); + if (is != null) { + wallpaperManager.setStream(is); + Utils.closeSilently(is); + } + } catch (IOException e) { + Log.w(LOGTAG, "cannot write stream to wallpaper", e); + failure = true; + } + return !failure; + } else { + // Find crop bounds (scaled to original image size) + Rect roundedTrueCrop = new Rect(); + Matrix rotateMatrix = new Matrix(); + Matrix inverseRotateMatrix = new Matrix(); + + Point bounds = getImageBounds(); + if (mRotation > 0) { + rotateMatrix.setRotate(mRotation); + inverseRotateMatrix.setRotate(-mRotation); + + mCropBounds.roundOut(roundedTrueCrop); + mCropBounds = new RectF(roundedTrueCrop); + + if (bounds == null) { + Log.w(LOGTAG, "cannot get bounds for image"); + failure = true; + return false; + } + + float[] rotatedBounds = new float[] { bounds.x, bounds.y }; + rotateMatrix.mapPoints(rotatedBounds); + rotatedBounds[0] = Math.abs(rotatedBounds[0]); + rotatedBounds[1] = Math.abs(rotatedBounds[1]); + + mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2); + inverseRotateMatrix.mapRect(mCropBounds); + mCropBounds.offset(bounds.x/2, bounds.y/2); + + } + + mCropBounds.roundOut(roundedTrueCrop); + + if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { + Log.w(LOGTAG, "crop has bad values for full size image"); + failure = true; + return false; + } + + // See how much we're reducing the size of the image + int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth, + roundedTrueCrop.height() / mOutHeight)); + // Attempt to open a region decoder + BitmapRegionDecoder decoder = null; + InputStream is = null; + try { + is = regenerateInputStream(); + if (is == null) { + Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); + failure = true; + return false; + } + decoder = BitmapRegionDecoder.newInstance(is, false); + Utils.closeSilently(is); + } catch (IOException e) { + Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); + } finally { + Utils.closeSilently(is); + is = null; + } + + Bitmap crop = null; + if (decoder != null) { + // Do region decoding to get crop bitmap + BitmapFactory.Options options = new BitmapFactory.Options(); + if (scaleDownSampleSize > 1) { + options.inSampleSize = scaleDownSampleSize; + } + crop = decoder.decodeRegion(roundedTrueCrop, options); + decoder.recycle(); + } + + if (crop == null) { + // BitmapRegionDecoder has failed, try to crop in-memory + is = regenerateInputStream(); + Bitmap fullSize = null; + if (is != null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + if (scaleDownSampleSize > 1) { + options.inSampleSize = scaleDownSampleSize; + } + fullSize = BitmapFactory.decodeStream(is, null, options); + Utils.closeSilently(is); + } + if (fullSize != null) { + // Find out the true sample size that was used by the decoder + scaleDownSampleSize = bounds.x / fullSize.getWidth(); + mCropBounds.left /= scaleDownSampleSize; + mCropBounds.top /= scaleDownSampleSize; + mCropBounds.bottom /= scaleDownSampleSize; + mCropBounds.right /= scaleDownSampleSize; + mCropBounds.roundOut(roundedTrueCrop); + + // Adjust values to account for issues related to rounding + if (roundedTrueCrop.width() > fullSize.getWidth()) { + // Adjust the width + roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth(); + } + if (roundedTrueCrop.right > fullSize.getWidth()) { + // Adjust the left value + int adjustment = roundedTrueCrop.left - + Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width()); + roundedTrueCrop.left -= adjustment; + roundedTrueCrop.right -= adjustment; + } + if (roundedTrueCrop.height() > fullSize.getHeight()) { + // Adjust the height + roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight(); + } + if (roundedTrueCrop.bottom > fullSize.getHeight()) { + // Adjust the top value + int adjustment = roundedTrueCrop.top - + Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height()); + roundedTrueCrop.top -= adjustment; + roundedTrueCrop.bottom -= adjustment; + } + + crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, + roundedTrueCrop.top, roundedTrueCrop.width(), + roundedTrueCrop.height()); + } + } + + if (crop == null) { + Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); + failure = true; + return false; + } + if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) { + float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() }; + rotateMatrix.mapPoints(dimsAfter); + dimsAfter[0] = Math.abs(dimsAfter[0]); + dimsAfter[1] = Math.abs(dimsAfter[1]); + + if (!(mOutWidth > 0 && mOutHeight > 0)) { + mOutWidth = Math.round(dimsAfter[0]); + mOutHeight = Math.round(dimsAfter[1]); + } + + RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]); + RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight); + + Matrix m = new Matrix(); + if (mRotation == 0) { + m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); + } else { + Matrix m1 = new Matrix(); + m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f); + Matrix m2 = new Matrix(); + m2.setRotate(mRotation); + Matrix m3 = new Matrix(); + m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f); + Matrix m4 = new Matrix(); + m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); + + Matrix c1 = new Matrix(); + c1.setConcat(m2, m1); + Matrix c2 = new Matrix(); + c2.setConcat(m4, m3); + m.setConcat(c2, c1); + } + + Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), + (int) returnRect.height(), Bitmap.Config.ARGB_8888); + if (tmp != null) { + Canvas c = new Canvas(tmp); + Paint p = new Paint(); + p.setFilterBitmap(true); + c.drawBitmap(crop, m, p); + crop = tmp; + } + } + + if (mSaveCroppedBitmap) { + mCroppedBitmap = crop; + } + + // Compress to byte array + ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); + if (crop.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, tmpOut)) { + // If we need to set to the wallpaper, set it + if (mSetWallpaper && wallpaperManager != null) { + try { + byte[] outByteArray = tmpOut.toByteArray(); + wallpaperManager.setStream(new ByteArrayInputStream(outByteArray)); + if (mOnBitmapCroppedHandler != null) { + mOnBitmapCroppedHandler.onBitmapCropped(outByteArray); + } + } catch (IOException e) { + Log.w(LOGTAG, "cannot write stream to wallpaper", e); + failure = true; + } + } + } else { + Log.w(LOGTAG, "cannot compress bitmap"); + failure = true; + } + } + return !failure; // True if any of the operations failed + } + + @Override + protected Boolean doInBackground(Void... params) { + return cropBitmap(); + } + + @Override + protected void onPostExecute(Boolean result) { + if (mOnEndRunnable != null) { + mOnEndRunnable.run(); + } + } +}
\ No newline at end of file diff --git a/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java index a671ed2b9..9ac5c1bf7 100644 --- a/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java +++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java @@ -16,87 +16,24 @@ package com.android.gallery3d.common; -import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.os.Build; -import android.util.FloatMath; +import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; import android.util.Log; -import java.io.ByteArrayOutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import com.android.gallery3d.exif.ExifInterface; -public class BitmapUtils { - private static final String TAG = "BitmapUtils"; - private static final int DEFAULT_JPEG_QUALITY = 90; - public static final int UNCONSTRAINED = -1; - - private BitmapUtils(){} - - /* - * Compute the sample size as a function of minSideLength - * and maxNumOfPixels. - * minSideLength is used to specify that minimal width or height of a - * bitmap. - * maxNumOfPixels is used to specify the maximal size in pixels that is - * tolerable in terms of memory usage. - * - * The function returns a sample size based on the constraints. - * Both size and minSideLength can be passed in as UNCONSTRAINED, - * which indicates no care of the corresponding constraint. - * The functions prefers returning a sample size that - * generates a smaller bitmap, unless minSideLength = UNCONSTRAINED. - * - * Also, the function rounds up the sample size to a power of 2 or multiple - * of 8 because BitmapFactory only honors sample size this way. - * For example, BitmapFactory downsamples an image by 2 even though the - * request is 3. So we round up the sample size to avoid OOM. - */ - public static int computeSampleSize(int width, int height, - int minSideLength, int maxNumOfPixels) { - int initialSize = computeInitialSampleSize( - width, height, minSideLength, maxNumOfPixels); - - return initialSize <= 8 - ? Utils.nextPowerOf2(initialSize) - : (initialSize + 7) / 8 * 8; - } - - private static int computeInitialSampleSize(int w, int h, - int minSideLength, int maxNumOfPixels) { - if (maxNumOfPixels == UNCONSTRAINED - && minSideLength == UNCONSTRAINED) return 1; - - int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 : - (int) FloatMath.ceil(FloatMath.sqrt((float) (w * h) / maxNumOfPixels)); - - if (minSideLength == UNCONSTRAINED) { - return lowerBound; - } else { - int sampleSize = Math.min(w / minSideLength, h / minSideLength); - return Math.max(sampleSize, lowerBound); - } - } +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; - // This computes a sample size which makes the longer side at least - // minSideLength long. If that's not possible, return 1. - public static int computeSampleSizeLarger(int w, int h, - int minSideLength) { - int initialSize = Math.max(w / minSideLength, h / minSideLength); - if (initialSize <= 1) return 1; +public class BitmapUtils { - return initialSize <= 8 - ? Utils.prevPowerOf2(initialSize) - : initialSize / 8 * 8; - } + private static final String TAG = "BitmapUtils"; // Find the min x that 1 / x >= scale public static int computeSampleSizeLarger(float scale) { - int initialSize = (int) FloatMath.floor(1f / scale); + int initialSize = (int) Math.floor(1f / scale); if (initialSize <= 1) return 1; return initialSize <= 8 @@ -104,157 +41,41 @@ public class BitmapUtils { : initialSize / 8 * 8; } - // Find the max x that 1 / x <= scale. - public static int computeSampleSize(float scale) { - Utils.assertTrue(scale > 0); - int initialSize = Math.max(1, (int) FloatMath.ceil(1 / scale)); - return initialSize <= 8 - ? Utils.nextPowerOf2(initialSize) - : (initialSize + 7) / 8 * 8; - } - - public static Bitmap resizeBitmapByScale( - Bitmap bitmap, float scale, boolean recycle) { - int width = Math.round(bitmap.getWidth() * scale); - int height = Math.round(bitmap.getHeight() * scale); - if (width == bitmap.getWidth() - && height == bitmap.getHeight()) return bitmap; - Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap)); - Canvas canvas = new Canvas(target); - canvas.scale(scale, scale); - Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); - canvas.drawBitmap(bitmap, 0, 0, paint); - if (recycle) bitmap.recycle(); - return target; + public static int getRotationFromExif(Context context, Uri uri) { + return BitmapUtils.getRotationFromExifHelper(null, 0, context, uri); } - private static Bitmap.Config getConfig(Bitmap bitmap) { - Bitmap.Config config = bitmap.getConfig(); - if (config == null) { - config = Bitmap.Config.ARGB_8888; - } - return config; + public static int getRotationFromExif(Resources res, int resId) { + return BitmapUtils.getRotationFromExifHelper(res, resId, null, null); } - public static Bitmap resizeDownBySideLength( - Bitmap bitmap, int maxLength, boolean recycle) { - int srcWidth = bitmap.getWidth(); - int srcHeight = bitmap.getHeight(); - float scale = Math.min( - (float) maxLength / srcWidth, (float) maxLength / srcHeight); - if (scale >= 1.0f) return bitmap; - return resizeBitmapByScale(bitmap, scale, recycle); - } - - public static Bitmap resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle) { - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - if (w == size && h == size) return bitmap; - - // scale the image so that the shorter side equals to the target; - // the longer side will be center-cropped. - float scale = (float) size / Math.min(w, h); - - Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap)); - int width = Math.round(scale * bitmap.getWidth()); - int height = Math.round(scale * bitmap.getHeight()); - Canvas canvas = new Canvas(target); - canvas.translate((size - width) / 2f, (size - height) / 2f); - canvas.scale(scale, scale); - Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); - canvas.drawBitmap(bitmap, 0, 0, paint); - if (recycle) bitmap.recycle(); - return target; - } - - public static void recycleSilently(Bitmap bitmap) { - if (bitmap == null) return; - try { - bitmap.recycle(); - } catch (Throwable t) { - Log.w(TAG, "unable recycle bitmap", t); - } - } - - public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) { - if (rotation == 0) return source; - int w = source.getWidth(); - int h = source.getHeight(); - Matrix m = new Matrix(); - m.postRotate(rotation); - Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true); - if (recycle) source.recycle(); - return bitmap; - } - - public static Bitmap createVideoThumbnail(String filePath) { - // MediaMetadataRetriever is available on API Level 8 - // but is hidden until API Level 10 - Class<?> clazz = null; - Object instance = null; + private static int getRotationFromExifHelper(Resources res, int resId, Context context, Uri uri) { + ExifInterface ei = new ExifInterface(); + InputStream is = null; + BufferedInputStream bis = null; try { - clazz = Class.forName("android.media.MediaMetadataRetriever"); - instance = clazz.newInstance(); - - Method method = clazz.getMethod("setDataSource", String.class); - method.invoke(instance, filePath); - - // The method name changes between API Level 9 and 10. - if (Build.VERSION.SDK_INT <= 9) { - return (Bitmap) clazz.getMethod("captureFrame").invoke(instance); + if (uri != null) { + is = context.getContentResolver().openInputStream(uri); + bis = new BufferedInputStream(is); + ei.readExif(bis); } else { - byte[] data = (byte[]) clazz.getMethod("getEmbeddedPicture").invoke(instance); - if (data != null) { - Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); - if (bitmap != null) return bitmap; - } - return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance); + is = res.openRawResource(resId); + bis = new BufferedInputStream(is); + ei.readExif(bis); } - } catch (IllegalArgumentException ex) { - // Assume this is a corrupt video file - } catch (RuntimeException ex) { - // Assume this is a corrupt video file. - } catch (InstantiationException e) { - Log.e(TAG, "createVideoThumbnail", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "createVideoThumbnail", e); - } catch (ClassNotFoundException e) { - Log.e(TAG, "createVideoThumbnail", e); - } catch (NoSuchMethodException e) { - Log.e(TAG, "createVideoThumbnail", e); - } catch (IllegalAccessException e) { - Log.e(TAG, "createVideoThumbnail", e); - } finally { - try { - if (instance != null) { - clazz.getMethod("release").invoke(instance); - } - } catch (Exception ignored) { + Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); + if (ori != null) { + return ExifInterface.getRotationForOrientationValue(ori.shortValue()); } + } catch (IOException e) { + Log.w(TAG, "Getting exif data failed", e); + } catch (NullPointerException e) { + // Sometimes the ExifInterface has an internal NPE if Exif data isn't valid + Log.w(TAG, "Getting exif data failed", e); + } finally { + Utils.closeSilently(bis); + Utils.closeSilently(is); } - return null; - } - - public static byte[] compressToBytes(Bitmap bitmap) { - return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY); - } - - public static byte[] compressToBytes(Bitmap bitmap, int quality) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(65536); - bitmap.compress(CompressFormat.JPEG, quality, baos); - return baos.toByteArray(); - } - - public static boolean isSupportedByRegionDecoder(String mimeType) { - if (mimeType == null) return false; - mimeType = mimeType.toLowerCase(); - return mimeType.startsWith("image/") && - (!mimeType.equals("image/gif") && !mimeType.endsWith("bmp")); - } - - public static boolean isRotationSupported(String mimeType) { - if (mimeType == null) return false; - mimeType = mimeType.toLowerCase(); - return mimeType.equals("image/jpeg"); + return 0; } } diff --git a/WallpaperPicker/src/com/android/gallery3d/common/Utils.java b/WallpaperPicker/src/com/android/gallery3d/common/Utils.java index 614a081c8..8466c22cb 100644 --- a/WallpaperPicker/src/com/android/gallery3d/common/Utils.java +++ b/WallpaperPicker/src/com/android/gallery3d/common/Utils.java @@ -16,32 +16,16 @@ package com.android.gallery3d.common; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; -import android.os.Build; +import android.graphics.RectF; import android.os.ParcelFileDescriptor; -import android.text.TextUtils; import android.util.Log; import java.io.Closeable; import java.io.IOException; -import java.io.InterruptedIOException; public class Utils { private static final String TAG = "Utils"; - private static final String DEBUG_TAG = "GalleryDebug"; - - private static final long POLY64REV = 0x95AC9329AC4BC9B5L; - private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL; - - private static long[] sCrcTable = new long[256]; - - private static final boolean IS_DEBUG_BUILD = - Build.TYPE.equals("eng") || Build.TYPE.equals("userdebug"); - - private static final String MASK_STRING = "********************************"; // Throws AssertionError if the input is false. public static void assertTrue(boolean cond) { @@ -50,28 +34,6 @@ public class Utils { } } - // Throws AssertionError with the message. We had a method having the form - // assertTrue(boolean cond, String message, Object ... args); - // However a call to that method will cause memory allocation even if the - // condition is false (due to autoboxing generated by "Object ... args"), - // so we don't use that anymore. - public static void fail(String message, Object ... args) { - throw new AssertionError( - args.length == 0 ? message : String.format(message, args)); - } - - // Throws NullPointerException if the input is null. - public static <T> T checkNotNull(T object) { - if (object == null) throw new NullPointerException(); - return object; - } - - // Returns true if two input Object are both null or equal - // to each other. - public static boolean equals(Object a, Object b) { - return (a == b) || (a == null ? false : a.equals(b)); - } - // Returns the next power of two. // Returns the input if it is already power of 2. // Throws IllegalArgumentException if the input is <= 0 or @@ -102,87 +64,6 @@ public class Utils { return x; } - // Returns the input value x clamped to the range [min, max]. - public static float clamp(float x, float min, float max) { - if (x > max) return max; - if (x < min) return min; - return x; - } - - // Returns the input value x clamped to the range [min, max]. - public static long clamp(long x, long min, long max) { - if (x > max) return max; - if (x < min) return min; - return x; - } - - public static boolean isOpaque(int color) { - return color >>> 24 == 0xFF; - } - - public static void swap(int[] array, int i, int j) { - int temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } - - /** - * A function thats returns a 64-bit crc for string - * - * @param in input string - * @return a 64-bit crc value - */ - public static final long crc64Long(String in) { - if (in == null || in.length() == 0) { - return 0; - } - return crc64Long(getBytes(in)); - } - - static { - // http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c - long part; - for (int i = 0; i < 256; i++) { - part = i; - for (int j = 0; j < 8; j++) { - long x = ((int) part & 1) != 0 ? POLY64REV : 0; - part = (part >> 1) ^ x; - } - sCrcTable[i] = part; - } - } - - public static final long crc64Long(byte[] buffer) { - long crc = INITIALCRC; - for (int k = 0, n = buffer.length; k < n; ++k) { - crc = sCrcTable[(((int) crc) ^ buffer[k]) & 0xff] ^ (crc >> 8); - } - return crc; - } - - public static byte[] getBytes(String in) { - byte[] result = new byte[in.length() * 2]; - int output = 0; - for (char ch : in.toCharArray()) { - result[output++] = (byte) (ch & 0xFF); - result[output++] = (byte) (ch >> 8); - } - return result; - } - - public static void closeSilently(Closeable c) { - if (c == null) return; - try { - c.close(); - } catch (IOException t) { - Log.w(TAG, "close fail ", t); - } - } - - public static int compare(long a, long b) { - return a < b ? -1 : a == b ? 0 : 1; - } - public static int ceilLog2(float value) { int i; for (i = 0; i < 31; i++) { @@ -199,6 +80,15 @@ public class Utils { return i - 1; } + public static void closeSilently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (IOException t) { + Log.w(TAG, "close fail ", t); + } + } + public static void closeSilently(ParcelFileDescriptor fd) { try { if (fd != null) fd.close(); @@ -215,126 +105,25 @@ public class Utils { } } - public static float interpolateAngle( - float source, float target, float progress) { - // interpolate the angle from source to target - // We make the difference in the range of [-179, 180], this is the - // shortest path to change source to target. - float diff = target - source; - if (diff < 0) diff += 360f; - if (diff > 180) diff -= 360f; - - float result = source + diff * progress; - return result < 0 ? result + 360f : result; - } - - public static float interpolateScale( - float source, float target, float progress) { - return source + progress * (target - source); - } - - public static String ensureNotNull(String value) { - return value == null ? "" : value; - } - - public static float parseFloatSafely(String content, float defaultValue) { - if (content == null) return defaultValue; - try { - return Float.parseFloat(content); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - public static int parseIntSafely(String content, int defaultValue) { - if (content == null) return defaultValue; - try { - return Integer.parseInt(content); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - public static boolean isNullOrEmpty(String exifMake) { - return TextUtils.isEmpty(exifMake); - } - - public static void waitWithoutInterrupt(Object object) { - try { - object.wait(); - } catch (InterruptedException e) { - Log.w(TAG, "unexpected interrupt: " + object); - } - } - - public static boolean handleInterrruptedException(Throwable e) { - // A helper to deal with the interrupt exception - // If an interrupt detected, we will setup the bit again. - if (e instanceof InterruptedIOException - || e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - return true; - } - return false; - } - - /** - * @return String with special XML characters escaped. - */ - public static String escapeXml(String s) { - StringBuilder sb = new StringBuilder(); - for (int i = 0, len = s.length(); i < len; ++i) { - char c = s.charAt(i); - switch (c) { - case '<': sb.append("<"); break; - case '>': sb.append(">"); break; - case '\"': sb.append("""); break; - case '\'': sb.append("'"); break; - case '&': sb.append("&"); break; - default: sb.append(c); - } - } - return sb.toString(); - } - - public static String getUserAgent(Context context) { - PackageInfo packageInfo; - try { - packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); - } catch (NameNotFoundException e) { - throw new IllegalStateException("getPackageInfo failed"); - } - return String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s", - packageInfo.packageName, - packageInfo.versionName, - Build.BRAND, - Build.DEVICE, - Build.MODEL, - Build.ID, - Build.VERSION.SDK_INT, - Build.VERSION.RELEASE, - Build.VERSION.INCREMENTAL); - } - - public static String[] copyOf(String[] source, int newSize) { - String[] result = new String[newSize]; - newSize = Math.min(source.length, newSize); - System.arraycopy(source, 0, result, 0, newSize); - return result; - } - - // Mask information for debugging only. It returns <code>info.toString()</code> directly - // for debugging build (i.e., 'eng' and 'userdebug') and returns a mask ("****") - // in release build to protect the information (e.g. for privacy issue). - public static String maskDebugInfo(Object info) { - if (info == null) return null; - String s = info.toString(); - int length = Math.min(s.length(), MASK_STRING.length()); - return IS_DEBUG_BUILD ? s : MASK_STRING.substring(0, length); - } - - // This method should be ONLY used for debugging. - public static void debug(String message, Object ... args) { - Log.v(DEBUG_TAG, String.format(message, args)); + public static RectF getMaxCropRect( + int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) { + RectF cropRect = new RectF(); + // Get a crop rect that will fit this + if (inWidth / (float) inHeight > outWidth / (float) outHeight) { + cropRect.top = 0; + cropRect.bottom = inHeight; + cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2; + cropRect.right = inWidth - cropRect.left; + if (leftAligned) { + cropRect.right -= cropRect.left; + cropRect.left = 0; + } + } else { + cropRect.left = 0; + cropRect.right = inWidth; + cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2; + cropRect.bottom = inHeight - cropRect.top; + } + return cropRect; } } diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java index 2e77b903f..0f3efb727 100644 --- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java @@ -27,7 +27,6 @@ import java.util.WeakHashMap; // If a BasicTexture is loaded into GL memory, it has a GL texture id. public abstract class BasicTexture implements Texture { - @SuppressWarnings("unused") private static final String TAG = "BasicTexture"; protected static final int UNSPECIFIED = -1; diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java index 4ead1315e..933260b48 100644 --- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java @@ -23,8 +23,6 @@ import android.opengl.GLUtils; import android.opengl.Matrix; import android.util.Log; -import com.android.gallery3d.util.IntArray; - import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -698,6 +696,7 @@ public class GLES20Canvas implements GLCanvas { } private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) { + deleteRecycledResources(); GLES20.glUseProgram(program); checkError(); enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA); diff --git a/WallpaperPicker/src/com/android/gallery3d/util/IntArray.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/IntArray.java index 2c4dc2c83..f123624d6 100644 --- a/WallpaperPicker/src/com/android/gallery3d/util/IntArray.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/IntArray.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.gallery3d.util; +package com.android.gallery3d.glrenderer; public class IntArray { private static final int INIT_CAPACITY = 8; diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java index f41a979b7..67abf6564 100644 --- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java @@ -20,6 +20,8 @@ import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.opengl.GLUtils; +import com.android.launcher3.util.Thunk; + import junit.framework.Assert; import java.util.HashMap; @@ -82,7 +84,7 @@ public abstract class UploadedTexture extends BasicTexture { return mIsUploading; } - private static class BorderKey implements Cloneable { + @Thunk static class BorderKey implements Cloneable { public boolean vertical; public Config config; public int length; diff --git a/WallpaperPicker/src/com/android/launcher3/CropView.java b/WallpaperPicker/src/com/android/launcher3/CropView.java index 578b8eafd..50f779add 100644 --- a/WallpaperPicker/src/com/android/launcher3/CropView.java +++ b/WallpaperPicker/src/com/android/launcher3/CropView.java @@ -21,7 +21,6 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.RectF; import android.util.AttributeSet; -import android.util.FloatMath; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector.OnScaleGestureListener; @@ -300,12 +299,12 @@ public class CropView extends TiledImageView implements OnScaleGestureListener { adjustment[0] = (edges.right - getWidth()) / scale; } if (edges.top > 0) { - adjustment[1] = FloatMath.ceil(edges.top / scale); + adjustment[1] = (float) Math.ceil(edges.top / scale); } else if (edges.bottom < getHeight()) { adjustment[1] = (edges.bottom - getHeight()) / scale; } for (int dim = 0; dim <= 1; dim++) { - if (coef[dim] > 0) adjustment[dim] = FloatMath.ceil(adjustment[dim]); + if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]); } mInverseRotateMatrix.mapPoints(adjustment); diff --git a/src/com/android/launcher3/LauncherWallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/LauncherWallpaperPickerActivity.java index 10fe013ee..091c05462 100644 --- a/src/com/android/launcher3/LauncherWallpaperPickerActivity.java +++ b/WallpaperPicker/src/com/android/launcher3/LauncherWallpaperPickerActivity.java @@ -16,15 +16,6 @@ package com.android.launcher3; -import android.content.Intent; - +// TODO: Remove this class public class LauncherWallpaperPickerActivity extends WallpaperPickerActivity { - @Override - public void startActivityForResultSafely(Intent intent, int requestCode) { - Utilities.startActivityForResultSafely(this, intent, requestCode); - } - @Override - public boolean enableRotation() { - return Utilities.isRotationEnabled(this); - } -} +}
\ No newline at end of file diff --git a/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java index 88f4461bf..b53fce119 100644 --- a/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java +++ b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java @@ -30,11 +30,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.TextView; +import com.android.launcher3.util.Thunk; + import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; @@ -50,7 +51,7 @@ public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter private final LayoutInflater mInflater; private final PackageManager mPackageManager; - private List<LiveWallpaperTile> mWallpapers; + @Thunk List<LiveWallpaperTile> mWallpapers; @SuppressWarnings("unchecked") public LiveWallpaperListAdapter(Context context) { @@ -90,8 +91,6 @@ public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter view = convertView; } - WallpaperPickerActivity.setWallpaperItemPaddingToZero((FrameLayout) view); - LiveWallpaperTile wallpaperInfo = mWallpapers.get(position); wallpaperInfo.setView(view); ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); @@ -111,8 +110,8 @@ public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter } public static class LiveWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo { - private Drawable mThumbnail; - private WallpaperInfo mInfo; + @Thunk Drawable mThumbnail; + @Thunk WallpaperInfo mInfo; public LiveWallpaperTile(Drawable thumbnail, WallpaperInfo info, Intent intent) { mThumbnail = thumbnail; mInfo = info; @@ -122,8 +121,8 @@ public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter Intent preview = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER); preview.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, mInfo.getComponent()); - a.onLiveWallpaperPickerLaunch(mInfo); - a.startActivityForResultSafely(preview, WallpaperPickerActivity.PICK_LIVE_WALLPAPER); + a.startActivityForResultSafely(preview, + WallpaperPickerActivity.PICK_WALLPAPER_THIRD_PARTY_ACTIVITY); } } diff --git a/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java b/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java index 9f92bc105..64b0ac466 100644 --- a/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java +++ b/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java @@ -16,7 +16,6 @@ package com.android.launcher3; -import android.app.Activity; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -60,13 +59,13 @@ public class SavedWallpaperImages extends BaseAdapter implements ListAdapter { } } - public SavedWallpaperImages(Activity context) { + public SavedWallpaperImages(Context context) { // We used to store the saved images in the cache directory, but that meant they'd get // deleted sometimes-- move them to the data directory ImageDb.moveFromCacheDirectoryIfNecessary(context); mDb = new ImageDb(context); mContext = context; - mLayoutInflater = context.getLayoutInflater(); + mLayoutInflater = LayoutInflater.from(context); } public void loadThumbnailsAndImageIdList() { diff --git a/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java index 7a4d48ca9..f46da53ec 100644 --- a/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java +++ b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java @@ -28,16 +28,15 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; -import android.widget.FrameLayout; import android.widget.ListAdapter; import android.widget.TextView; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; import java.util.List; public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements ListAdapter { - private static final String LOG_TAG = "LiveWallpaperListAdapter"; - private final LayoutInflater mInflater; private final PackageManager mPackageManager; private final int mIconSize; @@ -46,7 +45,7 @@ public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements new ArrayList<ThirdPartyWallpaperTile>(); public static class ThirdPartyWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo { - private ResolveInfo mResolveInfo; + @Thunk ResolveInfo mResolveInfo; public ThirdPartyWallpaperTile(ResolveInfo resolveInfo) { mResolveInfo = resolveInfo; } @@ -62,7 +61,7 @@ public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements } public ThirdPartyWallpaperPickerListAdapter(Context context) { - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mInflater = LayoutInflater.from(context); mPackageManager = context.getPackageManager(); mIconSize = context.getResources().getDimensionPixelSize(R.dimen.wallpaperItemIconSize); final PackageManager pm = mPackageManager; @@ -126,8 +125,6 @@ public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements view = convertView; } - WallpaperPickerActivity.setWallpaperItemPaddingToZero((FrameLayout) view); - ResolveInfo info = mThirdPartyWallpaperPickers.get(position).mResolveInfo; TextView label = (TextView) view.findViewById(R.id.wallpaper_item_label); label.setText(info.loadLabel(mPackageManager)); diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java index fa8ec64c2..142a9cb10 100644 --- a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java +++ b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java @@ -16,6 +16,7 @@ package com.android.launcher3; +import android.annotation.TargetApi; import android.app.ActionBar; import android.app.Activity; import android.app.WallpaperManager; @@ -25,42 +26,41 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; -import android.graphics.BitmapFactory; -import android.graphics.BitmapRegionDecoder; -import android.graphics.Canvas; import android.graphics.Matrix; -import android.graphics.Paint; import android.graphics.Point; -import android.graphics.Rect; import android.graphics.RectF; import android.net.Uri; -import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; import android.util.Log; import android.view.Display; import android.view.View; -import android.view.WindowManager; import android.widget.Toast; +import com.android.gallery3d.common.BitmapCropTask; +import com.android.gallery3d.common.BitmapUtils; import com.android.gallery3d.common.Utils; -import com.android.gallery3d.exif.ExifInterface; +import com.android.launcher3.base.BaseActivity; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.util.WallpaperUtils; import com.android.photos.BitmapRegionTileSource; import com.android.photos.BitmapRegionTileSource.BitmapSource; +import com.android.photos.BitmapRegionTileSource.BitmapSource.InBitmapProvider; +import com.android.photos.views.TiledImageRenderer.TileSource; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; -public class WallpaperCropActivity extends Activity { +public class WallpaperCropActivity extends BaseActivity implements Handler.Callback { private static final String LOGTAG = "Launcher3.CropActivity"; - protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; - protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; - private static final int DEFAULT_COMPRESS_QUALITY = 90; + protected static final String WALLPAPER_WIDTH_KEY = WallpaperUtils.WALLPAPER_WIDTH_KEY; + protected static final String WALLPAPER_HEIGHT_KEY = WallpaperUtils.WALLPAPER_HEIGHT_KEY; + /** * The maximum bitmap size we allow to be returned through the intent. * Intents have a maximum of 1MB in total size. However, the Bitmap seems to @@ -69,17 +69,31 @@ public class WallpaperCropActivity extends Activity { * array instead of a Bitmap instance to avoid overhead. */ public static final int MAX_BMAP_IN_INTENT = 750000; - private static final float WALLPAPER_SCREENS_SPAN = 2f; + public static final float WALLPAPER_SCREENS_SPAN = WallpaperUtils.WALLPAPER_SCREENS_SPAN; - protected static Point sDefaultWallpaperSize; + private static final int MSG_LOAD_IMAGE = 1; protected CropView mCropView; + protected View mProgressView; protected Uri mUri; protected View mSetWallpaperButton; + private HandlerThread mLoaderThread; + private Handler mLoaderHandler; + @Thunk LoadRequest mCurrentLoadRequest; + private byte[] mTempStorageForDecoding = new byte[16 * 1024]; + // A weak-set of reusable bitmaps + @Thunk Set<Bitmap> mReusableBitmaps = + Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>()); + @Override - protected void onCreate(Bundle savedInstanceState) { + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + mLoaderThread = new HandlerThread("wallpaper_loader"); + mLoaderThread.start(); + mLoaderHandler = new Handler(mLoaderThread.getLooper(), this); + init(); if (!enableRotation()) { setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT); @@ -90,6 +104,7 @@ public class WallpaperCropActivity extends Activity { setContentView(R.layout.wallpaper_cropper); mCropView = (CropView) findViewById(R.id.cropView); + mProgressView = findViewById(R.id.loading); Intent cropIntent = getIntent(); final Uri imageUri = cropIntent.getData(); @@ -116,13 +131,12 @@ public class WallpaperCropActivity extends Activity { // Load image in background final BitmapRegionTileSource.UriBitmapSource bitmapSource = - new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024); + new BitmapRegionTileSource.UriBitmapSource(getContext(), imageUri, 1024); mSetWallpaperButton.setEnabled(false); Runnable onLoad = new Runnable() { public void run() { if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) { - Toast.makeText(WallpaperCropActivity.this, - getString(R.string.wallpaper_load_fail), + Toast.makeText(getContext(), R.string.wallpaper_load_fail, Toast.LENGTH_LONG).show(); finish(); } else { @@ -130,188 +144,156 @@ public class WallpaperCropActivity extends Activity { } } }; - setCropViewTileSource(bitmapSource, true, false, onLoad); + setCropViewTileSource(bitmapSource, true, false, null, onLoad); } @Override - protected void onDestroy() { + public void onDestroy() { if (mCropView != null) { mCropView.destroy(); } + if (mLoaderThread != null) { + mLoaderThread.quit(); + } super.onDestroy(); } - public void setCropViewTileSource( - final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled, - final boolean moveToLeft, final Runnable postExecute) { - final Context context = WallpaperCropActivity.this; - final View progressView = findViewById(R.id.loading); - final AsyncTask<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void...args) { - if (!isCancelled()) { - try { - bitmapSource.loadInBackground(); - } catch (SecurityException securityException) { - if (isDestroyed()) { - // Temporarily granted permissions are revoked when the activity - // finishes, potentially resulting in a SecurityException here. - // Even though {@link #isDestroyed} might also return true in different - // situations where the configuration changes, we are fine with - // catching these cases here as well. - cancel(false); - } else { - // otherwise it had a different cause and we throw it further - throw securityException; - } - } - } - return null; - } - protected void onPostExecute(Void arg) { - if (!isCancelled()) { - progressView.setVisibility(View.INVISIBLE); - if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { - mCropView.setTileSource( - new BitmapRegionTileSource(context, bitmapSource), null); - mCropView.setTouchEnabled(touchEnabled); - if (moveToLeft) { - mCropView.moveToLeft(); + /** + * This is called on {@link #mLoaderThread} + */ + @Override + public boolean handleMessage(Message msg) { + if (msg.what == MSG_LOAD_IMAGE) { + final LoadRequest req = (LoadRequest) msg.obj; + try { + req.src.loadInBackground(new InBitmapProvider() { + + @Override + public Bitmap forPixelCount(int count) { + Bitmap bitmapToReuse = null; + // Find the smallest bitmap that satisfies the pixel count limit + synchronized (mReusableBitmaps) { + int currentBitmapSize = Integer.MAX_VALUE; + for (Bitmap b : mReusableBitmaps) { + int bitmapSize = b.getWidth() * b.getHeight(); + if ((bitmapSize >= count) && (bitmapSize < currentBitmapSize)) { + bitmapToReuse = b; + currentBitmapSize = bitmapSize; + } + } + + if (bitmapToReuse != null) { + mReusableBitmaps.remove(bitmapToReuse); + } } + return bitmapToReuse; } - } - if (postExecute != null) { - postExecute.run(); - } - } - }; - // We don't want to show the spinner every time we load an image, because that would be - // annoying; instead, only start showing the spinner if loading the image has taken - // longer than 1 sec (ie 1000 ms) - progressView.postDelayed(new Runnable() { - public void run() { - if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) { - progressView.setVisibility(View.VISIBLE); + }); + } catch (SecurityException securityException) { + if (isDestroyed()) { + // Temporarily granted permissions are revoked when the activity + // finishes, potentially resulting in a SecurityException here. + // Even though {@link #isDestroyed} might also return true in different + // situations where the configuration changes, we are fine with + // catching these cases here as well. + return true; + } else { + // otherwise it had a different cause and we throw it further + throw securityException; } } - }, 1000); - loadBitmapTask.execute(); - } - public boolean enableRotation() { - return getResources().getBoolean(R.bool.allow_rotation); - } + req.result = new BitmapRegionTileSource(getContext(), req.src, mTempStorageForDecoding); + runOnUiThread(new Runnable() { - public static String getSharedPreferencesKey() { - return LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; + @Override + public void run() { + if (req == mCurrentLoadRequest) { + onLoadRequestComplete(req, + req.src.getLoadingState() == BitmapSource.State.LOADED); + } else { + addReusableBitmap(req.result); + } + } + }); + return true; + } + return false; } - // As a ratio of screen height, the total distance we want the parallax effect to span - // horizontally - private static float wallpaperTravelToScreenWidthRatio(int width, int height) { - float aspectRatio = width / (float) height; - - // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width - // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width - // We will use these two data points to extrapolate how much the wallpaper parallax effect - // to span (ie travel) at any aspect ratio: - - final float ASPECT_RATIO_LANDSCAPE = 16/10f; - final float ASPECT_RATIO_PORTRAIT = 10/16f; - final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; - final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; - - // To find out the desired width at different aspect ratios, we use the following two - // formulas, where the coefficient on x is the aspect ratio (width/height): - // (16/10)x + y = 1.5 - // (10/16)x + y = 1.2 - // We solve for x and y and end up with a final formula: - final float x = - (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / - (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); - final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; - return x * aspectRatio + y; + @Thunk void addReusableBitmap(TileSource src) { + synchronized (mReusableBitmaps) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + && src instanceof BitmapRegionTileSource) { + Bitmap preview = ((BitmapRegionTileSource) src).getBitmap(); + if (preview != null && preview.isMutable()) { + mReusableBitmaps.add(preview); + } + } + } } - static protected Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) { - if (sDefaultWallpaperSize == null) { - Point minDims = new Point(); - Point maxDims = new Point(); - windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); - - int maxDim = Math.max(maxDims.x, maxDims.y); - int minDim = Math.max(minDims.x, minDims.y); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { - Point realSize = new Point(); - windowManager.getDefaultDisplay().getRealSize(realSize); - maxDim = Math.max(realSize.x, realSize.y); - minDim = Math.min(realSize.x, realSize.y); + protected void onLoadRequestComplete(LoadRequest req, boolean success) { + mCurrentLoadRequest = null; + if (success) { + TileSource oldSrc = mCropView.getTileSource(); + mCropView.setTileSource(req.result, null); + mCropView.setTouchEnabled(req.touchEnabled); + if (req.moveToLeft) { + mCropView.moveToLeft(); + } + if (req.scaleProvider != null) { + mCropView.setScale(req.scaleProvider.getScale(req.result)); } - // We need to ensure that there is enough extra space in the wallpaper - // for the intended parallax effects - final int defaultWidth, defaultHeight; - if (isScreenLarge(res)) { - defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); - defaultHeight = maxDim; - } else { - defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); - defaultHeight = maxDim; + // Free last image + if (oldSrc != null) { + // Call yield instead of recycle, as we only want to free GL resource. + // We can still reuse the bitmap for decoding any other image. + oldSrc.getPreview().yield(); } - sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); + addReusableBitmap(oldSrc); + } + if (req.postExecute != null) { + req.postExecute.run(); } - return sDefaultWallpaperSize; } - public static int getRotationFromExif(String path) { - return getRotationFromExifHelper(path, null, 0, null, null); - } + public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled, + boolean moveToLeft, CropViewScaleProvider scaleProvider, Runnable postExecute) { + final LoadRequest req = new LoadRequest(); + req.moveToLeft = moveToLeft; + req.src = bitmapSource; + req.touchEnabled = touchEnabled; + req.postExecute = postExecute; + req.scaleProvider = scaleProvider; + mCurrentLoadRequest = req; - public static int getRotationFromExif(Context context, Uri uri) { - return getRotationFromExifHelper(null, null, 0, context, uri); - } + // Remove any pending requests + mLoaderHandler.removeMessages(MSG_LOAD_IMAGE); + Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget(); - public static int getRotationFromExif(Resources res, int resId) { - return getRotationFromExifHelper(null, res, resId, null, null); + // We don't want to show the spinner every time we load an image, because that would be + // annoying; instead, only start showing the spinner if loading the image has taken + // longer than 1 sec (ie 1000 ms) + mProgressView.postDelayed(new Runnable() { + public void run() { + if (mCurrentLoadRequest == req) { + mProgressView.setVisibility(View.VISIBLE); + } + } + }, 1000); } - private static int getRotationFromExifHelper( - String path, Resources res, int resId, Context context, Uri uri) { - ExifInterface ei = new ExifInterface(); - InputStream is = null; - BufferedInputStream bis = null; - try { - if (path != null) { - ei.readExif(path); - } else if (uri != null) { - is = context.getContentResolver().openInputStream(uri); - bis = new BufferedInputStream(is); - ei.readExif(bis); - } else { - is = res.openRawResource(resId); - bis = new BufferedInputStream(is); - ei.readExif(bis); - } - Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); - if (ori != null) { - return ExifInterface.getRotationForOrientationValue(ori.shortValue()); - } - } catch (IOException e) { - Log.w(LOGTAG, "Getting exif data failed", e); - } catch (NullPointerException e) { - // Sometimes the ExifInterface has an internal NPE if Exif data isn't valid - Log.w(LOGTAG, "Getting exif data failed", e); - } finally { - Utils.closeSilently(bis); - Utils.closeSilently(is); - } - return 0; + + public boolean enableRotation() { + return getResources().getBoolean(R.bool.allow_rotation); } protected void setWallpaper(Uri uri, final boolean finishActivityWhenDone) { - int rotation = getRotationFromExif(this, uri); + int rotation = BitmapUtils.getRotationFromExif(getContext(), uri); BitmapCropTask cropTask = new BitmapCropTask( - this, uri, null, rotation, 0, 0, true, false, null); + getContext(), uri, null, rotation, 0, 0, true, false, null); final Point bounds = cropTask.getImageBounds(); Runnable onEndCrop = new Runnable() { public void run() { @@ -331,11 +313,11 @@ public class WallpaperCropActivity extends Activity { Resources res, int resId, final boolean finishActivityWhenDone) { // crop this image and scale it down to the default wallpaper size for // this device - int rotation = getRotationFromExif(res, resId); + int rotation = BitmapUtils.getRotationFromExif(res, resId); Point inSize = mCropView.getSourceDimensions(); - Point outSize = getDefaultWallpaperSize(getResources(), + Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), getWindowManager()); - RectF crop = getMaxCropRect( + RectF crop = Utils.getMaxCropRect( inSize.x, inSize.y, outSize.x, outSize.y, false); Runnable onEndCrop = new Runnable() { public void run() { @@ -348,18 +330,14 @@ public class WallpaperCropActivity extends Activity { } } }; - BitmapCropTask cropTask = new BitmapCropTask(this, res, resId, + BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, resId, crop, rotation, outSize.x, outSize.y, true, false, onEndCrop); cropTask.execute(); } - private static boolean isScreenLarge(Resources res) { - Configuration config = res.getConfiguration(); - return config.smallestScreenWidthDp >= 720; - } - + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) protected void cropImageAndSetWallpaper(Uri uri, - OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) { + BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) { boolean centerCrop = getResources().getBoolean(R.bool.center_crop); // Get the crop boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; @@ -370,7 +348,7 @@ public class WallpaperCropActivity extends Activity { d.getSize(displaySize); boolean isPortrait = displaySize.x < displaySize.y; - Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(), + Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), getWindowManager()); // Get the crop RectF cropRect = mCropView.getCrop(); @@ -444,7 +422,7 @@ public class WallpaperCropActivity extends Activity { } } }; - BitmapCropTask cropTask = new BitmapCropTask(this, uri, + BitmapCropTask cropTask = new BitmapCropTask(getContext(), uri, cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop); if (onBitmapCroppedHandler != null) { cropTask.setOnBitmapCropped(onBitmapCroppedHandler); @@ -452,375 +430,9 @@ public class WallpaperCropActivity extends Activity { cropTask.execute(); } - public interface OnBitmapCroppedHandler { - public void onBitmapCropped(byte[] imageBytes); - } - - protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> { - Uri mInUri = null; - Context mContext; - String mInFilePath; - byte[] mInImageBytes; - int mInResId = 0; - RectF mCropBounds = null; - int mOutWidth, mOutHeight; - int mRotation; - String mOutputFormat = "jpg"; // for now - boolean mSetWallpaper; - boolean mSaveCroppedBitmap; - Bitmap mCroppedBitmap; - Runnable mOnEndRunnable; - Resources mResources; - OnBitmapCroppedHandler mOnBitmapCroppedHandler; - boolean mNoCrop; - - public BitmapCropTask(Context c, String filePath, - RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mContext = c; - mInFilePath = filePath; - init(cropBounds, rotation, - outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); - } - - public BitmapCropTask(byte[] imageBytes, - RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mInImageBytes = imageBytes; - init(cropBounds, rotation, - outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); - } - - public BitmapCropTask(Context c, Uri inUri, - RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mContext = c; - mInUri = inUri; - init(cropBounds, rotation, - outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); - } - - public BitmapCropTask(Context c, Resources res, int inResId, - RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mContext = c; - mInResId = inResId; - mResources = res; - init(cropBounds, rotation, - outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); - } - - private void init(RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mCropBounds = cropBounds; - mRotation = rotation; - mOutWidth = outWidth; - mOutHeight = outHeight; - mSetWallpaper = setWallpaper; - mSaveCroppedBitmap = saveCroppedBitmap; - mOnEndRunnable = onEndRunnable; - } - - public void setOnBitmapCropped(OnBitmapCroppedHandler handler) { - mOnBitmapCroppedHandler = handler; - } - - public void setNoCrop(boolean value) { - mNoCrop = value; - } - - public void setOnEndRunnable(Runnable onEndRunnable) { - mOnEndRunnable = onEndRunnable; - } - - // Helper to setup input stream - private InputStream regenerateInputStream() { - if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) { - Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + - "image byte array given"); - } else { - try { - if (mInUri != null) { - return new BufferedInputStream( - mContext.getContentResolver().openInputStream(mInUri)); - } else if (mInFilePath != null) { - return mContext.openFileInput(mInFilePath); - } else if (mInImageBytes != null) { - return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes)); - } else { - return new BufferedInputStream(mResources.openRawResource(mInResId)); - } - } catch (FileNotFoundException e) { - Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); - } - } - return null; - } - - public Point getImageBounds() { - InputStream is = regenerateInputStream(); - if (is != null) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(is, null, options); - Utils.closeSilently(is); - if (options.outWidth != 0 && options.outHeight != 0) { - return new Point(options.outWidth, options.outHeight); - } - } - return null; - } - - public void setCropBounds(RectF cropBounds) { - mCropBounds = cropBounds; - } - - public Bitmap getCroppedBitmap() { - return mCroppedBitmap; - } - public boolean cropBitmap() { - boolean failure = false; - - - WallpaperManager wallpaperManager = null; - if (mSetWallpaper) { - wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext()); - } - - - if (mSetWallpaper && mNoCrop) { - try { - InputStream is = regenerateInputStream(); - if (is != null) { - wallpaperManager.setStream(is); - Utils.closeSilently(is); - } - } catch (IOException e) { - Log.w(LOGTAG, "cannot write stream to wallpaper", e); - failure = true; - } - return !failure; - } else { - // Find crop bounds (scaled to original image size) - Rect roundedTrueCrop = new Rect(); - Matrix rotateMatrix = new Matrix(); - Matrix inverseRotateMatrix = new Matrix(); - - Point bounds = getImageBounds(); - if (mRotation > 0) { - rotateMatrix.setRotate(mRotation); - inverseRotateMatrix.setRotate(-mRotation); - - mCropBounds.roundOut(roundedTrueCrop); - mCropBounds = new RectF(roundedTrueCrop); - - if (bounds == null) { - Log.w(LOGTAG, "cannot get bounds for image"); - failure = true; - return false; - } - - float[] rotatedBounds = new float[] { bounds.x, bounds.y }; - rotateMatrix.mapPoints(rotatedBounds); - rotatedBounds[0] = Math.abs(rotatedBounds[0]); - rotatedBounds[1] = Math.abs(rotatedBounds[1]); - - mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2); - inverseRotateMatrix.mapRect(mCropBounds); - mCropBounds.offset(bounds.x/2, bounds.y/2); - - } - - mCropBounds.roundOut(roundedTrueCrop); - - if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { - Log.w(LOGTAG, "crop has bad values for full size image"); - failure = true; - return false; - } - - // See how much we're reducing the size of the image - int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth, - roundedTrueCrop.height() / mOutHeight)); - // Attempt to open a region decoder - BitmapRegionDecoder decoder = null; - InputStream is = null; - try { - is = regenerateInputStream(); - if (is == null) { - Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); - failure = true; - return false; - } - decoder = BitmapRegionDecoder.newInstance(is, false); - Utils.closeSilently(is); - } catch (IOException e) { - Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); - } finally { - Utils.closeSilently(is); - is = null; - } - - Bitmap crop = null; - if (decoder != null) { - // Do region decoding to get crop bitmap - BitmapFactory.Options options = new BitmapFactory.Options(); - if (scaleDownSampleSize > 1) { - options.inSampleSize = scaleDownSampleSize; - } - crop = decoder.decodeRegion(roundedTrueCrop, options); - decoder.recycle(); - } - - if (crop == null) { - // BitmapRegionDecoder has failed, try to crop in-memory - is = regenerateInputStream(); - Bitmap fullSize = null; - if (is != null) { - BitmapFactory.Options options = new BitmapFactory.Options(); - if (scaleDownSampleSize > 1) { - options.inSampleSize = scaleDownSampleSize; - } - fullSize = BitmapFactory.decodeStream(is, null, options); - Utils.closeSilently(is); - } - if (fullSize != null) { - // Find out the true sample size that was used by the decoder - scaleDownSampleSize = bounds.x / fullSize.getWidth(); - mCropBounds.left /= scaleDownSampleSize; - mCropBounds.top /= scaleDownSampleSize; - mCropBounds.bottom /= scaleDownSampleSize; - mCropBounds.right /= scaleDownSampleSize; - mCropBounds.roundOut(roundedTrueCrop); - - // Adjust values to account for issues related to rounding - if (roundedTrueCrop.width() > fullSize.getWidth()) { - // Adjust the width - roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth(); - } - if (roundedTrueCrop.right > fullSize.getWidth()) { - // Adjust the left value - int adjustment = roundedTrueCrop.left - - Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width()); - roundedTrueCrop.left -= adjustment; - roundedTrueCrop.right -= adjustment; - } - if (roundedTrueCrop.height() > fullSize.getHeight()) { - // Adjust the height - roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight(); - } - if (roundedTrueCrop.bottom > fullSize.getHeight()) { - // Adjust the top value - int adjustment = roundedTrueCrop.top - - Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height()); - roundedTrueCrop.top -= adjustment; - roundedTrueCrop.bottom -= adjustment; - } - - crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, - roundedTrueCrop.top, roundedTrueCrop.width(), - roundedTrueCrop.height()); - } - } - - if (crop == null) { - Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); - failure = true; - return false; - } - if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) { - float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() }; - rotateMatrix.mapPoints(dimsAfter); - dimsAfter[0] = Math.abs(dimsAfter[0]); - dimsAfter[1] = Math.abs(dimsAfter[1]); - - if (!(mOutWidth > 0 && mOutHeight > 0)) { - mOutWidth = Math.round(dimsAfter[0]); - mOutHeight = Math.round(dimsAfter[1]); - } - - RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]); - RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight); - - Matrix m = new Matrix(); - if (mRotation == 0) { - m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); - } else { - Matrix m1 = new Matrix(); - m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f); - Matrix m2 = new Matrix(); - m2.setRotate(mRotation); - Matrix m3 = new Matrix(); - m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f); - Matrix m4 = new Matrix(); - m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); - - Matrix c1 = new Matrix(); - c1.setConcat(m2, m1); - Matrix c2 = new Matrix(); - c2.setConcat(m4, m3); - m.setConcat(c2, c1); - } - - Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), - (int) returnRect.height(), Bitmap.Config.ARGB_8888); - if (tmp != null) { - Canvas c = new Canvas(tmp); - Paint p = new Paint(); - p.setFilterBitmap(true); - c.drawBitmap(crop, m, p); - crop = tmp; - } - } - - if (mSaveCroppedBitmap) { - mCroppedBitmap = crop; - } - - // Get output compression format - CompressFormat cf = - convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); - - // Compress to byte array - ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); - if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) { - // If we need to set to the wallpaper, set it - if (mSetWallpaper && wallpaperManager != null) { - try { - byte[] outByteArray = tmpOut.toByteArray(); - wallpaperManager.setStream(new ByteArrayInputStream(outByteArray)); - if (mOnBitmapCroppedHandler != null) { - mOnBitmapCroppedHandler.onBitmapCropped(outByteArray); - } - } catch (IOException e) { - Log.w(LOGTAG, "cannot write stream to wallpaper", e); - failure = true; - } - } - } else { - Log.w(LOGTAG, "cannot compress bitmap"); - failure = true; - } - } - return !failure; // True if any of the operations failed - } - - @Override - protected Boolean doInBackground(Void... params) { - return cropBitmap(); - } - - @Override - protected void onPostExecute(Boolean result) { - if (mOnEndRunnable != null) { - mOnEndRunnable.run(); - } - } - } - protected void updateWallpaperDimensions(int width, int height) { - String spKey = getSharedPreferencesKey(); - SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); + String spKey = LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; + SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); SharedPreferences.Editor editor = sp.edit(); if (width != 0 && height != 0) { editor.putInt(WALLPAPER_WIDTH_KEY, width); @@ -830,69 +442,21 @@ public class WallpaperCropActivity extends Activity { editor.remove(WALLPAPER_HEIGHT_KEY); } editor.commit(); - - suggestWallpaperDimension(getResources(), - sp, getWindowManager(), WallpaperManager.getInstance(this), true); - } - - static public void suggestWallpaperDimension(Resources res, - final SharedPreferences sharedPrefs, - WindowManager windowManager, - final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { - final Point defaultWallpaperSize = getDefaultWallpaperSize(res, windowManager); - // If we have saved a wallpaper width/height, use that instead - - int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); - int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1); - - if (savedWidth == -1 || savedHeight == -1) { - if (!fallBackToDefaults) { - return; - } else { - savedWidth = defaultWallpaperSize.x; - savedHeight = defaultWallpaperSize.y; - } - } - - if (savedWidth != wallpaperManager.getDesiredMinimumWidth() || - savedHeight != wallpaperManager.getDesiredMinimumHeight()) { - wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight); - } + WallpaperUtils.suggestWallpaperDimension(getResources(), + sp, getWindowManager(), WallpaperManager.getInstance(getContext()), true); } - protected static RectF getMaxCropRect( - int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) { - RectF cropRect = new RectF(); - // Get a crop rect that will fit this - if (inWidth / (float) inHeight > outWidth / (float) outHeight) { - cropRect.top = 0; - cropRect.bottom = inHeight; - cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2; - cropRect.right = inWidth - cropRect.left; - if (leftAligned) { - cropRect.right -= cropRect.left; - cropRect.left = 0; - } - } else { - cropRect.left = 0; - cropRect.right = inWidth; - cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2; - cropRect.bottom = inHeight - cropRect.top; - } - return cropRect; - } + static class LoadRequest { + BitmapSource src; + boolean touchEnabled; + boolean moveToLeft; + Runnable postExecute; + CropViewScaleProvider scaleProvider; - protected static CompressFormat convertExtensionToCompressFormat(String extension) { - return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; + TileSource result; } - protected static String getFileExtension(String requestFormat) { - String outputFormat = (requestFormat == null) - ? "jpg" - : requestFormat; - outputFormat = outputFormat.toLowerCase(); - return (outputFormat.equals("png") || outputFormat.equals("gif")) - ? "png" // We don't support gif compression. - : "jpg"; + interface CropViewScaleProvider { + float getScale(TileSource src); } } diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java index 09e096396..93320919e 100644 --- a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java +++ b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java @@ -20,8 +20,8 @@ import android.animation.LayoutTransition; import android.annotation.TargetApi; import android.app.ActionBar; import android.app.Activity; -import android.app.WallpaperInfo; import android.app.WallpaperManager; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -35,11 +35,9 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PorterDuff; -import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.LevelListDrawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -70,8 +68,14 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; +import com.android.gallery3d.common.BitmapCropTask; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.util.WallpaperUtils; import com.android.photos.BitmapRegionTileSource; import com.android.photos.BitmapRegionTileSource.BitmapSource; +import com.android.photos.views.TiledImageRenderer.TileSource; import java.io.File; import java.io.FileOutputStream; @@ -83,28 +87,25 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { public static final int IMAGE_PICK = 5; public static final int PICK_WALLPAPER_THIRD_PARTY_ACTIVITY = 6; - public static final int PICK_LIVE_WALLPAPER = 7; private static final String TEMP_WALLPAPER_TILES = "TEMP_WALLPAPER_TILES"; private static final String SELECTED_INDEX = "SELECTED_INDEX"; private static final int FLAG_POST_DELAY_MILLIS = 200; - private View mSelectedTile; - private boolean mIgnoreNextTap; - private OnClickListener mThumbnailOnClickListener; + @Thunk View mSelectedTile; + @Thunk boolean mIgnoreNextTap; + @Thunk OnClickListener mThumbnailOnClickListener; - private LinearLayout mWallpapersView; - private View mWallpaperStrip; + @Thunk LinearLayout mWallpapersView; + @Thunk HorizontalScrollView mWallpaperScrollContainer; - private ActionMode.Callback mActionModeCallback; - private ActionMode mActionMode; + @Thunk ActionMode.Callback mActionModeCallback; + @Thunk ActionMode mActionMode; - private View.OnLongClickListener mLongClickListener; + @Thunk View.OnLongClickListener mLongClickListener; ArrayList<Uri> mTempWallpaperTiles = new ArrayList<Uri>(); private SavedWallpaperImages mSavedImages; - private WallpaperInfo mLiveWallpaperInfoOnPickerLaunch; - private int mSelectedIndex = -1; - private WallpaperInfo mLastClickedLiveWallpaperInfo; + @Thunk int mSelectedIndex = -1; public static abstract class WallpaperTileInfo { protected View mView; @@ -136,45 +137,37 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { public static class UriWallpaperInfo extends WallpaperTileInfo { private Uri mUri; - private boolean mFirstClick = true; - private BitmapRegionTileSource.UriBitmapSource mBitmapSource; public UriWallpaperInfo(Uri uri) { mUri = uri; } @Override public void onClick(final WallpaperPickerActivity a) { - final Runnable onLoad; - if (!mFirstClick) { - onLoad = null; - } else { - mFirstClick = false; - a.mSetWallpaperButton.setEnabled(false); - onLoad = new Runnable() { - public void run() { - if (mBitmapSource != null && - mBitmapSource.getLoadingState() == BitmapSource.State.LOADED) { - a.selectTile(mView); - a.mSetWallpaperButton.setEnabled(true); - } else { - ViewGroup parent = (ViewGroup) mView.getParent(); - if (parent != null) { - parent.removeView(mView); - Toast.makeText(a, - a.getString(R.string.image_load_fail), - Toast.LENGTH_SHORT).show(); - } + a.setWallpaperButtonEnabled(false); + final BitmapRegionTileSource.UriBitmapSource bitmapSource = + new BitmapRegionTileSource.UriBitmapSource( + a.getContext(), mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE); + a.setCropViewTileSource(bitmapSource, true, false, null, new Runnable() { + + @Override + public void run() { + if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + a.selectTile(mView); + a.setWallpaperButtonEnabled(true); + } else { + ViewGroup parent = (ViewGroup) mView.getParent(); + if (parent != null) { + parent.removeView(mView); + Toast.makeText(a.getContext(), R.string.image_load_fail, + Toast.LENGTH_SHORT).show(); } } - }; - } - mBitmapSource = new BitmapRegionTileSource.UriBitmapSource( - a, mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE); - a.setCropViewTileSource(mBitmapSource, true, false, onLoad); + } + }); } @Override public void onSave(final WallpaperPickerActivity a) { boolean finishActivityWhenDone = true; - OnBitmapCroppedHandler h = new OnBitmapCroppedHandler() { + BitmapCropTask.OnBitmapCroppedHandler h = new BitmapCropTask.OnBitmapCroppedHandler() { public void onBitmapCropped(byte[] imageBytes) { Point thumbSize = getDefaultThumbnailSize(a.getResources()); // rotation is set to 0 since imageBytes has already been correctly rotated @@ -203,10 +196,18 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { mThumb = thumb; } @Override - public void onClick(WallpaperPickerActivity a) { + public void onClick(final WallpaperPickerActivity a) { + a.setWallpaperButtonEnabled(false); BitmapRegionTileSource.UriBitmapSource bitmapSource = - new BitmapRegionTileSource.UriBitmapSource(a, Uri.fromFile(mFile), 1024); - a.setCropViewTileSource(bitmapSource, false, true, null); + new BitmapRegionTileSource.UriBitmapSource(a.getContext(), + Uri.fromFile(mFile), 1024); + a.setCropViewTileSource(bitmapSource, false, true, null, new Runnable() { + + @Override + public void run() { + a.setWallpaperButtonEnabled(true); + } + }); } @Override public void onSave(WallpaperPickerActivity a) { @@ -232,22 +233,29 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { mThumb = thumb; } @Override - public void onClick(WallpaperPickerActivity a) { + public void onClick(final WallpaperPickerActivity a) { + a.setWallpaperButtonEnabled(false); BitmapRegionTileSource.ResourceBitmapSource bitmapSource = new BitmapRegionTileSource.ResourceBitmapSource( mResources, mResId, BitmapRegionTileSource.MAX_PREVIEW_SIZE); - bitmapSource.loadInBackground(); - BitmapRegionTileSource source = new BitmapRegionTileSource(a, bitmapSource); - CropView v = a.getCropView(); - v.setTileSource(source, null); - Point wallpaperSize = WallpaperCropActivity.getDefaultWallpaperSize( - a.getResources(), a.getWindowManager()); - RectF crop = WallpaperCropActivity.getMaxCropRect( - source.getImageWidth(), source.getImageHeight(), - wallpaperSize.x, wallpaperSize.y, false); - v.setScale(wallpaperSize.x / crop.width()); - v.setTouchEnabled(false); - a.setSystemWallpaperVisiblity(false); + a.setCropViewTileSource(bitmapSource, false, false, new CropViewScaleProvider() { + + @Override + public float getScale(TileSource src) { + Point wallpaperSize = WallpaperUtils.getDefaultWallpaperSize( + a.getResources(), a.getWindowManager()); + RectF crop = Utils.getMaxCropRect( + src.getImageWidth(), src.getImageHeight(), + wallpaperSize.x, wallpaperSize.y, false); + return wallpaperSize.x / crop.width(); + } + }, new Runnable() { + + @Override + public void run() { + a.setWallpaperButtonEnabled(true); + } + }); } @Override public void onSave(WallpaperPickerActivity a) { @@ -272,27 +280,33 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { @Override public void onClick(WallpaperPickerActivity a) { CropView c = a.getCropView(); - - Drawable defaultWallpaper = WallpaperManager.getInstance(a).getBuiltInDrawable( - c.getWidth(), c.getHeight(), false, 0.5f, 0.5f); - + Drawable defaultWallpaper = WallpaperManager.getInstance(a.getContext()) + .getBuiltInDrawable(c.getWidth(), c.getHeight(), false, 0.5f, 0.5f); if (defaultWallpaper == null) { Log.w(TAG, "Null default wallpaper encountered."); c.setTileSource(null, null); return; } - c.setTileSource( - new DrawableTileSource(a, defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE), null); - c.setScale(1f); - c.setTouchEnabled(false); - a.setSystemWallpaperVisiblity(false); + LoadRequest req = new LoadRequest(); + req.moveToLeft = false; + req.touchEnabled = false; + req.scaleProvider = new CropViewScaleProvider() { + + @Override + public float getScale(TileSource src) { + return 1f; + } + }; + req.result = new DrawableTileSource(a.getContext(), + defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE); + a.onLoadRequestComplete(req, true); } @Override public void onSave(WallpaperPickerActivity a) { try { - WallpaperManager.getInstance(a).clear(); - a.setResult(RESULT_OK); + WallpaperManager.getInstance(a.getContext()).clear(); + a.setResult(Activity.RESULT_OK); } catch (IOException e) { Log.w("Setting wallpaper to default threw exception", e); } @@ -308,10 +322,6 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } } - public void setWallpaperStripYOffset(float offset) { - mWallpaperStrip.setPadding(0, 0, 0, (int) offset); - } - /** * shows the system wallpaper behind the window and hides the {@link * #mCropView} if visible @@ -338,7 +348,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { }, FLAG_POST_DELAY_MILLIS); } - private void changeWallpaperFlags(boolean visible) { + @Thunk void changeWallpaperFlags(boolean visible) { int desiredWallpaperFlag = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0; int currentWallpaperFlag = getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; @@ -349,24 +359,11 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } @Override - public void setCropViewTileSource(BitmapSource bitmapSource, - boolean touchEnabled, - boolean moveToLeft, - final Runnable postExecute) { - // we also want to show our own wallpaper instead of the one in the background - Runnable showPostExecuteRunnable = new Runnable() { - @Override - public void run() { - if(postExecute != null) { - postExecute.run(); - } - setSystemWallpaperVisiblity(false); - } - }; - super.setCropViewTileSource(bitmapSource, - touchEnabled, - moveToLeft, - showPostExecuteRunnable); + protected void onLoadRequestComplete(LoadRequest req, boolean success) { + super.onLoadRequestComplete(req, success); + if (success) { + setSystemWallpaperVisiblity(false); + } } // called by onCreate; this is subclassed to overwrite WallpaperCropActivity @@ -376,7 +373,8 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { mCropView = (CropView) findViewById(R.id.cropView); mCropView.setVisibility(View.INVISIBLE); - mWallpaperStrip = findViewById(R.id.wallpaper_strip); + mProgressView = findViewById(R.id.loading); + mWallpaperScrollContainer = (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container); mCropView.setTouchCallback(new CropView.TouchCallback() { ViewPropertyAnimator mAnim; @Override @@ -384,15 +382,15 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { if (mAnim != null) { mAnim.cancel(); } - if (mWallpaperStrip.getAlpha() == 1f) { + if (mWallpaperScrollContainer.getAlpha() == 1f) { mIgnoreNextTap = true; } - mAnim = mWallpaperStrip.animate(); + mAnim = mWallpaperScrollContainer.animate(); mAnim.alpha(0f) .setDuration(150) .withEndAction(new Runnable() { public void run() { - mWallpaperStrip.setVisibility(View.INVISIBLE); + mWallpaperScrollContainer.setVisibility(View.INVISIBLE); } }); mAnim.setInterpolator(new AccelerateInterpolator(0.75f)); @@ -410,8 +408,8 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { if (mAnim != null) { mAnim.cancel(); } - mWallpaperStrip.setVisibility(View.VISIBLE); - mAnim = mWallpaperStrip.animate(); + mWallpaperScrollContainer.setVisibility(View.VISIBLE); + mAnim = mWallpaperScrollContainer.animate(); mAnim.alpha(1f) .setDuration(150) .setInterpolator(new DecelerateInterpolator(0.75f)); @@ -429,7 +427,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } return; } - mSetWallpaperButton.setEnabled(true); + setWallpaperButtonEnabled(true); WallpaperTileInfo info = (WallpaperTileInfo) v.getTag(); if (info.isSelectable() && v.getVisibility() == View.VISIBLE) { selectTile(v); @@ -460,18 +458,18 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { // Populate the built-in wallpapers ArrayList<WallpaperTileInfo> wallpapers = findBundledWallpapers(); mWallpapersView = (LinearLayout) findViewById(R.id.wallpaper_list); - SimpleWallpapersAdapter ia = new SimpleWallpapersAdapter(this, wallpapers); + SimpleWallpapersAdapter ia = new SimpleWallpapersAdapter(getContext(), wallpapers); populateWallpapersFromAdapter(mWallpapersView, ia, false); // Populate the saved wallpapers - mSavedImages = new SavedWallpaperImages(this); + mSavedImages = new SavedWallpaperImages(getContext()); mSavedImages.loadThumbnailsAndImageIdList(); populateWallpapersFromAdapter(mWallpapersView, mSavedImages, true); // Populate the live wallpapers final LinearLayout liveWallpapersView = (LinearLayout) findViewById(R.id.live_wallpaper_list); - final LiveWallpaperListAdapter a = new LiveWallpaperListAdapter(this); + final LiveWallpaperListAdapter a = new LiveWallpaperListAdapter(getContext()); a.registerDataSetObserver(new DataSetObserver() { public void onChanged() { liveWallpapersView.removeAllViews(); @@ -485,14 +483,13 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { final LinearLayout thirdPartyWallpapersView = (LinearLayout) findViewById(R.id.third_party_wallpaper_list); final ThirdPartyWallpaperPickerListAdapter ta = - new ThirdPartyWallpaperPickerListAdapter(this); + new ThirdPartyWallpaperPickerListAdapter(getContext()); populateWallpapersFromAdapter(thirdPartyWallpapersView, ta, false); // Add a tile for the Gallery LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list); FrameLayout pickImageTile = (FrameLayout) getLayoutInflater(). inflate(R.layout.wallpaper_picker_image_picker_item, masterWallpaperList, false); - setWallpaperItemPaddingToZero(pickImageTile); masterWallpaperList.addView(pickImageTile, 0); // Make its background the last photo taken on external storage @@ -500,10 +497,9 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { if (lastPhoto != null) { ImageView galleryThumbnailBg = (ImageView) pickImageTile.findViewById(R.id.wallpaper_image); - galleryThumbnailBg.setImageBitmap(getThumbnailOfLastPhoto()); + galleryThumbnailBg.setImageBitmap(lastPhoto); int colorOverlay = getResources().getColor(R.color.wallpaper_picker_translucent_gray); galleryThumbnailBg.setColorFilter(colorOverlay, PorterDuff.Mode.SRC_ATOP); - } PickImageInfo pickImageInfo = new PickImageInfo(); @@ -650,7 +646,11 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { }; } - private void selectTile(View v) { + public void setWallpaperButtonEnabled(boolean enabled) { + mSetWallpaperButton.setEnabled(enabled); + } + + @Thunk void selectTile(View v) { if (mSelectedTile != null) { mSelectedTile.setSelected(false); mSelectedTile = null; @@ -661,28 +661,25 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { // TODO: Remove this once the accessibility framework and // services have better support for selection state. v.announceForAccessibility( - getString(R.string.announce_selection, v.getContentDescription())); + getContext().getString(R.string.announce_selection, v.getContentDescription())); } - private void initializeScrollForRtl() { - final HorizontalScrollView scroll = - (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container); - - if (scroll.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { - final ViewTreeObserver observer = scroll.getViewTreeObserver(); + @Thunk void initializeScrollForRtl() { + if (mWallpaperScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + final ViewTreeObserver observer = mWallpaperScrollContainer.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { public void onGlobalLayout() { LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list); - scroll.scrollTo(masterWallpaperList.getWidth(), 0); - scroll.getViewTreeObserver().removeOnGlobalLayoutListener(this); + mWallpaperScrollContainer.scrollTo(masterWallpaperList.getWidth(), 0); + mWallpaperScrollContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); } } protected Bitmap getThumbnailOfLastPhoto() { - Cursor cursor = MediaStore.Images.Media.query(getContentResolver(), + Cursor cursor = MediaStore.Images.Media.query(getContext().getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[] { MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DATE_TAKEN}, @@ -692,7 +689,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { if (cursor != null) { if (cursor.moveToNext()) { int id = cursor.getInt(0); - thumb = MediaStore.Images.Thumbnails.getThumbnail(getContentResolver(), + thumb = MediaStore.Images.Thumbnails.getThumbnail(getContext().getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null); } cursor.close(); @@ -700,16 +697,16 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { return thumb; } - protected void onStop() { + public void onStop() { super.onStop(); - mWallpaperStrip = findViewById(R.id.wallpaper_strip); - if (mWallpaperStrip.getAlpha() < 1f) { - mWallpaperStrip.setAlpha(1f); - mWallpaperStrip.setVisibility(View.VISIBLE); + mWallpaperScrollContainer = (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container); + if (mWallpaperScrollContainer.getAlpha() < 1f) { + mWallpaperScrollContainer.setAlpha(1f); + mWallpaperScrollContainer.setVisibility(View.VISIBLE); } } - protected void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(Bundle outState) { outState.putParcelableArrayList(TEMP_WALLPAPER_TILES, mTempWallpaperTiles); outState.putInt(SELECTED_INDEX, mSelectedIndex); } @@ -722,7 +719,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { mSelectedIndex = savedInstanceState.getInt(SELECTED_INDEX, -1); } - private void populateWallpapersFromAdapter(ViewGroup parent, BaseAdapter adapter, + @Thunk void populateWallpapersFromAdapter(ViewGroup parent, BaseAdapter adapter, boolean addLongPressHandler) { for (int i = 0; i < adapter.getCount(); i++) { FrameLayout thumbnail = (FrameLayout) adapter.getView(i, null, parent); @@ -737,7 +734,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } } - private void updateTileIndices() { + @Thunk void updateTileIndices() { LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list); final int childCount = masterWallpaperList.getChildCount(); final Resources res = getResources(); @@ -778,13 +775,13 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } } - private static Point getDefaultThumbnailSize(Resources res) { + @Thunk static Point getDefaultThumbnailSize(Resources res) { return new Point(res.getDimensionPixelSize(R.dimen.wallpaperThumbnailWidth), res.getDimensionPixelSize(R.dimen.wallpaperThumbnailHeight)); } - private static Bitmap createThumbnail(Point size, Context context, Uri uri, byte[] imageBytes, + @Thunk static Bitmap createThumbnail(Point size, Context context, Uri uri, byte[] imageBytes, Resources res, int resId, int rotation, boolean leftAligned) { int width = size.x; int height = size.y; @@ -812,7 +809,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { rotatedBounds[0] = Math.abs(rotatedBounds[0]); rotatedBounds[1] = Math.abs(rotatedBounds[1]); - RectF cropRect = WallpaperCropActivity.getMaxCropRect( + RectF cropRect = Utils.getMaxCropRect( (int) rotatedBounds[0], (int) rotatedBounds[1], width, height, leftAligned); cropTask.setCropBounds(cropRect); @@ -829,17 +826,16 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { final FrameLayout pickedImageThumbnail = (FrameLayout) getLayoutInflater(). inflate(R.layout.wallpaper_picker_item, mWallpapersView, false); pickedImageThumbnail.setVisibility(View.GONE); - setWallpaperItemPaddingToZero(pickedImageThumbnail); mWallpapersView.addView(pickedImageThumbnail, 0); // Load the thumbnail final ImageView image = (ImageView) pickedImageThumbnail.findViewById(R.id.wallpaper_image); final Point defaultSize = getDefaultThumbnailSize(this.getResources()); - final Context context = this; + final Context context = getContext(); new AsyncTask<Void, Bitmap, Bitmap>() { protected Bitmap doInBackground(Void...args) { try { - int rotation = WallpaperCropActivity.getRotationFromExif(context, uri); + int rotation = BitmapUtils.getRotationFromExif(context, uri); return createThumbnail(defaultSize, context, uri, null, null, 0, rotation, false); } catch (SecurityException securityException) { if (isDestroyed()) { @@ -879,45 +875,26 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } } - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == IMAGE_PICK && resultCode == RESULT_OK) { + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == IMAGE_PICK && resultCode == Activity.RESULT_OK) { if (data != null && data.getData() != null) { Uri uri = data.getData(); addTemporaryWallpaperTile(uri, false); } - } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY) { - setResult(RESULT_OK); + } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY + && resultCode == Activity.RESULT_OK) { + // Something was set on the third-party activity. + setResult(Activity.RESULT_OK); finish(); - } else if (requestCode == PICK_LIVE_WALLPAPER) { - WallpaperManager wm = WallpaperManager.getInstance(this); - final WallpaperInfo oldLiveWallpaper = mLiveWallpaperInfoOnPickerLaunch; - final WallpaperInfo clickedWallpaper = mLastClickedLiveWallpaperInfo; - WallpaperInfo newLiveWallpaper = wm.getWallpaperInfo(); - // Try to figure out if a live wallpaper was set; - if (newLiveWallpaper != null && - (oldLiveWallpaper == null - || !oldLiveWallpaper.getComponent() - .equals(newLiveWallpaper.getComponent()) - || clickedWallpaper.getComponent() - .equals(oldLiveWallpaper.getComponent()))) { - // Return if a live wallpaper was set - setResult(RESULT_OK); - finish(); - } } } - static void setWallpaperItemPaddingToZero(FrameLayout frameLayout) { - frameLayout.setPadding(0, 0, 0, 0); - frameLayout.setForeground(new ZeroPaddingDrawable(frameLayout.getForeground())); - } - private void addLongPressHandler(View v) { v.setOnLongClickListener(mLongClickListener); } private ArrayList<WallpaperTileInfo> findBundledWallpapers() { - final PackageManager pm = getPackageManager(); + final PackageManager pm = getContext().getPackageManager(); final ArrayList<WallpaperTileInfo> bundled = new ArrayList<WallpaperTileInfo>(24); Partner partner = Partner.get(pm); @@ -961,7 +938,8 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { Pair<ApplicationInfo, Integer> r = getWallpaperArrayResourceId(); if (r != null) { try { - Resources wallpaperRes = getPackageManager().getResourcesForApplication(r.first); + Resources wallpaperRes = getContext().getPackageManager() + .getResourcesForApplication(r.first); addWallpapers(bundled, wallpaperRes, r.first.packageName, r.second); } catch (PackageManager.NameNotFoundException e) { } @@ -984,7 +962,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { try { f.createNewFile(); FileOutputStream thumbFileStream = - openFileOutput(f.getName(), Context.MODE_PRIVATE); + getContext().openFileOutput(f.getName(), Context.MODE_PRIVATE); b.compress(Bitmap.CompressFormat.JPEG, 95, thumbFileStream); thumbFileStream.close(); return true; @@ -996,17 +974,18 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } private File getDefaultThumbFile() { - return new File(getFilesDir(), Build.VERSION.SDK_INT + return new File(getContext().getFilesDir(), Build.VERSION.SDK_INT + "_" + LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL); } private boolean saveDefaultWallpaperThumb(Bitmap b) { // Delete old thumbnails. - new File(getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL_OLD).delete(); - new File(getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); + new File(getContext().getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL_OLD).delete(); + new File(getContext().getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); for (int i = Build.VERSION_CODES.JELLY_BEAN; i < Build.VERSION.SDK_INT; i++) { - new File(getFilesDir(), i + "_" + LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); + new File(getContext().getFilesDir(), i + "_" + + LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); } return writeImageToFileAsJpeg(getDefaultThumbFile(), b); } @@ -1024,9 +1003,9 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } else { Resources res = getResources(); Point defaultThumbSize = getDefaultThumbnailSize(res); - int rotation = WallpaperCropActivity.getRotationFromExif(res, resId); + int rotation = BitmapUtils.getRotationFromExif(res, resId); thumb = createThumbnail( - defaultThumbSize, this, null, null, sysRes, resId, rotation, false); + defaultThumbSize, getContext(), null, null, sysRes, resId, rotation, false); if (thumb != null) { defaultWallpaperExists = saveDefaultWallpaperThumb(thumb); } @@ -1048,7 +1027,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } else { Resources res = getResources(); Point defaultThumbSize = getDefaultThumbnailSize(res); - Drawable wallpaperDrawable = WallpaperManager.getInstance(this).getBuiltInDrawable( + Drawable wallpaperDrawable = WallpaperManager.getInstance(getContext()).getBuiltInDrawable( defaultThumbSize.x, defaultThumbSize.y, true, 0.5f, 0.5f); if (wallpaperDrawable != null) { thumb = Bitmap.createBitmap( @@ -1075,7 +1054,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { // package name should be. final String packageName = getResources().getResourcePackageName(R.array.wallpapers); try { - ApplicationInfo info = getPackageManager().getApplicationInfo(packageName, 0); + ApplicationInfo info = getContext().getPackageManager().getApplicationInfo(packageName, 0); return new Pair<ApplicationInfo, Integer>(info, R.array.wallpapers); } catch (PackageManager.NameNotFoundException e) { return null; @@ -1110,31 +1089,12 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { return mSavedImages; } - public void onLiveWallpaperPickerLaunch(WallpaperInfo info) { - mLastClickedLiveWallpaperInfo = info; - mLiveWallpaperInfoOnPickerLaunch = WallpaperManager.getInstance(this).getWallpaperInfo(); - } - - static class ZeroPaddingDrawable extends LevelListDrawable { - public ZeroPaddingDrawable(Drawable d) { - super(); - addLevel(0, 0, d); - setLevel(0); - } - - @Override - public boolean getPadding(Rect padding) { - padding.set(0, 0, 0, 0); - return true; - } - } - private static class SimpleWallpapersAdapter extends ArrayAdapter<WallpaperTileInfo> { private final LayoutInflater mLayoutInflater; - SimpleWallpapersAdapter(Activity activity, ArrayList<WallpaperTileInfo> wallpapers) { - super(activity, R.layout.wallpaper_picker_item, wallpapers); - mLayoutInflater = activity.getLayoutInflater(); + SimpleWallpapersAdapter(Context context, ArrayList<WallpaperTileInfo> wallpapers) { + super(context, R.layout.wallpaper_picker_item, wallpapers); + mLayoutInflater = LayoutInflater.from(context); } public View getView(int position, View convertView, ViewGroup parent) { @@ -1156,8 +1116,6 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { view = convertView; } - setWallpaperItemPaddingToZero((FrameLayout) view); - ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); if (thumb != null) { @@ -1168,9 +1126,12 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { return view; } - // In Launcher3, we override this with a method that catches exceptions - // from starting activities; didn't want to copy and paste code into here public void startActivityForResultSafely(Intent intent, int requestCode) { - startActivityForResult(intent, requestCode); + Utilities.startActivityForResultSafely(getActivity(), intent, requestCode); + } + + @Override + public boolean enableRotation() { + return Utilities.isRotationEnabled(getContext()); } } diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java b/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java deleted file mode 100644 index ceaa043a7..000000000 --- a/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.widget.RelativeLayout; - -public class WallpaperRootView extends RelativeLayout { - private final WallpaperPickerActivity a; - public WallpaperRootView(Context context, AttributeSet attrs) { - super(context, attrs); - a = (WallpaperPickerActivity) context; - } - public WallpaperRootView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - a = (WallpaperPickerActivity) context; - } - - protected boolean fitSystemWindows(Rect insets) { - a.setWallpaperStripYOffset(insets.bottom); - return true; - } -} diff --git a/WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java b/WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java new file mode 100644 index 000000000..f8541188f --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java @@ -0,0 +1,21 @@ +package com.android.launcher3.base; + +import android.app.Activity; +import android.content.Context; + +/** + * A wrapper over {@link Activity} which allows to override some methods. + * The base implementation can change from an Activity to a Fragment (or any other custom + * implementation), Callers should not assume that the base class extends Context, instead use + * either {@link #getContext} or {@link #getActivity} + */ +public class BaseActivity extends Activity { + + public Context getContext() { + return this; + } + + public Activity getActivity() { + return this; + } +} diff --git a/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java b/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java index 66ece4ff6..15f97e5b1 100644 --- a/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java +++ b/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java @@ -20,7 +20,6 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Canvas; @@ -28,7 +27,6 @@ import android.graphics.Paint; import android.graphics.Rect; import android.net.Uri; import android.os.Build; -import android.os.Build.VERSION_CODES; import android.util.Log; import com.android.gallery3d.common.BitmapUtils; @@ -148,8 +146,6 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { private static final String TAG = "BitmapRegionTileSource"; - private static final boolean REUSE_BITMAP = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; private static final int GL_SIZE_LIMIT = 2048; // This must be no larger than half the size of the GL_SIZE_LIMIT // due to decodePreview being allowed to be up to 2x the size of the target @@ -158,14 +154,14 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { public static abstract class BitmapSource { private SimpleBitmapRegionDecoder mDecoder; private Bitmap mPreview; - private int mPreviewSize; + private final int mPreviewSize; private int mRotation; public enum State { NOT_LOADED, LOADED, ERROR_LOADING }; private State mState = State.NOT_LOADED; public BitmapSource(int previewSize) { - mPreviewSize = previewSize; + mPreviewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); } - public boolean loadInBackground() { + public boolean loadInBackground(InBitmapProvider bitmapProvider) { ExifInterface ei = new ExifInterface(); if (readExif(ei)) { Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); @@ -181,15 +177,33 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { int width = mDecoder.getWidth(); int height = mDecoder.getHeight(); if (mPreviewSize != 0) { - int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE); BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inPreferredConfig = Bitmap.Config.ARGB_8888; opts.inPreferQualityOverSpeed = true; - float scale = (float) previewSize / Math.max(width, height); + float scale = (float) mPreviewSize / Math.max(width, height); opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); opts.inJustDecodeBounds = false; - mPreview = loadPreviewBitmap(opts); + opts.inMutable = true; + + if (bitmapProvider != null) { + int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize); + Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles); + if (reusableBitmap != null) { + // Try loading with reusable bitmap + opts.inBitmap = reusableBitmap; + try { + mPreview = loadPreviewBitmap(opts); + } catch (IllegalArgumentException e) { + Log.d(TAG, "Unable to reusage bitmap", e); + opts.inBitmap = null; + mPreview = null; + } + } + } + if (mPreview == null) { + mPreview = loadPreviewBitmap(opts); + } } mState = State.LOADED; return true; @@ -208,10 +222,6 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { return mPreview; } - public int getPreviewSize() { - return mPreviewSize; - } - public int getRotation() { return mRotation; } @@ -219,6 +229,10 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { public abstract boolean readExif(ExifInterface ei); public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); + + public interface InBitmapProvider { + Bitmap forPixelCount(int count); + } } public static class FilePathBitmapSource extends BitmapSource { @@ -306,13 +320,13 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { Utils.closeSilently(is); return true; } catch (FileNotFoundException e) { - Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); + Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e); return false; } catch (IOException e) { - Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); + Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e); return false; } catch (NullPointerException e) { - Log.e("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e); + Log.d("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e); return false; } finally { Utils.closeSilently(is); @@ -372,11 +386,9 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { // For use only by getTile private Rect mWantRegion = new Rect(); - private Rect mOverlapRegion = new Rect(); private BitmapFactory.Options mOptions; - private Canvas mCanvas; - public BitmapRegionTileSource(Context context, BitmapSource source) { + public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) { mTileSize = TiledImageRenderer.suggestedTileSize(context); mRotation = source.getRotation(); mDecoder = source.getBitmapRegionDecoder(); @@ -386,27 +398,26 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { mOptions = new BitmapFactory.Options(); mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; mOptions.inPreferQualityOverSpeed = true; - mOptions.inTempStorage = new byte[16 * 1024]; - int previewSize = source.getPreviewSize(); - if (previewSize != 0) { - previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); - // Although this is the same size as the Bitmap that is likely already - // loaded, the lifecycle is different and interactions are on a different - // thread. Thus to simplify, this source will decode its own bitmap. - Bitmap preview = decodePreview(source, previewSize); - if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { + mOptions.inTempStorage = tempStorage; + + Bitmap preview = source.getPreviewBitmap(); + if (preview != null && + preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { mPreview = new BitmapTexture(preview); - } else { - Log.w(TAG, String.format( - "Failed to create preview of apropriate size! " - + " in: %dx%d, out: %dx%d", - mWidth, mHeight, - preview.getWidth(), preview.getHeight())); - } + } else { + Log.w(TAG, String.format( + "Failed to create preview of apropriate size! " + + " in: %dx%d, out: %dx%d", + mWidth, mHeight, + preview.getWidth(), preview.getHeight())); } } } + public Bitmap getBitmap() { + return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null; + } + @Override public int getTileSize() { return mTileSize; @@ -435,10 +446,6 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { @Override public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { int tileSize = getTileSize(); - if (!REUSE_BITMAP) { - return getTileWithoutReusingBitmap(level, x, y, tileSize); - } - int t = tileSize << level; mWantRegion.set(x, y, x + t, y + t); @@ -462,64 +469,4 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { } return bitmap; } - - private Bitmap getTileWithoutReusingBitmap( - int level, int x, int y, int tileSize) { - - int t = tileSize << level; - mWantRegion.set(x, y, x + t, y + t); - - mOverlapRegion.set(0, 0, mWidth, mHeight); - - mOptions.inSampleSize = (1 << level); - Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions); - - if (bitmap == null) { - Log.w(TAG, "fail in decoding region"); - } - - if (mWantRegion.equals(mOverlapRegion)) { - return bitmap; - } - - Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); - if (mCanvas == null) { - mCanvas = new Canvas(); - } - mCanvas.setBitmap(result); - mCanvas.drawBitmap(bitmap, - (mOverlapRegion.left - mWantRegion.left) >> level, - (mOverlapRegion.top - mWantRegion.top) >> level, null); - mCanvas.setBitmap(null); - return result; - } - - /** - * Note that the returned bitmap may have a long edge that's longer - * than the targetSize, but it will always be less than 2x the targetSize - */ - private Bitmap decodePreview(BitmapSource source, int targetSize) { - Bitmap result = source.getPreviewBitmap(); - if (result == null) { - return null; - } - - // We need to resize down if the decoder does not support inSampleSize - // or didn't support the specified inSampleSize (some decoders only do powers of 2) - float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight())); - - if (scale <= 0.5) { - result = BitmapUtils.resizeBitmapByScale(result, scale, true); - } - return ensureGLCompatibleBitmap(result); - } - - private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { - if (bitmap == null || bitmap.getConfig() != null) { - return bitmap; - } - Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); - bitmap.recycle(); - return newBitmap; - } } diff --git a/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java b/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java deleted file mode 100644 index 8a0505185..000000000 --- a/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java +++ /dev/null @@ -1,438 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.photos.views; - -import android.content.Context; -import android.graphics.SurfaceTexture; -import android.opengl.GLSurfaceView.Renderer; -import android.opengl.GLUtils; -import android.util.Log; -import android.view.TextureView; -import android.view.TextureView.SurfaceTextureListener; - -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; -import javax.microedition.khronos.egl.EGLSurface; -import javax.microedition.khronos.opengles.GL10; - -/** - * A TextureView that supports blocking rendering for synchronous drawing - */ -public class BlockingGLTextureView extends TextureView - implements SurfaceTextureListener { - - private RenderThread mRenderThread; - - public BlockingGLTextureView(Context context) { - super(context); - setSurfaceTextureListener(this); - } - - public void setRenderer(Renderer renderer) { - if (mRenderThread != null) { - throw new IllegalArgumentException("Renderer already set"); - } - mRenderThread = new RenderThread(renderer); - } - - public void render() { - mRenderThread.render(); - } - - public void destroy() { - if (mRenderThread != null) { - mRenderThread.finish(); - mRenderThread = null; - } - } - - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, - int height) { - mRenderThread.setSurface(surface); - mRenderThread.setSize(width, height); - } - - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, - int height) { - mRenderThread.setSize(width, height); - } - - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - if (mRenderThread != null) { - mRenderThread.setSurface(null); - } - return false; - } - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) { - } - - @Override - protected void finalize() throws Throwable { - try { - destroy(); - } catch (Throwable t) { - // Ignore - } - super.finalize(); - } - - /** - * An EGL helper class. - */ - - private static class EglHelper { - private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; - private static final int EGL_OPENGL_ES2_BIT = 4; - - EGL10 mEgl; - EGLDisplay mEglDisplay; - EGLSurface mEglSurface; - EGLConfig mEglConfig; - EGLContext mEglContext; - - private EGLConfig chooseEglConfig() { - int[] configsCount = new int[1]; - EGLConfig[] configs = new EGLConfig[1]; - int[] configSpec = getConfig(); - if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) { - throw new IllegalArgumentException("eglChooseConfig failed " + - GLUtils.getEGLErrorString(mEgl.eglGetError())); - } else if (configsCount[0] > 0) { - return configs[0]; - } - return null; - } - - private static int[] getConfig() { - return new int[] { - EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL10.EGL_RED_SIZE, 8, - EGL10.EGL_GREEN_SIZE, 8, - EGL10.EGL_BLUE_SIZE, 8, - EGL10.EGL_ALPHA_SIZE, 8, - EGL10.EGL_DEPTH_SIZE, 0, - EGL10.EGL_STENCIL_SIZE, 0, - EGL10.EGL_NONE - }; - } - - EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { - int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; - return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList); - } - - /** - * Initialize EGL for a given configuration spec. - */ - public void start() { - /* - * Get an EGL instance - */ - mEgl = (EGL10) EGLContext.getEGL(); - - /* - * Get to the default display. - */ - mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); - - if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { - throw new RuntimeException("eglGetDisplay failed"); - } - - /* - * We can now initialize EGL for that display - */ - int[] version = new int[2]; - if (!mEgl.eglInitialize(mEglDisplay, version)) { - throw new RuntimeException("eglInitialize failed"); - } - mEglConfig = chooseEglConfig(); - - /* - * Create an EGL context. We want to do this as rarely as we can, because an - * EGL context is a somewhat heavy object. - */ - mEglContext = createContext(mEgl, mEglDisplay, mEglConfig); - - if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { - mEglContext = null; - throwEglException("createContext"); - } - - mEglSurface = null; - } - - /** - * Create an egl surface for the current SurfaceTexture surface. If a surface - * already exists, destroy it before creating the new surface. - * - * @return true if the surface was created successfully. - */ - public boolean createSurface(SurfaceTexture surface) { - /* - * Check preconditions. - */ - if (mEgl == null) { - throw new RuntimeException("egl not initialized"); - } - if (mEglDisplay == null) { - throw new RuntimeException("eglDisplay not initialized"); - } - if (mEglConfig == null) { - throw new RuntimeException("mEglConfig not initialized"); - } - - /* - * The window size has changed, so we need to create a new - * surface. - */ - destroySurfaceImp(); - - /* - * Create an EGL surface we can render into. - */ - if (surface != null) { - mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, null); - } else { - mEglSurface = null; - } - - if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { - int error = mEgl.eglGetError(); - if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { - Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); - } - return false; - } - - /* - * Before we can issue GL commands, we need to make sure - * the context is current and bound to a surface. - */ - if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { - /* - * Could not make the context current, probably because the underlying - * SurfaceView surface has been destroyed. - */ - logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); - return false; - } - - return true; - } - - /** - * Create a GL object for the current EGL context. - */ - public GL10 createGL() { - return (GL10) mEglContext.getGL(); - } - - /** - * Display the current render surface. - * @return the EGL error code from eglSwapBuffers. - */ - public int swap() { - if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { - return mEgl.eglGetError(); - } - return EGL10.EGL_SUCCESS; - } - - public void destroySurface() { - destroySurfaceImp(); - } - - private void destroySurfaceImp() { - if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { - mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, - EGL10.EGL_NO_SURFACE, - EGL10.EGL_NO_CONTEXT); - mEgl.eglDestroySurface(mEglDisplay, mEglSurface); - mEglSurface = null; - } - } - - public void finish() { - if (mEglContext != null) { - mEgl.eglDestroyContext(mEglDisplay, mEglContext); - mEglContext = null; - } - if (mEglDisplay != null) { - mEgl.eglTerminate(mEglDisplay); - mEglDisplay = null; - } - } - - private void throwEglException(String function) { - throwEglException(function, mEgl.eglGetError()); - } - - public static void throwEglException(String function, int error) { - String message = formatEglError(function, error); - throw new RuntimeException(message); - } - - public static void logEglErrorAsWarning(String tag, String function, int error) { - Log.w(tag, formatEglError(function, error)); - } - - public static String formatEglError(String function, int error) { - return function + " failed: " + error; - } - - } - - private static class RenderThread extends Thread { - private static final int INVALID = -1; - private static final int RENDER = 1; - private static final int CHANGE_SURFACE = 2; - private static final int RESIZE_SURFACE = 3; - private static final int FINISH = 4; - - private EglHelper mEglHelper = new EglHelper(); - - private Object mLock = new Object(); - private int mExecMsgId = INVALID; - private SurfaceTexture mSurface; - private Renderer mRenderer; - private int mWidth, mHeight; - - private boolean mFinished = false; - private GL10 mGL; - - public RenderThread(Renderer renderer) { - super("RenderThread"); - mRenderer = renderer; - start(); - } - - private void checkRenderer() { - if (mRenderer == null) { - throw new IllegalArgumentException("Renderer is null!"); - } - } - - private void checkSurface() { - if (mSurface == null) { - throw new IllegalArgumentException("surface is null!"); - } - } - - public void setSurface(SurfaceTexture surface) { - // If the surface is null we're being torn down, don't need a - // renderer then - if (surface != null) { - checkRenderer(); - } - mSurface = surface; - exec(CHANGE_SURFACE); - } - - public void setSize(int width, int height) { - checkRenderer(); - checkSurface(); - mWidth = width; - mHeight = height; - exec(RESIZE_SURFACE); - } - - public void render() { - checkRenderer(); - if (mSurface != null) { - exec(RENDER); - mSurface.updateTexImage(); - } - } - - public void finish() { - mSurface = null; - exec(FINISH); - try { - join(); - } catch (InterruptedException e) { - // Ignore - } - } - - private void exec(int msgid) { - synchronized (mLock) { - if (mExecMsgId != INVALID) { - throw new IllegalArgumentException( - "Message already set - multithreaded access?"); - } - mExecMsgId = msgid; - mLock.notify(); - try { - mLock.wait(); - } catch (InterruptedException e) { - // Ignore - } - } - } - - private void handleMessageLocked(int what) { - switch (what) { - case CHANGE_SURFACE: - if (mEglHelper.createSurface(mSurface)) { - mGL = mEglHelper.createGL(); - mRenderer.onSurfaceCreated(mGL, mEglHelper.mEglConfig); - } - break; - case RESIZE_SURFACE: - mRenderer.onSurfaceChanged(mGL, mWidth, mHeight); - break; - case RENDER: - mRenderer.onDrawFrame(mGL); - mEglHelper.swap(); - break; - case FINISH: - mEglHelper.destroySurface(); - mEglHelper.finish(); - mFinished = true; - break; - } - } - - @Override - public void run() { - synchronized (mLock) { - mEglHelper.start(); - while (!mFinished) { - while (mExecMsgId == INVALID) { - try { - mLock.wait(); - } catch (InterruptedException e) { - // Ignore - } - } - handleMessageLocked(mExecMsgId); - mExecMsgId = INVALID; - mLock.notify(); - } - mExecMsgId = FINISH; - } - } - } -} diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java index c4e493b34..e57ce70b9 100644 --- a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java +++ b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java @@ -20,11 +20,11 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.RectF; -import android.support.v4.util.LongSparseArray; +import android.support.v4.util.Pools.Pool; +import android.support.v4.util.Pools.SynchronizedPool; import android.util.DisplayMetrics; import android.util.Log; -import android.util.Pools.Pool; -import android.util.Pools.SynchronizedPool; +import android.util.LongSparseArray; import android.view.View; import android.view.WindowManager; @@ -32,6 +32,7 @@ import com.android.gallery3d.common.Utils; import com.android.gallery3d.glrenderer.BasicTexture; import com.android.gallery3d.glrenderer.GLCanvas; import com.android.gallery3d.glrenderer.UploadedTexture; +import com.android.launcher3.util.Thunk; /** * Handles laying out, decoding, and drawing of tiles in GL @@ -67,12 +68,12 @@ public class TiledImageRenderer { private static final int STATE_RECYCLING = 0x20; private static final int STATE_RECYCLED = 0x40; - private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64); + @Thunk static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64); // TILE_SIZE must be 2^N - private int mTileSize; + @Thunk int mTileSize; - private TileSource mModel; + @Thunk TileSource mModel; private BasicTexture mPreview; protected int mLevelCount; // cache the value of mScaledBitmaps.length @@ -82,7 +83,7 @@ public class TiledImageRenderer { // half size of the previous one). If the value is in [0, mLevelCount), we // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value // is mLevelCount - private int mLevel = 0; + @Thunk int mLevel = 0; private int mOffsetX; private int mOffsetY; @@ -96,10 +97,10 @@ public class TiledImageRenderer { private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>(); // The following three queue are guarded by mQueueLock - private final Object mQueueLock = new Object(); + @Thunk final Object mQueueLock = new Object(); private final TileQueue mRecycledQueue = new TileQueue(); private final TileQueue mUploadQueue = new TileQueue(); - private final TileQueue mDecodeQueue = new TileQueue(); + @Thunk final TileQueue mDecodeQueue = new TileQueue(); // The width and height of the full-sized bitmap protected int mImageWidth = SIZE_UNKNOWN; @@ -489,7 +490,7 @@ public class TiledImageRenderer { } } - private void decodeTile(Tile tile) { + @Thunk void decodeTile(Tile tile) { synchronized (mQueueLock) { if (tile.mTileState != STATE_IN_QUEUE) { return; @@ -556,7 +557,7 @@ public class TiledImageRenderer { mActiveTiles.put(key, tile); } - private Tile getTile(int x, int y, int level) { + @Thunk Tile getTile(int x, int y, int level) { return mActiveTiles.get(makeTileKey(x, y, level)); } @@ -748,7 +749,7 @@ public class TiledImageRenderer { } } - private static class TileQueue { + @Thunk static class TileQueue { private Tile mHead; public Tile pop() { @@ -786,7 +787,7 @@ public class TiledImageRenderer { } } - private class TileDecoder extends Thread { + @Thunk class TileDecoder extends Thread { public void finishAndWait() { interrupt(); diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageView.java b/WallpaperPicker/src/com/android/photos/views/TiledImageView.java index 94063b027..7e3e1a936 100644 --- a/WallpaperPicker/src/com/android/photos/views/TiledImageView.java +++ b/WallpaperPicker/src/com/android/photos/views/TiledImageView.java @@ -16,8 +16,6 @@ package com.android.photos.views; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -28,35 +26,26 @@ import android.graphics.Paint.Align; import android.graphics.RectF; import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView.Renderer; -import android.os.Build; import android.util.AttributeSet; import android.view.Choreographer; import android.view.Choreographer.FrameCallback; -import android.view.View; import android.widget.FrameLayout; import com.android.gallery3d.glrenderer.BasicTexture; import com.android.gallery3d.glrenderer.GLES20Canvas; +import com.android.launcher3.util.Thunk; import com.android.photos.views.TiledImageRenderer.TileSource; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** - * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView} - * or {@link BlockingGLTextureView}. + * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}. */ public class TiledImageView extends FrameLayout { - private static final boolean USE_TEXTURE_VIEW = false; - private static final boolean IS_SUPPORTED = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; - private static final boolean USE_CHOREOGRAPHER = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; - - private BlockingGLTextureView mTextureView; - private GLSurfaceView mGLSurfaceView; - private boolean mInvalPending = false; + @Thunk GLSurfaceView mGLSurfaceView; + @Thunk boolean mInvalPending = false; private FrameCallback mFrameCallback; protected static class ImageRendererWrapper { @@ -79,35 +68,19 @@ public class TiledImageView extends FrameLayout { protected Object mLock = new Object(); protected ImageRendererWrapper mRenderer; - public static boolean isTilingSupported() { - return IS_SUPPORTED; - } - public TiledImageView(Context context) { this(context, null); } public TiledImageView(Context context, AttributeSet attrs) { super(context, attrs); - if (!IS_SUPPORTED) { - return; - } - mRenderer = new ImageRendererWrapper(); mRenderer.image = new TiledImageRenderer(this); - View view; - if (USE_TEXTURE_VIEW) { - mTextureView = new BlockingGLTextureView(context); - mTextureView.setRenderer(new TileRenderer()); - view = mTextureView; - } else { - mGLSurfaceView = new GLSurfaceView(context); - mGLSurfaceView.setEGLContextClientVersion(2); - mGLSurfaceView.setRenderer(new TileRenderer()); - mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); - view = mGLSurfaceView; - } - addView(view, new LayoutParams( + mGLSurfaceView = new GLSurfaceView(context); + mGLSurfaceView.setEGLContextClientVersion(2); + mGLSurfaceView.setRenderer(new TileRenderer()); + mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + addView(mGLSurfaceView, new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); //setTileSource(new ColoredTiles()); } @@ -117,22 +90,11 @@ public class TiledImageView extends FrameLayout { super.setVisibility(visibility); // need to update inner view's visibility because it seems like we're causing it to draw // from {@link #dispatchDraw} or {@link #invalidate} even if we are invisible. - if (USE_TEXTURE_VIEW) { - mTextureView.setVisibility(visibility); - } else { - mGLSurfaceView.setVisibility(visibility); - } + mGLSurfaceView.setVisibility(visibility); } public void destroy() { - if (!IS_SUPPORTED) { - return; - } - if (USE_TEXTURE_VIEW) { - mTextureView.destroy(); - } else { - mGLSurfaceView.queueEvent(mFreeTextures); - } + mGLSurfaceView.queueEvent(mFreeTextures); } private Runnable mFreeTextures = new Runnable() { @@ -144,27 +106,14 @@ public class TiledImageView extends FrameLayout { }; public void onPause() { - if (!IS_SUPPORTED) { - return; - } - if (!USE_TEXTURE_VIEW) { - mGLSurfaceView.onPause(); - } + mGLSurfaceView.onPause(); } public void onResume() { - if (!IS_SUPPORTED) { - return; - } - if (!USE_TEXTURE_VIEW) { - mGLSurfaceView.onResume(); - } + mGLSurfaceView.onResume(); } public void setTileSource(TileSource source, Runnable isReadyCallback) { - if (!IS_SUPPORTED) { - return; - } synchronized (mLock) { mRenderer.source = source; mRenderer.isReadyCallback = isReadyCallback; @@ -177,13 +126,14 @@ public class TiledImageView extends FrameLayout { invalidate(); } + public TileSource getTileSource() { + return mRenderer.source; + } + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (!IS_SUPPORTED) { - return; - } synchronized (mLock) { updateScaleIfNecessaryLocked(mRenderer); } @@ -200,43 +150,10 @@ public class TiledImageView extends FrameLayout { } @Override - protected void dispatchDraw(Canvas canvas) { - if (!IS_SUPPORTED) { - return; - } - if (USE_TEXTURE_VIEW) { - mTextureView.render(); - } - super.dispatchDraw(canvas); - } - - @SuppressLint("NewApi") - @Override - public void setTranslationX(float translationX) { - if (!IS_SUPPORTED) { - return; - } - super.setTranslationX(translationX); - } - - @Override public void invalidate() { - if (!IS_SUPPORTED) { - return; - } - if (USE_TEXTURE_VIEW) { - super.invalidate(); - mTextureView.invalidate(); - } else { - if (USE_CHOREOGRAPHER) { - invalOnVsync(); - } else { - mGLSurfaceView.requestRender(); - } - } + invalOnVsync(); } - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void invalOnVsync() { if (!mInvalPending) { mInvalPending = true; @@ -255,9 +172,6 @@ public class TiledImageView extends FrameLayout { private RectF mTempRectF = new RectF(); public void positionFromMatrix(Matrix matrix) { - if (!IS_SUPPORTED) { - return; - } if (mRenderer.source != null) { final int rotation = mRenderer.source.getRotation(); final boolean swap = !(rotation % 180 == 0); @@ -290,7 +204,7 @@ public class TiledImageView extends FrameLayout { } } - private class TileRenderer implements Renderer { + @Thunk class TileRenderer implements Renderer { private GLES20Canvas mCanvas; diff --git a/proguard.flags b/proguard.flags index 83a491dfd..e2a4b5b31 100644 --- a/proguard.flags +++ b/proguard.flags @@ -6,7 +6,6 @@ public void onClickVoiceButton(android.view.View); public void onClickConfigureButton(android.view.View); public void onClickAllAppsButton(android.view.View); - public void onClickAppMarketButton(android.view.View); public void dismissFirstRunCling(android.view.View); public void dismissMigrationClingCopyApps(android.view.View); public void dismissMigrationClingUseDefault(android.view.View); @@ -62,3 +61,8 @@ public int getBrightness(); public void setBrightness(int); } + +-keep class com.android.launcher3.AppsContainerRecyclerView { + public void setFastScrollerAlpha(float); + public float getFastScrollerAlpha(); +}
\ No newline at end of file diff --git a/protos/backup.proto b/protos/backup.proto index 44c4b09e3..09330ee06 100644 --- a/protos/backup.proto +++ b/protos/backup.proto @@ -72,6 +72,17 @@ message Journal { } message Favorite { + // Type of the app, this target represents + enum TargetType { + TARGET_NONE = 0; + TARGET_PHONE = 1; + TARGET_MESSENGER = 2; + TARGET_EMAIL = 3; + TARGET_BROWSER = 4; + TARGET_GALLERY = 5; + TARGET_CAMERA = 6; + } + required int64 id = 1; required int32 itemType = 2; optional string title = 3; @@ -90,6 +101,7 @@ message Favorite { optional string iconPackage = 16; optional string iconResource = 17; optional bytes icon = 18; + optional TargetType targetType = 19 [default = TARGET_NONE]; } message Screen { diff --git a/res/drawable-hdpi/ic_launcher_market_holo.png b/res/drawable-hdpi/ic_launcher_market_holo.png Binary files differdeleted file mode 100644 index dc7825114..000000000 --- a/res/drawable-hdpi/ic_launcher_market_holo.png +++ /dev/null diff --git a/res/drawable-hdpi/ic_pageindicator_current.png b/res/drawable-hdpi/ic_pageindicator_current.png Binary files differindex 283f44d37..423ca2b45 100644 --- a/res/drawable-hdpi/ic_pageindicator_current.png +++ b/res/drawable-hdpi/ic_pageindicator_current.png diff --git a/res/drawable-hdpi/ic_pageindicator_current_folder.png b/res/drawable-hdpi/ic_pageindicator_current_folder.png Binary files differnew file mode 100644 index 000000000..43fbb0e79 --- /dev/null +++ b/res/drawable-hdpi/ic_pageindicator_current_folder.png diff --git a/res/drawable-hdpi/ic_pageindicator_default.png b/res/drawable-hdpi/ic_pageindicator_default.png Binary files differindex 47b998967..83fa73fc6 100644 --- a/res/drawable-hdpi/ic_pageindicator_default.png +++ b/res/drawable-hdpi/ic_pageindicator_default.png diff --git a/res/drawable-hdpi/ic_pageindicator_default_folder.png b/res/drawable-hdpi/ic_pageindicator_default_folder.png Binary files differnew file mode 100644 index 000000000..55cab1c65 --- /dev/null +++ b/res/drawable-hdpi/ic_pageindicator_default_folder.png diff --git a/res/drawable-ldrtl/apps_list_fastscroll_bg.xml b/res/drawable-ldrtl/apps_list_fastscroll_bg.xml new file mode 100644 index 000000000..772975a71 --- /dev/null +++ b/res/drawable-ldrtl/apps_list_fastscroll_bg.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/apps_view_scrollbar_thumb_color" /> + <size + android:width="64dp" + android:height="64dp" /> + <corners + android:topLeftRadius="64dp" + android:topRightRadius="64dp" + android:bottomRightRadius="64dp" /> +</shape>
\ No newline at end of file diff --git a/res/drawable-mdpi/ic_launcher_market_holo.png b/res/drawable-mdpi/ic_launcher_market_holo.png Binary files differdeleted file mode 100644 index cacb37484..000000000 --- a/res/drawable-mdpi/ic_launcher_market_holo.png +++ /dev/null diff --git a/res/drawable-mdpi/ic_pageindicator_current.png b/res/drawable-mdpi/ic_pageindicator_current.png Binary files differindex b41e1bb67..ca889c4d1 100644 --- a/res/drawable-mdpi/ic_pageindicator_current.png +++ b/res/drawable-mdpi/ic_pageindicator_current.png diff --git a/res/drawable-mdpi/ic_pageindicator_current_folder.png b/res/drawable-mdpi/ic_pageindicator_current_folder.png Binary files differnew file mode 100644 index 000000000..5bbba9140 --- /dev/null +++ b/res/drawable-mdpi/ic_pageindicator_current_folder.png diff --git a/res/drawable-mdpi/ic_pageindicator_default.png b/res/drawable-mdpi/ic_pageindicator_default.png Binary files differindex e36c25cf1..34493b155 100644 --- a/res/drawable-mdpi/ic_pageindicator_default.png +++ b/res/drawable-mdpi/ic_pageindicator_default.png diff --git a/res/drawable-mdpi/ic_pageindicator_default_folder.png b/res/drawable-mdpi/ic_pageindicator_default_folder.png Binary files differnew file mode 100644 index 000000000..0a987a4d0 --- /dev/null +++ b/res/drawable-mdpi/ic_pageindicator_default_folder.png diff --git a/res/drawable-xhdpi/ic_launcher_market_holo.png b/res/drawable-xhdpi/ic_launcher_market_holo.png Binary files differdeleted file mode 100644 index 958f0de3c..000000000 --- a/res/drawable-xhdpi/ic_launcher_market_holo.png +++ /dev/null diff --git a/res/drawable-xhdpi/ic_pageindicator_current.png b/res/drawable-xhdpi/ic_pageindicator_current.png Binary files differindex 8fa774dee..3054f2f4f 100644 --- a/res/drawable-xhdpi/ic_pageindicator_current.png +++ b/res/drawable-xhdpi/ic_pageindicator_current.png diff --git a/res/drawable-xhdpi/ic_pageindicator_current_folder.png b/res/drawable-xhdpi/ic_pageindicator_current_folder.png Binary files differnew file mode 100644 index 000000000..cd92e9f21 --- /dev/null +++ b/res/drawable-xhdpi/ic_pageindicator_current_folder.png diff --git a/res/drawable-xhdpi/ic_pageindicator_default.png b/res/drawable-xhdpi/ic_pageindicator_default.png Binary files differindex 8eb5eb08d..38538dcf0 100644 --- a/res/drawable-xhdpi/ic_pageindicator_default.png +++ b/res/drawable-xhdpi/ic_pageindicator_default.png diff --git a/res/drawable-xhdpi/ic_pageindicator_default_folder.png b/res/drawable-xhdpi/ic_pageindicator_default_folder.png Binary files differnew file mode 100644 index 000000000..e7c46e3a1 --- /dev/null +++ b/res/drawable-xhdpi/ic_pageindicator_default_folder.png diff --git a/res/drawable-xxhdpi/ic_pageindicator_current.png b/res/drawable-xxhdpi/ic_pageindicator_current.png Binary files differindex 22b290e69..5941c8e4f 100644 --- a/res/drawable-xxhdpi/ic_pageindicator_current.png +++ b/res/drawable-xxhdpi/ic_pageindicator_current.png diff --git a/res/drawable-xxhdpi/ic_pageindicator_current_folder.png b/res/drawable-xxhdpi/ic_pageindicator_current_folder.png Binary files differnew file mode 100644 index 000000000..602b89a40 --- /dev/null +++ b/res/drawable-xxhdpi/ic_pageindicator_current_folder.png diff --git a/res/drawable-xxhdpi/ic_pageindicator_default.png b/res/drawable-xxhdpi/ic_pageindicator_default.png Binary files differindex e608cae30..3fa9e5fd7 100644 --- a/res/drawable-xxhdpi/ic_pageindicator_default.png +++ b/res/drawable-xxhdpi/ic_pageindicator_default.png diff --git a/res/drawable-xxhdpi/ic_pageindicator_default_folder.png b/res/drawable-xxhdpi/ic_pageindicator_default_folder.png Binary files differnew file mode 100644 index 000000000..bbcd7f91e --- /dev/null +++ b/res/drawable-xxhdpi/ic_pageindicator_default_folder.png diff --git a/res/drawable/apps_list_bg.xml b/res/drawable/apps_list_bg.xml new file mode 100644 index 000000000..0e56684b5 --- /dev/null +++ b/res/drawable/apps_list_bg.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="#ffffff" /> + <corners + android:radius="2dp" /> +</shape>
\ No newline at end of file diff --git a/res/drawable/apps_list_fastscroll_bg.xml b/res/drawable/apps_list_fastscroll_bg.xml new file mode 100644 index 000000000..780d3b0c3 --- /dev/null +++ b/res/drawable/apps_list_fastscroll_bg.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/apps_view_scrollbar_thumb_color" /> + <size + android:width="64dp" + android:height="64dp" /> + <corners + android:topLeftRadius="64dp" + android:topRightRadius="64dp" + android:bottomLeftRadius="64dp" /> +</shape>
\ No newline at end of file diff --git a/res/drawable/apps_list_scrollbar_thumb.xml b/res/drawable/apps_list_scrollbar_thumb.xml new file mode 100644 index 000000000..318d40678 --- /dev/null +++ b/res/drawable/apps_list_scrollbar_thumb.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/apps_view_scrollbar_thumb_color" /> + <size android:width="@dimen/apps_view_fast_scroll_bar_width" /> +</shape>
\ No newline at end of file diff --git a/res/drawable/apps_list_search_bg.xml b/res/drawable/apps_list_search_bg.xml new file mode 100644 index 000000000..63c4d5591 --- /dev/null +++ b/res/drawable/apps_list_search_bg.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="#ffffff" /> + <corners + android:bottomLeftRadius="2dp" + android:bottomRightRadius="2dp" /> +</shape>
\ No newline at end of file diff --git a/res/drawable/apps_reveal_bg.xml b/res/drawable/apps_reveal_bg.xml new file mode 100644 index 000000000..07505a596 --- /dev/null +++ b/res/drawable/apps_reveal_bg.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="#ffffff" /> + <corners android:radius="2dp" /> +</shape>
\ No newline at end of file diff --git a/res/drawable/apps_search_bg.xml b/res/drawable/apps_search_bg.xml new file mode 100644 index 000000000..405e8447c --- /dev/null +++ b/res/drawable/apps_search_bg.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="#ffffff" /> + <corners + android:topLeftRadius="2dp" + android:topRightRadius="2dp" /> +</shape>
\ No newline at end of file diff --git a/res/layout-land/launcher.xml b/res/layout-land/launcher.xml index 6f95bd506..d5dd91ab4 100644 --- a/res/layout-land/launcher.xml +++ b/res/layout-land/launcher.xml @@ -57,8 +57,14 @@ android:id="@+id/overview_panel" android:visibility="gone" /> - <include layout="@layout/apps_customize_pane" - android:id="@+id/apps_customize_pane" + <include layout="@layout/widgets_view" + android:id="@+id/widgets_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="invisible" /> + + <include layout="@layout/apps_view" + android:id="@+id/apps_view" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" /> diff --git a/res/layout-port/launcher.xml b/res/layout-port/launcher.xml index af30a32e5..5a018c516 100644 --- a/res/layout-port/launcher.xml +++ b/res/layout-port/launcher.xml @@ -66,8 +66,14 @@ android:id="@+id/search_drop_target_bar" layout="@layout/search_drop_target_bar" /> - <include layout="@layout/apps_customize_pane" - android:id="@+id/apps_customize_pane" + <include layout="@layout/widgets_view" + android:id="@+id/widgets_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="invisible" /> + + <include layout="@layout/apps_view" + android:id="@+id/apps_view" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" /> diff --git a/res/layout-sw600dp/apps_view.xml b/res/layout-sw600dp/apps_view.xml new file mode 100644 index 000000000..e6e0ec358 --- /dev/null +++ b/res/layout-sw600dp/apps_view.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<com.android.launcher3.AppsContainerView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/apps_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="@dimen/apps_container_inset" + android:descendantFocusability="afterDescendants"> + <include + layout="@layout/apps_reveal_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" /> + <include + layout="@layout/apps_list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" /> +</com.android.launcher3.AppsContainerView>
\ No newline at end of file diff --git a/res/layout-sw720dp/launcher.xml b/res/layout-sw720dp/launcher.xml index 960ccf330..a9601af6a 100644 --- a/res/layout-sw720dp/launcher.xml +++ b/res/layout-sw720dp/launcher.xml @@ -66,8 +66,14 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" /> - <include layout="@layout/apps_customize_pane" - android:id="@+id/apps_customize_pane" + <include layout="@layout/widgets_view" + android:id="@+id/widgets_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="invisible" /> + + <include layout="@layout/apps_view" + android:id="@+id/apps_view" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" /> diff --git a/res/layout/apps_customize_pane.xml b/res/layout/apps_customize_pane.xml deleted file mode 100644 index e42576ffe..000000000 --- a/res/layout/apps_customize_pane.xml +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- 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. ---> -<com.android.launcher3.AppsCustomizeTabHost - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:launcher="http://schemas.android.com/apk/res-auto" - android:clipChildren="false"> - - <LinearLayout - android:id="@+id/content" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:clipChildren="false" - android:orientation="vertical"> - - <FrameLayout - android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" - android:clipChildren="false"> - <FrameLayout - android:id="@+id/fake_page_container" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:clipChildren="false" - android:clipToPadding="false"> - <FrameLayout - android:id="@+id/fake_page" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="invisible" - android:clipToPadding="false" /> - </FrameLayout> - <com.android.launcher3.AppsCustomizePagedView - android:id="@+id/apps_customize_pane_content" - android:layout_width="match_parent" - android:layout_height="match_parent" - launcher:widgetCountX="@integer/apps_customize_widget_cell_count_x" - launcher:widgetCountY="@integer/apps_customize_widget_cell_count_y" - launcher:maxGap="@dimen/workspace_max_gap" - launcher:pageIndicator="@+id/apps_customize_page_indicator" /> - </FrameLayout> - <include - android:id="@+id/apps_customize_page_indicator" - layout="@layout/page_indicator" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" /> - </LinearLayout> -</com.android.launcher3.AppsCustomizeTabHost> diff --git a/res/layout/apps_empty_view.xml b/res/layout/apps_empty_view.xml new file mode 100644 index 000000000..8408077a2 --- /dev/null +++ b/res/layout/apps_empty_view.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/empty_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:paddingTop="24dp" + android:paddingBottom="24dp" + android:paddingRight="@dimen/apps_grid_view_start_margin" + android:textSize="16sp" + android:textColor="#4c4c4c" + android:focusable="false" /> + diff --git a/res/layout/apps_grid_row_icon_view.xml b/res/layout/apps_grid_row_icon_view.xml new file mode 100644 index 000000000..81e74b985 --- /dev/null +++ b/res/layout/apps_grid_row_icon_view.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<com.android.launcher3.BubbleTextView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:launcher="http://schemas.android.com/apk/res-auto" + style="@style/WorkspaceIcon.AppsCustomize" + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left|center_vertical" + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:focusable="true" + android:background="@drawable/focusable_view_bg" + launcher:deferShadowGeneration="true" /> + diff --git a/res/layout/apps_list_row_icon_view.xml b/res/layout/apps_list_row_icon_view.xml new file mode 100644 index 000000000..867dbdc99 --- /dev/null +++ b/res/layout/apps_list_row_icon_view.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> + +<com.android.launcher3.BubbleTextView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:launcher="http://schemas.android.com/apk/res-auto" + style="@style/WorkspaceIcon.AppsCustomize" + android:id="@+id/application_icon" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusable="true" + android:background="@drawable/focusable_view_bg" + launcher:iconPaddingOverride="24dp" + launcher:textSizeOverride="16dp" + launcher:layoutHorizontal="true" + launcher:deferShadowGeneration="true" /> diff --git a/res/layout/apps_list_row_view.xml b/res/layout/apps_list_row_view.xml new file mode 100644 index 000000000..e80285b95 --- /dev/null +++ b/res/layout/apps_list_row_view.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="@dimen/apps_view_row_height" + android:orientation="horizontal" + android:focusable="true" + android:background="@drawable/focusable_view_bg" + android:descendantFocusability="afterDescendants"> + <TextView + android:id="@+id/section" + android:layout_width="64dp" + android:layout_height="match_parent" + android:paddingLeft="16dp" + android:gravity="start|center_vertical" + android:textColor="@color/apps_view_section_text_color" + android:textSize="@dimen/apps_view_section_text_size" + android:focusable="false" /> +</LinearLayout> diff --git a/res/layout/apps_list_view.xml b/res/layout/apps_list_view.xml new file mode 100644 index 000000000..dfb7b588d --- /dev/null +++ b/res/layout/apps_list_view.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/apps_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:elevation="15dp" + android:visibility="gone" + android:focusableInTouchMode="true"> + <EditText + android:id="@+id/app_search_box" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:hint="@string/apps_view_search_bar_hint" + android:maxLines="1" + android:singleLine="true" + android:scrollHorizontally="true" + android:gravity="fill_horizontal" + android:textSize="16sp" + android:textColor="#4c4c4c" + android:textColorHint="#9c9c9c" + android:imeOptions="actionDone|flagNoExtractUi" + android:background="@drawable/apps_search_bg" + android:elevation="4dp" /> + <com.android.launcher3.AppsContainerRecyclerView + android:id="@+id/apps_list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:clipToPadding="false" + android:focusable="true" + android:descendantFocusability="afterDescendants" /> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/apps_reveal_view.xml b/res/layout/apps_reveal_view.xml new file mode 100644 index 000000000..2951ea4f4 --- /dev/null +++ b/res/layout/apps_reveal_view.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/apps_view_transition_overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:elevation="15dp" + android:visibility="invisible" + android:focusable="false" />
\ No newline at end of file diff --git a/res/layout/apps_view.xml b/res/layout/apps_view.xml new file mode 100644 index 000000000..7f09f7795 --- /dev/null +++ b/res/layout/apps_view.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<!-- The top and bottom paddings are defined in this container, but since we want + the list view to span the full width (for touch interception purposes), we + will bake the left/right padding into that view's background itself. --> +<com.android.launcher3.AppsContainerView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/apps_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:descendantFocusability="afterDescendants"> + <include + layout="@layout/apps_reveal_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" /> + <include + layout="@layout/apps_list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" /> +</com.android.launcher3.AppsContainerView>
\ No newline at end of file diff --git a/res/layout/page_indicator_marker.xml b/res/layout/page_indicator_marker.xml index 686d27569..564a95811 100644 --- a/res/layout/page_indicator_marker.xml +++ b/res/layout/page_indicator_marker.xml @@ -16,8 +16,8 @@ <com.android.launcher3.PageIndicatorMarker xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res-auto" - android:layout_width="16dp" - android:layout_height="16dp" + android:layout_width="12dp" + android:layout_height="12dp" android:layout_gravity="center_vertical"> <ImageView android:id="@+id/inactive" diff --git a/res/layout/search_drop_target_bar.xml b/res/layout/search_drop_target_bar.xml index 0d7167e5b..9b0da1d4e 100644 --- a/res/layout/search_drop_target_bar.xml +++ b/res/layout/search_drop_target_bar.xml @@ -45,6 +45,19 @@ style="@style/DropTargetButtonContainer" android:layout_weight="1" > + <!-- Uninstall target --> + + <com.android.launcher3.UninstallDropTarget + android:id="@+id/uninstall_target_text" + style="@style/DropTargetButton" + android:drawableStart="@drawable/uninstall_target_selector" + android:text="@string/delete_target_uninstall_label" /> + </FrameLayout> + + <FrameLayout + style="@style/DropTargetButtonContainer" + android:layout_weight="1" > + <!-- Info target --> <com.android.launcher3.InfoDropTarget diff --git a/res/layout/user_folder.xml b/res/layout/user_folder.xml index ed8d43e46..cd3a051ad 100644 --- a/res/layout/user_folder.xml +++ b/res/layout/user_folder.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2008 The Android Open Source Project +<!-- + Copyright (C) 2015 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. @@ -14,42 +15,63 @@ limitations under the License. --> -<com.android.launcher3.Folder - xmlns:android="http://schemas.android.com/apk/res/android" +<com.android.launcher3.Folder xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical" - android:background="@drawable/quantum_panel"> + android:background="@drawable/quantum_panel" + android:orientation="vertical" > - <ScrollView - android:id="@+id/scroll_view" + <FrameLayout + android:id="@+id/folder_content_wrapper" android:layout_width="match_parent" - android:layout_height="match_parent"> - <com.android.launcher3.CellLayout - android:id="@+id/folder_content" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:cacheColorHint="#ff333333" - android:hapticFeedbackEnabled="false" /> - </ScrollView> - - <com.android.launcher3.FolderEditText - android:id="@+id/folder_name" + android:layout_height="match_parent" > + + <!-- Actual size of the indicator doesn't matter as it is scaled to match the view size --> + + <com.android.launcher3.FocusIndicatorView + android:id="@+id/focus_indicator" + android:layout_width="20dp" + android:layout_height="20dp" /> + + <com.android.launcher3.FolderPagedView + android:id="@+id/folder_content" + android:layout_width="match_parent" + android:layout_height="match_parent" + launcher:pageIndicator="@+id/folder_page_indicator" /> + </FrameLayout> + + <LinearLayout + android:id="@+id/folder_footer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_centerHorizontal="true" - android:paddingTop="@dimen/folder_name_padding" - android:paddingBottom="@dimen/folder_name_padding" - android:background="#00000000" - android:hint="@string/folder_hint_text" - android:textSize="14sp" - android:textColor="#ff777777" - android:textColorHint="#ff808080" - android:textColorHighlight="#ffCCCCCC" - android:textCursorDrawable="@null" - android:gravity="center_horizontal" - android:singleLine="true" - android:imeOptions="flagNoExtractUi" - android:fontFamily="sans-serif-condensed"/> -</com.android.launcher3.Folder> + android:orientation="vertical" > + + <include + android:id="@+id/folder_page_indicator" + android:layout_width="wrap_content" + android:layout_height="12dp" + android:layout_gravity="center_horizontal" + layout="@layout/page_indicator" /> + + <com.android.launcher3.FolderEditText + android:id="@+id/folder_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:background="#00000000" + android:fontFamily="sans-serif-condensed" + android:gravity="center_horizontal" + android:hint="@string/folder_hint_text" + android:imeOptions="flagNoExtractUi" + android:paddingBottom="@dimen/folder_name_padding" + android:paddingTop="@dimen/folder_name_padding" + android:singleLine="true" + android:textColor="#ff777777" + android:textColorHighlight="#ffCCCCCC" + android:textColorHint="#ff808080" + android:textCursorDrawable="@null" + android:textSize="14sp" /> + </LinearLayout> + +</com.android.launcher3.Folder>
\ No newline at end of file diff --git a/res/layout/apps_customize_widget.xml b/res/layout/widget_cell.xml index a8344e3ff..f53b74ef8 100644 --- a/res/layout/apps_customize_widget.xml +++ b/res/layout/widget_cell.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2011 The Android Open Source Project +<!-- Copyright (C) 2015 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. @@ -13,58 +13,26 @@ See the License for the specific language governing permissions and limitations under the License. --> -<com.android.launcher3.PagedViewWidget +<com.android.launcher3.widget.WidgetCell xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res-auto" - - android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_width="@dimen/widget_preview_container_width" + android:layout_height="wrap_content" android:layout_weight="1" + android:paddingTop="@dimen/widget_preview_padding_top" android:orientation="vertical" - android:background="@drawable/focusable_view_bg" android:focusable="true"> <LinearLayout - android:orientation="horizontal" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_weight="1"> - <FrameLayout - android:id="@+id/left_border" - android:layout_width="1dp" - android:layout_height="match_parent" - android:background="@color/widget_text_panel" - android:visibility="gone" /> - - <!-- The preview of the widget or shortcut. --> - <com.android.launcher3.PagedViewWidgetImageView - android:id="@+id/widget_preview" - style="@style/PagedViewWidgetImageView" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_weight="1" - android:paddingTop="@dimen/app_widget_preview_padding_top" - android:paddingEnd="@dimen/app_widget_preview_padding_right" - android:paddingRight="@dimen/app_widget_preview_padding_right" - android:scaleType="matrix" /> - <FrameLayout - android:id="@+id/right_border" - android:layout_width="1dp" - android:layout_height="match_parent" - android:background="@color/widget_text_panel" - android:visibility="gone" /> - </LinearLayout> - - <LinearLayout - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingTop="@dimen/app_widget_preview_label_vertical_padding" - android:paddingBottom="@dimen/app_widget_preview_label_vertical_padding" - android:paddingLeft="@dimen/app_widget_preview_label_horizontal_padding" - android:paddingRight="@dimen/app_widget_preview_label_horizontal_padding" - android:background="@color/widget_text_panel" + android:paddingTop="@dimen/widget_preview_label_vertical_padding" + android:paddingBottom="@dimen/widget_preview_label_vertical_padding" + android:paddingLeft="@dimen/widget_preview_label_horizontal_padding" + android:paddingRight="@dimen/widget_preview_label_horizontal_padding" android:orientation="horizontal"> + <!-- The name of the widget. --> <TextView android:id="@+id/widget_name" @@ -73,7 +41,7 @@ android:layout_weight="1" android:gravity="start" android:singleLine="true" - android:ellipsize="marquee" + android:ellipsize="end" android:fadingEdge="horizontal" android:textColor="#FFFFFFFF" @@ -89,17 +57,26 @@ android:id="@+id/widget_dims" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center" android:layout_marginStart="5dp" + android:layout_marginLeft="5dp" android:layout_weight="0" android:gravity="start" android:textColor="#FFFFFFFF" android:textSize="12sp" + android:textAlignment="viewStart" android:fontFamily="sans-serif-condensed" android:shadowRadius="2.0" android:shadowColor="#B0000000" /> </LinearLayout> + <!-- The image of the widget. --> + <com.android.launcher3.widget.WidgetImageView + android:id="@+id/widget_preview" + style="@style/WidgetImageView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:scaleType="matrix" /> -</com.android.launcher3.PagedViewWidget> +</com.android.launcher3.widget.WidgetCell> diff --git a/res/layout/widgets_list_row_view.xml b/res/layout/widgets_list_row_view.xml new file mode 100644 index 000000000..017b45066 --- /dev/null +++ b/res/layout/widgets_list_row_view.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/widgets_cell_list_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:focusable="true" + android:background="@drawable/focusable_view_bg" + android:descendantFocusability="afterDescendants"> + + <!-- Section info --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:focusable="true" + android:background="@drawable/focusable_view_bg" + android:descendantFocusability="afterDescendants"> + <ImageView + android:id="@+id/section_image" + android:layout_width="@dimen/widget_section_height" + android:layout_height="@dimen/widget_section_height" + android:paddingLeft="@dimen/widget_section_icon_padding" + android:paddingRight="@dimen/widget_section_icon_padding" + android:paddingTop="@dimen/widget_section_icon_padding" + android:paddingBottom="@dimen/widget_section_icon_padding" + android:background="@color/widget_text_panel" + /> + <TextView + android:id="@+id/section" + android:layout_width="match_parent" + android:layout_height="@dimen/widget_section_height" + android:paddingTop="8dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:singleLine="true" + android:ellipsize="end" + android:gravity="start|center_vertical" + android:textColor="@color/widgets_view_section_text_color" + android:background="@color/widget_text_panel" + android:textSize="20sp" + android:focusable="false" /> + </LinearLayout> + + <!-- Widget list --> + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="end" + > + <!-- TODO(hyunyoungs): replace the indicator with actual assets. --> + <FrameLayout + android:id="@+id/scrollable_indicator" + android:layout_gravity="center_vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/ic_pageindicator_default" + android:visibility="invisible" + /> + <HorizontalScrollView + android:id="@+id/widgets_scroll_container" + android:layout_width="match_parent" + android:layout_height="@dimen/widget_cell_height" + android:scrollbars="none" > + <LinearLayout + android:id="@+id/widgets_cell_list" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" /> + </HorizontalScrollView> + </RelativeLayout> +</LinearLayout> diff --git a/res/layout/widgets_view.xml b/res/layout/widgets_view.xml new file mode 100644 index 000000000..0800f59aa --- /dev/null +++ b/res/layout/widgets_view.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<!-- The top and bottom paddings are defined in this container, but since we want + the list view to span the full width (for touch interception purposes), we + will bake the left/right padding into that view's background itself. --> +<com.android.launcher3.widget.WidgetsContainerView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/widgets_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="@dimen/widget_container_inset" + android:paddingBottom="@dimen/widget_container_inset" + android:descendantFocusability="afterDescendants"> + + <FrameLayout + android:id="@+id/widgets_reveal_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:visibility="invisible" + android:focusable="false" /> + + <LinearLayout + android:id="@+id/widgets_content" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipChildren="false" + android:orientation="vertical"> + + <android.support.v7.widget.RecyclerView + android:id="@+id/widgets_list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/quantum_panel_dark"/> + </LinearLayout> +</com.android.launcher3.widget.WidgetsContainerView>
\ No newline at end of file diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml index 2015158d9..2d3c633c3 100644 --- a/res/values-be/strings.xml +++ b/res/values-be/strings.xml @@ -199,10 +199,6 @@ <skip /> <!-- no translation found for folder_name_format (6629239338071103179) --> <skip /> - <string name="custom_workspace_cling_title_1" msgid="3750880082935033085"></string> - <string name="custom_workspace_cling_description_1" msgid="939966842147696724"></string> - <string name="custom_workspace_cling_title_2" msgid="662588444436552198"></string> - <string name="custom_workspace_cling_description_2" msgid="8097921091798539310"></string> <!-- no translation found for widget_button_text (2880537293434387943) --> <skip /> <!-- no translation found for wallpaper_button_text (8404103075899945851) --> diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml index 3b0deb85b..a4955ceda 100644 --- a/res/values-et/strings.xml +++ b/res/values-et/strings.xml @@ -23,13 +23,11 @@ <string name="home" msgid="5921706419368316758">"Kodu"</string> <string name="uid_name" msgid="3371120195364560632">"Androidi tuumrakendused"</string> <string name="folder_name" msgid="8551881338202938211"></string> - <string name="chooser_wallpaper" msgid="6063168087625352235">"Taustapildi valimiskoht:"</string> <string name="wallpaper_instructions" msgid="4215640646180727542">"Määra taustapilt"</string> <string name="pick_wallpaper" msgid="5630222540525626723">"Taustapildid"</string> <string name="activity_not_found" msgid="217823393239365967">"Rakendus pole installitud."</string> <string name="widgets_tab_label" msgid="9145860100000983599">"Vidinad"</string> <string name="long_press_widget_to_add" msgid="7395697462851217506">"Vidina valimiseks puudutage seda pikalt."</string> - <string name="market" msgid="2652226429823445833">"Pood"</string> <string name="widget_dims_format" msgid="1386418557719032947">"%1$d × %2$d"</string> <string name="external_drop_widget_error" msgid="2285187188524172774">"Üksust ei saa sellele avaekraanile tuua."</string> <string name="external_drop_widget_pick_title" msgid="7040647073452295370">"Valige loomiseks vidin"</string> @@ -52,23 +50,13 @@ <string name="title_select_application" msgid="1793455815754848652">"Rakenduse valimine"</string> <string name="all_apps_button_label" msgid="2578400570124163469">"Rakendused"</string> <string name="all_apps_home_button_label" msgid="1022222300329398558">"Kodu"</string> - <string name="delete_zone_label_workspace" msgid="7153615831493049150">"Eemalda"</string> <string name="delete_zone_label_all_apps" msgid="6664588234817475108">"Desinstalli"</string> <string name="delete_target_label" msgid="665300185123139530">"Eemalda"</string> <string name="delete_target_uninstall_label" msgid="748894921183769150">"Desinstalli"</string> <string name="info_target_label" msgid="4019495079517426980">"Rakenduse teave"</string> - <string name="accessibility_search_button" msgid="816822994629942611">"Otsing"</string> - <string name="accessibility_voice_search_button" msgid="3938249215065842475">"Häälotsing"</string> <string name="accessibility_all_apps_button" msgid="8803738611398979849">"Rakendused"</string> <string name="accessibility_delete_button" msgid="3628162007991023603">"Eemalda"</string> <string name="delete_zone_label_all_apps_system_app" msgid="3683920959591819044">"Desinstalli värskendus"</string> - <string name="menu_add" msgid="3065046628354640854">"Lisa"</string> - <string name="menu_manage_apps" msgid="2308685199463588895">"Rakenduste haldamine"</string> - <string name="menu_wallpaper" msgid="5837429080911269832">"Taustapilt"</string> - <string name="menu_search" msgid="4826514464423239041">"Otsing"</string> - <string name="menu_notifications" msgid="6424587053194766192">"Teadistused"</string> - <string name="menu_settings" msgid="3946232973327980394">"Süsteemiseaded"</string> - <string name="menu_help" msgid="4901160661634590633">"Abi"</string> <string name="cab_menu_delete_app" msgid="4089398025537640349">"Rakenduse desinstallimine"</string> <string name="cab_menu_app_info" msgid="914548323652698884">"Rakenduse üksikasjad"</string> <string name="cab_app_selection_text" msgid="6378522164293415735">"Valitud on 1 rakendus"</string> @@ -94,11 +82,7 @@ <string name="apps_customize_widgets_scroll_format" msgid="5383009742241717437">"Vidinate leht %1$d/%2$d"</string> <string name="workspace_cling_title" msgid="738396473989890567">"Tunne end nagu kodus"</string> <string name="workspace_cling_move_item" msgid="791013895761065070">"Võite panna oma lemmikrakendused siia."</string> - <string name="workspace_cling_open_all_apps" msgid="2459977609848572588">"Kõikide oma rakenduste nägemiseks puudutage ringi."</string> - <string name="all_apps_cling_title" msgid="2559734712581447107">"Valige mõned rakendused"</string> - <string name="all_apps_cling_add_item" msgid="5665035103260318891">"Rakenduse lisamiseks avakuvale puudutage seda pikalt."</string> <string name="folder_cling_title" msgid="4308949882377840953">"Korraldage oma rakendused kaustadesse"</string> - <string name="folder_cling_move_item" msgid="270598675060435169">"Rakenduse liigutamiseks pange sõrm rakendusele ja hoidke seda."</string> <string name="folder_cling_create_folder" msgid="8352867485656129478">"Avakuval uue kausta tegemiseks virnastage üks rakendus teisele."</string> <string name="cling_dismiss" msgid="2780907108735868381">"OK"</string> <string name="folder_opened" msgid="1262064100943801533">"Kaust on avatud, <xliff:g id="WIDTH">%1$d</xliff:g> x <xliff:g id="HEIGHT">%2$d</xliff:g>"</string> @@ -107,8 +91,4 @@ <string name="folder_closed" msgid="3130534551370511932">"Kaust suletud"</string> <string name="folder_renamed" msgid="7951233572858053642">"Kausta uus nimi: <xliff:g id="NAME">%1$s</xliff:g>"</string> <string name="folder_name_format" msgid="3051680259794759037">"<xliff:g id="NAME">%1$s</xliff:g>"</string> - <string name="custom_workspace_cling_title_1" msgid="1433009175359948587"></string> - <string name="custom_workspace_cling_description_1" msgid="6875529190849858047"></string> - <string name="custom_workspace_cling_title_2" msgid="5516006164661020362"></string> - <string name="custom_workspace_cling_description_2" msgid="2758258454975288377"></string> </resources> diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml index 1b3418154..06a99842e 100644 --- a/res/values-land/dimens.xml +++ b/res/values-land/dimens.xml @@ -18,9 +18,4 @@ <!-- QSB --> <dimen name="toolbar_button_vertical_padding">8dip</dimen> <dimen name="toolbar_button_horizontal_padding">0dip</dimen> - -<!-- AppsCustomize --> - <dimen name="apps_customize_tab_bar_height">42dp</dimen> - <integer name="apps_customize_widget_cell_count_x">3</integer> - <integer name="apps_customize_widget_cell_count_y">2</integer> </resources> diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml index f50a5db5b..248ca1517 100644 --- a/res/values-ms/strings.xml +++ b/res/values-ms/strings.xml @@ -23,13 +23,11 @@ <string name="home" msgid="5921706419368316758">"Laman Utama"</string> <string name="uid_name" msgid="3371120195364560632">"Apl Teras Android"</string> <string name="folder_name" msgid="8551881338202938211"></string> - <string name="chooser_wallpaper" msgid="6063168087625352235">"Pilih kertas dinding dari"</string> <string name="wallpaper_instructions" msgid="4215640646180727542">"Tetapkan kertas dinding"</string> <string name="pick_wallpaper" msgid="5630222540525626723">"Kertas dinding"</string> <string name="activity_not_found" msgid="217823393239365967">"Aplikasi tidak dipasang."</string> <string name="widgets_tab_label" msgid="9145860100000983599">"Widget"</string> <string name="long_press_widget_to_add" msgid="7395697462851217506">"Sentuh & tahan untuk mengambil widget."</string> - <string name="market" msgid="2652226429823445833">"Kedai"</string> <string name="widget_dims_format" msgid="1386418557719032947">"%1$d × %2$d"</string> <string name="external_drop_widget_error" msgid="2285187188524172774">"Tidak dapat melepaskan item pada skrin Utama ini."</string> <string name="external_drop_widget_pick_title" msgid="7040647073452295370">"Pilih widget untuk dibuat"</string> @@ -52,23 +50,13 @@ <string name="title_select_application" msgid="1793455815754848652">"Pilih aplikasi"</string> <string name="all_apps_button_label" msgid="2578400570124163469">"Apl"</string> <string name="all_apps_home_button_label" msgid="1022222300329398558">"Laman Utama"</string> - <string name="delete_zone_label_workspace" msgid="7153615831493049150">"Alih keluar"</string> <string name="delete_zone_label_all_apps" msgid="6664588234817475108">"Nyahpasang"</string> <string name="delete_target_label" msgid="665300185123139530">"Alih keluar"</string> <string name="delete_target_uninstall_label" msgid="748894921183769150">"Nyahpasang"</string> <string name="info_target_label" msgid="4019495079517426980">"Maklumat apl"</string> - <string name="accessibility_search_button" msgid="816822994629942611">"Carian"</string> - <string name="accessibility_voice_search_button" msgid="3938249215065842475">"Carian Suara"</string> <string name="accessibility_all_apps_button" msgid="8803738611398979849">"Aplikasi"</string> <string name="accessibility_delete_button" msgid="3628162007991023603">"Alih keluar"</string> <string name="delete_zone_label_all_apps_system_app" msgid="3683920959591819044">"Nyahpasang kemas kini"</string> - <string name="menu_add" msgid="3065046628354640854">"Tambah"</string> - <string name="menu_manage_apps" msgid="2308685199463588895">"Mengurus apl"</string> - <string name="menu_wallpaper" msgid="5837429080911269832">"Kertas dinding"</string> - <string name="menu_search" msgid="4826514464423239041">"Cari"</string> - <string name="menu_notifications" msgid="6424587053194766192">"Pemberitahuan"</string> - <string name="menu_settings" msgid="3946232973327980394">"Tetapan sistem"</string> - <string name="menu_help" msgid="4901160661634590633">"Bantuan"</string> <string name="cab_menu_delete_app" msgid="4089398025537640349">"Nyahpasang aplikasi"</string> <string name="cab_menu_app_info" msgid="914548323652698884">"Butiran aplikasi"</string> <string name="cab_app_selection_text" msgid="6378522164293415735">"1 aplikasi dipilih"</string> @@ -94,11 +82,7 @@ <string name="apps_customize_widgets_scroll_format" msgid="5383009742241717437">"Halaman widget %1$d dari %2$d"</string> <string name="workspace_cling_title" msgid="738396473989890567">"Buat diri anda seperti di rumah"</string> <string name="workspace_cling_move_item" msgid="791013895761065070">"Anda boleh meletakkan aplikasi kegemaran anda di sini."</string> - <string name="workspace_cling_open_all_apps" msgid="2459977609848572588">"Untuk melihat semua aplikasi anda, sentuh bulatan."</string> - <string name="all_apps_cling_title" msgid="2559734712581447107">"Pilih beberapa aplikasi"</string> - <string name="all_apps_cling_add_item" msgid="5665035103260318891">"Untuk menambahkan aplikasi pada skrin Utama anda, sentuh & tahankannya."</string> <string name="folder_cling_title" msgid="4308949882377840953">"Susun aplikasi anda dengan folder"</string> - <string name="folder_cling_move_item" msgid="270598675060435169">"Untuk memindahkan aplikasi, sentuh & tahankannya."</string> <string name="folder_cling_create_folder" msgid="8352867485656129478">"Untuk membuat folder baharu pada skrin Utama anda, tindihkan satu aplikasi di atas yang lain."</string> <string name="cling_dismiss" msgid="2780907108735868381">"OK"</string> <string name="folder_opened" msgid="1262064100943801533">"Folder dibuka, <xliff:g id="WIDTH">%1$d</xliff:g> kali <xliff:g id="HEIGHT">%2$d</xliff:g>"</string> @@ -107,8 +91,4 @@ <string name="folder_closed" msgid="3130534551370511932">"Folder ditutup"</string> <string name="folder_renamed" msgid="7951233572858053642">"Folder dinamakan semula kepada <xliff:g id="NAME">%1$s</xliff:g>"</string> <string name="folder_name_format" msgid="3051680259794759037">"Folder: <xliff:g id="NAME">%1$s</xliff:g>"</string> - <string name="custom_workspace_cling_title_1" msgid="1433009175359948587"></string> - <string name="custom_workspace_cling_description_1" msgid="6875529190849858047"></string> - <string name="custom_workspace_cling_title_2" msgid="5516006164661020362"></string> - <string name="custom_workspace_cling_description_2" msgid="2758258454975288377"></string> </resources> diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml index 28679be2e..9ecd07dd1 100644 --- a/res/values-sw600dp/dimens.xml +++ b/res/values-sw600dp/dimens.xml @@ -17,12 +17,16 @@ <resources> <dimen name="app_icon_size">64dp</dimen> +<!-- Apps view --> + <dimen name="apps_container_inset">24dp</dimen> + <dimen name="apps_grid_view_start_margin">64dp</dimen> + <dimen name="apps_view_section_text_size">26sp</dimen> + <dimen name="apps_view_row_height">76dp</dimen> + <!-- AppsCustomize --> - <dimen name="apps_customize_tab_bar_height">60dp</dimen> - <dimen name="apps_customize_tab_bar_margin_top">8dp</dimen> - <dimen name="app_widget_preview_label_margin_top">8dp</dimen> - <dimen name="app_widget_preview_label_margin_left">@dimen/app_widget_preview_padding_left</dimen> - <dimen name="app_widget_preview_label_margin_right">@dimen/app_widget_preview_padding_right</dimen> + <dimen name="widget_preview_label_margin_top">8dp</dimen> + <dimen name="widget_preview_label_margin_left">@dimen/widget_preview_label_horizontal_padding</dimen> + <dimen name="widget_preview_label_margin_right">@dimen/widget_preview_label_horizontal_padding</dimen> <!-- Cling --> <dimen name="cling_migration_logo_height">400dp</dimen> diff --git a/res/values-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml index 8be996474..68fc1ecaf 100644 --- a/res/values-sw720dp/dimens.xml +++ b/res/values-sw720dp/dimens.xml @@ -21,11 +21,6 @@ <dimen name="toolbar_button_vertical_padding">8dip</dimen> <dimen name="toolbar_button_horizontal_padding">8dip</dimen> - <!-- When dragging items on the workspace, the number of dps by which the position of - the drag view should be offset from the position of the original view. --> - <dimen name="dragViewOffsetX">0dp</dimen> - <dimen name="dragViewOffsetY">0dp</dimen> - <!-- Cling --> <dimen name="cling_migration_content_margin">96dp</dimen> <dimen name="cling_migration_content_width">320dp</dimen> diff --git a/res/values-v17/styles.xml b/res/values-v17/styles.xml index 11d2a1f82..3589e80ac 100644 --- a/res/values-v17/styles.xml +++ b/res/values-v17/styles.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <style name="PagedViewWidgetImageView"> - <item name="android:paddingStart">@dimen/app_widget_preview_padding_left</item> + <style name="WidgetImageView"> + <item name="android:paddingStart">@dimen/widget_preview_horizontal_padding</item> </style> </resources> diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 3331cdec4..a1f28452a 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -18,6 +18,16 @@ <resources> + <!-- BubbleTextView specific attributes. --> + <declare-styleable name="BubbleTextView"> + <attr name="layoutHorizontal" format="boolean" /> + <attr name="iconSizeOverride" format="dimension" /> + <attr name="iconPaddingOverride" format="dimension" /> + <attr name="textSizeOverride" format="dimension" /> + <attr name="deferShadowGeneration" format="boolean" /> + <attr name="customShadows" format="boolean" /> + </declare-styleable> + <!-- Page Indicator specific attributes. --> <declare-styleable name="PageIndicator"> <attr name="windowSize" format="integer" /> @@ -80,11 +90,6 @@ <attr name="pageIndicator" format="reference" /> </declare-styleable> - <declare-styleable name="BubbleTextView"> - <!-- A spacing override for the icons within a page --> - <attr name="customShadows" format="boolean" /> - </declare-styleable> - <!-- AppsCustomizePagedView specific attributes. These attributes are used to customize an AppsCustomizePagedView in xml files. --> <declare-styleable name="AppsCustomizePagedView"> diff --git a/res/values/colors.xml b/res/values/colors.xml index 2daf9fe12..3a06bd95a 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -36,4 +36,10 @@ <color name="outline_color">#FFFFFFFF</color> <color name="widget_text_panel">#FF374248</color> + <!-- Apps view --> + <color name="apps_view_scrollbar_thumb_color">#009688</color> + <color name="apps_view_section_text_color">#009688</color> + + <!-- Widgetss view --> + <color name="widgets_view_section_text_color">#009688</color> </resources> diff --git a/res/values/config.xml b/res/values/config.xml index cbec51216..1acace6f9 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -104,4 +104,5 @@ <item type="id" name="action_uninstall" /> <item type="id" name="action_info" /> <item type="id" name="action_add_to_workspace" /> + <item type="id" name="action_move" /> </resources> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 54689ec25..cad60fbae 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -46,19 +46,25 @@ <dimen name="toolbar_button_vertical_padding">4dip</dimen> <dimen name="toolbar_button_horizontal_padding">12dip</dimen> +<!-- Apps view --> + <dimen name="apps_container_width">0dp</dimen> + <dimen name="apps_container_height">0dp</dimen> + <dimen name="apps_container_inset">8dp</dimen> + <!-- Note: This needs to match the fixed insets for the search box --> + <dimen name="apps_container_fixed_bounds_inset">8dp</dimen> + <dimen name="apps_grid_view_start_margin">52dp</dimen> + <dimen name="apps_view_row_height">64dp</dimen> + <dimen name="apps_view_section_text_size">24sp</dimen> + <dimen name="apps_view_fast_scroll_bar_width">6dp</dimen> + <dimen name="apps_view_fast_scroll_bar_min_height">64dp</dimen> + <dimen name="apps_view_fast_scroll_scrubber_touch_inset">-16dp</dimen> + <dimen name="apps_view_fast_scroll_popup_size">64dp</dimen> + <dimen name="apps_view_fast_scroll_text_size">40dp</dimen> + <!-- AllApps/Customize/AppsCustomize --> - <!-- The height of the tab bar - if this changes, we should update the - external icon width/height above to compensate --> - <dimen name="apps_customize_tab_bar_height">52dp</dimen> - <dimen name="apps_customize_tab_bar_margin_top">0dp</dimen> <dimen name="app_icon_size">48dp</dimen> <dimen name="apps_customize_horizontal_padding">0dp</dimen> - <!-- The AppsCustomize page indicator --> - <dimen name="apps_customize_page_indicator_height">12dp</dimen> - <dimen name="apps_customize_page_indicator_margin">4dp</dimen> - <dimen name="apps_customize_page_indicator_offset">16dp</dimen> - <!-- Drag padding to add to the bottom of drop targets --> <dimen name="drop_target_drag_padding">14dp</dimen> <dimen name="drop_target_text_size">14sp</dimen> @@ -68,20 +74,24 @@ or right while you're dragging. --> <dimen name="scroll_zone">20dp</dimen> - <!-- When dragging items on the workspace, the number of dps by which the position of - the drag view should be offset from the position of the original view. --> - <dimen name="dragViewOffsetX">0dp</dimen> - <dimen name="dragViewOffsetY">0dp</dimen> <!-- When dragging an item, how much bigger (fixed dps) the dragged view should be. If 0, it will not be scaled at all. --> <dimen name="dragViewScale">12dp</dimen> - <!-- Padding applied to AppWidget previews --> - <dimen name="app_widget_preview_padding_left">16dp</dimen> - <dimen name="app_widget_preview_padding_right">16dp</dimen> - <dimen name="app_widget_preview_padding_top">32dp</dimen> - <dimen name="app_widget_preview_label_vertical_padding">8dp</dimen> - <dimen name="app_widget_preview_label_horizontal_padding">8dp</dimen> +<!-- Widget tray --> + <dimen name="widget_container_inset">8dp</dimen> + <dimen name="widget_preview_size">140dp</dimen> + <dimen name="widget_preview_padding_top">8dp</dimen> + <dimen name="widget_preview_label_vertical_padding">8dp</dimen> + <dimen name="widget_preview_label_horizontal_padding">8dp</dimen> + <dimen name="widget_preview_horizontal_padding">8dp</dimen> + + <dimen name="widget_section_height">52dp</dimen> + <dimen name="widget_section_icon_padding">8dp</dimen> + + <!-- Equation: widget_preview_size + 2 * widget_preview_padding_horizontal --> + <dimen name="widget_preview_container_width">156dp</dimen> + <dimen name="widget_cell_height">160dp</dimen> <!-- Padding applied to shortcut previews --> <dimen name="shortcut_preview_padding_left">0dp</dimen> diff --git a/res/values/strings.xml b/res/values/strings.xml index 8b7e6c199..52306e30e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -38,6 +38,8 @@ <string name="uid_name">Android Core Apps</string> <!-- Default folder name --> <string name="folder_name"></string> + <!-- Work folder name --> + <string name="work_folder_name">Work</string> <!-- Displayed when user selects a shortcut for an app that was uninstalled [CHAR_LIMIT=none]--> <string name="activity_not_found">App isn\'t installed.</string> <!-- Displayed when user selects a shortcut for an app that is current not available [CHAR_LIMIT=none]--> @@ -71,6 +73,14 @@ drop if there are multiple choices. [CHAR_LIMIT=35] --> <string name="external_drop_widget_pick_title">Choose widget to create</string> + <!-- Apps view --> + <!-- Search bar text in the apps view. [CHAR_LIMIT=50] --> + <string name="apps_view_search_bar_hint">Search Apps</string> + <!-- Loading apps text. [CHAR_LIMIT=50] --> + <string name="loading_apps_message">Loading Apps...</string> + <!-- No-search-results text. [CHAR_LIMIT=50] --> + <string name="apps_view_no_search_results">No Apps found matching \"<xliff:g id="query" example="Android">%1$s</xliff:g>\"</string> + <!-- Folders --> <skip /> <!-- Label of Folder name field in Rename folder dialog box --> @@ -81,6 +91,8 @@ <string name="rename_action">OK</string> <!-- Buttons in Rename folder dialog box --> <string name="cancel_action">Cancel</string> + <!-- Label for button to sort folder contents. [CHAR_LIMIT=10] --> + <string name="sort_alphabetical">A-Z</string> <!-- Shortcuts --> <skip /> @@ -103,8 +115,6 @@ s --> <string name="invalid_hotseat_item">This widget is too large for the Favorites tray</string> <!-- Message displayed when a shortcut is created by an external application --> <string name="shortcut_installed">Shortcut \"<xliff:g id="name" example="Browser">%s</xliff:g>\" created.</string> - <!-- Message displayed when a shortcut is uninstalled by an external application --> - <string name="shortcut_uninstalled">Shortcut \"<xliff:g id="name" example="Browser">%s</xliff:g>\" was removed.</string> <!-- Message displayed when an external application attemps to create a shortcut that already exists --> <string name="shortcut_duplicate">Shortcut \"<xliff:g id="name" example="Browser">%s</xliff:g>\" already exists.</string> @@ -166,11 +176,6 @@ s --> <string name="permdesc_install_shortcut">Allows an app to add shortcuts without user intervention.</string> <!-- Permission short label --> - <string name="permlab_uninstall_shortcut">uninstall shortcuts</string> - <!-- Permission description --> - <string name="permdesc_uninstall_shortcut">Allows the app to remove - shortcuts without user intervention.</string> - <!-- Permission short label --> <string name="permlab_read_settings">read Home settings and shortcuts</string> <!-- Permission description --> <string name="permdesc_read_settings">Allows the app to read the settings and @@ -303,6 +308,34 @@ s --> </string> <!-- Strings for accessibility actions --> - <!-- Accessibility action to add an app to workspace. [CHAR_LIMIT=30] --> + <!-- Accessibility action to add an app to workspace. [CHAR_LIMIT=30] [DO NOT TRANSLATE] --> <string name="action_add_to_workspace">Add To Workspace</string> + + <!-- Accessibility confirmation for item added to workspace [DO NOT TRANSLATE] --> + <string name="item_added_to_workspace">Item added to workspace</string> + + <!-- Accessibility confirmation for item removed [DO NOT TRANSLATE] --> + <string name="item_removed_from_workspace">Item removed from workspace</string> + + <!-- Accessibility action to move an item on the workspace. [CHAR_LIMIT=30] [DO NOT TRANSLATE] --> + <string name="action_move">Move Item</string> + + <!-- Accessibility description to move item to empty cell. [DO NOT TRANSLATE] --> + <string name="move_to_empty_cell">Move to empty cell <xliff:g id="number" example="1">%1$s</xliff:g>, <xliff:g id="number" example="1">%2$s</xliff:g></string> + + <!-- Accessibility confirmation for item move [DO NOT TRANSLATE]--> + <string name="item_moved">Item moved</string> + + <!-- Accessibility description to move item into an existing folder. [DO NOT TRANSLATE]--> + <string name="add_to_folder">Add to folder: <xliff:g id="name" example="Games">%1$s</xliff:g></string> + + <!-- Accessibility confirmation for item added to folder [DO NOT TRANSLATE] --> + <string name="added_to_folder">Item added to folder</string> + + <!-- Accessibility description to create folder with another item. [DO NOT TRANSLATE] --> + <string name="create_folder_with">Create folder with: <xliff:g id="name" example="Game">%1$s</xliff:g></string> + + <!-- Accessibility confirmation for folder created [DO NOT TRANSLATE] --> + <string name="folder_created">Folder created</string> + </resources> diff --git a/res/values/styles.xml b/res/values/styles.xml index 77798f174..16d4cbeed 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -91,8 +91,8 @@ </style> <!-- Overridden in device overlays --> - <style name="PagedViewWidgetImageView"> - <item name="android:paddingLeft">@dimen/app_widget_preview_padding_left</item> + <style name="WidgetImageView"> + <item name="android:paddingLeft">@dimen/widget_preview_horizontal_padding</item> </style> </resources> diff --git a/res/xml/app_target_browser.xml b/res/xml/app_target_browser.xml new file mode 100644 index 000000000..d7c3ed5fd --- /dev/null +++ b/res/xml/app_target_browser.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> + +<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" > + + <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_BROWSER;end" /> + <favorite launcher:uri="http://www.example.com/" /> + +</resolve>
\ No newline at end of file diff --git a/res/xml/app_target_camera.xml b/res/xml/app_target_camera.xml new file mode 100644 index 000000000..f65a2b168 --- /dev/null +++ b/res/xml/app_target_camera.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> + +<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" > + + <favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" /> + <favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" /> + +</resolve>
\ No newline at end of file diff --git a/res/xml/app_target_email.xml b/res/xml/app_target_email.xml new file mode 100644 index 000000000..44f0a407d --- /dev/null +++ b/res/xml/app_target_email.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> + +<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" > + + <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" /> + <favorite launcher:uri="mailto:" /> + +</resolve>
\ No newline at end of file diff --git a/res/xml/app_target_gallery.xml b/res/xml/app_target_gallery.xml new file mode 100644 index 000000000..c9d34924a --- /dev/null +++ b/res/xml/app_target_gallery.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> + +<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" > + + <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" /> + <favorite launcher:uri="#Intent;type=images/*;end" /> + +</resolve>
\ No newline at end of file diff --git a/res/xml/app_target_messenger.xml b/res/xml/app_target_messenger.xml new file mode 100644 index 000000000..278eb5ca8 --- /dev/null +++ b/res/xml/app_target_messenger.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> + +<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" > + + <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MESSAGING;end" /> + <favorite launcher:uri="sms:" /> + <favorite launcher:uri="smsto:" /> + <favorite launcher:uri="mms:" /> + <favorite launcher:uri="mmsto:" /> + +</resolve>
\ No newline at end of file diff --git a/res/xml/app_target_phone.xml b/res/xml/app_target_phone.xml new file mode 100644 index 000000000..5d6ca316e --- /dev/null +++ b/res/xml/app_target_phone.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> + +<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" > + + <favorite launcher:uri="#Intent;action=android.intent.action.DIAL;end" /> + <favorite launcher:uri="tel:123" /> + <favorite launcher:uri="#Intent;action=android.intent.action.CALL_BUTTON;end" /> + +</resolve>
\ No newline at end of file diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java index 72c6693b3..dd646bb22 100644 --- a/src/com/android/launcher3/AllAppsList.java +++ b/src/com/android/launcher3/AllAppsList.java @@ -98,14 +98,14 @@ class AllAppsList { user); for (LauncherActivityInfoCompat info : matches) { - add(new AppInfo(context, info, user, mIconCache, null)); + add(new AppInfo(context, info, user, mIconCache)); } } /** * Remove the apps for the given apk identified by packageName. */ - public void removePackage(String packageName, UserHandleCompat user, boolean clearCache) { + 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); @@ -115,9 +115,6 @@ class AllAppsList { data.remove(i); } } - if (clearCache) { - mIconCache.remove(packageName, user); - } } /** @@ -137,7 +134,6 @@ class AllAppsList { && packageName.equals(component.getPackageName())) { if (!findActivity(matches, component)) { removed.add(applicationInfo); - mIconCache.remove(component, user); data.remove(i); } } @@ -150,10 +146,9 @@ class AllAppsList { info.getComponentName().getPackageName(), user, info.getComponentName().getClassName()); if (applicationInfo == null) { - add(new AppInfo(context, info, user, mIconCache, null)); + add(new AppInfo(context, info, user, mIconCache)); } else { - mIconCache.remove(applicationInfo.componentName, user); - mIconCache.getTitleAndIcon(applicationInfo, info, null); + mIconCache.getTitleAndIcon(applicationInfo, info, true /* useLowResIcon */); modified.add(applicationInfo); } } diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java new file mode 100644 index 000000000..2ee5a62ed --- /dev/null +++ b/src/com/android/launcher3/AlphabeticalAppsList.java @@ -0,0 +1,316 @@ +package com.android.launcher3; + +import android.content.ComponentName; +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import com.android.launcher3.compat.AlphabeticIndexCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + + +/** + * A private class to manage access to an app name comparator. + */ +class AppNameComparator { + private UserManagerCompat mUserManager; + private Comparator<AppInfo> mAppNameComparator; + private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>(); + + public AppNameComparator(Context context) { + final Collator collator = Collator.getInstance(); + mUserManager = UserManagerCompat.getInstance(context); + mAppNameComparator = new Comparator<AppInfo>() { + public final int compare(AppInfo a, AppInfo b) { + // Order by the title + int result = collator.compare(a.title.toString().trim(), + b.title.toString().trim()); + if (result == 0) { + // If two apps have the same title, then order by the component name + result = a.componentName.compareTo(b.componentName); + if (result == 0) { + // If the two apps are the same component, then prioritize by the order that + // the app user was created (prioritizing the main user's apps) + if (UserHandleCompat.myUserHandle().equals(a.user)) { + return -1; + } else { + Long aUserSerial = getAndCacheUserSerial(a.user); + Long bUserSerial = getAndCacheUserSerial(b.user); + return aUserSerial.compareTo(bUserSerial); + } + } + } + return result; + } + }; + } + + /** + * Returns a locale-aware comparator that will alphabetically order a list of applications. + */ + public Comparator<AppInfo> getComparator() { + // Clear the user serial cache so that we get serials as needed in the comparator + mUserSerialCache.clear(); + return mAppNameComparator; + } + + /** + * Returns the user serial for this user, using a cached serial if possible. + */ + private Long getAndCacheUserSerial(UserHandleCompat user) { + Long userSerial = mUserSerialCache.get(user); + if (userSerial == null) { + userSerial = mUserManager.getSerialNumberForUser(user); + mUserSerialCache.put(user, userSerial); + } + return userSerial; + } +} + +/** + * The alphabetically sorted list of applications. + */ +public class AlphabeticalAppsList { + + /** + * Info about a section in the alphabetic list + */ + public static class SectionInfo { + // The name of this section + public String sectionName; + // The number of applications in this section + public int numAppsInSection; + // The first app AdapterItem for this section + public AdapterItem firstAppItem; + + public SectionInfo(String name) { + sectionName = name; + } + } + + /** + * Info about a particular adapter item (can be either section or app) + */ + public static class AdapterItem { + // The index of this adapter item in the list + public int position; + // Whether or not the item at this adapter position is a section or not + public boolean isSectionHeader; + // The name of this section, or the section that this app is contained in + public String sectionName; + // The associated AppInfo, or null if this adapter item is a section + public AppInfo appInfo; + // The index of this app (not including sections), or -1 if this adapter item is a section + public int appIndex; + + public static AdapterItem asSection(int pos, String name) { + AdapterItem item = new AdapterItem(); + item.position = pos; + item.isSectionHeader = true; + item.sectionName = name; + item.appInfo = null; + item.appIndex = -1; + return item; + } + + public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo, int appIndex) { + AdapterItem item = new AdapterItem(); + item.position = pos; + item.isSectionHeader = false; + item.sectionName = sectionName; + item.appInfo = appInfo; + item.appIndex = appIndex; + return item; + } + } + + /** + * A filter interface to limit the set of applications in the apps list. + */ + public interface Filter { + public boolean retainApp(AppInfo info, String sectionName); + } + + private List<AppInfo> mApps = new ArrayList<>(); + private List<AppInfo> mFilteredApps = new ArrayList<>(); + private List<AdapterItem> mSectionedFilteredApps = new ArrayList<>(); + private List<SectionInfo> mSections = new ArrayList<>(); + private RecyclerView.Adapter mAdapter; + private Filter mFilter; + private AlphabeticIndexCompat mIndexer; + private AppNameComparator mAppNameComparator; + + public AlphabeticalAppsList(Context context) { + mIndexer = new AlphabeticIndexCompat(context); + mAppNameComparator = new AppNameComparator(context); + } + + /** + * Sets the adapter to notify when this dataset changes. + */ + public void setAdapter(RecyclerView.Adapter adapter) { + mAdapter = adapter; + } + + /** + * Returns sections of all the current filtered applications. + */ + public List<SectionInfo> getSections() { + return mSections; + } + + /** + * Returns the current filtered list of applications broken down into their sections. + */ + public List<AdapterItem> getAdapterItems() { + return mSectionedFilteredApps; + } + + /** + * Returns the number of applications in this list. + */ + public int getSize() { + return mFilteredApps.size(); + } + + /** + * Returns whether there are no filtered results. + */ + public boolean hasNoFilteredResults() { + return (mFilter != null) && mFilteredApps.isEmpty(); + } + + /** + * Sets the current filter for this list of apps. + */ + public void setFilter(Filter f) { + mFilter = f; + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); + } + + /** + * Sets the current set of apps. + */ + public void setApps(List<AppInfo> apps) { + Collections.sort(apps, mAppNameComparator.getComparator()); + mApps.clear(); + mApps.addAll(apps); + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); + } + + /** + * Adds new apps to the list. + */ + public void addApps(List<AppInfo> apps) { + // We add it in place, in alphabetical order + for (AppInfo info : apps) { + addApp(info); + } + } + + /** + * Updates existing apps in the list + */ + public void updateApps(List<AppInfo> apps) { + for (AppInfo info : apps) { + int index = mApps.indexOf(info); + if (index != -1) { + mApps.set(index, info); + onAppsUpdated(); + mAdapter.notifyItemChanged(index); + } else { + addApp(info); + } + } + } + + /** + * Removes some apps from the list. + */ + public void removeApps(List<AppInfo> apps) { + for (AppInfo info : apps) { + int removeIndex = findAppByComponent(mApps, info); + if (removeIndex != -1) { + mApps.remove(removeIndex); + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); + } + } + } + + /** + * Finds the index of an app given a target AppInfo. + */ + private int findAppByComponent(List<AppInfo> apps, AppInfo targetInfo) { + ComponentName targetComponent = targetInfo.intent.getComponent(); + int length = apps.size(); + for (int i = 0; i < length; ++i) { + AppInfo info = apps.get(i); + if (info.user.equals(info.user) + && info.intent.getComponent().equals(targetComponent)) { + return i; + } + } + return -1; + } + + /** + * Implementation to actually add an app to the alphabetic list + */ + private void addApp(AppInfo info) { + int index = Collections.binarySearch(mApps, info, mAppNameComparator.getComparator()); + if (index < 0) { + mApps.add(-(index + 1), info); + onAppsUpdated(); + mAdapter.notifyDataSetChanged(); + } + } + + /** + * Updates internals when the set of apps are updated. + */ + private void onAppsUpdated() { + // Recreate the filtered and sectioned apps (for convenience for the grid layout) + mFilteredApps.clear(); + mSections.clear(); + mSectionedFilteredApps.clear(); + SectionInfo lastSectionInfo = null; + int position = 0; + int appIndex = 0; + for (AppInfo info : mApps) { + String sectionName = mIndexer.computeSectionName(info.title.toString().trim()); + + // Check if we want to retain this app + if (mFilter != null && !mFilter.retainApp(info, sectionName)) { + continue; + } + + // Create a new section if necessary + if (lastSectionInfo == null || !lastSectionInfo.sectionName.equals(sectionName)) { + lastSectionInfo = new SectionInfo(sectionName); + mSections.add(lastSectionInfo); + + // Create a new section item + AdapterItem sectionItem = AdapterItem.asSection(position++, sectionName); + mSectionedFilteredApps.add(sectionItem); + } + + // Create an app item + AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++); + lastSectionInfo.numAppsInSection++; + if (lastSectionInfo.firstAppItem == null) { + lastSectionInfo.firstAppItem = appItem; + } + mSectionedFilteredApps.add(appItem); + mFilteredApps.add(info); + } + } +} diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index a66bac08a..7c6b0664c 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -19,19 +19,15 @@ 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.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 com.android.launcher3.compat.UserManagerCompat; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; /** * Represents an app in AllAppsView. @@ -47,14 +43,19 @@ public class AppInfo extends ItemInfo { /** * A bitmap version of the application icon. */ - Bitmap iconBitmap; + public Bitmap iconBitmap; + + /** + * Indicates whether we're using a low res icon + */ + boolean usingLowResIcon; /** * The time at which the app was first installed. */ long firstInstallTime; - ComponentName componentName; + public ComponentName componentName; static final int DOWNLOADED_FLAG = 1; static final int UPDATED_SYSTEM_APP_FLAG = 2; @@ -77,13 +78,13 @@ public class AppInfo extends ItemInfo { * Must not hold the Context. */ public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user, - IconCache iconCache, HashMap<Object, CharSequence> labelCache) { + IconCache iconCache) { this.componentName = info.getComponentName(); this.container = ItemInfo.NO_ID; flags = initFlags(info); firstInstallTime = info.getFirstInstallTime(); - iconCache.getTitleAndIcon(this, info, labelCache); + iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */); intent = makeLaunchIntent(context, info, user); this.user = user; } @@ -120,12 +121,15 @@ public class AppInfo extends ItemInfo { + " user=" + user + ")"; } + /** + * Helper method used for debugging. + */ public static void dumpApplicationInfoList(String tag, String label, ArrayList<AppInfo> list) { Log.d(tag, label + " size=" + list.size()); for (AppInfo info: list) { - Log.d(tag, " title=\"" + info.title + "\" iconBitmap=" - + info.iconBitmap + " firstInstallTime=" - + info.firstInstallTime); + Log.d(tag, " title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap + + " firstInstallTime=" + info.firstInstallTime + + " componentName=" + info.componentName.getPackageName()); } } diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java index 880aaf1ec..5e7a012d2 100644 --- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java +++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java @@ -90,5 +90,10 @@ public class AppWidgetsRestoredReceiver extends BroadcastReceiver { } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } + + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app != null) { + app.reloadWorkspace(); + } } } diff --git a/src/com/android/launcher3/AppsContainerRecyclerView.java b/src/com/android/launcher3/AppsContainerRecyclerView.java new file mode 100644 index 000000000..16244ee35 --- /dev/null +++ b/src/com/android/launcher3/AppsContainerRecyclerView.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2015 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.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +import java.util.List; + +/** + * A RecyclerView with custom fastscroll support. This is the main container for the all apps + * icons. + */ +public class AppsContainerRecyclerView extends RecyclerView + implements RecyclerView.OnItemTouchListener { + + private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f; + + private AlphabeticalAppsList mApps; + private int mNumAppsPerRow; + + private Drawable mScrollbar; + private Drawable mFastScrollerBg; + private Rect mVerticalScrollbarBounds = new Rect(); + private boolean mDraggingFastScroller; + private String mFastScrollSectionName; + private Paint mFastScrollTextPaint; + private Rect mFastScrollTextBounds = new Rect(); + private float mFastScrollAlpha; + private int mDownX; + private int mDownY; + private int mLastX; + private int mLastY; + private int mScrollbarWidth; + private int mScrollbarMinHeight; + private int mScrollbarInset; + + public AppsContainerRecyclerView(Context context) { + this(context, null); + } + + public AppsContainerRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr); + + Resources res = context.getResources(); + int fastScrollerSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_popup_size); + mScrollbar = res.getDrawable(R.drawable.apps_list_scrollbar_thumb); + mFastScrollerBg = res.getDrawable(R.drawable.apps_list_fastscroll_bg); + mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize); + mFastScrollTextPaint = new Paint(); + mFastScrollTextPaint.setColor(Color.WHITE); + mFastScrollTextPaint.setAntiAlias(true); + mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize( + R.dimen.apps_view_fast_scroll_text_size)); + mScrollbarWidth = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_bar_width); + mScrollbarMinHeight = + res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_bar_min_height); + mScrollbarInset = + res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_scrubber_touch_inset); + setFastScrollerAlpha(getFastScrollerAlpha()); + } + + /** + * Sets the list of apps in this view, used to determine the fastscroll position. + */ + public void setApps(AlphabeticalAppsList apps) { + mApps = apps; + } + + /** + * Sets the number of apps per row in this recycler view. + */ + public void setNumAppsPerRow(int rowSize) { + mNumAppsPerRow = rowSize; + } + + /** + * Sets the fast scroller alpha. + */ + public void setFastScrollerAlpha(float alpha) { + mFastScrollAlpha = alpha; + invalidateFastScroller(); + } + + /** + * Gets the fast scroller alpha. + */ + public float getFastScrollerAlpha() { + return mFastScrollAlpha; + } + + /** + * Returns the scroll bar width. + */ + public int getScrollbarWidth() { + return mScrollbarWidth; + } + + @Override + protected void onFinishInflate() { + addOnItemTouchListener(this); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + drawVerticalScrubber(canvas); + drawFastScrollerPopup(canvas); + } + + /** + * We intercept the touch handling only to support fast scrolling when initiated from the + * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling. + */ + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { + return handleTouchEvent(ev); + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent ev) { + handleTouchEvent(ev); + } + + /** + * Handles the touch event and determines whether to show the fast scroller (or updates it if + * it is already showing). + */ + private boolean handleTouchEvent(MotionEvent ev) { + ViewConfiguration config = ViewConfiguration.get(getContext()); + + int action = ev.getAction(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + // Keep track of the down positions + mDownX = mLastX = x; + mDownY = mLastY = y; + stopScroll(); + break; + case MotionEvent.ACTION_MOVE: + // Check if we are scrolling + if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) && + Math.abs(y - mDownY) > config.getScaledTouchSlop()) { + getParent().requestDisallowInterceptTouchEvent(true); + mDraggingFastScroller = true; + animateFastScrollerVisibility(true); + } + if (mDraggingFastScroller) { + mLastX = x; + mLastY = y; + + // Scroll to the right position, and update the section name + int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2); + int bottom = getHeight() - getPaddingBottom() - + (mFastScrollerBg.getBounds().height() / 2); + float boundedY = (float) Math.max(top, Math.min(bottom, y)); + mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) / + (bottom - top)); + invalidateFastScroller(); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mDraggingFastScroller = false; + animateFastScrollerVisibility(false); + break; + } + return mDraggingFastScroller; + } + + /** + * Animates the visibility of the fast scroller popup. + */ + private void animateFastScrollerVisibility(boolean visible) { + ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f); + anim.setDuration(visible ? 200 : 150); + anim.start(); + } + + /** + * Returns whether a given point is near the scrollbar. + */ + private boolean isPointNearScrollbar(int x, int y) { + // Check if we are scrolling + updateVerticalScrollbarBounds(); + mVerticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset); + return mVerticalScrollbarBounds.contains(x, y); + } + + /** + * Draws the fast scroller popup. + */ + private void drawFastScrollerPopup(Canvas canvas) { + if (mFastScrollAlpha > 0f) { + int x; + int y; + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + + // Calculate the position for the fast scroller popup + Rect bgBounds = mFastScrollerBg.getBounds(); + if (isRtl) { + x = getPaddingLeft() + getScrollBarSize(); + } else { + x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width(); + } + y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height()); + y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() - + bgBounds.height())); + + // Draw the fast scroller popup + int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.translate(x, y); + mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255)); + mFastScrollerBg.draw(canvas); + mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255)); + mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0, + mFastScrollSectionName.length(), mFastScrollTextBounds); + canvas.drawText(mFastScrollSectionName, + (bgBounds.width() - mFastScrollTextBounds.width()) / 2, + bgBounds.height() - (bgBounds.height() - mFastScrollTextBounds.height()) / 2, + mFastScrollTextPaint); + canvas.restoreToCount(restoreCount); + } + } + + /** + * Draws the vertical scrollbar. + */ + private void drawVerticalScrubber(Canvas canvas) { + updateVerticalScrollbarBounds(); + + // Draw the scroll bar + int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.translate(mVerticalScrollbarBounds.left, mVerticalScrollbarBounds.top); + mScrollbar.setBounds(0, 0, mScrollbarWidth, mVerticalScrollbarBounds.height()); + mScrollbar.draw(canvas); + canvas.restoreToCount(restoreCount); + } + + /** + * Invalidates the fast scroller popup. + */ + private void invalidateFastScroller() { + invalidate(getWidth() - getPaddingRight() - getScrollBarSize() - + mFastScrollerBg.getIntrinsicWidth(), 0, getWidth(), getHeight()); + } + + /** + * Maps the progress (from 0..1) to the position that should be visible + */ + private String scrollToPositionAtProgress(float progress) { + List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections(); + if (sections.isEmpty()) { + return ""; + } + + // Find the position of the first application in the section that contains the row at the + // current progress + int rowAtProgress = (int) (progress * getNumRows()); + int rowCount = 0; + AlphabeticalAppsList.SectionInfo lastSectionInfo = null; + for (AlphabeticalAppsList.SectionInfo section : sections) { + int numRowsInSection = (int) Math.ceil((float) section.numAppsInSection / mNumAppsPerRow); + if (rowCount + numRowsInSection >= rowAtProgress) { + lastSectionInfo = section; + break; + } + rowCount += numRowsInSection; + } + int position = mApps.getAdapterItems().indexOf(lastSectionInfo.firstAppItem); + + // Scroll the position into view, anchored at the top of the screen if possible. We call the + // scroll method on the LayoutManager directly since it is not exposed by RecyclerView. + LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); + stopScroll(); + layoutManager.scrollToPositionWithOffset(position, 0); + + // Return the section name of the row + return mApps.getAdapterItems().get(position).sectionName; + } + + /** + * Returns the bounds for the scrollbar. + */ + private void updateVerticalScrollbarBounds() { + // Skip early if there are no items + if (mApps.getAdapterItems().isEmpty()) { + mVerticalScrollbarBounds.setEmpty(); + return; + } + + // Find the index and height of the first visible row (all rows have the same height) + int x; + int y; + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + int rowIndex = -1; + int rowTopOffset = -1; + int rowHeight = -1; + int rowCount = getNumRows(); + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + int position = getChildPosition(child); + if (position != NO_POSITION) { + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); + if (!item.isSectionHeader) { + rowIndex = findRowForAppIndex(item.appIndex); + rowTopOffset = getLayoutManager().getDecoratedTop(child); + rowHeight = child.getHeight(); + break; + } + } + } + + if (rowIndex != -1) { + int height = getHeight() - getPaddingTop() - getPaddingBottom(); + int totalScrollHeight = rowCount * rowHeight; + if (totalScrollHeight > height) { + int scrollbarHeight = Math.max(mScrollbarMinHeight, + (int) (height / ((float) totalScrollHeight / height))); + + // Calculate the position and size of the scroll bar + if (isRtl) { + x = getPaddingLeft(); + } else { + x = getWidth() - getPaddingRight() - mScrollbarWidth; + } + + // To calculate the offset, we compute the percentage of the total scrollable height + // that the user has already scrolled and then map that to the scroll bar bounds + int availableY = totalScrollHeight - height; + int availableScrollY = height - scrollbarHeight; + y = (rowIndex * rowHeight) - rowTopOffset; + y = getPaddingTop() + + (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY); + + mVerticalScrollbarBounds.set(x, y, x + mScrollbarWidth, y + scrollbarHeight); + return; + } + } + mVerticalScrollbarBounds.setEmpty(); + } + + /** + * Returns the row index for a given position in the list. + */ + private int findRowForAppIndex(int position) { + List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections(); + int appIndex = 0; + int rowCount = 0; + for (AlphabeticalAppsList.SectionInfo info : sections) { + int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow); + if (appIndex + info.numAppsInSection > position) { + return rowCount + ((position - appIndex) / mNumAppsPerRow); + } + appIndex += info.numAppsInSection; + rowCount += numRowsInSection; + } + return appIndex; + } + + /** + * Returns the total number of rows in the list. + */ + private int getNumRows() { + List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections(); + int rowCount = 0; + for (AlphabeticalAppsList.SectionInfo info : sections) { + int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow); + rowCount += numRowsInSection; + } + return rowCount; + } +} diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java new file mode 100644 index 000000000..b24714505 --- /dev/null +++ b/src/com/android/launcher3/AppsContainerView.java @@ -0,0 +1,549 @@ +/* + * Copyright (C) 2015 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.Point; +import android.graphics.Rect; +import android.graphics.drawable.InsetDrawable; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.util.Thunk; + +import java.util.List; + + +/** + * The all apps list view container. + */ +public class AppsContainerView extends FrameLayout implements DragSource, Insettable, TextWatcher, + TextView.OnEditorActionListener, LauncherTransitionable, View.OnTouchListener, + View.OnLongClickListener { + + private static final boolean ALLOW_SINGLE_APP_LAUNCH = true; + + private static final int GRID_LAYOUT = 0; + private static final int LIST_LAYOUT = 1; + private static final int USE_LAYOUT = GRID_LAYOUT; + + @Thunk Launcher mLauncher; + @Thunk AlphabeticalAppsList mApps; + private RecyclerView.Adapter mAdapter; + private RecyclerView.LayoutManager mLayoutManager; + private RecyclerView.ItemDecoration mItemDecoration; + + private LinearLayout mContentView; + @Thunk AppsContainerRecyclerView mAppsRecyclerView; + private EditText mSearchBarView; + + private int mNumAppsPerRow; + private Point mLastTouchDownPos = new Point(-1, -1); + private Point mLastTouchPos = new Point(); + private Rect mInsets = new Rect(); + private Rect mFixedBounds = new Rect(); + private int mContentMarginStart; + // Normal container insets + private int mContainerInset; + // Fixed bounds container insets + private int mFixedBoundsContainerInset; + + public AppsContainerView(Context context) { + this(context, null); + } + + public AppsContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + Resources res = context.getResources(); + + mContainerInset = context.getResources().getDimensionPixelSize( + R.dimen.apps_container_inset); + mFixedBoundsContainerInset = context.getResources().getDimensionPixelSize( + R.dimen.apps_container_fixed_bounds_inset); + mLauncher = (Launcher) context; + mApps = new AlphabeticalAppsList(context); + if (USE_LAYOUT == GRID_LAYOUT) { + mNumAppsPerRow = grid.appsViewNumCols; + AppsGridAdapter adapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this, + mLauncher, this); + adapter.setEmptySearchText(res.getString(R.string.loading_apps_message)); + mLayoutManager = adapter.getLayoutManager(context); + mItemDecoration = adapter.getItemDecoration(); + mAdapter = adapter; + mContentMarginStart = adapter.getContentMarginStart(); + } else if (USE_LAYOUT == LIST_LAYOUT) { + mNumAppsPerRow = 1; + AppsListAdapter adapter = new AppsListAdapter(context, mApps, this, mLauncher, this); + adapter.setEmptySearchText(res.getString(R.string.loading_apps_message)); + mLayoutManager = adapter.getLayoutManager(context); + mAdapter = adapter; + } + mApps.setAdapter(mAdapter); + } + + /** + * Sets the current set of apps. + */ + public void setApps(List<AppInfo> apps) { + mApps.setApps(apps); + } + + /** + * Adds new apps to the list. + */ + public void addApps(List<AppInfo> apps) { + mApps.addApps(apps); + } + + /** + * Updates existing apps in the list + */ + public void updateApps(List<AppInfo> apps) { + mApps.updateApps(apps); + } + + /** + * Removes some apps from the list. + */ + public void removeApps(List<AppInfo> apps) { + mApps.removeApps(apps); + } + + /** + * Hides the search bar + */ + public void hideSearchBar() { + mSearchBarView.setVisibility(View.GONE); + updateBackgrounds(); + updatePaddings(); + } + + /** + * Scrolls this list view to the top. + */ + public void scrollToTop() { + mAppsRecyclerView.scrollToPosition(0); + } + + /** + * Returns the content view used for the launcher transitions. + */ + public View getContentView() { + return mContentView; + } + + /** + * Returns the reveal view used for the launcher transitions. + */ + public View getRevealView() { + return findViewById(R.id.apps_view_transition_overlay); + } + + @Override + protected void onFinishInflate() { + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + if (USE_LAYOUT == GRID_LAYOUT) { + ((AppsGridAdapter) mAdapter).setRtl(isRtl); + } + + // Work around the search box getting first focus and showing the cursor by + // proxying the focus from the content view to the recycler view directly + mContentView = (LinearLayout) findViewById(R.id.apps_list); + mContentView.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (v == mContentView && hasFocus) { + mAppsRecyclerView.requestFocus(); + } + } + }); + mSearchBarView = (EditText) findViewById(R.id.app_search_box); + if (mSearchBarView != null) { + mSearchBarView.addTextChangedListener(this); + mSearchBarView.setOnEditorActionListener(this); + } + mAppsRecyclerView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view); + mAppsRecyclerView.setApps(mApps); + mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow); + mAppsRecyclerView.setLayoutManager(mLayoutManager); + mAppsRecyclerView.setAdapter(mAdapter); + mAppsRecyclerView.setHasFixedSize(true); + if (mItemDecoration != null) { + mAppsRecyclerView.addItemDecoration(mItemDecoration); + } + updateBackgrounds(); + updatePaddings(); + } + + @Override + public void setInsets(Rect insets) { + mInsets.set(insets); + updatePaddings(); + } + + /** + * Sets the fixed bounds for this Apps view. + */ + public void setFixedBounds(Context context, Rect fixedBounds) { + if (!fixedBounds.isEmpty() && !fixedBounds.equals(mFixedBounds)) { + // Update the number of items in the grid + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + if (grid.updateAppsViewNumCols(context.getResources(), fixedBounds.width())) { + mNumAppsPerRow = grid.appsViewNumCols; + mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow); + if (USE_LAYOUT == GRID_LAYOUT) { + ((AppsGridAdapter) mAdapter).setNumAppsPerRow(mNumAppsPerRow); + } + } + + mFixedBounds.set(fixedBounds); + } + updateBackgrounds(); + updatePaddings(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return handleTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return handleTouchEvent(ev); + } + + @Override + public boolean onTouch(View v, MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mLastTouchPos.set((int) ev.getX(), (int) ev.getY()); + break; + } + return false; + } + + @Override + public boolean onLongClick(View v) { + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return false; + // When we have exited all apps or are in transition, disregard long clicks + if (!mLauncher.isAppsViewVisible() || + mLauncher.getWorkspace().isSwitchingState()) return false; + // Return if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return false; + + // Start the drag + mLauncher.getWorkspace().beginDragShared(v, mLastTouchPos, this, false); + + // We delay entering spring-loaded mode slightly to make sure the UI + // thready is free of any work. + postDelayed(new Runnable() { + @Override + public void run() { + // We don't enter spring-loaded mode if the drag has been cancelled + if (mLauncher.getDragController().isDragging()) { + // Go into spring loaded mode (must happen before we startDrag()) + mLauncher.enterSpringLoadedDragMode(); + } + } + }, 150); + + return false; + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + @Override + public boolean supportsAppInfoDropTarget() { + return true; + } + + @Override + public boolean supportsDeleteDropTarget() { + return false; + } + + @Override + public float getIntrinsicIconScaleFactor() { + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + return (float) grid.allAppsIconSizePx / grid.iconSizePx; + } + + @Override + public void onFlingToDeleteCompleted() { + // We just dismiss the drag when we fling, so cleanup here + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + mLauncher.unlockScreenOrientation(false); + } + + @Override + public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, + boolean success) { + if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && + !(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.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + } + mLauncher.unlockScreenOrientation(false); + + // Display an error message if the drag failed due to there not being enough space on the + // target layout we were dropping on. + if (!success) { + boolean showOutOfSpaceMessage = false; + if (target instanceof Workspace) { + int currentScreen = mLauncher.getCurrentWorkspaceScreen(); + Workspace workspace = (Workspace) target; + CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); + ItemInfo itemInfo = (ItemInfo) d.dragInfo; + if (layout != null) { + layout.calculateSpans(itemInfo); + showOutOfSpaceMessage = + !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); + } + } + if (showOutOfSpaceMessage) { + mLauncher.showOutOfSpaceMessage(false); + } + + d.deferDragViewCleanupPostAnimation = false; + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } + + @Override + public void afterTextChanged(final Editable s) { + if (s.toString().isEmpty()) { + mApps.setFilter(null); + } else { + String formatStr = getResources().getString(R.string.apps_view_no_search_results); + if (USE_LAYOUT == GRID_LAYOUT) { + ((AppsGridAdapter) mAdapter).setEmptySearchText(String.format(formatStr, + s.toString())); + } else { + ((AppsListAdapter) mAdapter).setEmptySearchText(String.format(formatStr, + s.toString())); + } + + final String filterText = s.toString().toLowerCase().replaceAll("\\s+", ""); + mApps.setFilter(new AlphabeticalAppsList.Filter() { + @Override + public boolean retainApp(AppInfo info, String sectionName) { + String title = info.title.toString(); + return sectionName.toLowerCase().contains(filterText) || + title.toLowerCase().replaceAll("\\s+", "").contains(filterText); + } + }); + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) { + // Skip the quick-launch if there isn't exactly one item + if (mApps.getSize() != 1) { + return false; + } + + List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); + for (int i = 0; i < items.size(); i++) { + AlphabeticalAppsList.AdapterItem item = items.get(i); + if (!item.isSectionHeader) { + mAppsRecyclerView.getChildAt(i).performClick(); + InputMethodManager imm = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getWindowToken(), 0); + return true; + } + } + } + return false; + } + + @Override + public View getContent() { + return null; + } + + @Override + public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { + // Do nothing + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + // Do nothing + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + // Do nothing + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + if (mSearchBarView != null) { + if (toWorkspace) { + // Clear the search bar + mSearchBarView.setText(""); + } + } + } + + /** + * Handles the touch events to dismiss all apps when clicking outside the bounds of the + * recycler view. + */ + private boolean handleTouchEvent(MotionEvent ev) { + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + if (!mFixedBounds.isEmpty()) { + // Outset the fixed bounds and check if the touch is outside all apps + Rect tmpRect = new Rect(mFixedBounds); + tmpRect.inset(-grid.allAppsIconSizePx / 2, 0); + if (ev.getX() < tmpRect.left || ev.getX() > tmpRect.right) { + mLastTouchDownPos.set((int) ev.getX(), (int) ev.getY()); + return true; + } + } else { + // Check if the touch is outside all apps + if (ev.getX() < getPaddingLeft() || + ev.getX() > (getWidth() - getPaddingRight())) { + mLastTouchDownPos.set((int) ev.getX(), (int) ev.getY()); + return true; + } + } + break; + case MotionEvent.ACTION_UP: + if (mLastTouchDownPos.x > -1) { + ViewConfiguration viewConfig = ViewConfiguration.get(getContext()); + float dx = ev.getX() - mLastTouchDownPos.x; + float dy = ev.getY() - mLastTouchDownPos.y; + float distance = (float) Math.hypot(dx, dy); + if (distance < viewConfig.getScaledTouchSlop()) { + // The background was clicked, so just go home + Launcher launcher = (Launcher) getContext(); + launcher.showWorkspace(true); + return true; + } + } + // Fall through + case MotionEvent.ACTION_CANCEL: + mLastTouchDownPos.set(-1, -1); + break; + } + return false; + } + + /** + * Update the padding of the Apps view and children. To ensure that the RecyclerView has the + * full width to handle touches right to the edge of the screen, we only apply the top and + * bottom padding to the AppsContainerView and then the left/right padding on the RecyclerView + * itself. In particular, the left/right padding is applied to the background of the view, + * and then additionally inset by the start margin. + */ + private void updatePaddings() { + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + boolean hasSearchBar = (mSearchBarView != null) && + (mSearchBarView.getVisibility() == View.VISIBLE); + + if (mFixedBounds.isEmpty()) { + // If there are no fixed bounds, then use the default padding and insets + setPadding(mInsets.left, mContainerInset + mInsets.top, mInsets.right, + mContainerInset + mInsets.bottom); + } else { + // If there are fixed bounds, then we update the padding to reflect the fixed bounds. + setPadding(mFixedBounds.left, mFixedBounds.top + mFixedBoundsContainerInset, + getMeasuredWidth() - mFixedBounds.right, + mInsets.bottom + mFixedBoundsContainerInset); + } + + // Update the apps recycler view + int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset; + if (isRtl) { + mAppsRecyclerView.setPadding(inset, inset, inset + mContentMarginStart, inset); + } else { + mAppsRecyclerView.setPadding(inset + mContentMarginStart, inset, inset, inset); + } + + // Update the search bar + if (hasSearchBar) { + LinearLayout.LayoutParams lp = + (LinearLayout.LayoutParams) mSearchBarView.getLayoutParams(); + lp.leftMargin = lp.rightMargin = inset; + } + } + + /** + * Update the background of the Apps view and children. + */ + private void updateBackgrounds() { + int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset; + boolean hasSearchBar = (mSearchBarView != null) && + (mSearchBarView.getVisibility() == View.VISIBLE); + + // Update the background of the reveal view and list to be inset with the fixed bound + // insets instead of the default insets + mAppsRecyclerView.setBackground(new InsetDrawable( + getContext().getResources().getDrawable( + hasSearchBar ? R.drawable.apps_list_search_bg : R.drawable.apps_list_bg), + inset, 0, inset, 0)); + getRevealView().setBackground(new InsetDrawable( + getContext().getResources().getDrawable(R.drawable.apps_reveal_bg), + inset, 0, inset, 0)); + } +} diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java deleted file mode 100644 index 9e7e523e0..000000000 --- a/src/com/android/launcher3/AppsCustomizePagedView.java +++ /dev/null @@ -1,1574 +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.AnimatorSet; -import android.animation.ValueAnimator; -import android.appwidget.AppWidgetHostView; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Process; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; -import android.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; -import java.util.Iterator; -import java.util.List; - -/** - * A simple callback interface which also provides the results of the task. - */ -interface AsyncTaskCallback { - void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data); -} - -/** - * The data needed to perform either of the custom AsyncTasks. - */ -class AsyncTaskPageData { - enum Type { - LoadWidgetPreviewData - } - - AsyncTaskPageData(int p, ArrayList<Object> l, int cw, int ch, AsyncTaskCallback bgR, - AsyncTaskCallback postR, WidgetPreviewLoader w) { - page = p; - items = l; - generatedImages = new ArrayList<Bitmap>(); - maxImageWidth = cw; - maxImageHeight = ch; - doInBackgroundCallback = bgR; - postExecuteCallback = postR; - widgetPreviewLoader = w; - } - void cleanup(boolean cancelled) { - // Clean up any references to source/generated bitmaps - if (generatedImages != null) { - if (cancelled) { - for (int i = 0; i < generatedImages.size(); i++) { - widgetPreviewLoader.recycleBitmap(items.get(i), generatedImages.get(i)); - } - } - generatedImages.clear(); - } - } - int page; - ArrayList<Object> items; - ArrayList<Bitmap> sourceImages; - ArrayList<Bitmap> generatedImages; - int maxImageWidth; - int maxImageHeight; - AsyncTaskCallback doInBackgroundCallback; - AsyncTaskCallback postExecuteCallback; - WidgetPreviewLoader widgetPreviewLoader; -} - -/** - * A generic template for an async task used in AppsCustomize. - */ -class AppsCustomizeAsyncTask extends AsyncTask<AsyncTaskPageData, Void, AsyncTaskPageData> { - AppsCustomizeAsyncTask(int p, AsyncTaskPageData.Type ty) { - page = p; - threadPriority = Process.THREAD_PRIORITY_DEFAULT; - dataType = ty; - } - @Override - protected AsyncTaskPageData doInBackground(AsyncTaskPageData... params) { - if (params.length != 1) return null; - // Load each of the widget previews in the background - params[0].doInBackgroundCallback.run(this, params[0]); - return params[0]; - } - @Override - protected void onPostExecute(AsyncTaskPageData result) { - // All the widget previews are loaded, so we can just callback to inflate the page - result.postExecuteCallback.run(this, result); - } - - void setThreadPriority(int p) { - threadPriority = p; - } - void syncThreadPriority() { - Process.setThreadPriority(threadPriority); - } - - // The page that this async task is associated with - AsyncTaskPageData.Type dataType; - int page; - int threadPriority; -} - -/** - * The Apps/Customize page that displays all the applications, widgets, and shortcuts. - */ -public class AppsCustomizePagedView extends PagedViewWithDraggableItems implements - View.OnClickListener, View.OnKeyListener, DragSource, - PagedViewWidget.ShortPressListener, LauncherTransitionable { - static final String TAG = "AppsCustomizePagedView"; - - private static Rect sTmpRect = new Rect(); - - /** - * The different content types that this paged view can show. - */ - public enum ContentType { - Applications, - Widgets - } - private ContentType mContentType = ContentType.Applications; - - // Refs - private Launcher mLauncher; - private DragController mDragController; - private final LayoutInflater mLayoutInflater; - private final PackageManager mPackageManager; - - // Save and Restore - private int mSaveInstanceStateItemIndex = -1; - - // Content - private ArrayList<AppInfo> mApps; - private ArrayList<Object> mWidgets; - - // Caching - private IconCache mIconCache; - - // Dimens - private int mContentWidth, mContentHeight; - private int mWidgetCountX, mWidgetCountY; - private PagedViewCellLayout mWidgetSpacingLayout; - private int mNumAppsPages; - private int mNumWidgetPages; - private Rect mAllAppsPadding = new Rect(); - - // Previews & outlines - ArrayList<AppsCustomizeAsyncTask> mRunningTasks; - private static final int sPageSleepDelay = 200; - - private Runnable mInflateWidgetRunnable = null; - private Runnable mBindWidgetRunnable = null; - static final int WIDGET_NO_CLEANUP_REQUIRED = -1; - static final int WIDGET_PRELOAD_PENDING = 0; - static final int WIDGET_BOUND = 1; - static final int WIDGET_INFLATED = 2; - int mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; - int mWidgetLoadingId = -1; - PendingAddWidgetInfo mCreateWidgetInfo = null; - private boolean mDraggingWidget = false; - boolean mPageBackgroundsVisible = true; - - private Toast mWidgetInstructionToast; - - // Deferral of loading widget previews during launcher transitions - private boolean mInTransition; - private ArrayList<AsyncTaskPageData> mDeferredSyncWidgetPageItems = - new ArrayList<AsyncTaskPageData>(); - private ArrayList<Runnable> mDeferredPrepareLoadWidgetPreviewsTasks = - new ArrayList<Runnable>(); - - WidgetPreviewLoader mWidgetPreviewLoader; - - private boolean mInBulkBind; - private boolean mNeedToUpdatePageCountsAndInvalidateData; - - public AppsCustomizePagedView(Context context, AttributeSet attrs) { - super(context, attrs); - mLayoutInflater = LayoutInflater.from(context); - mPackageManager = context.getPackageManager(); - mApps = new ArrayList<AppInfo>(); - mWidgets = new ArrayList<Object>(); - mIconCache = (LauncherAppState.getInstance()).getIconCache(); - mRunningTasks = new ArrayList<AppsCustomizeAsyncTask>(); - - // Save the default widget preview background - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0); - mWidgetCountX = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountX, 2); - mWidgetCountY = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountY, 2); - a.recycle(); - mWidgetSpacingLayout = new PagedViewCellLayout(getContext()); - - // The padding on the non-matched dimension for the default widget preview icons - // (top + bottom) - mFadeInAdjacentScreens = false; - - // Unless otherwise specified this view is important for accessibility. - if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - setSinglePageInViewport(); - } - - @Override - protected void init() { - super.init(); - mCenterPagesVertically = false; - - Context context = getContext(); - Resources r = context.getResources(); - setDragSlopeThreshold(r.getInteger(R.integer.config_appsCustomizeDragSlopeThreshold)/100f); - } - - public void onFinishInflate() { - super.onFinishInflate(); - - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setPadding(grid.edgeMarginPx, 2 * grid.edgeMarginPx, - grid.edgeMarginPx, 2 * grid.edgeMarginPx); - } - - void setAllAppsPadding(Rect r) { - mAllAppsPadding.set(r); - } - - void setWidgetsPageIndicatorPadding(int pageIndicatorHeight) { - setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), pageIndicatorHeight); - } - - WidgetPreviewLoader getWidgetPreviewLoader() { - if (mWidgetPreviewLoader == null) { - mWidgetPreviewLoader = new WidgetPreviewLoader(mLauncher); - } - return mWidgetPreviewLoader; - } - - /** Returns the item index of the center item on this page so that we can restore to this - * item index when we rotate. */ - private int getMiddleComponentIndexOnCurrentPage() { - int i = -1; - if (getPageCount() > 0) { - int currentPage = getCurrentPage(); - if (mContentType == ContentType.Applications) { - AppsCustomizeCellLayout layout = (AppsCustomizeCellLayout) getPageAt(currentPage); - ShortcutAndWidgetContainer childrenLayout = layout.getShortcutsAndWidgets(); - int numItemsPerPage = mCellCountX * mCellCountY; - int childCount = childrenLayout.getChildCount(); - if (childCount > 0) { - i = (currentPage * numItemsPerPage) + (childCount / 2); - } - } else if (mContentType == ContentType.Widgets) { - int numApps = mApps.size(); - PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(currentPage); - int numItemsPerPage = mWidgetCountX * mWidgetCountY; - int childCount = layout.getChildCount(); - if (childCount > 0) { - i = numApps + - (currentPage * numItemsPerPage) + (childCount / 2); - } - } else { - throw new RuntimeException("Invalid ContentType"); - } - } - return i; - } - - /** Get the index of the item to restore to if we need to restore the current page. */ - int getSaveInstanceStateIndex() { - if (mSaveInstanceStateItemIndex == -1) { - mSaveInstanceStateItemIndex = getMiddleComponentIndexOnCurrentPage(); - } - return mSaveInstanceStateItemIndex; - } - - /** Returns the page in the current orientation which is expected to contain the specified - * item index. */ - int getPageForComponent(int index) { - if (index < 0) return 0; - - if (index < mApps.size()) { - int numItemsPerPage = mCellCountX * mCellCountY; - return (index / numItemsPerPage); - } else { - int numItemsPerPage = mWidgetCountX * mWidgetCountY; - return (index - mApps.size()) / numItemsPerPage; - } - } - - /** Restores the page for an item at the specified index */ - void restorePageForIndex(int index) { - if (index < 0) return; - mSaveInstanceStateItemIndex = index; - } - - private void updatePageCounts() { - mNumWidgetPages = (int) Math.ceil(mWidgets.size() / - (float) (mWidgetCountX * mWidgetCountY)); - mNumAppsPages = (int) Math.ceil((float) mApps.size() / (mCellCountX * mCellCountY)); - } - - protected void onDataReady(int width, int height) { - // Now that the data is ready, we can calculate the content width, the number of cells to - // use for each page - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mCellCountX = (int) grid.allAppsNumCols; - mCellCountY = (int) grid.allAppsNumRows; - updatePageCounts(); - - // Force a measure to update recalculate the gaps - mContentWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); - mContentHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); - int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - mWidgetSpacingLayout.measure(widthSpec, heightSpec); - - final boolean hostIsTransitioning = getTabHost().isInTransition(); - int page = getPageForComponent(mSaveInstanceStateItemIndex); - invalidatePageData(Math.max(0, page), hostIsTransitioning); - } - - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - - if (!isDataReady()) { - if ((LauncherAppState.isDisableAllApps() || !mApps.isEmpty()) && !mWidgets.isEmpty()) { - post(new Runnable() { - // This code triggers requestLayout so must be posted outside of the - // layout pass. - public void run() { - if (Utilities.isViewAttachedToWindow(AppsCustomizePagedView.this)) { - setDataIsReady(); - onDataReady(getMeasuredWidth(), getMeasuredHeight()); - } - } - }); - } - } - } - - public void onPackagesUpdated(ArrayList<Object> widgetsAndShortcuts) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - - // Get the list of widgets and shortcuts - mWidgets.clear(); - for (Object o : widgetsAndShortcuts) { - if (o instanceof LauncherAppWidgetProviderInfo) { - LauncherAppWidgetProviderInfo widget = (LauncherAppWidgetProviderInfo) o; - if (!app.shouldShowAppOrWidgetProvider(widget.provider) && !widget.isCustomWidget) { - continue; - } - - if (widget.minSpanX > 0 && widget.minSpanY > 0) { - // Ensure that all widgets we show can be added on a workspace of this size - int[] spanXY = Launcher.getSpanForWidget(mLauncher, widget); - int[] minSpanXY = Launcher.getMinSpanForWidget(mLauncher, widget); - int minSpanX = Math.min(spanXY[0], minSpanXY[0]); - int minSpanY = Math.min(spanXY[1], minSpanXY[1]); - if (minSpanX <= (int) grid.numColumns && - minSpanY <= (int) grid.numRows) { - mWidgets.add(widget); - } else { - Log.e(TAG, "Widget " + widget.provider + " can not fit on this device (" + - widget.minWidth + ", " + widget.minHeight + ")"); - } - } else { - Log.e(TAG, "Widget " + widget.provider + " has invalid dimensions (" + - widget.minWidth + ", " + widget.minHeight + ")"); - } - } else { - // just add shortcuts - mWidgets.add(o); - } - } - - updatePageCountsAndInvalidateData(); - } - - public void setBulkBind(boolean bulkBind) { - if (bulkBind) { - mInBulkBind = true; - } else { - mInBulkBind = false; - if (mNeedToUpdatePageCountsAndInvalidateData) { - updatePageCountsAndInvalidateData(); - } - } - } - - private void updatePageCountsAndInvalidateData() { - if (mInBulkBind) { - mNeedToUpdatePageCountsAndInvalidateData = true; - } else { - updatePageCounts(); - invalidateOnDataChange(); - mNeedToUpdatePageCountsAndInvalidateData = false; - } - } - - @Override - public void onClick(View v) { - // When we have exited all apps or are in transition, disregard clicks - if (!mLauncher.isAllAppsVisible() - || mLauncher.getWorkspace().isSwitchingState() - || !(v instanceof PagedViewWidget)) return; - - // Let the user know that they have to long press to add a widget - if (mWidgetInstructionToast != null) { - mWidgetInstructionToast.cancel(); - } - mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add, - Toast.LENGTH_SHORT); - mWidgetInstructionToast.show(); - - // Create a little animation to show that the widget can move - float offsetY = getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY); - final ImageView p = (ImageView) v.findViewById(R.id.widget_preview); - AnimatorSet bounce = LauncherAnimUtils.createAnimatorSet(); - ValueAnimator tyuAnim = LauncherAnimUtils.ofFloat(p, "translationY", offsetY); - tyuAnim.setDuration(125); - ValueAnimator tydAnim = LauncherAnimUtils.ofFloat(p, "translationY", 0f); - tydAnim.setDuration(100); - bounce.play(tyuAnim).before(tydAnim); - bounce.setInterpolator(new AccelerateInterpolator()); - bounce.start(); - } - - public boolean onKey(View v, int keyCode, KeyEvent event) { - return FocusHelper.handleAppsCustomizeKeyEvent(v, keyCode, event); - } - - /* - * PagedViewWithDraggableItems implementation - */ - @Override - protected void determineDraggingStart(android.view.MotionEvent ev) { - // Disable dragging by pulling an app down for now. - } - - private void beginDraggingApplication(View v) { - mLauncher.getWorkspace().beginDragShared(v, this); - } - - static Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { - Bundle options = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, sTmpRect); - Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher, - info.componentName, null); - - 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, - sTmpRect.left - xPaddingDips); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, - sTmpRect.top - yPaddingDips); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, - sTmpRect.right - xPaddingDips); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, - sTmpRect.bottom - yPaddingDips); - } - return options; - } - - private void preloadWidget(final PendingAddWidgetInfo info) { - final LauncherAppWidgetProviderInfo pInfo = info.info; - final Bundle options = pInfo.isCustomWidget ? null : - getDefaultOptionsForWidget(mLauncher, info); - - if (pInfo.configure != null) { - info.bindOptions = options; - return; - } - - mWidgetCleanupState = WIDGET_PRELOAD_PENDING; - mBindWidgetRunnable = new Runnable() { - @Override - public void run() { - if (pInfo.isCustomWidget) { - mWidgetCleanupState = WIDGET_BOUND; - return; - } - - mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId(); - if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed( - mWidgetLoadingId, pInfo, options)) { - mWidgetCleanupState = WIDGET_BOUND; - } - - } - }; - post(mBindWidgetRunnable); - - mInflateWidgetRunnable = new Runnable() { - @Override - public void run() { - if (mWidgetCleanupState != WIDGET_BOUND) { - return; - } - AppWidgetHostView hostView = mLauncher.getAppWidgetHost().createView( - getContext(), mWidgetLoadingId, pInfo); - info.boundWidget = hostView; - mWidgetCleanupState = WIDGET_INFLATED; - hostView.setVisibility(INVISIBLE); - int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(info.spanX, - info.spanY, info, false); - - // We want the first widget layout to be the correct size. This will be important - // for width size reporting to the AppWidgetManager. - DragLayer.LayoutParams lp = new DragLayer.LayoutParams(unScaledSize[0], - unScaledSize[1]); - lp.x = lp.y = 0; - lp.customPosition = true; - hostView.setLayoutParams(lp); - mLauncher.getDragLayer().addView(hostView); - } - }; - post(mInflateWidgetRunnable); - } - - @Override - public void onShortPress(View v) { - // We are anticipating a long press, and we use this time to load bind and instantiate - // the widget. This will need to be cleaned up if it turns out no long press occurs. - if (mCreateWidgetInfo != null) { - // Just in case the cleanup process wasn't properly executed. This shouldn't happen. - cleanupWidgetPreloading(false); - } - mCreateWidgetInfo = new PendingAddWidgetInfo((PendingAddWidgetInfo) v.getTag()); - preloadWidget(mCreateWidgetInfo); - } - - private void cleanupWidgetPreloading(boolean widgetWasAdded) { - if (!widgetWasAdded) { - // If the widget was not added, we may need to do further cleanup. - PendingAddWidgetInfo info = mCreateWidgetInfo; - mCreateWidgetInfo = null; - - if (mWidgetCleanupState == WIDGET_PRELOAD_PENDING) { - // We never did any preloading, so just remove pending callbacks to do so - removeCallbacks(mBindWidgetRunnable); - removeCallbacks(mInflateWidgetRunnable); - } else if (mWidgetCleanupState == WIDGET_BOUND) { - // Delete the widget id which was allocated - if (mWidgetLoadingId != -1 && !info.isCustomWidget()) { - mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); - } - - // We never got around to inflating the widget, so remove the callback to do so. - removeCallbacks(mInflateWidgetRunnable); - } else if (mWidgetCleanupState == WIDGET_INFLATED) { - // Delete the widget id which was allocated - if (mWidgetLoadingId != -1 && !info.isCustomWidget()) { - mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); - } - - // The widget was inflated and added to the DragLayer -- remove it. - AppWidgetHostView widget = info.boundWidget; - mLauncher.getDragLayer().removeView(widget); - } - } - mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; - mWidgetLoadingId = -1; - mCreateWidgetInfo = null; - PagedViewWidget.resetShortPressTarget(); - } - - @Override - public void cleanUpShortPress(View v) { - if (!mDraggingWidget) { - cleanupWidgetPreloading(false); - } - } - - private boolean beginDraggingWidget(View v) { - mDraggingWidget = true; - // Get the widget preview as the drag representation - ImageView image = (ImageView) v.findViewById(R.id.widget_preview); - PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); - - // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and - // we abort the drag. - if (image.getDrawable() == null) { - mDraggingWidget = false; - return false; - } - - // Compose the drag image - Bitmap preview; - Bitmap outline; - float scale = 1f; - Point previewPadding = null; - - if (createItemInfo instanceof PendingAddWidgetInfo) { - // This can happen in some weird cases involving multi-touch. We can't start dragging - // the widget if this is null, so we break out. - if (mCreateWidgetInfo == null) { - return false; - } - - PendingAddWidgetInfo createWidgetInfo = mCreateWidgetInfo; - createItemInfo = createWidgetInfo; - int spanX = createItemInfo.spanX; - int spanY = createItemInfo.spanY; - int[] size = mLauncher.getWorkspace().estimateItemSize(spanX, spanY, - createWidgetInfo, true); - - FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable(); - float minScale = 1.25f; - int maxWidth, maxHeight; - maxWidth = Math.min((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]); - maxHeight = Math.min((int) (previewDrawable.getIntrinsicHeight() * minScale), size[1]); - - int[] previewSizeBeforeScale = new int[1]; - preview = 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], - getWidgetPreviewLoader().maxWidthForWidgetPreview(spanX)); - scale = previewWidthInAppsCustomize / (float) preview.getWidth(); - - // The bitmap in the AppsCustomize tray is always the the same size, so there - // might be extra pixels around the preview itself - this accounts for that - if (previewWidthInAppsCustomize < previewDrawable.getIntrinsicWidth()) { - int padding = - (previewDrawable.getIntrinsicWidth() - previewWidthInAppsCustomize) / 2; - previewPadding = new Point(padding, 0); - } - } else { - PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); - Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.shortcutActivityInfo); - preview = Utilities.createIconBitmap(icon, mLauncher); - createItemInfo.spanX = createItemInfo.spanY = 1; - } - - // Don't clip alpha values for the drag outline if we're using the default widget preview - boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo && - (((PendingAddWidgetInfo) createItemInfo).previewImage == 0)); - - // Save the preview for the outline generation, then dim the preview - outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(), - false); - - // Start the drag - mLauncher.lockScreenOrientation(); - mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, clipAlpha); - mDragController.startDrag(image, preview, this, createItemInfo, - DragController.DRAG_ACTION_COPY, previewPadding, scale); - outline.recycle(); - preview.recycle(); - return true; - } - - @Override - protected boolean beginDragging(final View v) { - if (!super.beginDragging(v)) return false; - - if (v instanceof BubbleTextView) { - beginDraggingApplication(v); - } else if (v instanceof PagedViewWidget) { - if (!beginDraggingWidget(v)) { - return false; - } - } - - // We delay entering spring-loaded mode slightly to make sure the UI - // thready is free of any work. - postDelayed(new Runnable() { - @Override - public void run() { - // We don't enter spring-loaded mode if the drag has been cancelled - if (mLauncher.getDragController().isDragging()) { - // Go into spring loaded mode (must happen before we startDrag()) - mLauncher.enterSpringLoadedDragMode(); - } - } - }, 150); - - return true; - } - - /** - * Clean up after dragging. - * - * @param target where the item was dragged to (can be null if the item was flung) - */ - private void endDragging(View target, boolean isFlingToDelete, boolean success) { - if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && - !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { - // Exit spring loaded mode if we have not successfully dropped or have not handled the - // drop in Workspace - mLauncher.exitSpringLoadedDragModeDelayed(true, - Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); - mLauncher.unlockScreenOrientation(false); - } else { - mLauncher.unlockScreenOrientation(false); - } - } - - @Override - public View getContent() { - if (getChildCount() > 0) { - return getChildAt(0); - } - return null; - } - - @Override - public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { - mInTransition = true; - if (toWorkspace) { - cancelAllTasks(); - } - } - - @Override - public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { - } - - @Override - public void onLauncherTransitionStep(Launcher l, float t) { - } - - @Override - public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { - mInTransition = false; - for (AsyncTaskPageData d : mDeferredSyncWidgetPageItems) { - onSyncWidgetPageItems(d, false); - } - mDeferredSyncWidgetPageItems.clear(); - for (Runnable r : mDeferredPrepareLoadWidgetPreviewsTasks) { - r.run(); - } - mDeferredPrepareLoadWidgetPreviewsTasks.clear(); - mForceDrawAllChildrenNextFrame = !toWorkspace; - } - - @Override - public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, - boolean success) { - // Return early and wait for onFlingToDeleteCompleted if this was the result of a fling - if (isFlingToDelete) return; - - endDragging(target, false, success); - - // Display an error message if the drag failed due to there not being enough space on the - // target layout we were dropping on. - if (!success) { - boolean showOutOfSpaceMessage = false; - if (target instanceof Workspace) { - int currentScreen = mLauncher.getCurrentWorkspaceScreen(); - Workspace workspace = (Workspace) target; - CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); - ItemInfo itemInfo = (ItemInfo) d.dragInfo; - if (layout != null) { - layout.calculateSpans(itemInfo); - showOutOfSpaceMessage = - !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); - } - } - if (showOutOfSpaceMessage) { - mLauncher.showOutOfSpaceMessage(false); - } - - d.deferDragViewCleanupPostAnimation = false; - } - cleanupWidgetPreloading(success); - mDraggingWidget = false; - } - - @Override - public void onFlingToDeleteCompleted() { - // We just dismiss the drag when we fling, so cleanup here - endDragging(null, true, true); - cleanupWidgetPreloading(false); - mDraggingWidget = false; - } - - @Override - public boolean supportsFlingToDelete() { - return true; - } - - @Override - public boolean supportsAppInfoDropTarget() { - return true; - } - - @Override - public boolean supportsDeleteDropTarget() { - return false; - } - - @Override - public float getIntrinsicIconScaleFactor() { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - return (float) grid.allAppsIconSizePx / grid.iconSizePx; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - cancelAllTasks(); - } - - @Override - public void trimMemory() { - super.trimMemory(); - clearAllWidgetPages(); - } - - public void clearAllWidgetPages() { - cancelAllTasks(); - int count = getChildCount(); - for (int i = 0; i < count; i++) { - View v = getPageAt(i); - if (v instanceof PagedViewGridLayout) { - ((PagedViewGridLayout) v).removeAllViewsOnPage(); - mDirtyPageContent.set(i, true); - } - } - } - - private void cancelAllTasks() { - // Clean up all the async tasks - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - task.cancel(false); - iter.remove(); - mDirtyPageContent.set(task.page, true); - - // We've already preallocated the views for the data to load into, so clear them as well - View v = getPageAt(task.page); - if (v instanceof PagedViewGridLayout) { - ((PagedViewGridLayout) v).removeAllViewsOnPage(); - } - } - mDeferredSyncWidgetPageItems.clear(); - mDeferredPrepareLoadWidgetPreviewsTasks.clear(); - } - - public void setContentType(ContentType type) { - // Widgets appear to be cleared every time you leave, always force invalidate for them - if (mContentType != type || type == ContentType.Widgets) { - int page = (mContentType != type) ? 0 : getCurrentPage(); - mContentType = type; - invalidatePageData(page, true); - } - } - - public ContentType getContentType() { - return mContentType; - } - - protected void snapToPage(int whichPage, int delta, int duration) { - super.snapToPage(whichPage, delta, duration); - - // Update the thread priorities given the direction lookahead - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int pageIndex = task.page; - if ((mNextPage > mCurrentPage && pageIndex >= mCurrentPage) || - (mNextPage < mCurrentPage && pageIndex <= mCurrentPage)) { - task.setThreadPriority(getThreadPriorityForPage(pageIndex)); - } else { - task.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); - } - } - } - - /* - * Apps PagedView implementation - */ - private void setVisibilityOnChildren(ViewGroup layout, int visibility) { - int childCount = layout.getChildCount(); - for (int i = 0; i < childCount; ++i) { - layout.getChildAt(i).setVisibility(visibility); - } - } - private void setupPage(AppsCustomizeCellLayout layout) { - layout.setGridSize(mCellCountX, mCellCountY); - - // Note: We force a measure here to get around the fact that when we do layout calculations - // immediately after syncing, we don't have a proper width. That said, we already know the - // expected page width, so we can actually optimize by hiding all the TextView-based - // children that are expensive to measure, and let that happen naturally later. - setVisibilityOnChildren(layout, View.GONE); - int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - layout.measure(widthSpec, heightSpec); - - 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(); - int numCells = mCellCountX * mCellCountY; - int startIndex = page * numCells; - int endIndex = Math.min(startIndex + numCells, mApps.size()); - AppsCustomizeCellLayout layout = (AppsCustomizeCellLayout) getPageAt(page); - - layout.removeAllViewsOnPage(); - ArrayList<Object> items = new ArrayList<Object>(); - ArrayList<Bitmap> images = new ArrayList<Bitmap>(); - for (int i = startIndex; i < endIndex; ++i) { - AppInfo info = mApps.get(i); - BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( - R.layout.apps_customize_application, layout, false); - 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; - int y = index / mCellCountX; - if (isRtl) { - x = mCellCountX - x - 1; - } - layout.addViewToCellLayout(icon, -1, i, new CellLayout.LayoutParams(x,y, 1,1), false); - - items.add(info); - images.add(info.iconBitmap); - } - - enableHwLayersOnVisiblePages(); - } - - /** - * A helper to return the priority for loading of the specified widget page. - */ - private int getWidgetPageLoadPriority(int page) { - // If we are snapping to another page, use that index as the target page index - int toPage = mCurrentPage; - if (mNextPage > -1) { - toPage = mNextPage; - } - - // We use the distance from the target page as an initial guess of priority, but if there - // are no pages of higher priority than the page specified, then bump up the priority of - // the specified page. - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - int minPageDiff = Integer.MAX_VALUE; - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - minPageDiff = Math.abs(task.page - toPage); - } - - int rawPageDiff = Math.abs(page - toPage); - return rawPageDiff - Math.min(rawPageDiff, minPageDiff); - } - /** - * Return the appropriate thread priority for loading for a given page (we give the current - * page much higher priority) - */ - private int getThreadPriorityForPage(int page) { - // TODO-APPS_CUSTOMIZE: detect number of cores and set thread priorities accordingly below - int pageDiff = getWidgetPageLoadPriority(page); - if (pageDiff <= 0) { - return Process.THREAD_PRIORITY_LESS_FAVORABLE; - } else if (pageDiff <= 1) { - return Process.THREAD_PRIORITY_LOWEST; - } else { - return Process.THREAD_PRIORITY_LOWEST; - } - } - private int getSleepForPage(int page) { - int pageDiff = getWidgetPageLoadPriority(page); - return Math.max(0, pageDiff * sPageSleepDelay); - } - /** - * Creates and executes a new AsyncTask to load a page of widget previews. - */ - private void prepareLoadWidgetPreviewsTask(int page, ArrayList<Object> widgets, - int cellWidth, int cellHeight, int cellCountX) { - - // Prune all tasks that are no longer needed - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int taskPage = task.page; - if (taskPage < getAssociatedLowerPageBound(mCurrentPage) || - taskPage > getAssociatedUpperPageBound(mCurrentPage)) { - task.cancel(false); - iter.remove(); - } else { - task.setThreadPriority(getThreadPriorityForPage(taskPage)); - } - } - - // We introduce a slight delay to order the loading of side pages so that we don't thrash - final int sleepMs = getSleepForPage(page); - AsyncTaskPageData pageData = new AsyncTaskPageData(page, widgets, cellWidth, cellHeight, - new AsyncTaskCallback() { - @Override - public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { - try { - try { - Thread.sleep(sleepMs); - } catch (Exception e) {} - loadWidgetPreviewsInBackground(task, data); - } finally { - if (task.isCancelled()) { - data.cleanup(true); - } - } - } - }, - new AsyncTaskCallback() { - @Override - public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { - mRunningTasks.remove(task); - if (task.isCancelled()) return; - // do cleanup inside onSyncWidgetPageItems - onSyncWidgetPageItems(data, false); - } - }, getWidgetPreviewLoader()); - - // Ensure that the task is appropriately prioritized and runs in parallel - AppsCustomizeAsyncTask t = new AppsCustomizeAsyncTask(page, - AsyncTaskPageData.Type.LoadWidgetPreviewData); - t.setThreadPriority(getThreadPriorityForPage(page)); - t.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pageData); - mRunningTasks.add(t); - } - - /* - * Widgets PagedView implementation - */ - private void setupPage(PagedViewGridLayout layout) { - // Note: We force a measure here to get around the fact that when we do layout calculations - // immediately after syncing, we don't have a proper width. - int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - - 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 - 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; - for (int i = offset; i < Math.min(offset + numItemsPerPage, mWidgets.size()); ++i) { - items.add(mWidgets.get(i)); - } - - // Prepopulate the pages with the other widget info, and fill in the previews later - layout.setColumnCount(layout.getCellCountX()); - for (int i = 0; i < items.size(); ++i) { - Object rawInfo = items.get(i); - PendingAddItemInfo createItemInfo = null; - PagedViewWidget widget = (PagedViewWidget) mLayoutInflater.inflate( - R.layout.apps_customize_widget, layout, false); - - if (rawInfo instanceof LauncherAppWidgetProviderInfo) { - // Fill in the widget information - LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) rawInfo; - createItemInfo = new PendingAddWidgetInfo(info, null); - - widget.applyFromAppWidgetProviderInfo(info, -1, getWidgetPreviewLoader()); - widget.setTag(createItemInfo); - widget.setShortPressListener(this); - } else if (rawInfo instanceof ResolveInfo) { - // Fill in the shortcuts information - ResolveInfo info = (ResolveInfo) rawInfo; - createItemInfo = new PendingAddShortcutInfo(info.activityInfo); - createItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; - createItemInfo.componentName = new ComponentName(info.activityInfo.packageName, - info.activityInfo.name); - widget.applyFromResolveInfo(mPackageManager, info, getWidgetPreviewLoader()); - widget.setTag(createItemInfo); - } - - widget.setOnClickListener(this); - widget.setOnLongClickListener(this); - widget.setOnTouchListener(this); - widget.setOnKeyListener(this); - - // Layout each widget - int ix = i % mWidgetCountX; - int iy = i / mWidgetCountX; - - 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); - layout.addView(widget, lp); - } - - // wait until a call on onLayout to start loading, because - // PagedViewWidget.getPreviewSize() will return 0 if it hasn't been laid out - // TODO: can we do a measure/layout immediately? - layout.setOnLayoutListener(new Runnable() { - public void run() { - // Load the widget previews - int maxPreviewWidth = cellWidth; - int maxPreviewHeight = cellHeight; - if (layout.getChildCount() > 0) { - PagedViewWidget w = (PagedViewWidget) layout.getChildAt(0); - int[] maxSize = w.getPreviewSize(); - maxPreviewWidth = maxSize[0]; - maxPreviewHeight = maxSize[1]; - } - - getWidgetPreviewLoader().setPreviewSize( - maxPreviewWidth, maxPreviewHeight, mWidgetSpacingLayout); - if (immediate) { - AsyncTaskPageData data = new AsyncTaskPageData(page, items, - maxPreviewWidth, maxPreviewHeight, null, null, getWidgetPreviewLoader()); - loadWidgetPreviewsInBackground(null, data); - onSyncWidgetPageItems(data, immediate); - } else { - if (mInTransition) { - mDeferredPrepareLoadWidgetPreviewsTasks.add(this); - } else { - prepareLoadWidgetPreviewsTask(page, items, - maxPreviewWidth, maxPreviewHeight, mWidgetCountX); - } - } - layout.setOnLayoutListener(null); - } - }); - } - private void loadWidgetPreviewsInBackground(AppsCustomizeAsyncTask task, - AsyncTaskPageData data) { - // loadWidgetPreviewsInBackground can be called without a task to load a set of widget - // previews synchronously - if (task != null) { - // Ensure that this task starts running at the correct priority - task.syncThreadPriority(); - } - - // Load each of the widget/shortcut previews - ArrayList<Object> items = data.items; - ArrayList<Bitmap> images = data.generatedImages; - int count = items.size(); - for (int i = 0; i < count; ++i) { - if (task != null) { - // Ensure we haven't been cancelled yet - if (task.isCancelled()) break; - // Before work on each item, ensure that this task is running at the correct - // priority - task.syncThreadPriority(); - } - - images.add(getWidgetPreviewLoader().getPreview(items.get(i))); - } - } - - private void onSyncWidgetPageItems(AsyncTaskPageData data, boolean immediatelySyncItems) { - if (!immediatelySyncItems && mInTransition) { - mDeferredSyncWidgetPageItems.add(data); - return; - } - try { - int page = data.page; - PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); - - ArrayList<Object> items = data.items; - int count = items.size(); - for (int i = 0; i < count; ++i) { - PagedViewWidget widget = (PagedViewWidget) layout.getChildAt(i); - if (widget != null) { - Bitmap preview = data.generatedImages.get(i); - widget.applyPreview(new FastBitmapDrawable(preview), i); - } - } - - enableHwLayersOnVisiblePages(); - - // Update all thread priorities - Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int pageIndex = task.page; - task.setThreadPriority(getThreadPriorityForPage(pageIndex)); - } - } finally { - data.cleanup(false); - } - } - - @Override - public void syncPages() { - disablePagedViewAnimations(); - - removeAllViews(); - cancelAllTasks(); - - Context context = getContext(); - if (mContentType == ContentType.Applications) { - for (int i = 0; i < mNumAppsPages; ++i) { - AppsCustomizeCellLayout layout = new AppsCustomizeCellLayout(context); - setupPage(layout); - addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - } - } else if (mContentType == ContentType.Widgets) { - for (int j = 0; j < mNumWidgetPages; ++j) { - PagedViewGridLayout layout = new PagedViewGridLayout(context, mWidgetCountX, - mWidgetCountY); - setupPage(layout); - addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - } - } else { - throw new RuntimeException("Invalid ContentType"); - } - - enablePagedViewAnimations(); - } - - @Override - public void syncPageItems(int page, boolean immediate) { - if (mContentType == ContentType.Widgets) { - syncWidgetPageItems(page, immediate); - } else { - syncAppsPageItems(page, immediate); - } - } - - // We want our pages to be z-ordered such that the further a page is to the left, the higher - // it is in the z-order. This is important to insure touch events are handled correctly. - View getPageAt(int index) { - return getChildAt(indexToPage(index)); - } - - @Override - protected int indexToPage(int index) { - return getChildCount() - index - 1; - } - - // In apps customize, we have a scrolling effect which emulates pulling cards off of a stack. - @Override - protected void screenScrolled(int screenCenter) { - super.screenScrolled(screenCenter); - enableHwLayersOnVisiblePages(); - } - - private void enableHwLayersOnVisiblePages() { - final int screenCount = getChildCount(); - - getVisiblePages(mTempVisiblePagesRange); - int leftScreen = mTempVisiblePagesRange[0]; - int rightScreen = mTempVisiblePagesRange[1]; - int forceDrawScreen = -1; - if (leftScreen == rightScreen) { - // make sure we're caching at least two pages always - if (rightScreen < screenCount - 1) { - rightScreen++; - forceDrawScreen = rightScreen; - } else if (leftScreen > 0) { - leftScreen--; - forceDrawScreen = leftScreen; - } - } else { - forceDrawScreen = leftScreen + 1; - } - - for (int i = 0; i < screenCount; i++) { - final View layout = (View) getPageAt(i); - if (!(leftScreen <= i && i <= rightScreen && - (i == forceDrawScreen || shouldDrawChild(layout)))) { - layout.setLayerType(LAYER_TYPE_NONE, null); - } - } - - for (int i = 0; i < screenCount; i++) { - final View layout = (View) getPageAt(i); - - if (leftScreen <= i && i <= rightScreen && - (i == forceDrawScreen || shouldDrawChild(layout))) { - if (layout.getLayerType() != LAYER_TYPE_HARDWARE) { - layout.setLayerType(LAYER_TYPE_HARDWARE, null); - } - } - } - } - - protected void overScroll(float amount) { - dampedOverScroll(amount); - } - - /** - * Used by the parent to get the content width to set the tab bar to - * @return - */ - public int getPageContentWidth() { - return mContentWidth; - } - - @Override - protected void onPageEndMoving() { - super.onPageEndMoving(); - mForceDrawAllChildrenNextFrame = true; - // We reset the save index when we change pages so that it will be recalculated on next - // rotation - mSaveInstanceStateItemIndex = -1; - } - - /* - * AllAppsView implementation - */ - public void setup(Launcher launcher, DragController dragController) { - mLauncher = launcher; - mDragController = dragController; - } - - /** - * We should call thise method whenever the core data changes (mApps, mWidgets) so that we can - * appropriately determine when to invalidate the PagedView page data. In cases where the data - * has yet to be set, we can requestLayout() and wait for onDataReady() to be called in the - * next onMeasure() pass, which will trigger an invalidatePageData() itself. - */ - private void invalidateOnDataChange() { - if (!isDataReady()) { - // The next layout pass will trigger data-ready if both widgets and apps are set, so - // request a layout to trigger the page data when ready. - requestLayout(); - } else { - cancelAllTasks(); - invalidatePageData(); - } - } - - public void setApps(ArrayList<AppInfo> list) { - if (!LauncherAppState.isDisableAllApps()) { - mApps = list; - Collections.sort(mApps, LauncherModel.getAppNameComparator()); - updatePageCountsAndInvalidateData(); - } - } - - public ArrayList<AppInfo> getApps() { - return mApps; - } - - private void addAppsWithoutInvalidate(ArrayList<AppInfo> list) { - // We add it in place, in alphabetical order - int count = list.size(); - for (int i = 0; i < count; ++i) { - AppInfo info = list.get(i); - int index = Collections.binarySearch(mApps, info, LauncherModel.getAppNameComparator()); - if (index < 0) { - mApps.add(-(index + 1), info); - } - } - } - public void addApps(ArrayList<AppInfo> list) { - if (!LauncherAppState.isDisableAllApps()) { - addAppsWithoutInvalidate(list); - updatePageCountsAndInvalidateData(); - } - } - private int findAppByComponent(List<AppInfo> list, AppInfo item) { - ComponentName removeComponent = item.intent.getComponent(); - int length = list.size(); - for (int i = 0; i < length; ++i) { - AppInfo info = list.get(i); - if (info.user.equals(item.user) - && info.intent.getComponent().equals(removeComponent)) { - return i; - } - } - return -1; - } - private void removeAppsWithoutInvalidate(ArrayList<AppInfo> list) { - // loop through all the apps and remove apps that have the same component - int length = list.size(); - for (int i = 0; i < length; ++i) { - AppInfo info = list.get(i); - int removeIndex = findAppByComponent(mApps, info); - if (removeIndex > -1) { - mApps.remove(removeIndex); - } - } - } - public void removeApps(ArrayList<AppInfo> appInfos) { - if (!LauncherAppState.isDisableAllApps()) { - removeAppsWithoutInvalidate(appInfos); - updatePageCountsAndInvalidateData(); - } - } - public void updateApps(ArrayList<AppInfo> list) { - // We remove and re-add the updated applications list because it's properties may have - // changed (ie. the title), and this will ensure that the items will be in their proper - // place in the list. - if (!LauncherAppState.isDisableAllApps()) { - removeAppsWithoutInvalidate(list); - addAppsWithoutInvalidate(list); - updatePageCountsAndInvalidateData(); - } - } - - public void reset() { - // If we have reset, then we should not continue to restore the previous state - mSaveInstanceStateItemIndex = -1; - - if (mContentType != ContentType.Applications) { - setContentType(ContentType.Applications); - } - - if (mCurrentPage != 0) { - invalidatePageData(0); - } - } - - private AppsCustomizeTabHost getTabHost() { - return (AppsCustomizeTabHost) mLauncher.findViewById(R.id.apps_customize_pane); - } - - public void dumpState() { - // TODO: Dump information related to current list of Applications, Widgets, etc. - AppInfo.dumpApplicationInfoList(TAG, "mApps", mApps); - dumpAppWidgetProviderInfoList(TAG, "mWidgets", mWidgets); - } - - private void dumpAppWidgetProviderInfoList(String tag, String label, - ArrayList<Object> list) { - Log.d(tag, label + " size=" + list.size()); - for (Object i: list) { - if (i instanceof AppWidgetProviderInfo) { - AppWidgetProviderInfo info = (AppWidgetProviderInfo) i; - Log.d(tag, " label=\"" + info.label + "\" previewImage=" + info.previewImage - + " resizeMode=" + info.resizeMode + " configure=" + info.configure - + " initialLayout=" + info.initialLayout - + " minWidth=" + info.minWidth + " minHeight=" + info.minHeight); - } else if (i instanceof ResolveInfo) { - ResolveInfo info = (ResolveInfo) i; - Log.d(tag, " label=\"" + info.loadLabel(mPackageManager) + "\" icon=" - + info.icon); - } - } - } - - public void surrender() { - // TODO: If we are in the middle of any process (ie. for holographic outlines, etc) we - // should stop this now. - - // Stop all background tasks - cancelAllTasks(); - } - - /* - * We load an extra page on each side to prevent flashes from scrolling and loading of the - * widget previews in the background with the AsyncTasks. - */ - final static int sLookBehindPageCount = 2; - final static int sLookAheadPageCount = 2; - protected int getAssociatedLowerPageBound(int page) { - final int count = getChildCount(); - int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1); - int windowMinIndex = Math.max(Math.min(page - sLookBehindPageCount, count - windowSize), 0); - return windowMinIndex; - } - protected int getAssociatedUpperPageBound(int page) { - final int count = getChildCount(); - int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1); - int windowMaxIndex = Math.min(Math.max(page + sLookAheadPageCount, windowSize - 1), - count - 1); - return windowMaxIndex; - } - - protected String getCurrentPageDescription() { - int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; - int stringId = R.string.default_scroll_format; - int count = 0; - - if (mContentType == ContentType.Applications) { - stringId = R.string.apps_customize_apps_scroll_format; - count = mNumAppsPages; - } else if (mContentType == ContentType.Widgets) { - stringId = R.string.apps_customize_widgets_scroll_format; - count = mNumWidgetPages; - } else { - throw new RuntimeException("Invalid ContentType"); - } - - return String.format(getContext().getString(stringId), page + 1, count); - } -} diff --git a/src/com/android/launcher3/AppsCustomizeTabHost.java b/src/com/android/launcher3/AppsCustomizeTabHost.java deleted file mode 100644 index a2717126d..000000000 --- a/src/com/android/launcher3/AppsCustomizeTabHost.java +++ /dev/null @@ -1,220 +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.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityManager; -import android.widget.FrameLayout; - -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 AppsCustomizePagedView mPagedView; - private View mContent; - private boolean mInTransition = false; - - private final Rect mInsets = new Rect(); - - public AppsCustomizeTabHost(Context context, AttributeSet attrs) { - super(context, attrs); - } - - /** - * Convenience methods to select specific tabs. We want to set the content type immediately - * in these cases, but we note that we still call setCurrentTabByTag() so that the tab view - * reflects the new content (but doesn't do the animation and logic associated with changing - * tabs manually). - */ - void setContentTypeImmediate(AppsCustomizePagedView.ContentType type) { - mPagedView.setContentType(type); - } - - public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) { - setContentTypeImmediate(type); - } - - @Override - public void setInsets(Rect insets) { - mInsets.set(insets); - LayoutParams flp = (LayoutParams) mContent.getLayoutParams(); - flp.topMargin = insets.top; - flp.bottomMargin = insets.bottom; - flp.leftMargin = insets.left; - flp.rightMargin = insets.right; - mContent.setLayoutParams(flp); - } - - /** - * Setup the tab host and create all necessary tabs. - */ - @Override - protected void onFinishInflate() { - mPagedView = (AppsCustomizePagedView) findViewById(R.id.apps_customize_pane_content); - mContent = findViewById(R.id.content); - } - - public String getContentTag() { - return getTabTagForContentType(mPagedView.getContentType()); - } - - /** - * Returns the content type for the specified tab tag. - */ - public AppsCustomizePagedView.ContentType getContentTypeForTabTag(String tag) { - if (tag.equals(APPS_TAB_TAG)) { - return AppsCustomizePagedView.ContentType.Applications; - } else if (tag.equals(WIDGETS_TAB_TAG)) { - return AppsCustomizePagedView.ContentType.Widgets; - } - return AppsCustomizePagedView.ContentType.Applications; - } - - /** - * Returns the tab tag for a given content type. - */ - public String getTabTagForContentType(AppsCustomizePagedView.ContentType type) { - if (type == AppsCustomizePagedView.ContentType.Applications) { - return APPS_TAB_TAG; - } else if (type == AppsCustomizePagedView.ContentType.Widgets) { - return WIDGETS_TAB_TAG; - } - return APPS_TAB_TAG; - } - - /** - * Disable focus on anything under this view in the hierarchy if we are not visible. - */ - @Override - public int getDescendantFocusability() { - if (getVisibility() != View.VISIBLE) { - return ViewGroup.FOCUS_BLOCK_DESCENDANTS; - } - return super.getDescendantFocusability(); - } - - void reset() { - // Reset immediately - mPagedView.reset(); - } - - void trimMemory() { - mPagedView.trimMemory(); - } - - 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()); - } - } - @Override - public ViewGroup getContent() { - return mPagedView; - } - - public boolean isInTransition() { - return mInTransition; - } - - /* LauncherTransitionable overrides */ - @Override - public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { - mPagedView.onLauncherTransitionPrepare(l, animated, toWorkspace); - mInTransition = true; - - if (toWorkspace) { - // Going from All Apps -> Workspace - setVisibilityOfSiblingsWithLowerZOrder(VISIBLE); - } else { - // Going from Workspace -> All Apps - mContent.setVisibility(VISIBLE); - - // Make sure the current page is loaded (we start loading the side pages after the - // transition to prevent slowing down the animation) - // TODO: revisit this - mPagedView.loadAssociatedPages(mPagedView.getCurrentPage()); - } - } - - @Override - public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { - mPagedView.onLauncherTransitionStart(l, animated, toWorkspace); - } - - @Override - public void onLauncherTransitionStep(Launcher l, float t) { - mPagedView.onLauncherTransitionStep(l, t); - } - - @Override - public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { - mPagedView.onLauncherTransitionEnd(l, animated, toWorkspace); - mInTransition = false; - - if (!toWorkspace) { - // Make sure adjacent pages are loaded (we wait until after the transition to - // prevent slowing down the animation) - 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 - // cling initialization/dismiss code above. - setVisibilityOfSiblingsWithLowerZOrder(INVISIBLE); - } - } - - private void setVisibilityOfSiblingsWithLowerZOrder(int visibility) { - ViewGroup parent = (ViewGroup) getParent(); - if (parent == null) return; - - View overviewPanel = ((Launcher) getContext()).getOverviewPanel(); - final int count = parent.getChildCount(); - if (!isChildrenDrawingOrderEnabled()) { - for (int i = 0; i < count; i++) { - final View child = parent.getChildAt(i); - if (child == this) { - break; - } else { - if (child.getVisibility() == GONE || child == overviewPanel) { - continue; - } - child.setVisibility(visibility); - } - } - } else { - throw new RuntimeException("Failed; can't get z-order of views"); - } - } -} diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java new file mode 100644 index 000000000..954c59ffc --- /dev/null +++ b/src/com/android/launcher3/AppsGridAdapter.java @@ -0,0 +1,258 @@ +package com.android.launcher3; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.launcher3.util.Thunk; + +import java.util.List; + + +/** + * The grid view adapter of all the apps. + */ +class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> { + + public static final String TAG = "AppsGridAdapter"; + + private static final int SECTION_BREAK_VIEW_TYPE = 0; + private static final int ICON_VIEW_TYPE = 1; + private static final int EMPTY_VIEW_TYPE = 2; + + /** + * ViewHolder for each icon. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + public View mContent; + public boolean mIsSectionRow; + public boolean mIsEmptyRow; + + public ViewHolder(View v, boolean isSectionRow, boolean isEmptyRow) { + super(v); + mContent = v; + mIsSectionRow = isSectionRow; + mIsEmptyRow = isEmptyRow; + } + } + + /** + * Helper class to size the grid items. + */ + public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { + @Override + public int getSpanSize(int position) { + if (mApps.hasNoFilteredResults()) { + // Empty view spans full width + return mAppsPerRow; + } + + if (mApps.getAdapterItems().get(position).isSectionHeader) { + // Section break spans full width + return mAppsPerRow; + } else { + return 1; + } + } + } + + /** + * Helper class to draw the section headers + */ + public class GridItemDecoration extends RecyclerView.ItemDecoration { + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); + if (items.isEmpty()) { + return; + } + + for (int i = 0; i < parent.getChildCount(); i++) { + View child = parent.getChildAt(i); + ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child); + if (holder != null) { + GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) + child.getLayoutParams(); + if (!holder.mIsSectionRow && !holder.mIsEmptyRow && !lp.isItemRemoved()) { + if (items.get(holder.getPosition() - 1).isSectionHeader) { + // Draw at the parent + AlphabeticalAppsList.AdapterItem item = + items.get(holder.getPosition()); + String section = item.sectionName; + mSectionTextPaint.getTextBounds(section, 0, section.length(), + mTmpBounds); + if (mIsRtl) { + int left = parent.getWidth() - mPaddingStart - mStartMargin; + c.drawText(section, left + (mStartMargin - mTmpBounds.width()) / 2, + child.getTop() + (2 * child.getPaddingTop()) + + mTmpBounds.height(), mSectionTextPaint); + } else { + int left = mPaddingStart; + c.drawText(section, left + (mStartMargin - mTmpBounds.width()) / 2, + child.getTop() + (2 * child.getPaddingTop()) + + mTmpBounds.height(), mSectionTextPaint); + } + } + } + } + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + // Do nothing + } + } + + private LayoutInflater mLayoutInflater; + @Thunk AlphabeticalAppsList mApps; + private GridLayoutManager mGridLayoutMgr; + private GridSpanSizer mGridSizer; + private GridItemDecoration mItemDecoration; + private View.OnTouchListener mTouchListener; + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + @Thunk int mAppsPerRow; + @Thunk boolean mIsRtl; + private String mEmptySearchText; + + // Section drawing + @Thunk int mPaddingStart; + @Thunk int mStartMargin; + @Thunk Paint mSectionTextPaint; + @Thunk Rect mTmpBounds = new Rect(); + + + public AppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow, + View.OnTouchListener touchListener, View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener) { + Resources res = context.getResources(); + mApps = apps; + mAppsPerRow = appsPerRow; + mGridSizer = new GridSpanSizer(); + mGridLayoutMgr = new GridLayoutManager(context, appsPerRow, GridLayoutManager.VERTICAL, + false); + mGridLayoutMgr.setSpanSizeLookup(mGridSizer); + mItemDecoration = new GridItemDecoration(); + mLayoutInflater = LayoutInflater.from(context); + mTouchListener = touchListener; + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin); + mPaddingStart = res.getDimensionPixelSize(R.dimen.apps_container_inset); + mSectionTextPaint = new Paint(); + mSectionTextPaint.setTextSize(res.getDimensionPixelSize( + R.dimen.apps_view_section_text_size)); + mSectionTextPaint.setColor(res.getColor(R.color.apps_view_section_text_color)); + mSectionTextPaint.setAntiAlias(true); + } + + /** + * Sets the number of apps per row. + */ + public void setNumAppsPerRow(int appsPerRow) { + mAppsPerRow = appsPerRow; + mGridLayoutMgr.setSpanCount(appsPerRow); + } + + /** + * Sets whether we are in RTL mode. + */ + public void setRtl(boolean rtl) { + mIsRtl = rtl; + } + + /** + * Sets the text to show when there are no apps. + */ + public void setEmptySearchText(String query) { + mEmptySearchText = query; + } + + /** + * Returns the grid layout manager. + */ + public GridLayoutManager getLayoutManager(Context context) { + return mGridLayoutMgr; + } + + /** + * Returns the item decoration for the recycler view. + */ + public RecyclerView.ItemDecoration getItemDecoration() { + return mItemDecoration; + } + + /** + * Returns the left padding for the recycler view. + */ + public int getContentMarginStart() { + return mStartMargin; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case EMPTY_VIEW_TYPE: + return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent, + false), false /* isSectionRow */, true /* isEmptyRow */); + case SECTION_BREAK_VIEW_TYPE: + return new ViewHolder(new View(parent.getContext()), true /* isSectionRow */, + false /* isEmptyRow */); + case ICON_VIEW_TYPE: + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( + R.layout.apps_grid_row_icon_view, parent, false); + icon.setOnTouchListener(mTouchListener); + icon.setOnClickListener(mIconClickListener); + icon.setOnLongClickListener(mIconLongClickListener); + icon.setFocusable(true); + return new ViewHolder(icon, false /* isSectionRow */, false /* isEmptyRow */); + default: + throw new RuntimeException("Unexpected view type"); + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case ICON_VIEW_TYPE: + AppInfo info = mApps.getAdapterItems().get(position).appInfo; + BubbleTextView icon = (BubbleTextView) holder.mContent; + icon.applyFromApplicationInfo(info); + break; + case EMPTY_VIEW_TYPE: + TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text); + emptyViewText.setText(mEmptySearchText); + break; + } + } + + @Override + public int getItemCount() { + if (mApps.hasNoFilteredResults()) { + // For the empty view + return 1; + } + return mApps.getAdapterItems().size(); + } + + @Override + public int getItemViewType(int position) { + if (mApps.hasNoFilteredResults()) { + return EMPTY_VIEW_TYPE; + } else if (mApps.getAdapterItems().get(position).isSectionHeader) { + return SECTION_BREAK_VIEW_TYPE; + } + return ICON_VIEW_TYPE; + } +} diff --git a/src/com/android/launcher3/AppsListAdapter.java b/src/com/android/launcher3/AppsListAdapter.java new file mode 100644 index 000000000..ffd309261 --- /dev/null +++ b/src/com/android/launcher3/AppsListAdapter.java @@ -0,0 +1,143 @@ +package com.android.launcher3; + +import android.content.Context; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * The linear list view adapter for all the apps. + */ +class AppsListAdapter extends RecyclerView.Adapter<AppsListAdapter.ViewHolder> { + + /** + * ViewHolder for each row. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + public View mContent; + + public ViewHolder(View v) { + super(v); + mContent = v; + } + } + + private static final int SECTION_BREAK_VIEW_TYPE = 0; + private static final int ICON_VIEW_TYPE = 1; + private static final int EMPTY_VIEW_TYPE = 2; + + private LayoutInflater mLayoutInflater; + private AlphabeticalAppsList mApps; + private View.OnTouchListener mTouchListener; + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + private String mEmptySearchText; + + public AppsListAdapter(Context context, AlphabeticalAppsList apps, + View.OnTouchListener touchListener, View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener) { + mApps = apps; + mLayoutInflater = LayoutInflater.from(context); + mTouchListener = touchListener; + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + } + + public RecyclerView.LayoutManager getLayoutManager(Context context) { + return new LinearLayoutManager(context); + } + + /** + * Sets the text to show when there are no apps. + */ + public void setEmptySearchText(String query) { + mEmptySearchText = query; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case EMPTY_VIEW_TYPE: + return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent, + false)); + case SECTION_BREAK_VIEW_TYPE: + return new ViewHolder(new View(parent.getContext())); + case ICON_VIEW_TYPE: + // Inflate the row and all the icon children necessary + ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.apps_list_row_view, + parent, false); + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( + R.layout.apps_list_row_icon_view, row, false); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, + ViewGroup.LayoutParams.WRAP_CONTENT, 1); + lp.gravity = Gravity.CENTER_VERTICAL; + icon.setLayoutParams(lp); + icon.setOnTouchListener(mTouchListener); + icon.setOnClickListener(mIconClickListener); + icon.setOnLongClickListener(mIconLongClickListener); + icon.setFocusable(true); + row.addView(icon); + return new ViewHolder(row); + default: + throw new RuntimeException("Unexpected view type"); + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case ICON_VIEW_TYPE: + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); + ViewGroup content = (ViewGroup) holder.mContent; + String sectionDescription = item.sectionName; + + // Bind the section header + boolean showSectionHeader = true; + if (position > 0) { + AlphabeticalAppsList.AdapterItem prevItem = + mApps.getAdapterItems().get(position - 1); + showSectionHeader = prevItem.isSectionHeader; + } + TextView tv = (TextView) content.findViewById(R.id.section); + if (showSectionHeader) { + tv.setText(sectionDescription); + tv.setVisibility(View.VISIBLE); + } else { + tv.setVisibility(View.INVISIBLE); + } + + // Bind the icon + BubbleTextView icon = (BubbleTextView) content.getChildAt(1); + icon.applyFromApplicationInfo(item.appInfo); + break; + case EMPTY_VIEW_TYPE: + TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text); + emptyViewText.setText(mEmptySearchText); + break; + } + } + + @Override + public int getItemCount() { + if (mApps.hasNoFilteredResults()) { + // For the empty view + return 1; + } + return mApps.getAdapterItems().size(); + } + + @Override + public int getItemViewType(int position) { + if (mApps.hasNoFilteredResults()) { + return EMPTY_VIEW_TYPE; + } else if (mApps.getAdapterItems().get(position).isSectionHeader) { + return SECTION_BREAK_VIEW_TYPE; + } + return ICON_VIEW_TYPE; + } +} diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java index 382066094..dac79a8ef 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -37,6 +37,7 @@ import android.util.Patterns; import com.android.launcher3.LauncherProvider.SqlArguments; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -44,6 +45,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.Locale; /** * Layout parsing code for auto installs layout @@ -56,6 +58,11 @@ public class AutoInstallsLayout { static final String ACTION_LAUNCHER_CUSTOMIZATION = "android.autoinstalls.config.action.PLAY_AUTO_INSTALL"; + /** + * Layout resource which also includes grid size and hotseat count, e.g., default_layout_6x6_h5 + */ + private static final String FORMATTED_LAYOUT_RES_WITH_HOSTEAT = "default_layout_%dx%d_h%s"; + private static final String FORMATTED_LAYOUT_RES = "default_layout_%dx%d"; private static final String LAYOUT_RES = "default_layout"; static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost, @@ -65,15 +72,39 @@ public class AutoInstallsLayout { if (customizationApkInfo == null) { return null; } + return get(context, customizationApkInfo.first, customizationApkInfo.second, + appWidgetHost, callback); + } + + static AutoInstallsLayout get(Context context, String pkg, Resources targetRes, + AppWidgetHost appWidgetHost, LayoutParserCallback callback) { + DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); + + // Try with grid size and hotseat count + String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT, + (int) grid.numColumns, (int) grid.numRows, (int) grid.numHotseatIcons); + int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg); + + // Try with only grid size + if (layoutId == 0) { + Log.d(TAG, "Formatted layout: " + layoutName + + " not found. Trying layout without hosteat"); + layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES, + (int) grid.numColumns, (int) grid.numRows); + layoutId = targetRes.getIdentifier(layoutName, "xml", pkg); + } + + // Try the default layout + if (layoutId == 0) { + Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying the default layout"); + layoutId = targetRes.getIdentifier(LAYOUT_RES, "xml", pkg); + } - 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, res, layoutId, + return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId, TAG_WORKSPACE); } @@ -114,9 +145,9 @@ public class AutoInstallsLayout { 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; + @Thunk final Context mContext; + @Thunk final AppWidgetHost mAppWidgetHost; + protected final LayoutParserCallback mCallback; protected final PackageManager mPackageManager; protected final Resources mSourceRes; @@ -125,14 +156,21 @@ public class AutoInstallsLayout { private final int mHotseatAllAppsRank; private final long[] mTemp = new long[2]; - private final ContentValues mValues; - private final String mRootTag; + @Thunk final ContentValues mValues; + protected final String mRootTag; protected SQLiteDatabase mDb; public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, LayoutParserCallback callback, Resources res, int layoutId, String rootTag) { + this(context, appWidgetHost, callback, res, layoutId, rootTag, + LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile().hotseatAllAppsRank); + } + + public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, + LayoutParserCallback callback, Resources res, + int layoutId, String rootTag, int hotseatAllAppsRank) { mContext = context; mAppWidgetHost = appWidgetHost; mCallback = callback; @@ -143,8 +181,7 @@ public class AutoInstallsLayout { mSourceRes = res; mLayoutId = layoutId; - mHotseatAllAppsRank = LauncherAppState.getInstance() - .getDynamicGrid().getDeviceProfile().hotseatAllAppsRank; + mHotseatAllAppsRank = hotseatAllAppsRank; } /** @@ -569,7 +606,7 @@ public class AutoInstallsLayout { // failed to add, and less than 2 were actually added if (folderItems.size() < 2) { // Delete the folder - Uri uri = Favorites.getContentUri(folderId, false); + Uri uri = Favorites.getContentUri(folderId); SqlArguments args = new SqlArguments(uri, null, null); mDb.delete(args.table, args.where, args.args); addedId = -1; @@ -642,7 +679,7 @@ public class AutoInstallsLayout { long insertAndCheck(SQLiteDatabase db, ContentValues values); } - private static void copyInteger(ContentValues from, ContentValues to, String key) { + @Thunk static void copyInteger(ContentValues from, ContentValues to, String key) { to.put(key, from.getAsInteger(key)); } } diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index f9255e6bd..ae6ebba34 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -28,11 +28,14 @@ import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.SparseArray; import android.util.TypedValue; +import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.widget.TextView; +import com.android.launcher3.IconCache.IconLoadRequest; + /** * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan * because we want to make the bubble taller than the text and TextView's clip is @@ -49,7 +52,7 @@ public class BubbleTextView extends TextView { private static final int SHADOW_SMALL_COLOUR = 0xCC000000; static final float PADDING_V = 3.0f; - + private Drawable mIcon; private final Drawable mBackground; private final CheckLongPressHelper mLongPressHelper; private final HolographicOutlineHelper mOutlineHelper; @@ -62,13 +65,19 @@ public class BubbleTextView extends TextView { private float mSlop; - private int mTextColor; + private final boolean mDeferShadowGenerationOnTouch; private final boolean mCustomShadowsEnabled; - private boolean mIsTextVisible; + private final boolean mLayoutHorizontal; + private final int mIconSize; + private final int mIconPaddingSize; + private final int mTextSize; + private int mTextColor; private boolean mStayPressed; private boolean mIgnorePressedStateChange; + private IconLoadRequest mIconLoadRequest; + public BubbleTextView(Context context) { this(context, null, 0); } @@ -79,10 +88,21 @@ public class BubbleTextView extends TextView { public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BubbleTextView, defStyle, 0); mCustomShadowsEnabled = a.getBoolean(R.styleable.BubbleTextView_customShadows, true); + mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); + mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, + grid.allAppsIconSizePx); + mIconPaddingSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconPaddingOverride, + grid.iconDrawablePaddingPx); + mTextSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_textSizeOverride, + grid.allAppsIconTextSizePx); + mDeferShadowGenerationOnTouch = + a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false); a.recycle(); if (mCustomShadowsEnabled) { @@ -92,6 +112,12 @@ public class BubbleTextView extends TextView { } else { mBackground = null; } + + // If we are laying out horizontal, then center the text vertically + if (mLayoutHorizontal) { + setGravity(Gravity.CENTER_VERTICAL); + } + mLongPressHelper = new CheckLongPressHelper(this); mOutlineHelper = HolographicOutlineHelper.obtain(getContext()); @@ -106,9 +132,7 @@ public class BubbleTextView extends TextView { 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.iconTextSizePx); + setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); } public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, @@ -119,16 +143,11 @@ public class BubbleTextView extends TextView { 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 != 0); - setCompoundDrawables(null, iconDrawable, null, null); - if (setDefaultPadding) { - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setCompoundDrawablePadding(grid.iconDrawablePaddingPx); - } + setIcon(iconDrawable, mIconSize, setDefaultPadding ? mIconPaddingSize : -1); if (info.contentDescription != null) { setContentDescription(info.contentDescription); } @@ -141,20 +160,17 @@ public class BubbleTextView extends TextView { } public void applyFromApplicationInfo(AppInfo info) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - - Drawable topDrawable = Utilities.createIconDrawable(info.iconBitmap); - topDrawable.setBounds(0, 0, grid.allAppsIconSizePx, grid.allAppsIconSizePx); - setCompoundDrawables(null, topDrawable, null, null); - setCompoundDrawablePadding(grid.iconDrawablePaddingPx); + setIcon(Utilities.createIconDrawable(info.iconBitmap), mIconSize, mIconPaddingSize); setText(info.title); if (info.contentDescription != null) { setContentDescription(info.contentDescription); } - setTag(info); - } + // We don't need to check the info since it's not a ShortcutInfo + super.setTag(info); + // Verify high res immediately + verifyHighRes(); + } @Override protected boolean setFrame(int left, int top, int right, int bottom) { @@ -186,10 +202,19 @@ public class BubbleTextView extends TextView { } } + /** Returns the icon for this view. */ + public Drawable getIcon() { + return mIcon; + } + + /** Returns whether the layout is horizontal. */ + public boolean isLayoutHorizontal() { + return mLayoutHorizontal; + } + private void updateIconState() { - Drawable top = getCompoundDrawables()[1]; - if (top instanceof FastBitmapDrawable) { - ((FastBitmapDrawable) top).setPressed(isPressed() || mStayPressed); + if (mIcon instanceof FastBitmapDrawable) { + ((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed); } } @@ -204,7 +229,7 @@ public class BubbleTextView extends TextView { // 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 (mPressedBackground == null) { + if (!mDeferShadowGenerationOnTouch && mPressedBackground == null) { mPressedBackground = mOutlineHelper.createMediumDropShadow(this); } @@ -233,6 +258,10 @@ public class BubbleTextView extends TextView { mStayPressed = stayPressed; if (!stayPressed) { mPressedBackground = null; + } else { + if (mPressedBackground == null) { + mPressedBackground = mOutlineHelper.createMediumDropShadow(this); + } } // Only show the shadow effect when persistent pressed state is set. @@ -325,10 +354,9 @@ public class BubbleTextView extends TextView { super.onAttachedToWindow(); if (mBackground != null) mBackground.setCallback(this); - Drawable top = getCompoundDrawables()[1]; - if (top instanceof PreloadIconDrawable) { - ((PreloadIconDrawable) top).applyPreloaderTheme(getPreloaderTheme()); + if (mIcon instanceof PreloadIconDrawable) { + ((PreloadIconDrawable) mIcon).applyPreloaderTheme(getPreloaderTheme()); } mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @@ -358,16 +386,6 @@ public class BubbleTextView extends TextView { } else { super.setTextColor(res.getColor(android.R.color.transparent)); } - mIsTextVisible = visible; - } - - public boolean isTextVisible() { - return mIsTextVisible; - } - - @Override - protected boolean onSetAlpha(int alpha) { - return true; } @Override @@ -385,15 +403,13 @@ public class BubbleTextView extends TextView { ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ? info.getInstallProgress() : 0)) : 100; - Drawable[] drawables = getCompoundDrawables(); - Drawable top = drawables[1]; - if (top != null) { + if (mIcon != null) { final PreloadIconDrawable preloadDrawable; - if (top instanceof PreloadIconDrawable) { - preloadDrawable = (PreloadIconDrawable) top; + if (mIcon instanceof PreloadIconDrawable) { + preloadDrawable = (PreloadIconDrawable) mIcon; } else { - preloadDrawable = new PreloadIconDrawable(top, getPreloaderTheme()); - setCompoundDrawables(drawables[0], preloadDrawable, drawables[2], drawables[3]); + preloadDrawable = new PreloadIconDrawable(mIcon, getPreloaderTheme()); + setIcon(preloadDrawable, mIconSize, -1); } preloadDrawable.setLevel(progressLevel); @@ -417,4 +433,61 @@ public class BubbleTextView extends TextView { } return theme; } + + /** + * Sets the icon for this view based on the layout direction. + */ + private Drawable setIcon(Drawable icon, int iconSize, int drawablePadding) { + mIcon = icon; + if (iconSize != -1) { + mIcon.setBounds(0, 0, iconSize, iconSize); + } + if (mLayoutHorizontal) { + setCompoundDrawablesRelative(mIcon, null, null, null); + } else { + setCompoundDrawablesRelative(null, mIcon, null, null); + } + if (drawablePadding != -1) { + setCompoundDrawablePadding(drawablePadding); + } + return icon; + } + + /** + * Applies the item info if it is same as what the view is pointing to currently. + */ + public void reapplyItemInfo(final ItemInfo info) { + if (getTag() == info) { + mIconLoadRequest = null; + if (info instanceof AppInfo) { + applyFromApplicationInfo((AppInfo) info); + } else if (info instanceof ShortcutInfo) { + applyFromShortcutInfo((ShortcutInfo) info, + LauncherAppState.getInstance().getIconCache(), false); + } + } + } + + /** + * Verifies that the current icon is high-res otherwise posts a request to load the icon. + */ + public void verifyHighRes() { + if (mIconLoadRequest != null) { + mIconLoadRequest.cancel(); + mIconLoadRequest = null; + } + if (getTag() instanceof AppInfo) { + AppInfo info = (AppInfo) getTag(); + if (info.usingLowResIcon) { + mIconLoadRequest = LauncherAppState.getInstance().getIconCache() + .updateIconInBackground(BubbleTextView.this, info); + } + } else if (getTag() instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) getTag(); + if (info.usingLowResIcon) { + mIconLoadRequest = LauncherAppState.getInstance().getIconCache() + .updateIconInBackground(BubbleTextView.this, info); + } + } + } } diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index 019f86c21..5b399087a 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -17,19 +17,29 @@ package com.android.launcher3; import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; import android.widget.TextView; +import com.android.launcher3.R; +import com.android.launcher3.util.Thunk; /** * Implements a DropTarget. */ -public class ButtonDropTarget extends TextView implements DropTarget, DragController.DragListener { +public abstract class ButtonDropTarget extends TextView implements DropTarget, DragController.DragListener { + + private static int DRAG_VIEW_DROP_DURATION = 285; protected final int mTransitionDuration; @@ -44,6 +54,9 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro /** The paint applied to the drag view on hover */ protected int mHoverColor = 0; + protected ColorStateList mOriginalTextColor; + protected TransitionDrawable mDrawable; + public ButtonDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -56,12 +69,37 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro mBottomDragPadding = r.getDimensionPixelSize(R.dimen.drop_target_drag_padding); } - void setLauncher(Launcher launcher) { - mLauncher = launcher; + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mOriginalTextColor = getTextColors(); + + // Remove the text in the Phone UI in landscape + int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + if (!LauncherAppState.getInstance().isScreenLarge()) { + setText(""); + } + } } - public boolean acceptDrop(DragObject d) { - return false; + protected void setDrawable(int resId) { + // Get the hover color + mDrawable = (TransitionDrawable) getCurrentDrawable(); + + if (mDrawable == null) { + // TODO: investigate why this is ever happening. Presently only on one known device. + mDrawable = (TransitionDrawable) getResources().getDrawable(resId); + setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null); + } + + if (null != mDrawable) { + mDrawable.setCrossFadeEnabled(true); + } + } + + public void setLauncher(Launcher launcher) { + mLauncher = launcher; } public void setSearchDropTargetBar(SearchDropTargetBar searchDropTargetBar) { @@ -78,37 +116,94 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro return null; } - public void onDrop(DragObject d) { + @Override + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { } + + @Override + public final void onDragEnter(DragObject d) { + d.dragView.setColor(mHoverColor); + mDrawable.startTransition(mTransitionDuration); + setTextColor(mHoverColor); } - public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + @Override + public void onDragOver(DragObject d) { // Do nothing } - public void onDragEnter(DragObject d) { - d.dragView.setColor(mHoverColor); + protected void resetHoverColor() { + mDrawable.resetTransition(); + setTextColor(mOriginalTextColor); } - public void onDragOver(DragObject d) { - // Do nothing + @Override + public final void onDragExit(DragObject d) { + if (!d.dragComplete) { + d.dragView.setColor(0); + resetHoverColor(); + } else { + // Restore the hover color + d.dragView.setColor(mHoverColor); + } } - public void onDragExit(DragObject d) { - d.dragView.setColor(0); + @Override + public final void onDragStart(DragSource source, Object info, int dragAction) { + mActive = supportsDrop(source, info); + mDrawable.resetTransition(); + setTextColor(mOriginalTextColor); + ((ViewGroup) getParent()).setVisibility(mActive ? View.VISIBLE : View.GONE); } - public void onDragStart(DragSource source, Object info, int dragAction) { - // Do nothing + @Override + public final boolean acceptDrop(DragObject dragObject) { + return supportsDrop(dragObject.dragSource, dragObject.dragInfo); } + protected abstract boolean supportsDrop(DragSource source, Object info); + + @Override public boolean isDropEnabled() { return mActive; } + @Override public void onDragEnd() { - // Do nothing + mActive = false; } + /** + * On drop animate the dropView to the icon. + */ + @Override + public void onDrop(final DragObject d) { + final DragLayer dragLayer = mLauncher.getDragLayer(); + final Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, from); + + int width = mDrawable.getIntrinsicWidth(); + int height = mDrawable.getIntrinsicHeight(); + final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + width, height); + final float scale = (float) to.width() / from.width(); + mSearchDropTargetBar.deferOnDragEnd(); + + Runnable onAnimationEndRunnable = new Runnable() { + @Override + public void run() { + completeDrop(d); + mSearchDropTargetBar.onDragEnd(); + mLauncher.exitSpringLoadedDragModeDelayed(true, 0, null); + } + }; + dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, + DRAG_VIEW_DROP_DURATION, new DecelerateInterpolator(2), + new LinearInterpolator(), onAnimationEndRunnable, + DragLayer.ANIMATION_END_DISAPPEAR, null); + } + + @Thunk abstract void completeDrop(DragObject d); + @Override public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) { super.getHitRect(outRect); @@ -120,10 +215,10 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro } private boolean isRtl() { - return (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); } - Rect getIconRect(int viewWidth, int viewHeight, int drawableWidth, int drawableHeight) { + protected Rect getIconRect(int viewWidth, int viewHeight, int drawableWidth, int drawableHeight) { DragLayer dragLayer = mLauncher.getDragLayer(); // Find the rect to animate to (the view is center aligned) @@ -157,6 +252,7 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro return to; } + @Override public void getLocationInDragLayer(int[] loc) { mLauncher.getDragLayer().getLocationInDragLayer(this, loc); } diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index e6865b2e6..f4afb954d 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -22,6 +22,7 @@ import android.animation.AnimatorSet; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -33,7 +34,12 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; import android.os.Parcelable; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.ExploreByTouchHelper; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -41,35 +47,40 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.view.animation.Animation; import android.view.animation.DecelerateInterpolator; import android.view.animation.LayoutAnimationController; import com.android.launcher3.FolderIcon.FolderRingAnimator; +import com.android.launcher3.LauncherAccessibilityDelegate.DragType; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.PendingAddWidgetInfo; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Stack; public class CellLayout extends ViewGroup { static final String TAG = "CellLayout"; private Launcher mLauncher; - private int mCellWidth; - private int mCellHeight; + @Thunk int mCellWidth; + @Thunk int mCellHeight; private int mFixedCellWidth; private int mFixedCellHeight; - private int mCountX; - private int mCountY; + @Thunk int mCountX; + @Thunk int mCountY; private int mOriginalWidthGap; private int mOriginalHeightGap; - private int mWidthGap; - private int mHeightGap; + @Thunk int mWidthGap; + @Thunk int mHeightGap; private int mMaxGap; private boolean mDropPending = false; private boolean mIsDragTarget = true; @@ -77,7 +88,7 @@ public class CellLayout extends ViewGroup { // These are temporary variables to prevent having to allocate a new object just to // return an (x, y) value from helper functions. Do NOT use them to maintain other state. private final int[] mTmpXY = new int[2]; - private final int[] mTmpPoint = new int[2]; + @Thunk final int[] mTmpPoint = new int[2]; int[] mTempLocation = new int[2]; boolean[][] mOccupied; @@ -114,8 +125,8 @@ public class CellLayout extends ViewGroup { // These arrays are used to implement the drag visualization on x-large screens. // They are used as circular arrays, indexed by mDragOutlineCurrent. - private Rect[] mDragOutlines = new Rect[4]; - private float[] mDragOutlineAlphas = new float[mDragOutlines.length]; + @Thunk Rect[] mDragOutlines = new Rect[4]; + @Thunk float[] mDragOutlineAlphas = new float[mDragOutlines.length]; private InterruptibleInOutAnimator[] mDragOutlineAnims = new InterruptibleInOutAnimator[mDragOutlines.length]; @@ -125,7 +136,7 @@ public class CellLayout extends ViewGroup { private final FastBitmapView mTouchFeedbackView; - private HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new + @Thunk HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new HashMap<CellLayout.LayoutParams, Animator>(); private HashMap<View, ReorderPreviewAnimation> mShakeAnimators = new HashMap<View, ReorderPreviewAnimation>(); @@ -156,7 +167,7 @@ public class CellLayout extends ViewGroup { private static final float REORDER_PREVIEW_MAGNITUDE = 0.12f; private static final int REORDER_ANIMATION_DURATION = 150; - private float mReorderPreviewAnimationMagnitude; + @Thunk float mReorderPreviewAnimationMagnitude; private ArrayList<View> mIntersectingViews = new ArrayList<View>(); private Rect mOccupiedRect = new Rect(); @@ -169,6 +180,14 @@ public class CellLayout extends ViewGroup { private final static Paint sPaint = new Paint(); + // Related to accessible drag and drop + DragAndDropAccessibilityDelegate mTouchHelper = new DragAndDropAccessibilityDelegate(this); + private boolean mUseTouchHelper = false; + OnClickListener mOldClickListener = null; + OnClickListener mOldWorkspaceListener = null; + @Thunk int mDownX = 0; + @Thunk int mDownY = 0; + public CellLayout(Context context) { this(context, null); } @@ -294,6 +313,301 @@ public class CellLayout extends ViewGroup { addView(mShortcutsAndWidgets); } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void enableAccessibleDrag(boolean enable) { + mUseTouchHelper = enable; + if (!enable) { + ViewCompat.setAccessibilityDelegate(this, null); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + setOnClickListener(mLauncher); + } else { + ViewCompat.setAccessibilityDelegate(this, mTouchHelper); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + setOnClickListener(mTouchHelper); + } + + // Invalidate the accessibility hierarchy + if (getParent() != null) { + getParent().notifySubtreeAccessibilityStateChanged( + this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); + } + } + + @Override + public boolean dispatchHoverEvent(MotionEvent event) { + // Always attempt to dispatch hover events to accessibility first. + if (mUseTouchHelper && mTouchHelper.dispatchHoverEvent(event)) { + return true; + } + return super.dispatchHoverEvent(event); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mDownX = (int) event.getX(); + mDownY = (int) event.getY(); + } + return super.dispatchTouchEvent(event); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mUseTouchHelper || + (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev))) { + return true; + } + return false; + } + + class DragAndDropAccessibilityDelegate extends ExploreByTouchHelper implements OnClickListener { + private final Rect mTempRect = new Rect(); + + public DragAndDropAccessibilityDelegate(View forView) { + super(forView); + } + + private int getViewIdAt(float x, float y) { + if (x < 0 || y < 0 || x > getMeasuredWidth() || y > getMeasuredHeight()) { + return ExploreByTouchHelper.INVALID_ID; + } + + // Map coords to cell + int cellX = (int) Math.floor(x / (mCellWidth + mWidthGap)); + int cellY = (int) Math.floor(y / (mCellHeight + mHeightGap)); + + // Map cell to id + int id = cellX * mCountY + cellY; + return id; + } + + @Override + protected int getVirtualViewAt(float x, float y) { + return nearestDropLocation(getViewIdAt(x, y)); + } + + protected int nearestDropLocation(int id) { + int count = mCountX * mCountY; + for (int delta = 0; delta < count; delta++) { + if (id + delta <= (count - 1)) { + int target = intersectsValidDropTarget(id + delta); + if (target >= 0) { + return target; + } + } else if (id - delta >= 0) { + int target = intersectsValidDropTarget(id - delta); + if (target >= 0) { + return target; + } + } + } + return ExploreByTouchHelper.INVALID_ID; + } + + /** + * Find the virtual view id corresponding to the top left corner of any drop region by which + * the passed id is contained. For an icon, this is simply + * + * @param id the id we're interested examining (ie. does it fit there?) + * @return the view id of the top left corner of a valid drop region or -1 if there is no + * such valid region. For the icon, this can just be -1 or id. + */ + protected int intersectsValidDropTarget(int id) { + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + if (delegate == null) { + return -1; + } + + int y = id % mCountY; + int x = id / mCountY; + LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo(); + + if (dragInfo.dragType == DragType.WIDGET) { + // For a widget, every cell must be vacant. In addition, we will return any valid + // drop target by which the passed id is contained. + boolean fits = false; + + // These represent the amount that we can back off if we hit a problem. They + // get consumed as we move up and to the right, trying new regions. + int spanX = dragInfo.info.spanX; + int spanY = dragInfo.info.spanY; + + for (int m = 0; m < spanX; m++) { + for (int n = 0; n < spanY; n++) { + + fits = true; + int x0 = x - m; + int y0 = y - n; + + if (x0 < 0 || y0 < 0) continue; + + for (int i = x0; i < x0 + spanX; i++) { + if (!fits) break; + for (int j = y0; j < y0 + spanY; j++) { + if (i >= mCountX || j >= mCountY || mOccupied[i][j]) { + fits = false; + break; + } + } + } + if (fits) { + return x0 * mCountY + y0; + } + } + } + return -1; + } else { + // For an icon, we simply check the view directly below + View child = getChildAt(x, y); + if (child == null || child == dragInfo.item) { + // Empty cell. Good for an icon or folder. + return id; + } else if (dragInfo.dragType != DragType.FOLDER) { + // For icons, we can consider cells that have another icon or a folder. + ItemInfo info = (ItemInfo) child.getTag(); + if (info instanceof AppInfo || info instanceof FolderInfo || + info instanceof ShortcutInfo) { + return id; + } + } + return -1; + } + } + + @Override + protected void getVisibleVirtualViews(List<Integer> virtualViews) { + // We create a virtual view for each cell of the grid + // The cell ids correspond to cells in reading order. + int nCells = mCountX * mCountY; + + for (int i = 0; i < nCells; i++) { + if (intersectsValidDropTarget(i) >= 0) { + virtualViews.add(i); + } + } + } + + @Override + protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) { + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + if (delegate == null) { + return false; + } + + if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { + String confirmation = getConfirmationForIconDrop(viewId); + delegate.handleAccessibleDrop(CellLayout.this, getItemBounds(viewId), confirmation); + return true; + } + return false; + } + + @Override + public void onClick(View arg0) { + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + if (delegate == null) { + return; + } + + int viewId = getViewIdAt(mDownX, mDownY); + + String confirmation = getConfirmationForIconDrop(viewId); + delegate.handleAccessibleDrop(CellLayout.this, getItemBounds(viewId), confirmation); + } + + @Override + protected void onPopulateEventForVirtualView(int id, AccessibilityEvent event) { + if (id == ExploreByTouchHelper.INVALID_ID) { + throw new IllegalArgumentException("Invalid virtual view id"); + } + // We're required to set something here. + event.setContentDescription(""); + } + + @Override + protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) { + if (id == ExploreByTouchHelper.INVALID_ID) { + throw new IllegalArgumentException("Invalid virtual view id"); + } + + node.setContentDescription(getLocationDescriptionForIconDrop(id)); + node.setBoundsInParent(getItemBounds(id)); + + node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + node.setClickable(true); + node.setFocusable(true); + } + + private String getLocationDescriptionForIconDrop(int id) { + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + if (delegate == null) { + return ""; + } + + int y = id % mCountY; + int x = id / mCountY; + LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo(); + + Resources res = getContext().getResources(); + View child = getChildAt(x, y); + if (child == null || child == dragInfo.item) { + return res.getString(R.string.move_to_empty_cell, x, y); + } else { + ItemInfo info = (ItemInfo) child.getTag(); + if (info instanceof AppInfo || info instanceof ShortcutInfo) { + return res.getString(R.string.create_folder_with, info.title); + } else if (info instanceof FolderInfo) { + return res.getString(R.string.add_to_folder, info.title); + } + } + return ""; + } + + private String getConfirmationForIconDrop(int id) { + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + if (delegate == null) { + return ""; + } + + int y = id % mCountY; + int x = id / mCountY; + LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo(); + + Resources res = getContext().getResources(); + View child = getChildAt(x, y); + if (child == null || child == dragInfo.item) { + return res.getString(R.string.item_moved); + } else { + ItemInfo info = (ItemInfo) child.getTag(); + if (info instanceof AppInfo || info instanceof ShortcutInfo) { + return res.getString(R.string.folder_created); + + } else if (info instanceof FolderInfo) { + return res.getString(R.string.added_to_folder); + } + } + return ""; + } + + private Rect getItemBounds(int id) { + int cellY = id % mCountY; + int cellX = id / mCountY; + int x = getPaddingLeft() + (int) (cellX * (mCellWidth + mWidthGap)); + int y = getPaddingTop() + (int) (cellY * (mCellHeight + mHeightGap)); + + Rect bounds = mTempRect; + bounds.set(x, y, x + mCellWidth, y + mCellHeight); + return bounds; + } + } + public void enableHardwareLayer(boolean hasLayer) { mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint); } @@ -578,11 +892,11 @@ public class CellLayout extends ViewGroup { mInterceptTouchListener = listener; } - int getCountX() { + public int getCountX() { return mCountX; } - int getCountY() { + public int getCountY() { return mCountY; } @@ -613,11 +927,7 @@ public class CellLayout extends ViewGroup { if (lp.cellVSpan < 0) lp.cellVSpan = mCountY; child.setId(childId); - if (inLayout) { - mShortcutsAndWidgets.addView(child, index, lp, true); - } else { - mShortcutsAndWidgets.addView(child, index, lp, false); - } + mShortcutsAndWidgets.addView(child, index, lp, inLayout); if (markCells) markCellsAsOccupiedForView(child); @@ -683,18 +993,6 @@ public class CellLayout extends ViewGroup { mShortcutsAndWidgets.removeViewsInLayout(start, count); } - @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. - if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) { - return true; - } - - return false; - } - /** * Given a point, return the cell that strictly encloses that point * @param x X coordinate of the point @@ -2272,7 +2570,7 @@ public class CellLayout extends ViewGroup { } } - private void completeAnimationImmediately() { + @Thunk void completeAnimationImmediately() { if (a != null) { a.cancel(); } @@ -2593,7 +2891,7 @@ public class CellLayout extends ViewGroup { return mItemPlacementDirty; } - private class ItemConfiguration { + @Thunk class ItemConfiguration { HashMap<View, CellAndSpan> map = new HashMap<View, CellAndSpan>(); private HashMap<View, CellAndSpan> savedMap = new HashMap<View, CellAndSpan>(); ArrayList<View> sortedViews = new ArrayList<View>(); @@ -2727,7 +3025,7 @@ public class CellLayout extends ViewGroup { * * @return True if a vacant cell of the specified dimension was found, false otherwise. */ - boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { + public boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, null, mOccupied); } diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java index 81149793d..10ca6a371 100644 --- a/src/com/android/launcher3/CheckLongPressHelper.java +++ b/src/com/android/launcher3/CheckLongPressHelper.java @@ -18,9 +18,11 @@ package com.android.launcher3; import android.view.View; +import com.android.launcher3.util.Thunk; + public class CheckLongPressHelper { - private View mView; - private boolean mHasPerformedLongPress; + @Thunk View mView; + @Thunk boolean mHasPerformedLongPress; private CheckForLongPress mPendingCheckForLongPress; class CheckForLongPress implements Runnable { diff --git a/src/com/android/launcher3/CommonAppTypeParser.java b/src/com/android/launcher3/CommonAppTypeParser.java new file mode 100644 index 000000000..31641799d --- /dev/null +++ b/src/com/android/launcher3/CommonAppTypeParser.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.res.XmlResourceParser; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.backup.BackupProtos.Favorite; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * A class that parses content values corresponding to some common app types. + */ +public class CommonAppTypeParser implements LayoutParserCallback { + private static final String TAG = "CommonAppTypeParser"; + + // Including TARGET_NONE + public static final int SUPPORTED_TYPE_COUNT = 7; + + private static final int RESTORE_FLAG_BIT_SHIFT = 4; + + + private final long mItemId; + private final int mResId; + private final Context mContext; + + ContentValues parsedValues; + Intent parsedIntent; + String parsedTitle; + + public CommonAppTypeParser(long itemId, int itemType, Context context) { + mItemId = itemId; + mContext = context; + mResId = getResourceForItemType(itemType); + } + + @Override + public long generateNewItemId() { + return mItemId; + } + + @Override + public long insertAndCheck(SQLiteDatabase db, ContentValues values) { + parsedValues = values; + + // Remove unwanted values + values.put(Favorites.ICON_TYPE, (Integer) null); + values.put(Favorites.ICON_PACKAGE, (String) null); + values.put(Favorites.ICON_RESOURCE, (String) null); + values.put(Favorites.ICON, (byte[]) null); + return 1; + } + + /** + * Tries to find a suitable app to the provided app type. + */ + public boolean findDefaultApp() { + if (mResId == 0) { + return false; + } + + parsedIntent = null; + parsedValues = null; + new MyLayoutParser().parseValues(); + return (parsedValues != null) && (parsedIntent != null); + } + + private class MyLayoutParser extends DefaultLayoutParser { + + public MyLayoutParser() { + super(CommonAppTypeParser.this.mContext, null, CommonAppTypeParser.this, + CommonAppTypeParser.this.mContext.getResources(), mResId, TAG_RESOLVE, 0); + } + + @Override + protected long addShortcut(String title, Intent intent, int type) { + if (type == Favorites.ITEM_TYPE_APPLICATION) { + parsedIntent = intent; + parsedTitle = title; + } + return super.addShortcut(title, intent, type); + } + + public void parseValues() { + XmlResourceParser parser = mSourceRes.getXml(mLayoutId); + try { + beginDocument(parser, mRootTag); + new ResolveParser().parseAndAdd(parser); + } catch (IOException | XmlPullParserException e) { + Log.e(TAG, "Unable to parse default app info", e); + } + parser.close(); + } + } + + public static int getResourceForItemType(int type) { + switch (type) { + case Favorite.TARGET_PHONE: + return R.xml.app_target_phone; + + case Favorite.TARGET_MESSENGER: + return R.xml.app_target_messenger; + + case Favorite.TARGET_EMAIL: + return R.xml.app_target_email; + + case Favorite.TARGET_BROWSER: + return R.xml.app_target_browser; + + case Favorite.TARGET_GALLERY: + return R.xml.app_target_gallery; + + case Favorite.TARGET_CAMERA: + return R.xml.app_target_camera; + + default: + return 0; + } + } + + public static int encodeItemTypeToFlag(int itemType) { + return itemType << RESTORE_FLAG_BIT_SHIFT; + } + + public static int decodeItemTypeFromFlag(int flag) { + return (flag & ShortcutInfo.FLAG_RESTORED_APP_TYPE) >> RESTORE_FLAG_BIT_SHIFT; + } + +} diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java index 986ae81f5..7b91c675b 100644 --- a/src/com/android/launcher3/DefaultLayoutParser.java +++ b/src/com/android/launcher3/DefaultLayoutParser.java @@ -13,6 +13,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -28,15 +29,15 @@ import java.util.List; public class DefaultLayoutParser extends AutoInstallsLayout { private static final String TAG = "DefaultLayoutParser"; - private static final String TAG_RESOLVE = "resolve"; + protected static final String TAG_RESOLVE = "resolve"; private static final String TAG_FAVORITES = "favorites"; - private static final String TAG_FAVORITE = "favorite"; + protected static final String TAG_FAVORITE = "favorite"; 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 ATTR_URI = "uri"; + protected static final String ATTR_URI = "uri"; private static final String ATTR_CONTAINER = "container"; private static final String ATTR_SCREEN = "screen"; private static final String ATTR_FOLDER_ITEMS = "folderItems"; @@ -44,7 +45,12 @@ public class DefaultLayoutParser extends AutoInstallsLayout { public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost, LayoutParserCallback callback, Resources sourceRes, int layoutId) { super(context, appWidgetHost, callback, sourceRes, layoutId, TAG_FAVORITES); - Log.e(TAG, "Default layout parser initialized"); + } + + public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost, + LayoutParserCallback callback, Resources sourceRes, int layoutId, String rootTag, + int hotseatAllAppsRank) { + super(context, appWidgetHost, callback, sourceRes, layoutId, rootTag, hotseatAllAppsRank); } @Override @@ -52,7 +58,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { return getFolderElementsMap(mSourceRes); } - private HashMap<String, TagParser> getFolderElementsMap(Resources res) { + @Thunk HashMap<String, TagParser> getFolderElementsMap(Resources res) { HashMap<String, TagParser> parsers = new HashMap<String, TagParser>(); parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser()); parsers.put(TAG_SHORTCUT, new UriShortcutParser(res)); @@ -84,7 +90,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * AppShortcutParser which also supports adding URI based intents */ - private class AppShortcutWithUriParser extends AppShortcutParser { + @Thunk class AppShortcutWithUriParser extends AppShortcutParser { @Override protected long invalidPackageOrClass(XmlResourceParser parser) { @@ -196,7 +202,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * Contains a list of <favorite> nodes, and accepts the first successfully parsed node. */ - private class ResolveParser implements TagParser { + protected class ResolveParser implements TagParser { private final AppShortcutWithUriParser mChildParser = new AppShortcutWithUriParser(); @@ -226,7 +232,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * A parser which adds a folder whose contents come from partner apk. */ - private class PartnerFolderParser implements TagParser { + @Thunk class PartnerFolderParser implements TagParser { @Override public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, @@ -252,7 +258,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * An extension of FolderParser which allows adding items from a different xml. */ - private class MyFolderParser extends FolderParser { + @Thunk class MyFolderParser extends FolderParser { @Override public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, diff --git a/src/com/android/launcher3/DeferredHandler.java b/src/com/android/launcher3/DeferredHandler.java index a2d121d63..eb7c26a28 100644 --- a/src/com/android/launcher3/DeferredHandler.java +++ b/src/com/android/launcher3/DeferredHandler.java @@ -22,6 +22,8 @@ import android.os.Message; import android.os.MessageQueue; import android.util.Pair; +import com.android.launcher3.util.Thunk; + import java.util.LinkedList; import java.util.ListIterator; @@ -33,11 +35,11 @@ import java.util.ListIterator; * This class is fifo. */ public class DeferredHandler { - private LinkedList<Pair<Runnable, Integer>> mQueue = new LinkedList<Pair<Runnable, Integer>>(); + @Thunk LinkedList<Pair<Runnable, Integer>> mQueue = new LinkedList<Pair<Runnable, Integer>>(); private MessageQueue mMessageQueue = Looper.myQueue(); private Impl mHandler = new Impl(); - private class Impl extends Handler implements MessageQueue.IdleHandler { + @Thunk class Impl extends Handler implements MessageQueue.IdleHandler { public void handleMessage(Message msg) { Pair<Runnable, Integer> p; Runnable r; diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index ebe874f38..aa3e66c09 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -19,31 +19,22 @@ package com.android.launcher3; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.annotation.TargetApi; -import android.content.ComponentName; import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.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; -import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; -import android.view.animation.LinearInterpolator; -import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.R; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.WidgetsContainerView; public class DeleteDropTarget extends ButtonDropTarget { - private static int DELETE_ANIMATION_DURATION = 285; + private static int FLING_DELETE_ANIMATION_DURATION = 350; private static float FLING_TO_DELETE_FRICTION = 0.035f; private static int MODE_FLING_DELETE_TO_TRASH = 0; @@ -51,13 +42,6 @@ public class DeleteDropTarget extends ButtonDropTarget { private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR; - private ColorStateList mOriginalTextColor; - private TransitionDrawable mUninstallDrawable; - private TransitionDrawable mRemoveDrawable; - private TransitionDrawable mCurrentDrawable; - - private boolean mWaitingForUninstall = false; - public DeleteDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -69,271 +53,27 @@ public class DeleteDropTarget extends ButtonDropTarget { @Override protected void onFinishInflate() { super.onFinishInflate(); - - // Get the drawable - mOriginalTextColor = getTextColors(); - // Get the hover color - Resources r = getResources(); - mHoverColor = r.getColor(R.color.delete_target_hover_tint); - mUninstallDrawable = (TransitionDrawable) - r.getDrawable(R.drawable.uninstall_target_selector); - mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector); - - mRemoveDrawable.setCrossFadeEnabled(true); - mUninstallDrawable.setCrossFadeEnabled(true); - - // The current drawable is set to either the remove drawable or the uninstall drawable - // and is initially set to the remove drawable, as set in the layout xml. - mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); - - // Remove the text in the Phone UI in landscape - int orientation = getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - if (!LauncherAppState.getInstance().isScreenLarge()) { - setText(""); - } - } - } - - private boolean isAllAppsApplication(DragSource source, Object info) { - return source.supportsAppInfoDropTarget() && (info instanceof AppInfo); - } - private boolean isAllAppsWidget(DragSource source, Object info) { - if (source instanceof AppsCustomizePagedView) { - if (info instanceof PendingAddItemInfo) { - PendingAddItemInfo addInfo = (PendingAddItemInfo) info; - switch (addInfo.itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: - case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: - case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: - return true; - } - } - } - return false; - } - private boolean isDragSourceWorkspaceOrFolder(DragObject d) { - return (d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder); - } - - private void setHoverColor() { - if (mCurrentDrawable != null) { - mCurrentDrawable.startTransition(mTransitionDuration); - } - setTextColor(mHoverColor); - } - private void resetHoverColor() { - if (mCurrentDrawable != null) { - mCurrentDrawable.resetTransition(); - } - setTextColor(mOriginalTextColor); - } + mHoverColor = getResources().getColor(R.color.delete_target_hover_tint); - @Override - public boolean acceptDrop(DragObject d) { - return willAcceptDrop(d.dragInfo); + setDrawable(R.drawable.remove_target_selector); } - public static boolean willAcceptDrop(Object info) { - if (info instanceof ItemInfo) { - ItemInfo item = (ItemInfo) info; - if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET || - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET || - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { - return true; - } - - if (!LauncherAppState.isDisableAllApps() && - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { - return true; - } - - if (!LauncherAppState.isDisableAllApps() && - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && - item instanceof AppInfo) { - AppInfo appInfo = (AppInfo) info; - return (appInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; - } - - if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && - item instanceof ShortcutInfo) { - if (LauncherAppState.isDisableAllApps()) { - ShortcutInfo shortcutInfo = (ShortcutInfo) info; - return (shortcutInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; - } else { - return true; - } - } - } - return false; + public static boolean willAcceptDrop(DragSource source, Object info) { + return (info instanceof ItemInfo) && source.supportsDeleteDropTarget(); } - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) @Override - public void onDragStart(DragSource source, Object info, int dragAction) { - boolean isVisible = true; - boolean useUninstallLabel = !LauncherAppState.isDisableAllApps() && - isAllAppsApplication(source, info); - boolean useDeleteLabel = !useUninstallLabel && source.supportsDeleteDropTarget(); - - // If we are dragging an application from AppsCustomize, only show the control if we can - // delete the app (it was downloaded), and rename the string to "uninstall" in such a case. - // Hide the delete target if it is a widget from AppsCustomize. - 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); - } else if (useDeleteLabel) { - setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null); - } else { - isVisible = false; - } - mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); - - mActive = isVisible; - resetHoverColor(); - ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); - if (isVisible && getText().length() > 0) { - setText(useUninstallLabel ? R.string.delete_target_uninstall_label - : R.string.delete_target_label); - } + protected boolean supportsDrop(DragSource source, Object info) { + return willAcceptDrop(source, info); } @Override - public void onDragEnd() { - super.onDragEnd(); - mActive = false; - } - - public void onDragEnter(DragObject d) { - super.onDragEnter(d); - - setHoverColor(); - } - - public void onDragExit(DragObject d) { - super.onDragExit(d); - - if (!d.dragComplete) { - resetHoverColor(); - } else { - // Restore the hover color if we are deleting - d.dragView.setColor(mHoverColor); - } - } - - private void animateToTrashAndCompleteDrop(final DragObject d) { - 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(), - width, height); - final float scale = (float) to.width() / from.width(); - - mSearchDropTargetBar.deferOnDragEnd(); - deferCompleteDropIfUninstalling(d); - - Runnable onAnimationEndRunnable = new Runnable() { - @Override - public void run() { - completeDrop(d); - mSearchDropTargetBar.onDragEnd(); - mLauncher.exitSpringLoadedDragModeDelayed(true, 0, null); - } - }; - dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, - DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2), - new LinearInterpolator(), onAnimationEndRunnable, - DragLayer.ANIMATION_END_DISAPPEAR, null); - } - - private void deferCompleteDropIfUninstalling(DragObject d) { - mWaitingForUninstall = false; - if (isUninstallFromWorkspace(d)) { - if (d.dragSource instanceof Folder) { - ((Folder) d.dragSource).deferCompleteDropAfterUninstallActivity(); - } else if (d.dragSource instanceof Workspace) { - ((Workspace) d.dragSource).deferCompleteDropAfterUninstallActivity(); - } - mWaitingForUninstall = true; - } - } - - private boolean isUninstallFromWorkspace(DragObject d) { - if (LauncherAppState.isDisableAllApps() && isDragSourceWorkspaceOrFolder(d) - && (d.dragInfo instanceof ShortcutInfo)) { - ShortcutInfo shortcut = (ShortcutInfo) d.dragInfo; - // Only allow manifest shortcuts to initiate an un-install. - return !InstallShortcutReceiver.isValidShortcutLaunchIntent(shortcut.intent); - } - return false; - } - - private void completeDrop(DragObject d) { + @Thunk void completeDrop(DragObject d) { ItemInfo item = (ItemInfo) d.dragInfo; - boolean wasWaitingForUninstall = mWaitingForUninstall; - mWaitingForUninstall = false; - if (isAllAppsApplication(d.dragSource, item)) { - uninstallApp(mLauncher, (AppInfo) item); - } 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; - 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(); - boolean uninstallSuccessful = !AllAppsList.packageHasActivities( - getContext(), packageName, user); - if (dragSource instanceof Folder) { - ((Folder) dragSource). - onUninstallActivityReturned(uninstallSuccessful); - } else if (dragSource instanceof Workspace) { - ((Workspace) dragSource). - onUninstallActivityReturned(uninstallSuccessful); - } - } - }; - mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess); - } - } - } else if (isDragSourceWorkspaceOrFolder(d)) { + if ((d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder)) { removeWorkspaceOrFolderItem(mLauncher, item, null); } - if (wasWaitingForUninstall && !mWaitingForUninstall) { - if (d.dragSource instanceof Folder) { - ((Folder) d.dragSource).onUninstallActivityReturned(false); - } else if (d.dragSource instanceof Workspace) { - ((Workspace) d.dragSource).onUninstallActivityReturned(false); - } - } - } - - public static void uninstallApp(Launcher launcher, AppInfo info) { - launcher.startApplicationUninstallActivity(info.componentName, info.flags, info.user); } /** @@ -365,7 +105,7 @@ public class DeleteDropTarget extends ButtonDropTarget { appWidgetHost.deleteAppWidgetId(widget.appWidgetId); return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } else { return false; @@ -378,18 +118,14 @@ public class DeleteDropTarget extends ButtonDropTarget { return true; } - public void onDrop(DragObject d) { - animateToTrashAndCompleteDrop(d); - } - /** * Creates an animation from the current drag view to the delete trash icon. */ private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer, DragObject d, PointF vel, ViewConfiguration config) { - int width = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicWidth(); - int height = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicHeight(); + int width = mDrawable.getIntrinsicWidth(); + int height = mDrawable.getIntrinsicHeight(); final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), width, height); final Rect from = new Rect(); @@ -502,13 +238,14 @@ public class DeleteDropTarget extends ButtonDropTarget { } public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) { - final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView; + final boolean isWidgets = d.dragSource instanceof WidgetsContainerView; + final boolean isAllapps = d.dragSource instanceof AppsContainerView; // Don't highlight the icon as it's animating d.dragView.setColor(0); d.dragView.updateInitialScaleToCurrentScale(); // Don't highlight the target if we are flinging from AllApps - if (isAllApps) { + if (isWidgets || isAllapps) { resetHoverColor(); } @@ -551,14 +288,13 @@ public class DeleteDropTarget extends ButtonDropTarget { updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime, duration, config); } - deferCompleteDropIfUninstalling(d); Runnable onAnimationEndRunnable = new Runnable() { @Override public void run() { // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up // itself, otherwise, complete the drop to initiate the deletion process - if (!isAllApps) { + if (!isWidgets || !isAllapps) { mLauncher.exitSpringLoadedDragMode(); completeDrop(d); } diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 34e1f3c5f..22fb6a049 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -38,6 +38,8 @@ import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.LinearLayout; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -80,7 +82,8 @@ public class DeviceProfile { boolean isLandscape; boolean isTablet; boolean isLargeTablet; - boolean isLayoutRtl; + public boolean isLayoutRtl; + boolean transposeLayoutWithOrientation; int desiredWorkspaceLeftRightMarginPx; @@ -103,8 +106,8 @@ public class DeviceProfile { public int cellWidthPx; public int cellHeightPx; - int iconSizePx; - int iconTextSizePx; + public int iconSizePx; + public int iconTextSizePx; int iconDrawablePaddingPx; int allAppsIconSizePx; int allAppsIconTextSizePx; @@ -122,6 +125,7 @@ public class DeviceProfile { int hotseatAllAppsRank; int allAppsNumRows; int allAppsNumCols; + int appsViewNumCols; int searchBarSpaceWidthPx; int searchBarSpaceHeightPx; int pageIndicatorHeightPx; @@ -137,7 +141,7 @@ public class DeviceProfile { DeviceProfile(String n, float w, float h, float r, float c, float is, float its, float hs, float his, int dlId) { // Ensure that we have an odd number of hotseat items (since we need to place all apps) - if (!LauncherAppState.isDisableAllApps() && hs % 2 == 0) { + if (hs % 2 == 0) { throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); } @@ -363,7 +367,7 @@ public class DeviceProfile { } } - private void updateIconSize(float scale, int drawablePadding, Resources resources, + private void updateIconSize(float scale, int drawablePadding, Resources res, DisplayMetrics dm) { iconSizePx = (int) (DynamicGrid.pxFromDp(iconSize, dm) * scale); iconTextSizePx = (int) (DynamicGrid.pxFromSp(iconTextSize, dm) * scale); @@ -372,9 +376,9 @@ public class DeviceProfile { // Search Bar searchBarSpaceWidthPx = Math.min(widthPx, - resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); searchBarSpaceHeightPx = getSearchBarTopOffset() - + resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); + + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); // Calculate the actual text height Paint textPaint = new Paint(); @@ -382,7 +386,7 @@ public class DeviceProfile { FontMetrics fm = textPaint.getFontMetrics(); cellWidthPx = iconSizePx; cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top); - final float scaleDps = resources.getDimensionPixelSize(R.dimen.dragViewScale); + final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale); dragViewScale = (iconSizePx + scaleDps) / iconSizePx; // Hotseat @@ -400,11 +404,11 @@ public class DeviceProfile { allAppsCellWidthPx = allAppsIconSizePx; allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx; int maxLongEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count); + res.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count); int maxShortEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count); + res.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count); int minEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count); + res.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count); int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount); int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount); @@ -415,10 +419,25 @@ public class DeviceProfile { allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) / (allAppsCellHeightPx + allAppsCellPaddingPx); allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows)); - allAppsNumCols = (availableWidthPx) / - (allAppsCellWidthPx + allAppsCellPaddingPx); + allAppsNumCols = (availableWidthPx) / (allAppsCellWidthPx + allAppsCellPaddingPx); allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols)); } + + int appsContainerViewWidthPx = res.getDimensionPixelSize(R.dimen.apps_container_width); + updateAppsViewNumCols(res, appsContainerViewWidthPx); + } + + public boolean updateAppsViewNumCols(Resources res, int containerWidth) { + int appsViewLeftMarginPx = + res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin); + int availableAppsWidthPx = (containerWidth > 0) ? containerWidth : availableWidthPx; + int numCols = (availableAppsWidthPx - appsViewLeftMarginPx) / + (allAppsCellWidthPx + 2 * allAppsCellPaddingPx); + if (numCols != appsViewNumCols) { + appsViewNumCols = numCols; + return true; + } + return false; } void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx, @@ -440,7 +459,7 @@ public class DeviceProfile { updateAvailableDimensions(context); } - private float dist(PointF p0, PointF p1) { + @Thunk float dist(PointF p0, PointF p1) { return (float) Math.sqrt((p1.x - p0.x)*(p1.x-p0.x) + (p1.y-p0.y)*(p1.y-p0.y)); } @@ -691,6 +710,10 @@ public class DeviceProfile { return isLargeTablet; } + /** + * When {@code true}, hotseat is on the bottom row when in landscape mode. + * If {@code false}, hotseat is on the right column when in landscape mode. + */ boolean isVerticalBarLayout() { return isLandscape && transposeLayoutWithOrientation; } @@ -788,64 +811,6 @@ public class DeviceProfile { } } - // Layout AllApps - AppsCustomizeTabHost host = (AppsCustomizeTabHost) - launcher.findViewById(R.id.apps_customize_pane); - if (host != null) { - // Center the all apps page indicator - int pageIndicatorHeight = (int) (pageIndicatorHeightPx * Math.min(1f, - (allAppsIconSizePx / DynamicGrid.DEFAULT_ICON_SIZE_PX))); - pageIndicator = host.findViewById(R.id.apps_customize_page_indicator); - if (pageIndicator != null) { - 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 - int paddingLR = (availableWidthPx - (allAppsCellWidthPx * allAppsNumCols)) / - (2 * (allAppsNumCols + 1)); - int paddingTB = (availableHeightPx - (allAppsCellHeightPx * allAppsNumRows)) / - (2 * (allAppsNumRows + 1)); - paddingLR = Math.min(paddingLR, (int)((paddingLR + paddingTB) * 0.75f)); - paddingTB = Math.min(paddingTB, (int)((paddingLR + paddingTB) * 0.75f)); - int maxAllAppsWidth = (allAppsNumCols * (allAppsCellWidthPx + 2 * paddingLR)); - int gridPaddingLR = (availableWidthPx - maxAllAppsWidth) / 2; - // Only adjust the side paddings on landscape phones, or tablets - 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.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); - - } - } - // Layout the Overview Mode ViewGroup overviewMode = launcher.getOverviewPanel(); if (overviewMode != null) { diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java index 480dce999..b24608cb1 100644 --- a/src/com/android/launcher3/DragController.java +++ b/src/com/android/launcher3/DragController.java @@ -34,6 +34,8 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.inputmethod.InputMethodManager; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; import java.util.HashSet; @@ -49,8 +51,8 @@ public class DragController { /** Indicates the drag is a copy. */ public static int DRAG_ACTION_COPY = 1; - private static final int SCROLL_DELAY = 500; - private static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; + public static final int SCROLL_DELAY = 500; + public static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; private static final boolean PROFILE_DRAWING_DURING_DRAG = false; @@ -63,7 +65,7 @@ public class DragController { private static final float MAX_FLING_DEGREES = 35f; - private Launcher mLauncher; + @Thunk Launcher mLauncher; private Handler mHandler; // temporaries to avoid gc thrash @@ -73,6 +75,9 @@ public class DragController { /** Whether or not we're dragging. */ private boolean mDragging; + /** Whether or not this is an accessible drag operation */ + private boolean mIsAccessibleDrag; + /** X coordinate of the down event. */ private int mMotionDownX; @@ -99,17 +104,17 @@ public class DragController { private View mMoveTarget; - private DragScroller mDragScroller; - private int mScrollState = SCROLL_OUTSIDE_ZONE; + @Thunk DragScroller mDragScroller; + @Thunk int mScrollState = SCROLL_OUTSIDE_ZONE; private ScrollRunnable mScrollRunnable = new ScrollRunnable(); private DropTarget mLastDropTarget; private InputMethodManager mInputMethodManager; - private int mLastTouch[] = new int[2]; - private long mLastTouchUpTime = -1; - private int mDistanceSinceScroll = 0; + @Thunk int mLastTouch[] = new int[2]; + @Thunk long mLastTouchUpTime = -1; + @Thunk int mDistanceSinceScroll = 0; private int mTmpPoint[] = new int[2]; private Rect mDragLayerRect = new Rect(); @@ -120,7 +125,7 @@ public class DragController { /** * Interface to receive notifications when a drag starts or stops */ - interface DragListener { + public interface DragListener { /** * A drag has begun * @@ -182,7 +187,7 @@ public class DragController { (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2); startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, - null, initialDragViewScale); + null, initialDragViewScale, false); if (dragAction == DRAG_ACTION_MOVE) { v.setVisibility(View.GONE); @@ -202,10 +207,11 @@ public class DragController { * {@link #DRAG_ACTION_COPY} * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. * Makes dragging feel more precise, e.g. you can clip out a transparent border + * @param accessible whether this drag should occur in accessibility mode */ public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY, DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion, - float initialDragViewScale) { + float initialDragViewScale, boolean accessible) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); } @@ -228,12 +234,21 @@ public class DragController { final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; mDragging = true; + mIsAccessibleDrag = accessible; mDragObject = new DropTarget.DragObject(); mDragObject.dragComplete = false; - mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); - mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); + if (mIsAccessibleDrag) { + // For an accessible drag, we assume the view is being dragged from the center. + mDragObject.xOffset = b.getWidth() / 2; + mDragObject.yOffset = b.getHeight() / 2; + mDragObject.accessibleDrag = true; + } else { + mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); + mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); + } + mDragObject.dragSource = source; mDragObject.dragInfo = dragInfo; @@ -349,6 +364,7 @@ public class DragController { private void endDrag() { if (mDragging) { mDragging = false; + mIsAccessibleDrag = false; clearScrollRunnable(); boolean isDeferred = false; if (mDragObject.dragView != null) { @@ -361,7 +377,7 @@ public class DragController { // Only end the drag if we are not deferred if (!isDeferred) { - for (DragListener listener : mListeners) { + for (DragListener listener : new ArrayList<>(mListeners)) { listener.onDragEnd(); } } @@ -378,13 +394,13 @@ public class DragController { if (mDragObject.deferDragViewCleanupPostAnimation) { // If we skipped calling onDragEnd() before, do it now - for (DragListener listener : mListeners) { + for (DragListener listener : new ArrayList<>(mListeners)) { listener.onDragEnd(); } } } - void onDeferredEndFling(DropTarget.DragObject d) { + public void onDeferredEndFling(DropTarget.DragObject d) { d.dragSource.onFlingToDeleteCompleted(); } @@ -421,6 +437,10 @@ public class DragController { + mDragging); } + if (mIsAccessibleDrag) { + return false; + } + // Update the velocity tracker acquireVelocityTrackerAndAddMovement(ev); @@ -442,7 +462,8 @@ public class DragController { mLastTouchUpTime = System.currentTimeMillis(); if (mDragging) { PointF vec = isFlingingToDelete(mDragObject.dragSource); - if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) { + if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragSource, + mDragObject.dragInfo)) { vec = null; } if (vec != null) { @@ -525,7 +546,7 @@ public class DragController { mLastDropTarget = dropTarget; } - private void checkScrollState(int x, int y) { + @Thunk void checkScrollState(int x, int y) { final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop(); final int delay = mDistanceSinceScroll < slop ? RESCROLL_DELAY : SCROLL_DELAY; final DragLayer dragLayer = mLauncher.getDragLayer(); @@ -560,7 +581,7 @@ public class DragController { * Call this from a drag source view. */ public boolean onTouchEvent(MotionEvent ev) { - if (!mDragging) { + if (!mDragging || mIsAccessibleDrag) { return false; } @@ -596,7 +617,7 @@ public class DragController { if (mDragging) { PointF vec = isFlingingToDelete(mDragObject.dragSource); - if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) { + if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragSource, mDragObject.dragInfo)) { vec = null; } if (vec != null) { @@ -617,6 +638,34 @@ public class DragController { } /** + * Since accessible drag and drop won't cause the same sequence of touch events, we manually + * inject the appropriate state. + */ + public void prepareAccessibleDrag(int x, int y) { + mMotionDownX = x; + mMotionDownY = y; + mLastDropTarget = null; + } + + /** + * As above, since accessible drag and drop won't cause the same sequence of touch events, + * we manually ensure appropriate drag and drop events get emulated for accessible drag. + */ + public void completeAccessibleDrag(int[] location) { + final int[] coordinates = mCoordinatesTemp; + + // We make sure that we prime the target for drop. + DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates); + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + checkTouchMove(dropTarget); + + // Perform the drop + drop(location[0], location[1]); + endDrag(); + } + + /** * Determines whether the user flung the current item to delete it. * * @return the vector at which the item was flung, or null if no fling was detected. diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index a352b7914..ab2e094cb 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -39,6 +39,7 @@ import android.widget.FrameLayout; import android.widget.TextView; import com.android.launcher3.InsettableFrameLayout.LayoutParams; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -46,7 +47,7 @@ import java.util.ArrayList; * A ViewGroup that coordinates dragging across its descendants */ public class DragLayer extends InsettableFrameLayout { - private DragController mDragController; + @Thunk DragController mDragController; private int[] mTmpXY = new int[2]; private int mXDown, mYDown; @@ -61,9 +62,9 @@ public class DragLayer extends InsettableFrameLayout { private ValueAnimator mDropAnim = null; private ValueAnimator mFadeOutAnim = null; private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); - private DragView mDropView = null; - private int mAnchorViewInitialScrollX = 0; - private View mAnchorView = null; + @Thunk DragView mDropView = null; + @Thunk int mAnchorViewInitialScrollX = 0; + @Thunk View mAnchorView = null; private boolean mHoverPointClosesFolder = false; private Rect mHitRect = new Rect(); @@ -779,7 +780,7 @@ public class DragLayer extends InsettableFrameLayout { return mDropView; } - private void fadeOutDragView() { + @Thunk void fadeOutDragView() { mFadeOutAnim = new ValueAnimator(); mFadeOutAnim.setDuration(150); mFadeOutAnim.setFloatValues(0f, 1f); diff --git a/src/com/android/launcher3/DragSource.java b/src/com/android/launcher3/DragSource.java index 7369eeac2..2a1346ef5 100644 --- a/src/com/android/launcher3/DragSource.java +++ b/src/com/android/launcher3/DragSource.java @@ -22,9 +22,9 @@ import com.android.launcher3.DropTarget.DragObject; /** * Interface defining an object that can originate a drag. - * */ public interface DragSource { + /** * @return whether items dragged from this source supports */ diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java index ea34e46f9..b1a6266cc 100644 --- a/src/com/android/launcher3/DragView.java +++ b/src/com/android/launcher3/DragView.java @@ -29,8 +29,10 @@ import android.graphics.Rect; import android.view.View; import android.view.animation.DecelerateInterpolator; +import com.android.launcher3.util.Thunk; + public class DragView extends View { - private static float sDragAlpha = 1f; + @Thunk static float sDragAlpha = 1f; private Bitmap mBitmap; private Bitmap mCrossFadeBitmap; @@ -42,11 +44,11 @@ public class DragView extends View { private Rect mDragRegion = null; private DragLayer mDragLayer = null; private boolean mHasDrawn = false; - private float mCrossFadeProgress = 0f; + @Thunk float mCrossFadeProgress = 0f; ValueAnimator mAnim; - private float mOffsetX = 0.0f; - private float mOffsetY = 0.0f; + @Thunk float mOffsetX = 0.0f; + @Thunk float mOffsetY = 0.0f; private float mInitialScale = 1f; // The intrinsic icon scale factor is the scale factor for a drag icon over the workspace // size. This is ignored for non-icons. @@ -70,8 +72,6 @@ public class DragView extends View { mInitialScale = initialScale; final Resources res = getResources(); - final float offsetX = res.getDimensionPixelSize(R.dimen.dragViewOffsetX); - final float offsetY = res.getDimensionPixelSize(R.dimen.dragViewOffsetY); final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale); final float scale = (width + scaleDps) / width; @@ -87,8 +87,8 @@ public class DragView extends View { public void onAnimationUpdate(ValueAnimator animation) { final float value = (Float) animation.getAnimatedValue(); - final int deltaX = (int) ((value * offsetX) - mOffsetX); - final int deltaY = (int) ((value * offsetY) - mOffsetY); + final int deltaX = (int) (-mOffsetX); + final int deltaY = (int) (-mOffsetY); mOffsetX += deltaX; mOffsetY += deltaY; diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java index 64f0ac867..c5cca3b28 100644 --- a/src/com/android/launcher3/DropTarget.java +++ b/src/com/android/launcher3/DropTarget.java @@ -54,6 +54,9 @@ public interface DropTarget { /** Where the drag originated */ public DragSource dragSource = null; + /** The object is part of an accessible drag operation */ + public boolean accessibleDrag; + /** Post drag animation runnable */ public Runnable postAnimationRunnable = null; @@ -65,6 +68,29 @@ public interface DropTarget { public DragObject() { } + + /** + * This is used to compute the visual center of the dragView. This point is then + * used to visualize drop locations and determine where to drop an item. The idea is that + * the visual center represents the user's interpretation of where the item is, and hence + * is the appropriate point to use when determining drop location. + */ + public final float[] getVisualCenter(float[] recycle) { + final float res[] = (recycle == null) ? new float[2] : recycle; + + // These represent the visual top and left of drag view if a dragRect was provided. + // If a dragRect was not provided, then they correspond to the actual view left and + // top, as the dragRect is in that case taken to be the entire dragView. + // R.dimen.dragViewOffsetY. + int left = x - xOffset; + int top = y - yOffset; + + // In order to find the visual center, we shift by half the dragRect + res[0] = left + dragView.getDragRegion().width() / 2; + res[1] = top + dragView.getDragRegion().height() / 2; + + return res; + } } public static class DragEnforcer implements DragController.DragListener { @@ -113,7 +139,7 @@ public interface DropTarget { /** * Handle an object being dropped on the DropTarget - * + * * @param source DragSource where the drag started * @param x X coordinate of the drop location * @param y Y coordinate of the drop location @@ -143,7 +169,7 @@ public interface DropTarget { /** * Check if a drop action can occur at, or near, the requested location. * This will be called just before onDrop. - * + * * @param source DragSource where the drag started * @param x X coordinate of the drop location * @param y Y coordinate of the drop location diff --git a/src/com/android/launcher3/DynamicGrid.java b/src/com/android/launcher3/DynamicGrid.java index aa08148d2..24da97fc6 100644 --- a/src/com/android/launcher3/DynamicGrid.java +++ b/src/com/android/launcher3/DynamicGrid.java @@ -56,23 +56,22 @@ public class DynamicGrid { DisplayMetrics dm = resources.getDisplayMetrics(); ArrayList<DeviceProfile> deviceProfiles = new ArrayList<DeviceProfile>(); - boolean hasAA = !LauncherAppState.isDisableAllApps(); 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 ? 3 : 5), 48, R.xml.default_workspace_4x4)); + 255, 300, 2, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Shorter Stubby", - 255, 400, 3, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4)); + 255, 400, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Short Stubby", - 275, 420, 3, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4)); + 275, 420, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Stubby", - 255, 450, 3, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4)); + 255, 450, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Nexus S", - 296, 491.33f, 4, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4)); + 296, 491.33f, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Nexus 4", - 335, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4)); + 335, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Nexus 5", - 359, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4)); + 359, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Large Phone", 406, 694, 5, 5, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); // The tablet profile is odd in that the landscape orientation diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index ff02bbbc3..28e923e67 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -32,7 +32,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.SparseArray; -class FastBitmapDrawable extends Drawable { +public class FastBitmapDrawable extends Drawable { static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { @@ -72,7 +72,7 @@ class FastBitmapDrawable extends Drawable { private boolean mPressed = false; private ObjectAnimator mPressedAnimator; - FastBitmapDrawable(Bitmap b) { + public FastBitmapDrawable(Bitmap b) { mAlpha = 255; mBitmap = b; setBounds(0, 0, b.getWidth(), b.getHeight()); diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java index 095c5631d..a51ddd4b8 100644 --- a/src/com/android/launcher3/FirstFrameAnimatorHelper.java +++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java @@ -24,6 +24,8 @@ import android.view.View; import android.view.ViewPropertyAnimator; import android.view.ViewTreeObserver; +import com.android.launcher3.util.Thunk; + /* * This is a helper class that listens to updates from the corresponding animation. * For the first two frames, it adjusts the current play time of the animation to @@ -41,7 +43,7 @@ public class FirstFrameAnimatorHelper extends AnimatorListenerAdapter private boolean mAdjustedSecondFrameTime; private static ViewTreeObserver.OnDrawListener sGlobalDrawListener; - private static long sGlobalFrameCounter; + @Thunk static long sGlobalFrameCounter; private static boolean sVisible; public FirstFrameAnimatorHelper(ValueAnimator animator, View target) { diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java index e60704718..c77d41657 100644 --- a/src/com/android/launcher3/FocusHelper.java +++ b/src/com/android/launcher3/FocusHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2015 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. @@ -16,658 +16,439 @@ 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.widget.ScrollView; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; +import com.android.launcher3.util.FocusLogic; +import com.android.launcher3.util.Thunk; /** * A keyboard listener we set on all the workspace icons. */ class IconKeyEventListener implements View.OnKeyListener { + @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return FocusHelper.handleIconKeyEvent(v, keyCode, event); } } /** - * A keyboard listener we set on all the workspace icons. - */ -class FolderKeyEventListener implements View.OnKeyListener { - public boolean onKey(View v, int keyCode, KeyEvent event) { - return FocusHelper.handleFolderKeyEvent(v, keyCode, event); - } -} - -/** * A keyboard listener we set on all the hotseat buttons. */ class HotseatIconKeyEventListener implements View.OnKeyListener { + @Override public boolean onKey(View v, int keyCode, KeyEvent event) { - final Configuration configuration = v.getResources().getConfiguration(); - return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation); + return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event); } } public class FocusHelper { - /** - * Returns the Viewgroup containing page contents for the page at the index specified. - */ - private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) { - ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index); - if (page instanceof CellLayout) { - // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren - page = ((CellLayout) page).getShortcutsAndWidgets(); - } - return page; - } + private static final String TAG = "FocusHelper"; + private static final boolean DEBUG = false; /** - * Handles key events in a PageViewCellLayout containing PagedViewIcons. + * Handles key events in paged folder. */ - static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) { - ViewGroup parentLayout; - ViewGroup itemContainer; - int countX; - int countY; - if (v.getParent() instanceof ShortcutAndWidgetContainer) { - itemContainer = (ViewGroup) v.getParent(); - parentLayout = (ViewGroup) itemContainer.getParent(); - countX = ((CellLayout) parentLayout).getCountX(); - countY = ((CellLayout) parentLayout).getCountY(); - } else { - itemContainer = parentLayout = (ViewGroup) v.getParent(); - countX = ((PagedViewGridLayout) parentLayout).getCellCountX(); - countY = ((PagedViewGridLayout) parentLayout).getCellCountY(); + public static class PagedFolderKeyEventListener implements View.OnKeyListener { + + private final Folder mFolder; + + public PagedFolderKeyEventListener(Folder folder) { + mFolder = folder; } - // Note we have an extra parent because of the - // PagedViewCellLayout/PagedViewCellLayoutChildren relationship - final PagedView container = (PagedView) parentLayout.getParent(); - final int iconIndex = itemContainer.indexOfChild(v); - final int itemCount = itemContainer.getChildCount(); - final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parentLayout)); - final int pageCount = container.getChildCount(); - - final int x = iconIndex % countX; - final int y = iconIndex / countX; - - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - ViewGroup newParent = null; - // Side pages do not always load synchronously, so check before focusing child siblings - // willy-nilly - View child = null; - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon or the last icon on the previous page - if (iconIndex > 0) { - itemContainer.getChildAt(iconIndex - 1).requestFocus(); - 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(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } - } - } + @Override + public boolean onKey(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP) { + return consume; + } + if (DEBUG) { + Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].", + KeyEvent.keyCodeToString(keyCode))); + } + + + if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) { + if (LauncherAppState.isDogfoodBuild()) { + throw new IllegalStateException("Parent of the focused item is not supported."); + } else { + return false; } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon or the first icon on the next page - if (iconIndex < (itemCount - 1)) { - itemContainer.getChildAt(iconIndex + 1).requestFocus(); - 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(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - } - } + } + + // Initialize variables. + final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout cellLayout = (CellLayout) itemContainer.getParent(); + final int countX = cellLayout.getCountX(); + final int countY = cellLayout.getCountY(); + + final int iconIndex = itemContainer.indexOfChild(v); + final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent(); + + final int pageIndex = pagedView.indexOfChild(cellLayout); + final int pageCount = pagedView.getPageCount(); + + int[][] matrix = FocusLogic.createSparseMatrix(cellLayout); + // Process focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, + iconIndex, pageIndex, pageCount); + if (newIconIndex == FocusLogic.NOOP) { + handleNoopKey(keyCode, v); + return consume; + } + ShortcutAndWidgetContainer newParent = null; + View child = null; + + switch (newIconIndex) { + case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: + case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); + if (newParent != null) { + int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; + pagedView.snapToPage(pageIndex - 1); + child = newParent.getChildAt( + ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) + ^ newParent.invertLayoutHorizontally()) ? 0 : countX - 1, row); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - // Select the closest icon in the previous row, otherwise select the tab bar - if (y > 0) { - int newiconIndex = ((y - 1) * countX) + x; - itemContainer.getChildAt(newiconIndex).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); + break; + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex - 1); + child = newParent.getChildAt(0, 0); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - if (handleKeyEvent) { - // 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); - int newIconY = newiconIndex / countX; - if (newIconY != y) { - itemContainer.getChildAt(newiconIndex).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } + break; + case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex - 1); + child = newParent.getChildAt(countX - 1, countY - 1); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_PAGE_UP: - if (handleKeyEvent) { - // Select the first icon on the previous page, or the first icon on this page - // if there is no previous page - if (pageIndex > 0) { - newParent = getAppsCustomizePage(container, pageIndex - 1); - if (newParent != null) { - container.snapToPage(pageIndex - 1); - child = newParent.getChildAt(0); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - } else { - itemContainer.getChildAt(0).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); + break; + case FocusLogic.NEXT_PAGE_FIRST_ITEM: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex + 1); + child = newParent.getChildAt(0, 0); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_PAGE_DOWN: - if (handleKeyEvent) { - // Select the first icon on the next page, or the last icon on this page - // if there is no next page - if (pageIndex < (pageCount - 1)) { - newParent = getAppsCustomizePage(container, pageIndex + 1); - if (newParent != null) { - container.snapToPage(pageIndex + 1); - child = newParent.getChildAt(0); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - } else { - itemContainer.getChildAt(itemCount - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); + break; + case FocusLogic.NEXT_PAGE_LEFT_COLUMN: + case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex + 1); + child = FocusLogic.getAdjacentChildInNextPage(newParent, v, newIconIndex); } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - itemContainer.getChildAt(0).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_MOVE_END: - if (handleKeyEvent) { - // Select the last icon on this page - itemContainer.getChildAt(itemCount - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - wasHandled = true; - break; - default: break; + break; + case FocusLogic.CURRENT_PAGE_FIRST_ITEM: + child = cellLayout.getChildAt(0, 0); + break; + case FocusLogic.CURRENT_PAGE_LAST_ITEM: + child = pagedView.getLastItem(); + break; + default: // Go to some item on the current page. + child = itemContainer.getChildAt(newIconIndex); + break; + } + if (child != null) { + child.requestFocus(); + playSoundEffect(keyCode, v); + } else { + handleNoopKey(keyCode, v); + } + return consume; } - return wasHandled; - } - /** - * Handles key events in the workspace hotseat (bottom of the screen). - */ - static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) { - 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 - // 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) { - 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) { - 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) { - 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; - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - // Do nothing - wasHandled = true; - break; - default: break; + public void handleNoopKey(int keyCode, View v) { + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mFolder.mFolderName.requestFocus(); + playSoundEffect(keyCode, v); + } } - return wasHandled; } /** - * Private helper method to get the CellLayoutChildren given a CellLayout index. + * Handles key events in the workspace hot seat (bottom of the screen). + * <p>Currently we don't special case for the phone UI in different orientations, even though + * the hotseat is on the side in landscape mode. This is to ensure that accessibility + * consistency is maintained across rotations. */ - private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( - ViewGroup container, int i) { - CellLayout parent = (CellLayout) container.getChildAt(i); - return parent.getShortcutsAndWidgets(); - } + static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP || !consume) { + return consume; + } - /** - * Private helper method to sort all the CellLayout children in order of their (x,y) spatially - * from top left to bottom right. - */ - private static ArrayList<View> getCellLayoutChildrenSortedSpatially(CellLayout layout, - ViewGroup parent) { - // First we order each the CellLayout children by their x,y coordinates - final int cellCountX = layout.getCountX(); - final int count = parent.getChildCount(); - ArrayList<View> views = new ArrayList<View>(); - for (int j = 0; j < count; ++j) { - views.add(parent.getChildAt(j)); + DeviceProfile profile = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); + if (DEBUG) { + Log.v(TAG, String.format( + "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s", + KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); } - Collections.sort(views, new Comparator<View>() { - @Override - public int compare(View lhs, View rhs) { - CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams(); - CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams(); - int lvIndex = (llp.cellY * cellCountX) + llp.cellX; - int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX; - return lvIndex - rvIndex; - } - }); - return views; - } - /** - * Private helper method to find the index of the next BubbleTextView or FolderIcon in the - * direction delta. - * - * @param delta either -1 or 1 depending on the direction we want to search - */ - private static View findIndexOfIcon(ArrayList<View> views, int i, int delta) { - // Then we find the next BubbleTextView offset by delta from i - final int count = views.size(); - int newI = i + delta; - while (0 <= newI && newI < count) { - View newV = views.get(newI); - if (newV instanceof BubbleTextView || newV instanceof FolderIcon) { - return newV; - } - newI += delta; + + // Initialize the variables. + final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent(); + Hotseat hotseat = (Hotseat) hotseatLayout.getParent(); + + Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); + int pageIndex = workspace.getNextPage(); + int pageCount = workspace.getChildCount(); + int countX = -1; + int countY = -1; + int iconIndex = hotseatParent.indexOfChild(v); + int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets() + .getChildAt(iconIndex).getLayoutParams()).cellX; + + final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex); + if (iconLayout == null) { + // This check is to guard against cases where key strokes rushes in when workspace + // child creation/deletion is still in flux. (e.g., during drop or fling + // animation.) + return consume; } - return null; - } - private static View getIconInDirection(CellLayout layout, ViewGroup parent, int i, - int delta) { - final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - return findIndexOfIcon(views, i, delta); - } - private static View getIconInDirection(CellLayout layout, ViewGroup parent, View v, - int delta) { - final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - return findIndexOfIcon(views, views.indexOf(v), delta); - } - /** - * Private helper method to find the next closest BubbleTextView or FolderIcon in the direction - * delta on the next line. - * - * @param delta either -1 or 1 depending on the line and direction we want to search - */ - private static View getClosestIconOnLine(CellLayout layout, ViewGroup parent, View v, - int lineDelta) { - final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); - final int cellCountY = layout.getCountY(); - final int row = lp.cellY; - final int newRow = row + lineDelta; - if (0 <= newRow && newRow < cellCountY) { - float closestDistance = Float.MAX_VALUE; - int closestIndex = -1; - int index = views.indexOf(v); - int endIndex = (lineDelta < 0) ? -1 : views.size(); - while (index != endIndex) { - View newV = views.get(index); - CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams(); - boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row); - if (satisfiesRow && - (newV instanceof BubbleTextView || newV instanceof FolderIcon)) { - float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) + - Math.pow(tmpLp.cellY - lp.cellY, 2)); - if (tmpDistance < closestDistance) { - closestIndex = index; - closestDistance = tmpDistance; - } - } - if (index <= endIndex) { - ++index; - } else { - --index; - } + final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + + ViewGroup parent = null; + int[][] matrix = null; + + if (keyCode == KeyEvent.KEYCODE_DPAD_UP && + !profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, + true /* hotseat horizontal */, hotseat.getAllAppsButtonRank(), + iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */); + iconIndex += iconParent.getChildCount(); + countX = iconLayout.getCountX(); + countY = iconLayout.getCountY() + hotseatLayout.getCountY(); + parent = iconParent; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && + profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, + false /* hotseat horizontal */, hotseat.getAllAppsButtonRank(), + iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */); + iconIndex += iconParent.getChildCount(); + countX = iconLayout.getCountX() + hotseatLayout.getCountX(); + countY = iconLayout.getCountY(); + parent = iconParent; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && + profile.isVerticalBarLayout()) { + keyCode = KeyEvent.KEYCODE_PAGE_DOWN; + }else { + // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the + // matrix extended with hotseat. + matrix = FocusLogic.createSparseMatrix(hotseatLayout); + countX = hotseatLayout.getCountX(); + countY = hotseatLayout.getCountY(); + parent = hotseatParent; + } + + // Process the focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, + iconIndex, pageIndex, pageCount); + + View newIcon = null; + if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = parent.getChildAt(0); + // TODO(hyunyoungs): handle cases where the child is not an icon but + // a folder or a widget. + workspace.snapToPage(pageIndex + 1); + } + if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) { + newIconIndex -= iconParent.getChildCount(); + } + if (parent != null) { + if (newIcon == null && newIconIndex >=0) { + newIcon = parent.getChildAt(newIconIndex); } - if (closestIndex > -1) { - return views.get(closestIndex); + if (newIcon != null) { + newIcon.requestFocus(); + playSoundEffect(keyCode, v); } } - return null; + return consume; } /** - * Handles key events in a Workspace containing. + * Handles key events in a workspace containing icons. */ static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP || !consume) { + return consume; + } + + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile profile = app.getDynamicGrid().getDeviceProfile(); + + if (DEBUG) { + Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s", + KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); + } + + // Initialize the variables. ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); - final CellLayout layout = (CellLayout) parent.getParent(); - final Workspace workspace = (Workspace) layout.getParent(); + CellLayout iconLayout = (CellLayout) parent.getParent(); + final Workspace workspace = (Workspace) iconLayout.getParent(); final ViewGroup launcher = (ViewGroup) workspace.getParent(); final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar); - final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat); - int pageIndex = workspace.indexOfChild(layout); - int pageCount = workspace.getChildCount(); + final Hotseat hotseat = (Hotseat) launcher.findViewById(R.id.hotseat); - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon or the last icon on the previous page if possible - View newIcon = getIconInDirection(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } else { - if (pageIndex > 0) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); - newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the previous page - workspace.snapToPage(pageIndex - 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } + final int iconIndex = parent.indexOfChild(v); + final int pageIndex = workspace.indexOfChild(iconLayout); + final int pageCount = workspace.getChildCount(); + int countX = iconLayout.getCountX(); + int countY = iconLayout.getCountY(); + + CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0); + ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets(); + int[][] matrix; + + // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed + // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended + // with the hotseat. + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */, + hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */); + countY = countY + 1; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && + profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */, + hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */); + countX = countX + 1; + } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) { + workspace.removeWorkspaceItem(v); + return consume; + } else { + matrix = FocusLogic.createSparseMatrix(iconLayout); + } + + // Process the focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, + iconIndex, pageIndex, pageCount); + View newIcon = null; + switch (newIconIndex) { + case FocusLogic.NOOP: + if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + newIcon = tabs; } - wasHandled = true; break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon or the first icon on the next page if possible - View newIcon = getIconInDirection(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } else { - if (pageIndex < (pageCount - 1)) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); - newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the next page - workspace.snapToPage(pageIndex + 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - } + case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: + case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: + int newPageIndex = pageIndex - 1; + if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) { + newPageIndex = pageIndex + 1; } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - // Select the closest icon in the previous line, otherwise select the tab bar - View newIcon = getClosestIconOnLine(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - wasHandled = true; - } else { - tabs.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); + int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; + parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); + workspace.snapToPage(newPageIndex); + if (parent != null) { + workspace.snapToPage(newPageIndex); + iconLayout = (CellLayout) parent.getParent(); + matrix = FocusLogic.createSparseMatrix(iconLayout, + iconLayout.getCountX(), row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix, + FocusLogic.PIVOT, newPageIndex, pageCount); + newIcon = parent.getChildAt(newIconIndex); } break; - case KeyEvent.KEYCODE_DPAD_DOWN: - if (handleKeyEvent) { - // Select the closest icon in the next line, otherwise select the button bar - View newIcon = getClosestIconOnLine(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - wasHandled = true; - } else if (hotseat != null) { - hotseat.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(0); + workspace.snapToPage(pageIndex - 1); break; - case KeyEvent.KEYCODE_PAGE_UP: - if (handleKeyEvent) { - // Select the first icon on the previous page or the first icon on this page - // if there is no previous page - if (pageIndex > 0) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the previous page - workspace.snapToPage(pageIndex - 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } else { - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - } - wasHandled = true; + case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(parent.getChildCount() - 1); + workspace.snapToPage(pageIndex - 1); break; - case KeyEvent.KEYCODE_PAGE_DOWN: - if (handleKeyEvent) { - // Select the first icon on the next page or the last icon on this page - // if there is no previous page - if (pageIndex < (pageCount - 1)) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the next page - workspace.snapToPage(pageIndex + 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } else { - View newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - } - wasHandled = true; + case FocusLogic.NEXT_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = parent.getChildAt(0); + workspace.snapToPage(pageIndex + 1); break; - case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } + case FocusLogic.NEXT_PAGE_LEFT_COLUMN: + case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: + newPageIndex = pageIndex + 1; + if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) { + newPageIndex = pageIndex - 1; + } + workspace.snapToPage(newPageIndex); + row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; + parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); + if (parent != null) { + workspace.snapToPage(newPageIndex); + iconLayout = (CellLayout) parent.getParent(); + matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix, + FocusLogic.PIVOT, newPageIndex, pageCount); + newIcon = parent.getChildAt(newIconIndex); } - wasHandled = true; break; - case KeyEvent.KEYCODE_MOVE_END: - if (handleKeyEvent) { - // Select the last icon on this page - View newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } + case FocusLogic.CURRENT_PAGE_FIRST_ITEM: + newIcon = parent.getChildAt(0); + break; + case FocusLogic.CURRENT_PAGE_LAST_ITEM: + newIcon = parent.getChildAt(parent.getChildCount() - 1); + break; + default: + // current page, some item. + if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) { + newIcon = parent.getChildAt(newIconIndex); + } else if (parent.getChildCount() <= newIconIndex && + newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) { + newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount()); } - wasHandled = true; break; - default: break; } - return wasHandled; + if (newIcon != null) { + newIcon.requestFocus(); + playSoundEffect(keyCode, v); + } + return consume; } + // + // Helper methods. + // + /** - * Handles key events for items in a Folder. + * Private helper method to get the CellLayoutChildren given a CellLayout index. */ - static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) { - ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); - final CellLayout layout = (CellLayout) parent.getParent(); - final ScrollView scrollView = (ScrollView) layout.getParent(); - final Folder folder = (Folder) scrollView.getParent(); - View title = folder.mFolderName; - - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; + private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( + ViewGroup container, int i) { + CellLayout parent = (CellLayout) container.getChildAt(i); + return parent.getShortcutsAndWidgets(); + } + + /** + * Helper method to be used for playing sound effects. + */ + @Thunk static void playSoundEffect(int keyCode, View v) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon - View newIcon = getIconInDirection(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } - wasHandled = true; + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); break; case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon - View newIcon = getIconInDirection(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - title.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - // Select the closest icon in the previous line - View newIcon = getClosestIconOnLine(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; + v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); break; case KeyEvent.KEYCODE_DPAD_DOWN: - if (handleKeyEvent) { - // Select the closest icon in the next line - View newIcon = getClosestIconOnLine(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - title.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - wasHandled = true; + case KeyEvent.KEYCODE_PAGE_DOWN: + case KeyEvent.KEYCODE_MOVE_END: + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_PAGE_UP: case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); break; - case KeyEvent.KEYCODE_MOVE_END: - if (handleKeyEvent) { - // Select the last icon on this page - View newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - wasHandled = true; + default: break; - default: break; } - return wasHandled; } } diff --git a/src/com/android/launcher3/FocusIndicatorView.java b/src/com/android/launcher3/FocusIndicatorView.java index 7d4664abb..ecf93e4b3 100644 --- a/src/com/android/launcher3/FocusIndicatorView.java +++ b/src/com/android/launcher3/FocusIndicatorView.java @@ -23,7 +23,8 @@ import android.graphics.Canvas; import android.util.AttributeSet; import android.util.Pair; import android.view.View; -import android.view.ViewParent; + +import com.android.launcher3.util.Thunk; public class FocusIndicatorView extends View implements View.OnFocusChangeListener { @@ -32,9 +33,6 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen private static final float MIN_VISIBLE_ALPHA = 0.2f; private static final long ANIM_DURATION = 150; - 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]; @@ -80,7 +78,8 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen } if (!mInitiated) { - getLocationRelativeToParentPagedView(this, mIndicatorPos); + // The parent view should always the a parent of the target view. + computeLocationRelativeToParent(this, (View) getParent(), mIndicatorPos); mInitiated = true; } @@ -93,7 +92,7 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth; nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight; - getLocationRelativeToParentPagedView(v, mTargetViewPos); + computeLocationRelativeToParent(v, (View) getParent(), mTargetViewPos); nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2; nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2; @@ -150,31 +149,36 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen } /** - * Gets the location of a view relative in the window, off-setting any shift due to - * page view scroll + * Computes the location of a view relative to {@param parent}, off-setting + * any shift due to page view scroll. + * @param pos an array of two integers in which to hold the coordinates */ - 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 computeLocationRelativeToParent(View v, View parent, int[] pos) { + pos[0] = pos[1] = 0; + computeLocationRelativeToParentHelper(v, parent, pos); + + // If a view is scaled, its position will also shift accordingly. For optimization, only + // consider this for the last node. + pos[0] += (1 - v.getScaleX()) * v.getWidth() / 2; + pos[1] += (1 - v.getScaleY()) * v.getHeight() / 2; } - private static void getPagedViewScrollShift(View child, int[] shift) { - ViewParent parent = child.getParent(); + private static void computeLocationRelativeToParentHelper(View child, + View commonParent, int[] shift) { + View parent = (View) child.getParent(); + shift[0] += child.getLeft(); + shift[1] += child.getTop(); 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; + PagedView page = (PagedView) parent; + shift[0] -= page.getScrollForPage(page.indexOfChild(child)); + } + + if (parent != commonParent) { + computeLocationRelativeToParentHelper(parent, commonParent, shift); } } - private static final class ViewAnimState { + @Thunk static final class ViewAnimState { float x, y, scaleX, scaleY; } } diff --git a/src/com/android/launcher3/FocusOnlyTabWidget.java b/src/com/android/launcher3/FocusOnlyTabWidget.java deleted file mode 100644 index 08fc311bc..000000000 --- a/src/com/android/launcher3/FocusOnlyTabWidget.java +++ /dev/null @@ -1,86 +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.util.AttributeSet; -import android.view.View; -import android.widget.TabWidget; - -public class FocusOnlyTabWidget extends TabWidget { - public FocusOnlyTabWidget(Context context) { - super(context); - } - - public FocusOnlyTabWidget(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public FocusOnlyTabWidget(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public View getSelectedTab() { - final int count = getTabCount(); - for (int i = 0; i < count; ++i) { - View v = getChildTabViewAt(i); - if (v.isSelected()) { - return v; - } - } - return null; - } - - public int getChildTabIndex(View v) { - final int tabCount = getTabCount(); - for (int i = 0; i < tabCount; ++i) { - if (getChildTabViewAt(i) == v) { - return i; - } - } - return -1; - } - - public void setCurrentTabToFocusedTab() { - View tab = null; - int index = -1; - final int count = getTabCount(); - for (int i = 0; i < count; ++i) { - View v = getChildTabViewAt(i); - if (v.hasFocus()) { - tab = v; - index = i; - break; - } - } - if (index > -1) { - super.setCurrentTab(index); - super.onFocusChange(tab, true); - } - } - public void superOnFocusChange(View v, boolean hasFocus) { - super.onFocusChange(v, hasFocus); - } - - @Override - public void onFocusChange(android.view.View v, boolean hasFocus) { - if (v == this && hasFocus && getTabCount() > 0) { - getSelectedTab().requestFocus(); - return; - } - } -} diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index 66b656882..c35ce944f 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -21,12 +21,13 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; -import android.os.SystemClock; -import android.support.v4.widget.AutoScrollHelper; +import android.os.Build; import android.text.InputType; import android.text.Selection; import android.text.Spannable; @@ -39,124 +40,135 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.AccelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout; -import android.widget.ScrollView; import android.widget.TextView; +import com.android.launcher3.DragController.DragListener; import com.android.launcher3.FolderInfo.FolderListener; +import com.android.launcher3.UninstallDropTarget.UninstallSource; +import com.android.launcher3.Workspace.ItemOperator; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; /** * Represents a set of icons chosen by the user or generated by the system. */ public class Folder extends LinearLayout implements DragSource, View.OnClickListener, View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, - View.OnFocusChangeListener { + View.OnFocusChangeListener, DragListener, UninstallSource { private static final String TAG = "Launcher.Folder"; - protected DragController mDragController; - protected Launcher mLauncher; - protected FolderInfo mInfo; + /** + * We avoid measuring {@link #mContentWrapper} with a 0 width or height, as this + * results in CellLayout being measured as UNSPECIFIED, which it does not support. + */ + private static final int MIN_CONTENT_DIMEN = 5; static final int STATE_NONE = -1; static final int STATE_SMALL = 0; static final int STATE_ANIMATING = 1; static final int STATE_OPEN = 2; - private int mExpandDuration; - private int mMaterialExpandDuration; - private int mMaterialExpandStagger; - protected CellLayout mContent; - private ScrollView mScrollView; - private final LayoutInflater mInflater; - private final IconCache mIconCache; - private int mState = STATE_NONE; - private static final int REORDER_ANIMATION_DURATION = 230; + /** + * Time for which the scroll hint is shown before automatically changing page. + */ + public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY; + + /** + * Time in milliseconds for which an icon sticks to the target position + * in case of a sorted folder. + */ + private static final int SORTED_STICKY_REORDER_DELAY = 1500; + + /** + * Fraction of icon width which behave as scroll region. + */ + private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f; + private static final int REORDER_DELAY = 250; private static final int ON_EXIT_CLOSE_DELAY = 400; + private static final Rect sTempRect = new Rect(); + + private static String sDefaultFolderName; + private static String sHintText; + + private final Alarm mReorderAlarm = new Alarm(); + private final Alarm mOnExitAlarm = new Alarm(); + private final Alarm mOnScrollHintAlarm = new Alarm(); + @Thunk final Alarm mScrollPauseAlarm = new Alarm(); + + @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); + + private final int mExpandDuration; + private final int mMaterialExpandDuration; + private final int mMaterialExpandStagger; + + private final InputMethodManager mInputMethodManager; + + protected final Launcher mLauncher; + protected DragController mDragController; + protected FolderInfo mInfo; + + @Thunk FolderIcon mFolderIcon; + + @Thunk FolderPagedView mContent; + @Thunk View mContentWrapper; + FolderEditText mFolderName; + + private View mFooter; + private int mFooterHeight; + + // Cell ranks used for drag and drop + @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank; + + @Thunk int mState = STATE_NONE; private boolean mRearrangeOnClose = false; - private FolderIcon mFolderIcon; - private int mMaxCountX; - private int mMaxCountY; - private int mMaxNumItems; - private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); boolean mItemsInvalidated = false; private ShortcutInfo mCurrentDragInfo; private View mCurrentDragView; private boolean mIsExternalDrag; boolean mSuppressOnAdd = false; - private int[] mTargetCell = new int[2]; - private int[] mPreviousTargetCell = new int[2]; - private int[] mEmptyCell = new int[2]; - private Alarm mReorderAlarm = new Alarm(); - private Alarm mOnExitAlarm = new Alarm(); - private int mFolderNameHeight; - private Rect mTempRect = new Rect(); private boolean mDragInProgress = false; private boolean mDeleteFolderOnDropCompleted = false; private boolean mSuppressFolderDeletion = false; private boolean mItemAddedBackToSelfViaIcon = false; - FolderEditText mFolderName; - private float mFolderIconPivotX; - private float mFolderIconPivotY; - + @Thunk float mFolderIconPivotX; + @Thunk float mFolderIconPivotY; private boolean mIsEditingName = false; - private InputMethodManager mInputMethodManager; - - private static String sDefaultFolderName; - private static String sHintText; - - 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 - // not support. - private static final int MIN_CONTENT_DIMEN = 5; private boolean mDestroyed; - private AutoScrollHelper mAutoScrollHelper; - - private Runnable mDeferredAction; + @Thunk Runnable mDeferredAction; private boolean mDeferDropAfterUninstall; private boolean mUninstallSuccessful; + // Folder scrolling + private int mScrollAreaOffset; + + @Thunk int mScrollHintDir = DragController.SCROLL_NONE; + @Thunk int mCurrentScrollDir = DragController.SCROLL_NONE; + /** * Used to inflate the Workspace from XML. * * @param context The application's context. - * @param attrs The attribtues set containing the Workspace's customization values. + * @param attrs The attributes set containing the Workspace's customization values. */ public Folder(Context context, AttributeSet attrs) { super(context, attrs); - - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); setAlwaysDrawnWithCacheEnabled(false); - mInflater = LayoutInflater.from(context); - mIconCache = app.getIconCache(); - - Resources res = getResources(); - mMaxCountX = (int) grid.numColumns; - // Allow scrolling folders when DISABLE_ALL_APPS is true. - if (LauncherAppState.isDisableAllApps()) { - mMaxCountY = mMaxNumItems = Integer.MAX_VALUE; - } else { - mMaxCountY = (int) grid.numRows; - mMaxNumItems = mMaxCountX * mMaxCountY; - } - mInputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + Resources res = getResources(); mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration); mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger); @@ -170,45 +182,38 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mLauncher = (Launcher) context; // We need this view to be focusable in touch mode so that when text editing of the folder // name is complete, we have something to focus on, thus hiding the cursor and giving - // reliable behvior when clicking the text field (since it will always gain focus on click). + // reliable behavior when clicking the text field (since it will always gain focus on click). setFocusableInTouchMode(true); } @Override protected void onFinishInflate() { super.onFinishInflate(); - mScrollView = (ScrollView) findViewById(R.id.scroll_view); - mContent = (CellLayout) findViewById(R.id.folder_content); + mContentWrapper = findViewById(R.id.folder_content_wrapper); + mContent = (FolderPagedView) findViewById(R.id.folder_content); + mContent.setFolder(this); - 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(); - - mContent.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); - mContent.setGridSize(0, 0); - mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); - mContent.setInvertIfRtl(true); mFolderName = (FolderEditText) findViewById(R.id.folder_name); mFolderName.setFolder(this); mFolderName.setOnFocusChangeListener(this); - // We find out how tall the text view wants to be (it is set to wrap_content), so that - // we can allocate the appropriate amount of space for it. - int measureSpec = MeasureSpec.UNSPECIFIED; - mFolderName.measure(measureSpec, measureSpec); - mFolderNameHeight = mFolderName.getMeasuredHeight(); - // We disable action mode for now since it messes up the view on phones mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback); mFolderName.setOnEditorActionListener(this); mFolderName.setSelectAllOnFocus(true); mFolderName.setInputType(mFolderName.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); - mAutoScrollHelper = new FolderAutoScrollHelper(mScrollView); + + mFooter = findViewById(R.id.folder_footer); + updateFooterHeight(); + } + + public void updateFooterHeight() { + // We find out how tall footer wants to be (it is set to wrap_content), so that + // we can allocate the appropriate amount of space for it. + int measureSpec = MeasureSpec.UNSPECIFIED; + mFooter.measure(measureSpec, measureSpec); + mFooterHeight = mFooter.getMeasuredHeight(); } private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @@ -246,14 +251,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return false; } - mLauncher.getWorkspace().beginDragShared(v, this); + mLauncher.getWorkspace().beginDragShared(v, new Point(), this, false); mCurrentDragInfo = item; - mEmptyCell[0] = item.cellX; - mEmptyCell[1] = item.cellY; + mEmptyCellRank = item.rank; mCurrentDragView = v; - mContent.removeView(mCurrentDragView); + mContent.removeItem(mCurrentDragView); mInfo.remove(mCurrentDragInfo); mDragInProgress = true; mItemAddedBackToSelfViaIcon = false; @@ -307,10 +311,6 @@ 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. */ @@ -323,7 +323,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mDragController = dragController; } - void setFolderIcon(FolderIcon icon) { + public void setFolderIcon(FolderIcon icon) { mFolderIcon = icon; } @@ -343,30 +343,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList void bind(FolderInfo info) { mInfo = info; ArrayList<ShortcutInfo> children = info.contents; - ArrayList<ShortcutInfo> overflow = new ArrayList<ShortcutInfo>(); - - final int totalChildren = children.size(); - setupContentForNumItems(totalChildren); - - // Arrange children in the grid based on the rank. Collections.sort(children, Utilities.RANK_COMPARATOR); - final int countX = mContent.getCountX(); - - int visibleChildren = 0; - for (int i = 0; i < children.size(); i++) { - ShortcutInfo child = children.get(i); - child.cellX = i % countX; - child.cellY = i / countX; - - if (createAndAddShortcut(child) == null) { - overflow.add(child); - } else { - visibleChildren++; - } - } - // We rearrange the items in case there are any empty gaps - setupContentForNumItems(visibleChildren); + ArrayList<ShortcutInfo> overflow = mContent.bindItems(children); // If our folder has too many items we prune them from the list. This is an issue // when upgrading from the old Folders implementation which could contain an unlimited @@ -376,6 +355,14 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList LauncherModel.deleteItemFromDatabase(mLauncher, item); } + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + if (lp == null) { + lp = new DragLayer.LayoutParams(0, 0); + lp.customPosition = true; + setLayoutParams(lp); + } + centerAboutIcon(); + mItemsInvalidated = true; updateTextViewFocus(); mInfo.addListener(this); @@ -429,6 +416,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void animateOpen() { if (!(getParent() instanceof DragLayer)) return; + mContent.completePendingPageChanges(); + if (!(mDragInProgress && mContent.mIsSorted)) { + // Open on the first page. + mContent.snapToPageImmediately(0); + } + Animator openFolderAnim = null; final Runnable onCompleteRunnable; if (!Utilities.isLmpOrAbove()) { @@ -473,14 +466,14 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList reveal.setDuration(mMaterialExpandDuration); reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); - mContent.setAlpha(0f); - Animator iconsAlpha = LauncherAnimUtils.ofFloat(mContent, "alpha", 0f, 1f); + mContentWrapper.setAlpha(0f); + Animator iconsAlpha = LauncherAnimUtils.ofFloat(mContentWrapper, "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); + mFooter.setAlpha(0f); + Animator textAlpha = LauncherAnimUtils.ofFloat(mFooter, "alpha", 0f, 1f); textAlpha.setDuration(mMaterialExpandDuration); textAlpha.setStartDelay(mMaterialExpandStagger); textAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); @@ -497,11 +490,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList openFolderAnim = anim; - mContent.setLayerType(LAYER_TYPE_HARDWARE, null); + mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null); onCompleteRunnable = new Runnable() { @Override public void run() { - mContent.setLayerType(LAYER_TYPE_NONE, null); + mContentWrapper.setLayerType(LAYER_TYPE_NONE, null); } }; } @@ -509,8 +502,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void onAnimationStart(Animator animation) { sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, - String.format(getContext().getString(R.string.folder_opened), - mContent.getCountX(), mContent.getCountY())); + mContent.getAccessibilityDescription()); mState = STATE_ANIMATING; } @Override @@ -521,7 +513,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList onCompleteRunnable.run(); } - setFocusOnFirstChild(); + mContent.setFocusOnFirstChild(); } }); openFolderAnim.start(); @@ -530,21 +522,40 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList if (mDragController.isDragging()) { mDragController.forceTouchMove(); } + + FolderPagedView pages = (FolderPagedView) mContent; + pages.verifyVisibleHighResIcons(pages.getNextPage()); } public void beginExternalDrag(ShortcutInfo item) { - setupContentForNumItems(getItemCount() + 1); - findAndSetEmptyCells(item); - mCurrentDragInfo = item; - mEmptyCell[0] = item.cellX; - mEmptyCell[1] = item.cellY; + mEmptyCellRank = mContent.allocateRankForNewItem(item); mIsExternalDrag = true; - mDragInProgress = true; + + if (mContent.mIsSorted) { + mScrollPauseAlarm.setOnAlarmListener(null); + mScrollPauseAlarm.cancelAlarm(); + mScrollPauseAlarm.setAlarm(SORTED_STICKY_REORDER_DELAY); + } + + // Since this folder opened by another controller, it might not get onDrop or + // onDropComplete. Perform cleanup once drag-n-drop ends. + mDragController.addDragListener(this); + } + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { } + + @Override + public void onDragEnd() { + if (mIsExternalDrag && mDragInProgress) { + completeDragExit(); + } + mDragController.removeDragListener(this); } - private void sendCustomAccessibilityEvent(int type, String text) { + @Thunk void sendCustomAccessibilityEvent(int type, String text) { AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (accessibilityManager.isEnabled()) { @@ -555,13 +566,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } - private void setFocusOnFirstChild() { - View firstChild = mContent.getChildAt(0, 0); - if (firstChild != null) { - firstChild.requestFocus(); - } - } - public void animateClosed() { if (!(getParent() instanceof DragLayer)) return; PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); @@ -597,174 +601,90 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList !isFull()); } - protected boolean findAndSetEmptyCells(ShortcutInfo item) { - int[] emptyCell = new int[2]; - if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) { - item.cellX = emptyCell[0]; - item.cellY = emptyCell[1]; - return true; - } else { - return false; - } - } - - protected View createAndAddShortcut(ShortcutInfo item) { - final BubbleTextView textView = - (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. - Log.e(TAG, "Folder order not properly persisted during bind"); - if (!findAndSetEmptyCells(item)) { - return null; - } - } - - CellLayout.LayoutParams lp = - new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); - boolean insert = false; - textView.setOnKeyListener(new FolderKeyEventListener()); - mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true); - return textView; - } - public void onDragEnter(DragObject d) { - mPreviousTargetCell[0] = -1; - mPreviousTargetCell[1] = -1; + mPrevTargetRank = -1; mOnExitAlarm.cancelAlarm(); + // Get the area offset such that the folder only closes if half the drag icon width + // is outside the folder area + mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset; } OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { public void onAlarm(Alarm alarm) { - realTimeReorder(mEmptyCell, mTargetCell); + mContent.realTimeReorder(mEmptyCellRank, mTargetRank); + mEmptyCellRank = mTargetRank; } }; - boolean readingOrderGreaterThan(int[] v1, int[] v2) { - if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) { - return true; - } else { - return false; - } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); } - private void realTimeReorder(int[] empty, int[] target) { - boolean wrap; - int startX; - int endX; - int startY; - int delay = 0; - float delayAmount = 30; - if (readingOrderGreaterThan(target, empty)) { - wrap = empty[0] >= mContent.getCountX() - 1; - startY = wrap ? empty[1] + 1 : empty[1]; - for (int y = startY; y <= target[1]; y++) { - startX = y == empty[1] ? empty[0] + 1 : 0; - endX = y < target[1] ? mContent.getCountX() - 1 : target[0]; - for (int x = startX; x <= endX; x++) { - View v = mContent.getChildAt(x,y); - if (mContent.animateChildToPosition(v, empty[0], empty[1], - REORDER_ANIMATION_DURATION, delay, true, true)) { - empty[0] = x; - empty[1] = y; - delay += delayAmount; - delayAmount *= 0.9; - } - } - } - } else { - wrap = empty[0] == 0; - startY = wrap ? empty[1] - 1 : empty[1]; - for (int y = startY; y >= target[1]; y--) { - startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1; - endX = y > target[1] ? 0 : target[0]; - for (int x = startX; x >= endX; x--) { - View v = mContent.getChildAt(x,y); - if (mContent.animateChildToPosition(v, empty[0], empty[1], - REORDER_ANIMATION_DURATION, delay, true, true)) { - empty[0] = x; - empty[1] = y; - delay += delayAmount; - delayAmount *= 0.9; - } - } - } - } + @Override + public void onDragOver(DragObject d) { + onDragOver(d, REORDER_DELAY); } - public boolean isLayoutRtl() { - return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + private int getTargetRank(DragObject d, float[] recycle) { + recycle = d.getVisualCenter(recycle); + return mContent.findNearestArea( + (int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop()); } - public void onDragOver(DragObject d) { - final DragView dragView = d.dragView; - final int scrollOffset = mScrollView.getScrollY(); - final float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, dragView, null); - r[0] -= getPaddingLeft(); - r[1] -= getPaddingTop(); - - final long downTime = SystemClock.uptimeMillis(); - final MotionEvent translatedEv = MotionEvent.obtain( - downTime, downTime, MotionEvent.ACTION_MOVE, d.x, d.y, 0); - - if (!mAutoScrollHelper.isEnabled()) { - mAutoScrollHelper.setEnabled(true); + @Thunk void onDragOver(DragObject d, int reorderDelay) { + if (mScrollPauseAlarm.alarmPending()) { + return; } + final float[] r = new float[2]; + mTargetRank = getTargetRank(d, r); - final boolean handled = mAutoScrollHelper.onTouch(this, translatedEv); - translatedEv.recycle(); - - if (handled) { + if (mTargetRank != mPrevTargetRank) { mReorderAlarm.cancelAlarm(); + mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); + mReorderAlarm.setAlarm(REORDER_DELAY); + mPrevTargetRank = mTargetRank; + } + + float x = r[0]; + int currentPage = mContent.getNextPage(); + + float cellOverlap = mContent.getCurrentCellLayout().getCellWidth() + * ICON_OVERSCROLL_WIDTH_FACTOR; + boolean isOutsideLeftEdge = x < cellOverlap; + boolean isOutsideRightEdge = x > (getWidth() - cellOverlap); + + if (currentPage > 0 && (mContent.rtlLayout ? isOutsideRightEdge : isOutsideLeftEdge)) { + showScrollHint(DragController.SCROLL_LEFT, d); + } else if (currentPage < (mContent.getPageCount() - 1) + && (mContent.rtlLayout ? isOutsideLeftEdge : isOutsideRightEdge)) { + showScrollHint(DragController.SCROLL_RIGHT, d); } else { - mTargetCell = mContent.findNearestArea( - (int) r[0], (int) r[1] + scrollOffset, 1, 1, mTargetCell); - if (isLayoutRtl()) { - mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1; - } - if (mTargetCell[0] != mPreviousTargetCell[0] - || mTargetCell[1] != mPreviousTargetCell[1]) { - mReorderAlarm.cancelAlarm(); - mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); - mReorderAlarm.setAlarm(REORDER_DELAY); - mPreviousTargetCell[0] = mTargetCell[0]; - mPreviousTargetCell[1] = mTargetCell[1]; + mOnScrollHintAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mContent.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; } } } - // This is used to compute the visual center of the dragView. The idea is that - // the visual center represents the user's interpretation of where the item is, and hence - // is the appropriate point to use when determining drop location. - private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, - DragView dragView, float[] recycle) { - float res[]; - if (recycle == null) { - res = new float[2]; - } else { - res = recycle; + private void showScrollHint(int direction, DragObject d) { + // Show scroll hint on the right + if (mScrollHintDir != direction) { + mContent.showScrollHint(direction); + mScrollHintDir = direction; } - // These represent the visual top and left of drag view if a dragRect was provided. - // If a dragRect was not provided, then they correspond to the actual view left and - // top, as the dragRect is in that case taken to be the entire dragView. - // R.dimen.dragViewOffsetY. - int left = x - xOffset; - int top = y - yOffset; - - // In order to find the visual center, we shift by half the dragRect - res[0] = left + dragView.getDragRegion().width() / 2; - res[1] = top + dragView.getDragRegion().height() / 2; + // Set alarm for when the hint is complete + if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) { + mCurrentScrollDir = direction; + mOnScrollHintAlarm.cancelAlarm(); + mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d)); + mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION); - return res; + mReorderAlarm.cancelAlarm(); + mTargetRank = mEmptyCellRank; + } } OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { @@ -774,17 +694,19 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList }; public void completeDragExit() { - mLauncher.closeFolder(); + if (mInfo.opened) { + mLauncher.closeFolder(); + mRearrangeOnClose = true; + } else { + rearrangeChildren(); + } mCurrentDragInfo = null; mCurrentDragView = null; mSuppressOnAdd = false; - mRearrangeOnClose = true; mIsExternalDrag = false; } public void onDragExit(DragObject d) { - // Exiting folder; stop the auto scroller. - mAutoScrollHelper.setEnabled(false); // We only close the folder if this is a true drag exit, ie. not because // a drop has occurred above the folder. if (!d.dragComplete) { @@ -792,6 +714,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); } mReorderAlarm.cancelAlarm(); + + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mContent.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; + } } public void onDropCompleted(final View target, final DragObject d, @@ -816,7 +745,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList replaceFolderWithFinalItem(); } } else { - setupContentForNumItems(getItemCount()); + rearrangeChildren(); // The drag failed, we need to return the item to the folder mFolderIcon.onDrop(d); } @@ -827,6 +756,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList if (!successfulDrop) { mSuppressFolderDeletion = true; } + mScrollPauseAlarm.cancelAlarm(); completeDragExit(); } } @@ -843,10 +773,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList updateItemLocationsInDatabaseBatch(); } + @Override public void deferCompleteDropAfterUninstallActivity() { mDeferDropAfterUninstall = true; } + @Override public void onUninstallActivityReturned(boolean success) { mDeferDropAfterUninstall = false; mUninstallSuccessful = success; @@ -903,7 +835,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList View v = list.get(i); ItemInfo info = (ItemInfo) v.getTag(); LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0, - info.cellX, info.cellY, false); + info.cellX, info.cellY); } } @@ -917,37 +849,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return true; } - private void setupContentDimensions(int count) { - ArrayList<View> list = getItemsInReadingOrder(); - - int countX = mContent.getCountX(); - int countY = mContent.getCountY(); - boolean done = false; - - while (!done) { - int oldCountX = countX; - int oldCountY = countY; - if (countX * countY < count) { - // Current grid is too small, expand it - if ((countX <= countY || countY == mMaxCountY) && countX < mMaxCountX) { - countX++; - } else if (countY < mMaxCountY) { - countY++; - } - if (countY == 0) countY++; - } else if ((countY - 1) * countX >= count && countY >= countX) { - countY = Math.max(0, countY - 1); - } else if ((countX - 1) * countY >= count) { - countX = Math.max(0, countX - 1); - } - done = countX == oldCountX && countY == oldCountY; - } - mContent.setGridSize(countX, countY); - arrangeChildren(list); - } - public boolean isFull() { - return getItemCount() >= mMaxNumItems; + return mContent.isFull(); } private void centerAboutIcon() { @@ -957,13 +860,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); int height = getFolderHeight(); - float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect); + float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect); LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2); - int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2); + int centerX = (int) (sTempRect.left + sTempRect.width() * scale / 2); + int centerY = (int) (sTempRect.top + sTempRect.height() * scale / 2); int centeredLeft = centerX - width / 2; int centeredTop = centerY - height / 2; int currentPage = mLauncher.getWorkspace().getNextPage(); @@ -1016,18 +919,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mFolderIconPivotY; } - private void setupContentForNumItems(int count) { - setupContentDimensions(count); - - DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); - if (lp == null) { - lp = new DragLayer.LayoutParams(0, 0); - lp.customPosition = true; - setLayoutParams(lp); - } - centerAboutIcon(); - } - private int getContentAreaHeight() { LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); @@ -1035,7 +926,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList CellLayout.LANDSCAPE : CellLayout.PORTRAIT); int maxContentAreaHeight = grid.availableHeightPx - workspacePadding.top - workspacePadding.bottom - - mFolderNameHeight; + mFooterHeight; int height = Math.min(maxContentAreaHeight, mContent.getDesiredHeight()); return Math.max(height, MIN_CONTENT_DIMEN); @@ -1046,67 +937,58 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } private int getFolderHeight() { - int height = getPaddingTop() + getPaddingBottom() - + getContentAreaHeight() + mFolderNameHeight; - return height; + return getFolderHeight(getContentAreaHeight()); + } + + private int getFolderHeight(int contentAreaHeight) { + return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight; } protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); - int height = getFolderHeight(); - int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(getContentAreaWidth(), - MeasureSpec.EXACTLY); - int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(getContentAreaHeight(), - MeasureSpec.EXACTLY); - - if (LauncherAppState.isDisableAllApps()) { - // Don't cap the height of the content to allow scrolling. - mContent.setFixedSize(getContentAreaWidth(), mContent.getDesiredHeight()); - } else { - mContent.setFixedSize(getContentAreaWidth(), getContentAreaHeight()); - } + int contentWidth = getContentAreaWidth(); + int contentHeight = getContentAreaHeight(); + + int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY); + int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY); + + mContent.setFixedSize(contentWidth, contentHeight); + mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec); + mFooter.measure(contentAreaWidthSpec, + MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY)); - mScrollView.measure(contentAreaWidthSpec, contentAreaHeightSpec); - mFolderName.measure(contentAreaWidthSpec, - MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY)); - setMeasuredDimension(width, height); + int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth; + int folderHeight = getFolderHeight(contentHeight); + setMeasuredDimension(folderWidth, folderHeight); } - private void arrangeChildren(ArrayList<View> list) { - int[] vacant = new int[2]; - if (list == null) { - list = getItemsInReadingOrder(); - } - mContent.removeAllViews(); + /** + * Rearranges the children based on their rank. + */ + public void rearrangeChildren() { + rearrangeChildren(-1); + } - for (int i = 0; i < list.size(); i++) { - View v = list.get(i); - mContent.getVacantCell(vacant, 1, 1); - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); - lp.cellX = vacant[0]; - lp.cellY = vacant[1]; - ItemInfo info = (ItemInfo) v.getTag(); - if (info.cellX != vacant[0] || info.cellY != vacant[1]) { - info.cellX = vacant[0]; - info.cellY = vacant[1]; - LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0, - info.cellX, info.cellY); - } - boolean insert = false; - mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true); - } + /** + * Rearranges the children based on their rank. + * @param itemCount if greater than the total children count, empty spaces are left at the end, + * otherwise it is ignored. + */ + public void rearrangeChildren(int itemCount) { + ArrayList<View> views = getItemsInReadingOrder(); + mContent.arrangeChildren(views, Math.max(itemCount, views.size())); mItemsInvalidated = true; } - public int getItemCount() { - return mContent.getShortcutsAndWidgets().getChildCount(); + // TODO remove this once GSA code fix is submitted + public ViewGroup getContent() { + return (ViewGroup) mContent; } - public View getItemAt(int index) { - return mContent.getShortcutsAndWidgets().getChildAt(index); + public int getItemCount() { + return mContent.getItemCount(); } - private void onCloseComplete() { + @Thunk void onCloseComplete() { DragLayer parent = (DragLayer) getParent(); if (parent != null) { parent.removeView(this); @@ -1116,7 +998,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mFolderIcon.requestFocus(); if (mRearrangeOnClose) { - setupContentForNumItems(getItemCount()); + rearrangeChildren(); mRearrangeOnClose = false; } if (getItemCount() <= 1) { @@ -1129,7 +1011,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mSuppressFolderDeletion = false; } - private void replaceFolderWithFinalItem() { + @Thunk void replaceFolderWithFinalItem() { // Add the last remaining child to the workspace in place of the folder Runnable onCompleteRunnable = new Runnable() { @Override @@ -1166,7 +1048,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } }; - View finalChild = getItemAt(0); + View finalChild = mContent.getLastItem(); if (finalChild != null) { mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable); } else { @@ -1181,9 +1063,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // This method keeps track of the last item in the folder for the purposes // of keyboard focus - private void updateTextViewFocus() { - View lastChild = getItemAt(getItemCount() - 1); - getItemAt(getItemCount() - 1); + public void updateTextViewFocus() { + View lastChild = mContent.getLastItem(); if (lastChild != null) { mFolderName.setNextFocusDownId(lastChild.getId()); mFolderName.setNextFocusRightId(lastChild.getId()); @@ -1208,12 +1089,24 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList }; } + // If the icon was dropped while the page was being scrolled, we need to compute + // the target location again such that the icon is placed of the final page. + if (!mContent.rankOnCurrentPage(mEmptyCellRank)) { + // Reorder again. + mTargetRank = getTargetRank(d, null); + + // Rearrange items immediately. + mReorderAlarmListener.onAlarm(mReorderAlarm); + + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + } + mContent.completePendingPageChanges(); + View currentDragView; ShortcutInfo si = mCurrentDragInfo; if (mIsExternalDrag) { - si.cellX = mEmptyCell[0]; - si.cellY = mEmptyCell[1]; - + currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank); // Actually move the item in the database if it was an external drag. Call this // before creating the view, so that ShortcutInfo is updated appropriately. LauncherModel.addOrMoveItemInDatabase( @@ -1224,14 +1117,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList updateItemLocationsInDatabaseBatch(); } mIsExternalDrag = false; - - currentDragView = createAndAddShortcut(si); } else { currentDragView = mCurrentDragView; - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) currentDragView.getLayoutParams(); - si.cellX = lp.cellX = mEmptyCell[0]; - si.cellX = lp.cellY = mEmptyCell[1]; - mContent.addViewToCellLayout(currentDragView, -1, (int) si.id, lp, true); + mContent.addViewForRank(currentDragView, si, mEmptyCellRank); } if (d.dragView.hasDrawn()) { @@ -1250,7 +1138,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList currentDragView.setVisibility(VISIBLE); } mItemsInvalidated = true; - setupContentDimensions(getItemCount()); + rearrangeChildren(); // Temporarily suppress the listener, as we did all the work already here. mSuppressOnAdd = true; @@ -1258,6 +1146,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mSuppressOnAdd = false; // Clear the drag info, as it is no longer being dragged. mCurrentDragInfo = null; + mDragInProgress = false; } // This is used so the item doesn't immediately appear in the folder when added. In one case @@ -1273,16 +1162,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } public void onAdd(ShortcutInfo item) { - mItemsInvalidated = true; // If the item was dropped onto this open folder, we have done the work associated // with adding the item to the folder, as indicated by mSuppressOnAdd being set if (mSuppressOnAdd) return; - if (!findAndSetEmptyCells(item)) { - // The current layout is full, can we expand it? - setupContentForNumItems(getItemCount() + 1); - findAndSetEmptyCells(item); - } - createAndAddShortcut(item); + mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem(item)); + mItemsInvalidated = true; LauncherModel.addOrMoveItemInDatabase( mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); } @@ -1293,27 +1177,25 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // the work associated with removing the item, so we don't have to do anything here. if (item == mCurrentDragInfo) return; View v = getViewForInfo(item); - mContent.removeView(v); + mContent.removeItem(v); if (mState == STATE_ANIMATING) { mRearrangeOnClose = true; } else { - setupContentForNumItems(getItemCount()); + rearrangeChildren(); } if (getItemCount() <= 1) { replaceFolderWithFinalItem(); } } - private View getViewForInfo(ShortcutInfo item) { - for (int j = 0; j < mContent.getCountY(); j++) { - for (int i = 0; i < mContent.getCountX(); i++) { - View v = mContent.getChildAt(i, j); - if (v.getTag() == item) { - return v; - } + private View getViewForInfo(final ShortcutInfo item) { + return mContent.iterateOverItems(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View view, View parent) { + return info == item; } - } - return null; + }); } public void onItemsChanged() { @@ -1326,14 +1208,14 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public ArrayList<View> getItemsInReadingOrder() { if (mItemsInvalidated) { mItemsInReadingOrder.clear(); - for (int j = 0; j < mContent.getCountY(); j++) { - for (int i = 0; i < mContent.getCountX(); i++) { - View v = mContent.getChildAt(i, j); - if (v != null) { - mItemsInReadingOrder.add(v); - } + mContent.iterateOverItems(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View view, View parent) { + mItemsInReadingOrder.add(view); + return false; } - } + }); mItemsInvalidated = false; } return mItemsInReadingOrder; @@ -1352,5 +1234,56 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void getHitRectRelativeToDragLayer(Rect outRect) { getHitRect(outRect); + outRect.left -= mScrollAreaOffset; + outRect.right += mScrollAreaOffset; + } + + private class OnScrollHintListener implements OnAlarmListener { + + private final DragObject mDragObject; + + OnScrollHintListener(DragObject object) { + mDragObject = object; + } + + /** + * Scroll hint has been shown long enough. Now scroll to appropriate page. + */ + @Override + public void onAlarm(Alarm alarm) { + if (mCurrentScrollDir == DragController.SCROLL_LEFT) { + mContent.scrollLeft(); + mScrollHintDir = DragController.SCROLL_NONE; + } else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) { + mContent.scrollRight(); + mScrollHintDir = DragController.SCROLL_NONE; + } else { + // This should not happen + return; + } + mCurrentScrollDir = DragController.SCROLL_NONE; + + // Pause drag event until the scrolling is finished + mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject)); + mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY); + } + } + + private class OnScrollFinishedListener implements OnAlarmListener { + + private final DragObject mDragObject; + + OnScrollFinishedListener(DragObject object) { + mDragObject = object; + } + + /** + * Page scroll is complete. + */ + @Override + public void onAlarm(Alarm alarm) { + // Reorder immediately on page change. + onDragOver(mDragObject, 1); + } } } diff --git a/src/com/android/launcher3/FolderAutoScrollHelper.java b/src/com/android/launcher3/FolderAutoScrollHelper.java deleted file mode 100644 index 40e888464..000000000 --- a/src/com/android/launcher3/FolderAutoScrollHelper.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.support.v4.widget.AutoScrollHelper; -import android.widget.ScrollView; - -/** - * An implementation of {@link AutoScrollHelper} that knows how to scroll - * through a {@link Folder}. - */ -public class FolderAutoScrollHelper extends AutoScrollHelper { - private static final float MAX_SCROLL_VELOCITY = 1500f; - - private final ScrollView mTarget; - - public FolderAutoScrollHelper(ScrollView target) { - super(target); - - mTarget = target; - - setActivationDelay(0); - setEdgeType(EDGE_TYPE_INSIDE_EXTEND); - setExclusive(true); - setMaximumVelocity(MAX_SCROLL_VELOCITY, MAX_SCROLL_VELOCITY); - setRampDownDuration(0); - setRampUpDuration(0); - } - - @Override - public void scrollTargetBy(int deltaX, int deltaY) { - mTarget.scrollBy(deltaX, deltaY); - } - - @Override - public boolean canTargetScrollHorizontally(int direction) { - // List do not scroll horizontally. - return false; - } - - @Override - public boolean canTargetScrollVertically(int direction) { - return mTarget.canScrollVertically(direction); - } -}
\ No newline at end of file diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index a3e82959a..f5836c295 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -43,6 +43,7 @@ import android.widget.TextView; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.FolderInfo.FolderListener; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -50,15 +51,15 @@ import java.util.ArrayList; * An icon that can appear on in the workspace representing an {@link UserFolder}. */ public class FolderIcon extends FrameLayout implements FolderListener { - private Launcher mLauncher; - private Folder mFolder; + @Thunk Launcher mLauncher; + @Thunk Folder mFolder; private FolderInfo mInfo; - private static boolean sStaticValuesDirty = true; + @Thunk static boolean sStaticValuesDirty = true; private CheckLongPressHelper mLongPressHelper; // The number of icons to display in the - private static final int NUM_ITEMS_IN_PREVIEW = 3; + public static final int NUM_ITEMS_IN_PREVIEW = 3; private static final int CONSUMPTION_ANIMATION_DURATION = 100; private static final int DROP_IN_ANIMATION_DURATION = 400; private static final int INITIAL_ITEM_ANIMATION_DURATION = 350; @@ -88,8 +89,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { public static Drawable sSharedFolderLeaveBehind = null; - private ImageView mPreviewBackground; - private BubbleTextView mFolderName; + @Thunk ImageView mPreviewBackground; + @Thunk BubbleTextView mFolderName; FolderRingAnimator mFolderRingAnimator = null; @@ -109,11 +110,11 @@ public class FolderIcon extends FrameLayout implements FolderListener { 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>(); + @Thunk PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0); + @Thunk ArrayList<ShortcutInfo> mHiddenItems = new ArrayList<ShortcutInfo>(); private Alarm mOpenAlarm = new Alarm(); - private ItemInfo mDragInfo; + @Thunk ItemInfo mDragInfo; public FolderIcon(Context context, AttributeSet attrs) { super(context, attrs); @@ -192,7 +193,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { public static class FolderRingAnimator { public int mCellX; public int mCellY; - private CellLayout mCellLayout; + @Thunk CellLayout mCellLayout; public float mOuterRingSize; public float mInnerRingSize; public FolderIcon mFolderIcon = null; diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java index 85a792f4b..80b156413 100644 --- a/src/com/android/launcher3/FolderInfo.java +++ b/src/com/android/launcher3/FolderInfo.java @@ -29,19 +29,33 @@ import java.util.Arrays; */ public class FolderInfo extends ItemInfo { + public static final int NO_FLAGS = 0x00000000; + + /** + * The folder is locked in sorted mode + */ + public static final int FLAG_ITEMS_SORTED = 0x00000001; + + /** + * It is a work folder + */ + public static final int FLAG_WORK_FOLDER = 0x00000002; + /** * Whether this folder has been opened */ boolean opened; + public int options; + /** * The apps and shortcuts */ - ArrayList<ShortcutInfo> contents = new ArrayList<ShortcutInfo>(); + public ArrayList<ShortcutInfo> contents = new ArrayList<ShortcutInfo>(); ArrayList<FolderListener> listeners = new ArrayList<FolderListener>(); - FolderInfo() { + public FolderInfo() { itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; user = UserHandleCompat.myUserHandle(); } @@ -83,6 +97,8 @@ public class FolderInfo extends ItemInfo { void onAddToDatabase(Context context, ContentValues values) { super.onAddToDatabase(context, values); values.put(LauncherSettings.Favorites.TITLE, title.toString()); + values.put(LauncherSettings.Favorites.OPTIONS, options); + } void addListener(FolderListener listener) { @@ -121,4 +137,25 @@ public class FolderInfo extends ItemInfo { + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + ")"; } + + public boolean hasOption(int optionFlag) { + return (options & optionFlag) != 0; + } + + /** + * @param option flag to set or clear + * @param isEnabled whether to set or clear the flag + * @param context if not null, save changes to the db. + */ + public void setOption(int option, boolean isEnabled, Context context) { + int oldOptions = options; + if (isEnabled) { + options |= option; + } else { + options &= ~option; + } + if (context != null && oldOptions != options) { + LauncherModel.updateItemInDatabase(context, this); + } + } } diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java new file mode 100644 index 000000000..617489271 --- /dev/null +++ b/src/com/android/launcher3/FolderPagedView.java @@ -0,0 +1,842 @@ +/** + * Copyright (C) 2015 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.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; +import android.widget.Switch; + +import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener; +import com.android.launcher3.PageIndicator.PageMarkerResources; +import com.android.launcher3.Workspace.ItemOperator; +import com.android.launcher3.util.Thunk; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class FolderPagedView extends PagedView { + + private static final String TAG = "FolderPagedView"; + + private static final boolean ALLOW_FOLDER_SCROLL = true; + + // To enable this flag, user_folder.xml needs to be modified to add sort button. + private static final boolean ALLOW_ITEM_SORTING = false; + + private static final int REORDER_ANIMATION_DURATION = 230; + private static final int START_VIEW_REORDER_DELAY = 30; + private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f; + + private static final int SPAN_TO_PAGE_DURATION = 350; + private static final int SORT_ANIM_HIDE_DURATION = 130; + private static final int SORT_ANIM_SHOW_DURATION = 160; + + /** + * Fraction of the width to scroll when showing the next page hint. + */ + private static final float SCROLL_HINT_FRACTION = 0.07f; + + private static final int[] sTempPosArray = new int[2]; + + // TODO: Remove this restriction + private static final int MAX_ITEMS_PER_PAGE = 4; + + public final boolean rtlLayout; + + private final LayoutInflater mInflater; + private final IconCache mIconCache; + + @Thunk final HashMap<View, Runnable> mPendingAnimations = new HashMap<>(); + + private final int mMaxCountX; + private final int mMaxCountY; + private final int mMaxItemsPerPage; + + private int mAllocatedContentSize; + private int mGridCountX; + private int mGridCountY; + + private Folder mFolder; + private FocusIndicatorView mFocusIndicatorView; + private PagedFolderKeyEventListener mKeyListener; + + private View mSortButton; + private Switch mSortSwitch; + private View mPageIndicator; + + private boolean mSortOperationPending; + boolean mIsSorted; + + public FolderPagedView(Context context, AttributeSet attrs) { + super(context, attrs); + LauncherAppState app = LauncherAppState.getInstance(); + setDataIsReady(); + + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + if (ALLOW_FOLDER_SCROLL) { + mMaxCountX = Math.min((int) grid.numColumns, MAX_ITEMS_PER_PAGE); + mMaxCountY = Math.min((int) grid.numRows, MAX_ITEMS_PER_PAGE); + } else { + mMaxCountX = (int) grid.numColumns; + mMaxCountY = (int) grid.numRows; + } + + mMaxItemsPerPage = mMaxCountX * mMaxCountY; + + mInflater = LayoutInflater.from(context); + mIconCache = app.getIconCache(); + + rtlLayout = getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_RTL; + } + + public void setFolder(Folder folder) { + mFolder = folder; + mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator); + mKeyListener = new PagedFolderKeyEventListener(folder); + mPageIndicator = folder.findViewById(R.id.folder_page_indicator); + + if (ALLOW_ITEM_SORTING) { + // Initialize {@link #mSortSwitch} and {@link #mSortButton}. + } + } + + /** + * Called when sort button is clicked. + */ + private void onSortClicked() { + if (mSortOperationPending) { + return; + } + if (mIsSorted) { + setIsSorted(false, true); + } else { + mSortOperationPending = true; + doSort(); + } + } + + private void setIsSorted(boolean isSorted, boolean saveChanges) { + mIsSorted = isSorted; + if (ALLOW_ITEM_SORTING) { + mSortSwitch.setChecked(isSorted); + mFolder.mInfo.setOption(FolderInfo.FLAG_ITEMS_SORTED, isSorted, + saveChanges ? mFolder.mLauncher : null); + } + } + + /** + * Sorts the contents of the folder and animates the icons on the first page to reflect + * the changes. + * Steps: + * 1. Scroll to first page + * 2. Sort all icons in one go + * 3. Re-apply the old IconInfos on the first page (so that there is no instant change) + * 4. Animate each view individually to reflect the new icon. + */ + private void doSort() { + if (!mSortOperationPending) { + return; + } + if (getNextPage() != 0) { + snapToPage(0, SPAN_TO_PAGE_DURATION, new DecelerateInterpolator()); + return; + } + + mSortOperationPending = false; + ShortcutInfo[][] oldItems = new ShortcutInfo[mGridCountX][mGridCountY]; + CellLayout currentPage = getCurrentCellLayout(); + for (int x = 0; x < mGridCountX; x++) { + for (int y = 0; y < mGridCountY; y++) { + View v = currentPage.getChildAt(x, y); + if (v != null) { + oldItems[x][y] = (ShortcutInfo) v.getTag(); + } + } + } + + ArrayList<View> views = new ArrayList<View>(mFolder.getItemsInReadingOrder()); + Collections.sort(views, new ViewComparator()); + arrangeChildren(views, views.size()); + + int delay = 0; + float delayAmount = START_VIEW_REORDER_DELAY; + final Interpolator hideInterpolator = new DecelerateInterpolator(2); + final Interpolator showInterpolator = new OvershootInterpolator(0.8f); + + currentPage = getCurrentCellLayout(); + for (int x = 0; x < mGridCountX; x++) { + for (int y = 0; y < mGridCountY; y++) { + final BubbleTextView v = (BubbleTextView) currentPage.getChildAt(x, y); + if (v != null) { + final ShortcutInfo info = (ShortcutInfo) v.getTag(); + final Runnable clearPending = new Runnable() { + + @Override + public void run() { + mPendingAnimations.remove(v); + v.setScaleX(1); + v.setScaleY(1); + } + }; + if (oldItems[x][y] == null) { + v.setScaleX(0); + v.setScaleY(0); + v.animate().setDuration(SORT_ANIM_SHOW_DURATION) + .setStartDelay(SORT_ANIM_HIDE_DURATION + delay) + .scaleX(1).scaleY(1).setInterpolator(showInterpolator) + .withEndAction(clearPending); + mPendingAnimations.put(v, clearPending); + } else { + // Apply the old iconInfo so that there is no sudden change. + v.applyFromShortcutInfo(oldItems[x][y], mIconCache, false); + v.animate().setStartDelay(delay).setDuration(SORT_ANIM_HIDE_DURATION) + .scaleX(0).scaleY(0) + .setInterpolator(hideInterpolator) + .withEndAction(new Runnable() { + + @Override + public void run() { + // Apply the new iconInfo as part of the animation. + v.applyFromShortcutInfo(info, mIconCache, false); + v.animate().scaleX(1).scaleY(1) + .setDuration(SORT_ANIM_SHOW_DURATION).setStartDelay(0) + .setInterpolator(showInterpolator) + .withEndAction(clearPending); + } + }); + mPendingAnimations.put(v, new Runnable() { + + @Override + public void run() { + clearPending.run(); + v.applyFromShortcutInfo(info, mIconCache, false); + } + }); + } + delay += delayAmount; + delayAmount *= VIEW_REORDER_DELAY_FACTOR; + } + } + } + + setIsSorted(true, true); + } + + /** + * Sets up the grid size such that {@param count} items can fit in the grid. + * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while + * maintaining the restrictions of {@link #mMaxCountX} & {@link #mMaxCountY}. + */ + private void setupContentDimensions(int count) { + mAllocatedContentSize = count; + boolean done; + if (count >= mMaxItemsPerPage) { + mGridCountX = mMaxCountX; + mGridCountY = mMaxCountY; + done = true; + } else { + done = false; + } + + while (!done) { + int oldCountX = mGridCountX; + int oldCountY = mGridCountY; + if (mGridCountX * mGridCountY < count) { + // Current grid is too small, expand it + if ((mGridCountX <= mGridCountY || mGridCountY == mMaxCountY) && mGridCountX < mMaxCountX) { + mGridCountX++; + } else if (mGridCountY < mMaxCountY) { + mGridCountY++; + } + if (mGridCountY == 0) mGridCountY++; + } else if ((mGridCountY - 1) * mGridCountX >= count && mGridCountY >= mGridCountX) { + mGridCountY = Math.max(0, mGridCountY - 1); + } else if ((mGridCountX - 1) * mGridCountY >= count) { + mGridCountX = Math.max(0, mGridCountX - 1); + } + done = mGridCountX == oldCountX && mGridCountY == oldCountY; + } + + // Update grid size + for (int i = getPageCount() - 1; i >= 0; i--) { + getPageAt(i).setGridSize(mGridCountX, mGridCountY); + } + } + + /** + * Binds items to the layout. + * @return list of items that could not be bound, probably because we hit the max size limit. + */ + public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) { + mIsSorted = ALLOW_ITEM_SORTING && mFolder.mInfo.hasOption(FolderInfo.FLAG_ITEMS_SORTED); + ArrayList<View> icons = new ArrayList<View>(); + ArrayList<ShortcutInfo> extra = new ArrayList<ShortcutInfo>(); + + for (ShortcutInfo item : items) { + if (!ALLOW_FOLDER_SCROLL && icons.size() >= mMaxItemsPerPage) { + extra.add(item); + } else { + icons.add(createNewView(item)); + } + } + arrangeChildren(icons, icons.size(), false); + return extra; + } + + /** + * Create space for a new item at the end, and returns the rank for that item. + * Also sets the current page to the last page. + */ + public int allocateRankForNewItem(ShortcutInfo info) { + int rank = getItemCount(); + ArrayList<View> views = new ArrayList<View>(mFolder.getItemsInReadingOrder()); + if (ALLOW_ITEM_SORTING && mIsSorted) { + View tmp = new View(getContext()); + tmp.setTag(info); + int index = Collections.binarySearch(views, tmp, new ViewComparator()); + if (index < 0) { + rank = -index - 1; + } else { + // Item with same name already exists. + // We will just insert it before that item. + rank = index; + } + + } + + views.add(rank, null); + arrangeChildren(views, views.size(), false); + setCurrentPage(rank / mMaxItemsPerPage); + return rank; + } + + public View createAndAddViewForRank(ShortcutInfo item, int rank) { + View icon = createNewView(item); + addViewForRank(icon, item, rank); + return icon; + } + + /** + * Adds the {@param view} to the layout based on {@param rank} and updated the position + * related attributes. It assumes that {@param item} is already attached to the view. + */ + public void addViewForRank(View view, ShortcutInfo item, int rank) { + int pagePos = rank % mMaxItemsPerPage; + int pageNo = rank / mMaxItemsPerPage; + + item.rank = rank; + item.cellX = pagePos % mGridCountX; + item.cellY = pagePos / mGridCountX; + + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); + lp.cellX = item.cellX; + lp.cellY = item.cellY; + getPageAt(pageNo).addViewToCellLayout( + view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true); + } + + @SuppressLint("InflateParams") + private View createNewView(ShortcutInfo item) { + final BubbleTextView textView = (BubbleTextView) mInflater.inflate( + R.layout.folder_application, null, false); + textView.applyFromShortcutInfo(item, mIconCache, false); + textView.setOnClickListener(mFolder); + textView.setOnLongClickListener(mFolder); + textView.setOnFocusChangeListener(mFocusIndicatorView); + textView.setOnKeyListener(mKeyListener); + + textView.setLayoutParams(new CellLayout.LayoutParams( + item.cellX, item.cellY, item.spanX, item.spanY)); + return textView; + } + + @Override + public CellLayout getPageAt(int index) { + return (CellLayout) getChildAt(index); + } + + public void removeCellLayoutView(View view) { + for (int i = getChildCount() - 1; i >= 0; i --) { + getPageAt(i).removeView(view); + } + } + + public CellLayout getCurrentCellLayout() { + return getPageAt(getNextPage()); + } + + private CellLayout createAndAddNewPage() { + DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); + CellLayout page = new CellLayout(getContext()); + page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); + page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); + page.setInvertIfRtl(true); + page.setGridSize(mGridCountX, mGridCountY); + + LayoutParams lp = generateDefaultLayoutParams(); + lp.isFullScreenPage = true; + addView(page, -1, lp); + return page; + } + + public void setFixedSize(int width, int height) { + for (int i = getChildCount() - 1; i >= 0; i --) { + ((CellLayout) getChildAt(i)).setFixedSize(width, height); + } + } + + public void removeItem(View v) { + for (int i = getChildCount() - 1; i >= 0; i --) { + getPageAt(i).removeView(v); + } + } + + /** + * Updates position and rank of all the children in the view. + * It essentially removes all views from all the pages and then adds them again in appropriate + * page. + * + * @param list the ordered list of children. + * @param itemCount if greater than the total children count, empty spaces are left + * at the end, otherwise it is ignored. + * + */ + public void arrangeChildren(ArrayList<View> list, int itemCount) { + arrangeChildren(list, itemCount, true); + } + + private void arrangeChildren(ArrayList<View> list, int itemCount, boolean saveChanges) { + ArrayList<CellLayout> pages = new ArrayList<CellLayout>(); + for (int i = 0; i < getChildCount(); i++) { + CellLayout page = (CellLayout) getChildAt(i); + page.removeAllViews(); + pages.add(page); + } + setupContentDimensions(itemCount); + + Iterator<CellLayout> pageItr = pages.iterator(); + CellLayout currentPage = null; + + int position = 0; + int newX, newY, rank; + + boolean isSorted = mIsSorted; + + ViewComparator comparator = new ViewComparator(); + View lastView = null; + rank = 0; + for (int i = 0; i < itemCount; i++) { + View v = list.size() > i ? list.get(i) : null; + if (currentPage == null || position >= mMaxItemsPerPage) { + // Next page + if (pageItr.hasNext()) { + currentPage = pageItr.next(); + } else { + currentPage = createAndAddNewPage(); + } + position = 0; + } + + if (v != null) { + if (lastView != null) { + isSorted &= comparator.compare(lastView, v) <= 0; + } + + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); + newX = position % mGridCountX; + newY = position / mGridCountX; + ItemInfo info = (ItemInfo) v.getTag(); + if (info.cellX != newX || info.cellY != newY || info.rank != rank) { + info.cellX = newX; + info.cellY = newY; + info.rank = rank; + if (saveChanges) { + LauncherModel.addOrMoveItemInDatabase(getContext(), info, + mFolder.mInfo.id, 0, info.cellX, info.cellY); + } + } + lp.cellX = info.cellX; + lp.cellY = info.cellY; + currentPage.addViewToCellLayout( + v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true); + } + + lastView = v; + rank ++; + position++; + } + + // Remove extra views. + boolean removed = false; + while (pageItr.hasNext()) { + removeView(pageItr.next()); + removed = true; + } + if (removed) { + setCurrentPage(0); + } + + setEnableOverscroll(getPageCount() > 1); + + // Update footer + if (ALLOW_ITEM_SORTING) { + setIsSorted(isSorted, saveChanges); + if (getPageCount() > 1) { + mPageIndicator.setVisibility(View.VISIBLE); + mSortButton.setVisibility(View.VISIBLE); + mFolder.mFolderName.setGravity(rtlLayout ? Gravity.RIGHT : Gravity.LEFT); + } else { + mPageIndicator.setVisibility(View.GONE); + mSortButton.setVisibility(View.GONE); + mFolder.mFolderName.setGravity(Gravity.CENTER_HORIZONTAL); + } + } else { + int indicatorVisibility = mPageIndicator.getVisibility(); + mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE); + if (indicatorVisibility != mPageIndicator.getVisibility()) { + mFolder.updateFooterHeight(); + } + } + } + + @Override + protected void loadAssociatedPages(int page, boolean immediateAndOnly) { } + + @Override + public void syncPages() { } + + @Override + public void syncPageItems(int page, boolean immediate) { } + + public int getDesiredWidth() { + return getPageCount() > 0 ? getPageAt(0).getDesiredWidth() : 0; + } + + public int getDesiredHeight() { + return getPageCount() > 0 ? getPageAt(0).getDesiredHeight() : 0; + } + + public int getItemCount() { + int lastPageIndex = getChildCount() - 1; + if (lastPageIndex < 0) { + // If there are no pages, nothing has yet been added to the folder. + return 0; + } + return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount() + + lastPageIndex * mMaxItemsPerPage; + } + + /** + * @return the rank of the cell nearest to the provided pixel position. + */ + public int findNearestArea(int pixelX, int pixelY) { + int pageIndex = getNextPage(); + CellLayout page = getPageAt(pageIndex); + page.findNearestArea(pixelX, pixelY, 1, 1, null, false, sTempPosArray); + if (mFolder.isLayoutRtl()) { + sTempPosArray[0] = page.getCountX() - sTempPosArray[0] - 1; + } + return Math.min(mAllocatedContentSize - 1, + pageIndex * mMaxItemsPerPage + sTempPosArray[1] * mGridCountX + sTempPosArray[0]); + } + + @Override + protected PageMarkerResources getPageIndicatorMarker(int pageIndex) { + return new PageMarkerResources(R.drawable.ic_pageindicator_current_folder, + R.drawable.ic_pageindicator_default_folder); + } + + public boolean isFull() { + return !ALLOW_FOLDER_SCROLL && getItemCount() >= mMaxItemsPerPage; + } + + public View getLastItem() { + if (getChildCount() < 1) { + return null; + } + ShortcutAndWidgetContainer lastContainer = getCurrentCellLayout().getShortcutsAndWidgets(); + int lastRank = lastContainer.getChildCount() - 1; + if (mGridCountX > 0) { + return lastContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX); + } else { + return lastContainer.getChildAt(lastRank); + } + } + + /** + * Iterates over all its items in a reading order. + * @return the view for which the operator returned true. + */ + public View iterateOverItems(ItemOperator op) { + for (int k = 0 ; k < getChildCount(); k++) { + CellLayout page = getPageAt(k); + for (int j = 0; j < page.getCountY(); j++) { + for (int i = 0; i < page.getCountX(); i++) { + View v = page.getChildAt(i, j); + if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v, this)) { + return v; + } + } + } + } + return null; + } + + public String getAccessibilityDescription() { + return String.format(getContext().getString(R.string.folder_opened), + mGridCountX, mGridCountY); + } + + /** + * Sets the focus on the first visible child. + */ + public void setFocusOnFirstChild() { + View firstChild = getCurrentCellLayout().getChildAt(0, 0); + if (firstChild != null) { + firstChild.requestFocus(); + } + } + + @Override + protected void notifyPageSwitchListener() { + super.notifyPageSwitchListener(); + if (mFolder != null) { + mFolder.updateTextViewFocus(); + } + if (ALLOW_ITEM_SORTING && mSortOperationPending && getNextPage() == 0) { + post(new Runnable() { + + @Override + public void run() { + if (mSortOperationPending) { + doSort(); + } + } + }); + } + } + + /** + * Scrolls the current view by a fraction + */ + public void showScrollHint(int direction) { + float fraction = (direction == DragController.SCROLL_LEFT) ^ rtlLayout + ? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION; + int hint = (int) (fraction * getWidth()); + int scroll = getScrollForPage(getNextPage()) + hint; + int delta = scroll - mUnboundedScrollX; + if (delta != 0) { + mScroller.setInterpolator(new DecelerateInterpolator()); + mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, Folder.SCROLL_HINT_DURATION); + invalidate(); + } + } + + public void clearScrollHint() { + if (mUnboundedScrollX != getScrollForPage(getNextPage())) { + snapToPage(getNextPage()); + } + } + + /** + * Finish animation all the views which are animating across pages + */ + public void completePendingPageChanges() { + if (!mPendingAnimations.isEmpty()) { + HashMap<View, Runnable> pendingViews = new HashMap<>(mPendingAnimations); + for (Map.Entry<View, Runnable> e : pendingViews.entrySet()) { + e.getKey().animate().cancel(); + e.getValue().run(); + } + } + } + + public boolean rankOnCurrentPage(int rank) { + int p = rank / mMaxItemsPerPage; + return p == getNextPage(); + } + + @Override + protected void onPageBeginMoving() { + super.onPageBeginMoving(); + getVisiblePages(sTempPosArray); + for (int i = sTempPosArray[0]; i <= sTempPosArray[1]; i++) { + verifyVisibleHighResIcons(i); + } + } + + /** + * Ensures that all the icons on the given page are of high-res + */ + public void verifyVisibleHighResIcons(int pageNo) { + CellLayout page = getPageAt(pageNo); + if (page != null) { + ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets(); + for (int i = parent.getChildCount() - 1; i >= 0; i--) { + ((BubbleTextView) parent.getChildAt(i)).verifyHighRes(); + } + } + } + + /** + * Reorders the items such that the {@param empty} spot moves to {@param target} + */ + public void realTimeReorder(int empty, int target) { + completePendingPageChanges(); + int delay = 0; + float delayAmount = START_VIEW_REORDER_DELAY; + + // Animation only happens on the current page. + int pageToAnimate = getNextPage(); + + int pageT = target / mMaxItemsPerPage; + int pagePosT = target % mMaxItemsPerPage; + + if (pageT != pageToAnimate) { + Log.e(TAG, "Cannot animate when the target cell is invisible"); + } + int pagePosE = empty % mMaxItemsPerPage; + int pageE = empty / mMaxItemsPerPage; + + int startPos, endPos; + int moveStart, moveEnd; + int direction; + + if (target == empty) { + // No animation + return; + } else if (target > empty) { + // Items will move backwards to make room for the empty cell. + direction = 1; + + // If empty cell is in a different page, move them instantly. + if (pageE < pageToAnimate) { + moveStart = empty; + // Instantly move the first item in the current page. + moveEnd = pageToAnimate * mMaxItemsPerPage; + // Animate the 2nd item in the current page, as the first item was already moved to + // the last page. + startPos = 0; + } else { + moveStart = moveEnd = -1; + startPos = pagePosE; + } + + endPos = pagePosT; + } else { + // The items will move forward. + direction = -1; + + if (pageE > pageToAnimate) { + // Move the items immediately. + moveStart = empty; + // Instantly move the last item in the current page. + moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1; + + // Animations start with the second last item in the page + startPos = mMaxItemsPerPage - 1; + } else { + moveStart = moveEnd = -1; + startPos = pagePosE; + } + + endPos = pagePosT; + } + + // Instant moving views. + while (moveStart != moveEnd) { + int rankToMove = moveStart + direction; + int p = rankToMove / mMaxItemsPerPage; + int pagePos = rankToMove % mMaxItemsPerPage; + int x = pagePos % mGridCountX; + int y = pagePos / mGridCountX; + + final CellLayout page = getPageAt(p); + final View v = page.getChildAt(x, y); + if (v != null) { + if (pageToAnimate != p) { + page.removeView(v); + addViewForRank(v, (ShortcutInfo) v.getTag(), moveStart); + } else { + // Do a fake animation before removing it. + final int newRank = moveStart; + final float oldTranslateX = v.getTranslationX(); + + Runnable endAction = new Runnable() { + + @Override + public void run() { + mPendingAnimations.remove(v); + v.setTranslationX(oldTranslateX); + ((CellLayout) v.getParent().getParent()).removeView(v); + addViewForRank(v, (ShortcutInfo) v.getTag(), newRank); + } + }; + v.animate() + .translationXBy((direction > 0 ^ rtlLayout) ? -v.getWidth() : v.getWidth()) + .setDuration(REORDER_ANIMATION_DURATION) + .setStartDelay(0) + .withEndAction(endAction); + mPendingAnimations.put(v, endAction); + } + } + moveStart = rankToMove; + } + + if ((endPos - startPos) * direction <= 0) { + // No animation + return; + } + + CellLayout page = getPageAt(pageToAnimate); + for (int i = startPos; i != endPos; i += direction) { + int nextPos = i + direction; + View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX); + if (v != null) { + ((ItemInfo) v.getTag()).rank -= direction; + } + if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX, + REORDER_ANIMATION_DURATION, delay, true, true)) { + delay += delayAmount; + delayAmount *= VIEW_REORDER_DELAY_FACTOR; + } + } + } + + private static class ViewComparator implements Comparator<View> { + private final Collator mCollator = Collator.getInstance(); + + @Override + public int compare(View lhs, View rhs) { + return mCollator.compare( ((ShortcutInfo) lhs.getTag()).title.toString(), + ((ShortcutInfo) rhs.getTag()).title.toString()); + } + } +} diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java index b08272f36..b614bc628 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -16,24 +16,18 @@ package com.android.launcher3; -import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; -import java.util.ArrayList; - public class Hotseat extends FrameLayout { - private static final String TAG = "Hotseat"; private CellLayout mContent; @@ -86,19 +80,22 @@ public class Hotseat extends FrameLayout { int getOrderInHotseat(int x, int y) { return hasVerticalHotseat() ? (mContent.getCountY() - y - 1) : x; } + /* Get the orientation specific coordinates given an invariant order in the hotseat. */ int getCellXFromOrder(int rank) { return hasVerticalHotseat() ? 0 : rank; } + int getCellYFromOrder(int rank) { return hasVerticalHotseat() ? (mContent.getCountY() - (rank + 1)) : 0; } + + public int getAllAppsButtonRank() { + return mAllAppsButtonRank; + } + public boolean isAllAppsButtonRank(int rank) { - if (LauncherAppState.isDisableAllApps()) { - return false; - } else { - return rank == mAllAppsButtonRank; - } + return rank == mAllAppsButtonRank; } /** This returns the coordinates of an app in a given cell, relative to the DragLayer */ @@ -141,35 +138,33 @@ public class Hotseat extends FrameLayout { void resetLayout() { mContent.removeAllViewsInLayout(); - if (!LauncherAppState.isDisableAllApps()) { - // Add the Apps button - Context context = getContext(); - - LayoutInflater inflater = LayoutInflater.from(context); - 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); - } - - // Note: We do this to ensure that the hotseat is always laid out in the orientation of - // the hotseat in order regardless of which orientation they were added - int x = getCellXFromOrder(mAllAppsButtonRank); - int y = getCellYFromOrder(mAllAppsButtonRank); - CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1); - lp.canReorder = false; - mContent.addViewToCellLayout(allAppsButton, -1, allAppsButton.getId(), lp, true); + // Add the Apps button + Context context = getContext(); + + LayoutInflater inflater = LayoutInflater.from(context); + 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); } + + // Note: We do this to ensure that the hotseat is always laid out in the orientation of + // the hotseat in order regardless of which orientation they were added + int x = getCellXFromOrder(mAllAppsButtonRank); + int y = getCellYFromOrder(mAllAppsButtonRank); + CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1); + lp.canReorder = false; + mContent.addViewToCellLayout(allAppsButton, -1, allAppsButton.getId(), lp, true); } @Override @@ -181,55 +176,4 @@ public class Hotseat extends FrameLayout { } return false; } - - void addAllAppsFolder(IconCache iconCache, - ArrayList<AppInfo> allApps, ArrayList<ComponentName> onWorkspace, - Launcher launcher, Workspace workspace) { - if (LauncherAppState.isDisableAllApps()) { - FolderInfo fi = new FolderInfo(); - - fi.cellX = getCellXFromOrder(mAllAppsButtonRank); - fi.cellY = getCellYFromOrder(mAllAppsButtonRank); - fi.spanX = 1; - fi.spanY = 1; - fi.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT; - fi.screenId = mAllAppsButtonRank; - fi.itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; - fi.title = "More Apps"; - LauncherModel.addItemToDatabase(launcher, fi, fi.container, fi.screenId, fi.cellX, - fi.cellY, false); - FolderIcon folder = FolderIcon.fromXml(R.layout.folder_icon, launcher, - getLayout(), fi, iconCache); - workspace.addInScreen(folder, fi.container, fi.screenId, fi.cellX, fi.cellY, - fi.spanX, fi.spanY); - - for (AppInfo info: allApps) { - ComponentName cn = info.intent.getComponent(); - if (!onWorkspace.contains(cn)) { - Log.d(TAG, "Adding to 'more apps': " + info.intent); - ShortcutInfo si = info.makeShortcut(); - fi.add(si); - } - } - } - } - - void addAppsToAllAppsFolder(ArrayList<AppInfo> apps) { - if (LauncherAppState.isDisableAllApps()) { - View v = mContent.getChildAt(getCellXFromOrder(mAllAppsButtonRank), getCellYFromOrder(mAllAppsButtonRank)); - FolderIcon fi = null; - - if (v instanceof FolderIcon) { - fi = (FolderIcon) v; - } else { - return; - } - - FolderInfo info = fi.getFolderInfo(); - for (AppInfo a: apps) { - ShortcutInfo si = a.makeShortcut(); - info.add(si); - } - } - } } diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 5a0875b30..48b38f182 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -18,17 +18,23 @@ package com.android.launcher3; import android.app.ActivityManager; 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.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.drawable.Drawable; +import android.os.Handler; import android.text.TextUtils; import android.util.Log; @@ -36,16 +42,15 @@ 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 com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.PackageItemInfo; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; +import java.util.Locale; import java.util.Map.Entry; /** @@ -56,49 +61,34 @@ public class IconCache { private static final String TAG = "Launcher.IconCache"; private static final int INITIAL_ICON_CACHE_CAPACITY = 50; - private static final String RESOURCE_FILE_PREFIX = "icon_"; // 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 { + private static final int LOW_RES_SCALE_FACTOR = 8; + + @Thunk static class CacheEntry { public Bitmap icon; public CharSequence title; public CharSequence contentDescription; + public boolean isLowResIcon; } - private static class CacheKey { - public ComponentName componentName; - public UserHandleCompat user; - - CacheKey(ComponentName componentName, UserHandleCompat user) { - this.componentName = componentName; - this.user = user; - } + private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = new HashMap<>(); + private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); - @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 HashMap<UserHandleCompat, Bitmap> mDefaultIcons = - new HashMap<UserHandleCompat, Bitmap>(); private final Context mContext; private final PackageManager mPackageManager; 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; + private final HashMap<ComponentKey, CacheEntry> mCache = + new HashMap<ComponentKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); + private final int mIconDpi; + private final IconDB mIconDb; + + private final Handler mWorkerHandler; public IconCache(Context context) { ActivityManager activityManager = @@ -109,13 +99,12 @@ public class IconCache { mUserManager = UserManagerCompat.getInstance(mContext); mLauncherApps = LauncherAppsCompat.getInstance(mContext); mIconDpi = activityManager.getLauncherLargeIconDensity(); + mIconDb = new IconDB(context); - // need to set mIconDpi before getting default icon - UserHandleCompat myUser = UserHandleCompat.myUserHandle(); - mDefaultIcons.put(myUser, makeDefaultIcon(myUser)); + mWorkerHandler = new Handler(LauncherModel.sWorkerThread.getLooper()); } - public Drawable getFullResDefaultActivityIcon() { + private Drawable getFullResDefaultActivityIcon() { return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon); } @@ -184,26 +173,171 @@ public class IconCache { * Remove any records for the supplied ComponentName. */ public synchronized void remove(ComponentName componentName, UserHandleCompat user) { - mCache.remove(new CacheKey(componentName, user)); + mCache.remove(new ComponentKey(componentName, user)); } /** - * Remove any records for the supplied package name. + * Remove any records for the supplied package name from memory. */ - public synchronized void remove(String packageName, UserHandleCompat user) { - HashSet<CacheKey> forDeletion = new HashSet<CacheKey>(); - for (CacheKey key: mCache.keySet()) { + private void removeFromMemCacheLocked(String packageName, UserHandleCompat user) { + HashSet<ComponentKey> forDeletion = new HashSet<ComponentKey>(); + for (ComponentKey key: mCache.keySet()) { if (key.componentName.getPackageName().equals(packageName) && key.user.equals(user)) { forDeletion.add(key); } } - for (CacheKey condemned: forDeletion) { + for (ComponentKey condemned: forDeletion) { mCache.remove(condemned); } } /** + * Updates the entries related to the given package in memory and persistent DB. + */ + public synchronized void updateIconsForPkg(String packageName, UserHandleCompat user) { + removeIconsForPkg(packageName, user); + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, + PackageManager.GET_UNINSTALLED_PACKAGES); + long userSerial = mUserManager.getSerialNumberForUser(user); + for (LauncherActivityInfoCompat app : mLauncherApps.getActivityList(packageName, user)) { + addIconToDBAndMemCache(app, info, userSerial); + } + } catch (NameNotFoundException e) { + Log.d(TAG, "Package not found", e); + return; + } + } + + /** + * Removes the entries related to the given package in memory and persistent DB. + */ + public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) { + removeFromMemCacheLocked(packageName, user); + long userSerial = mUserManager.getSerialNumberForUser(user); + mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, + IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {packageName + "/%", Long.toString(userSerial)}); + } + + /** + * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in + * the DB and are updated. + * @return The set of packages for which icons have updated. + */ + public HashSet<String> updateDBIcons(UserHandleCompat user, List<LauncherActivityInfoCompat> apps) { + mIconDb.updateSystemStateString(mContext); + long userSerial = mUserManager.getSerialNumberForUser(user); + PackageManager pm = mContext.getPackageManager(); + HashMap<String, PackageInfo> pkgInfoMap = new HashMap<String, PackageInfo>(); + for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) { + pkgInfoMap.put(info.packageName, info); + } + + HashMap<ComponentName, LauncherActivityInfoCompat> componentMap = new HashMap<>(); + for (LauncherActivityInfoCompat app : apps) { + componentMap.put(app.getComponentName(), app); + } + + Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, + new String[] {IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, + IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION, + IconDB.COLUMN_SYSTEM_STATE}, + IconDB.COLUMN_USER + " = ? ", + new String[] {Long.toString(userSerial)}, + null, null, null); + + final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); + final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED); + final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION); + final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); + final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE); + + HashSet<Integer> itemsToRemove = new HashSet<Integer>(); + HashSet<String> updatedPackages = new HashSet<String>(); + + while (c.moveToNext()) { + String cn = c.getString(indexComponent); + ComponentName component = ComponentName.unflattenFromString(cn); + PackageInfo info = pkgInfoMap.get(component.getPackageName()); + if (info == null) { + itemsToRemove.add(c.getInt(rowIndex)); + continue; + } + if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) { + // Application is not present + continue; + } + + long updateTime = c.getLong(indexLastUpdate); + int version = c.getInt(indexVersion); + LauncherActivityInfoCompat app = componentMap.remove(component); + if (version == info.versionCode && updateTime == info.lastUpdateTime && + TextUtils.equals(mIconDb.mSystemState, c.getString(systemStateIndex))) { + continue; + } + if (app == null) { + itemsToRemove.add(c.getInt(rowIndex)); + continue; + } + ContentValues values = updateCacheAndGetContentValues(app); + mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values, + IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {cn, Long.toString(userSerial)}); + + updatedPackages.add(component.getPackageName()); + } + c.close(); + if (!itemsToRemove.isEmpty()) { + mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, + IconDB.COLUMN_ROWID + " IN ( " + TextUtils.join(", ", itemsToRemove) +" )", + null); + } + + // Insert remaining apps. + for (LauncherActivityInfoCompat app : componentMap.values()) { + PackageInfo info = pkgInfoMap.get(app.getComponentName().getPackageName()); + if (info == null) { + continue; + } + addIconToDBAndMemCache(app, info, userSerial); + } + return updatedPackages; + } + + private void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info, + long userSerial) { + ContentValues values = updateCacheAndGetContentValues(app); + addIconToDB(values, app.getComponentName(), info, userSerial); + values.put(IconDB.COLUMN_COMPONENT, app.getComponentName().flattenToString()); + } + + /** + * Updates {@param values} to contain versoning information and adds it to the DB. + * @param values {@link ContentValues} containing icon & title + */ + private void addIconToDB(ContentValues values, ComponentName key, + PackageInfo info, long userSerial) { + values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); + values.put(IconDB.COLUMN_USER, userSerial); + values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime); + values.put(IconDB.COLUMN_VERSION, info.versionCode); + mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + + private ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app) { + CacheEntry entry = new CacheEntry(); + entry.icon = Utilities.createIconBitmap(app.getBadgedIcon(mIconDpi), mContext); + entry.title = app.getLabel(); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser()); + mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry); + + return mIconDb.newContentValues(entry.icon, entry.title.toString()); + } + + /** * Empty out the cache. */ public synchronized void flush() { @@ -214,7 +348,7 @@ public class IconCache { * Empty out the cache that aren't of the correct grid size */ public synchronized void flushInvalidIcons(DeviceProfile grid) { - Iterator<Entry<CacheKey, CacheEntry>> it = mCache.entrySet().iterator(); + Iterator<Entry<ComponentKey, CacheEntry>> it = mCache.entrySet().iterator(); while (it.hasNext()) { final CacheEntry e = it.next().getValue(); if ((e.icon != null) && (e.icon.getWidth() < grid.iconSizePx @@ -225,18 +359,52 @@ public class IconCache { } /** - * Fill in "application" with the icon and label for "info." + * Fetches high-res icon for the provided ItemInfo and updates the caller when done. + * @return a request ID that can be used to cancel the request. */ - public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info, - HashMap<Object, CharSequence> labelCache) { - CacheEntry entry = cacheLocked(application.componentName, info, labelCache, - info.getUser(), false); + public IconLoadRequest updateIconInBackground(final BubbleTextView caller, final ItemInfo info) { + Runnable request = new Runnable() { + + @Override + public void run() { + if (info instanceof AppInfo) { + getTitleAndIcon((AppInfo) info, null, false); + } else if (info instanceof ShortcutInfo) { + ShortcutInfo st = (ShortcutInfo) info; + getTitleAndIcon(st, + st.promisedIntent != null ? st.promisedIntent : st.intent, + st.user, false); + } + mMainThreadExecutor.execute(new Runnable() { + + @Override + public void run() { + caller.reapplyItemInfo(info); + } + }); + } + }; + mWorkerHandler.post(request); + return new IconLoadRequest(request, mWorkerHandler); + } + /** + * Fill in "application" with the icon and label for "info." + */ + public synchronized void getTitleAndIcon(AppInfo application, + LauncherActivityInfoCompat info, boolean useLowResIcon) { + CacheEntry entry = cacheLocked(application.componentName, info, + info == null ? application.user : info.getUser(), + false, useLowResIcon); application.title = entry.title; application.iconBitmap = entry.icon; application.contentDescription = entry.contentDescription; + application.usingLowResIcon = entry.isLowResIcon; } + /** + * Returns a high res icon for the given intent and user + */ public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) { ComponentName component = intent.getComponent(); // null info means not installed, but if we have a component from the intent then @@ -246,15 +414,16 @@ public class IconCache { } LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user); - CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, true); + CacheEntry entry = cacheLocked(component, launcherActInfo, user, true, true); return entry.icon; } /** - * Fill in "shortcutInfo" with the icon and label for "info." + * Fill in {@param shortcutInfo} with the icon and label for {@param intent}. If the + * corresponding activity is not found, it reverts to the package icon. */ public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent, - UserHandleCompat user, boolean usePkgIcon) { + UserHandleCompat user, boolean useLowResIcon) { 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. @@ -262,17 +431,38 @@ public class IconCache { shortcutInfo.setIcon(getDefaultIcon(user)); shortcutInfo.title = ""; shortcutInfo.usingFallbackIcon = true; + shortcutInfo.usingLowResIcon = false; } 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); + LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user); + getTitleAndIcon(shortcutInfo, component, info, user, true, useLowResIcon); } } + /** + * Fill in {@param shortcutInfo} with the icon and label for {@param info} + */ + public synchronized void getTitleAndIcon( + ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info, + UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) { + CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon); + shortcutInfo.setIcon(entry.icon); + shortcutInfo.title = entry.title; + shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + shortcutInfo.usingLowResIcon = entry.isLowResIcon; + } + + /** + * Fill in {@param appInfo} with the icon and label for {@param packageName} + */ + public synchronized void getTitleAndIconForApp( + String packageName, UserHandleCompat user, boolean useLowResIcon, + PackageItemInfo infoOut) { + CacheEntry entry = getEntryForPackageLocked(packageName, user, useLowResIcon); + infoOut.iconBitmap = entry.icon; + infoOut.title = entry.title; + infoOut.usingLowResIcon = entry.isLowResIcon; + infoOut.contentDescription = entry.contentDescription; + } public synchronized Bitmap getDefaultIcon(UserHandleCompat user) { if (!mDefaultIcons.containsKey(user)) { @@ -281,16 +471,6 @@ public class IconCache { return mDefaultIcons.get(user); } - public synchronized Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info, - HashMap<Object, CharSequence> labelCache) { - if (info == null || component == null) { - return null; - } - - CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser(), false); - return entry.icon; - } - public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) { return mDefaultIcons.get(user) == icon; } @@ -300,44 +480,27 @@ public class IconCache { * This method is not thread safe, it must be called from a synchronized method. */ private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info, - HashMap<Object, CharSequence> labelCache, UserHandleCompat user, boolean usePackageIcon) { - CacheKey cacheKey = new CacheKey(componentName, user); + UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) { + ComponentKey cacheKey = new ComponentKey(componentName, user); CacheEntry entry = mCache.get(cacheKey); - if (entry == null) { + if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { entry = new CacheEntry(); - mCache.put(cacheKey, entry); - if (info != null) { - ComponentName labelKey = info.getComponentName(); - if (labelCache != null && labelCache.containsKey(labelKey)) { - entry.title = labelCache.get(labelKey).toString(); - } else { - entry.title = info.getLabel().toString(); - if (labelCache != null) { - labelCache.put(labelKey, entry.title); - } - } - - entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); - entry.icon = Utilities.createIconBitmap( - info.getBadgedIcon(mIconDpi), mContext); - } else { - entry.title = ""; - Bitmap preloaded = getPreloadedIcon(componentName, user); - if (preloaded != null) { - if (DEBUG) Log.d(TAG, "using preloaded icon for " + - componentName.toShortString()); - entry.icon = preloaded; + // Check the DB first. + if (!getEntryFromDB(componentName, user, entry, useLowResIcon)) { + if (info != null) { + entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext); } else { if (usePackageIcon) { - CacheEntry packageEntry = getEntryForPackage( - componentName.getPackageName(), user); + CacheEntry packageEntry = getEntryForPackageLocked( + componentName.getPackageName(), user, false); if (packageEntry != null) { if (DEBUG) Log.d(TAG, "using package default icon for " + componentName.toShortString()); entry.icon = packageEntry.icon; entry.title = packageEntry.title; + entry.contentDescription = packageEntry.contentDescription; } } if (entry.icon == null) { @@ -347,6 +510,11 @@ public class IconCache { } } } + + if (TextUtils.isEmpty(entry.title) && info != null) { + entry.title = info.getLabel().toString(); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); + } } return entry; } @@ -357,9 +525,9 @@ public class IconCache { */ public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user, Bitmap icon, CharSequence title) { - remove(packageName, user); + removeFromMemCacheLocked(packageName, user); - CacheEntry entry = getEntryForPackage(packageName, user); + CacheEntry entry = getEntryForPackageLocked(packageName, user, false); if (!TextUtils.isEmpty(title)) { entry.title = title; } @@ -371,56 +539,61 @@ public class IconCache { /** * Gets an entry for the package, which can be used as a fallback entry for various components. * This method is not thread safe, it must be called from a synchronized method. + * */ - private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) { - ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);; - CacheKey cacheKey = new CacheKey(cn, user); + private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user, + boolean useLowResIcon) { + ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME); + ComponentKey cacheKey = new ComponentKey(cn, user); CacheEntry entry = mCache.get(cacheKey); - if (entry == null) { + if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { 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); + // Check the DB first. + if (!getEntryFromDB(cn, user, entry, useLowResIcon)) { + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, 0); + ApplicationInfo appInfo = info.applicationInfo; + if (appInfo == null) { + throw new NameNotFoundException("ApplicationInfo is null"); + } + Drawable drawable = mUserManager.getBadgedDrawableForUser( + appInfo.loadIcon(mPackageManager), user); + entry.icon = Utilities.createIconBitmap(drawable, mContext); + entry.title = appInfo.loadLabel(mPackageManager); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); + entry.isLowResIcon = false; + + // Add the icon in the DB here, since these do not get written during + // package updates. + ContentValues values = + mIconDb.newContentValues(entry.icon, entry.title.toString()); + addIconToDB(values, cn, info, mUserManager.getSerialNumberForUser(user)); + + } catch (NameNotFoundException e) { + if (DEBUG) Log.d(TAG, "Application not installed " + packageName); + } } } return entry; } - public synchronized HashMap<ComponentName,Bitmap> getAllIcons() { - HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>(); - for (CacheKey ck : mCache.keySet()) { - final CacheEntry e = mCache.get(ck); - set.put(ck.componentName, e.icon); - } - return set; - } - /** * Pre-load an icon into the persistent cache. * * <P>Queries for a component that does not exist in the package manager * will be answered by the persistent cache. * - * @param context application context * @param componentName the icon should be returned for this component * @param icon the icon to be persisted * @param dpi the native density of the icon */ - public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon, - int dpi) { + public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label, + long userSerial) { // TODO rescale to the correct native DPI try { - PackageManager packageManager = context.getPackageManager(); + PackageManager packageManager = mContext.getPackageManager(); packageManager.getActivityIcon(componentName); // component is present on the system already, do nothing return; @@ -428,100 +601,135 @@ public class IconCache { // pass } - final String key = componentName.flattenToString(); - FileOutputStream resourceFile = null; + ContentValues values = mIconDb.newContentValues(icon, label); + values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString()); + values.put(IconDB.COLUMN_USER, userSerial); + mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + + private boolean getEntryFromDB(ComponentName component, UserHandleCompat user, + CacheEntry entry, boolean lowRes) { + Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, + new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON, + IconDB.COLUMN_LABEL}, + IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {component.flattenToString(), + Long.toString(mUserManager.getSerialNumberForUser(user))}, + null, null, null); try { - resourceFile = context.openFileOutput(getResourceFilename(componentName), - Context.MODE_PRIVATE); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) { - byte[] buffer = os.toByteArray(); - resourceFile.write(buffer, 0, buffer.length); - } else { - Log.w(TAG, "failed to encode cache for " + key); - return; - } - } catch (FileNotFoundException e) { - Log.w(TAG, "failed to pre-load cache for " + key, e); - } catch (IOException e) { - Log.w(TAG, "failed to pre-load cache for " + key, e); - } finally { - if (resourceFile != null) { - try { - resourceFile.close(); - } catch (IOException e) { - Log.d(TAG, "failed to save restored icon for: " + key, e); + if (c.moveToNext()) { + entry.icon = loadIconNoResize(c, 0); + entry.isLowResIcon = lowRes; + entry.title = c.getString(1); + if (entry.title == null) { + entry.title = ""; + entry.contentDescription = ""; + } else { + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); } + return true; } + } finally { + c.close(); } + return false; } - /** - * Read a pre-loaded icon from the persistent icon cache. - * - * @param componentName the component that should own the icon - * @returns a bitmap if one is cached, or null. - */ - private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) { - final String key = componentName.flattenToShortString(); + public static class IconLoadRequest { + private final Runnable mRunnable; + private final Handler mHandler; - // We don't keep icons for other profiles in persistent cache. - if (!user.equals(UserHandleCompat.myUserHandle())) { - return null; + IconLoadRequest(Runnable runnable, Handler handler) { + mRunnable = runnable; + mHandler = handler; } - if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key); - Bitmap icon = null; - FileInputStream resourceFile = null; - try { - resourceFile = mContext.openFileInput(getResourceFilename(componentName)); - byte[] buffer = new byte[1024]; - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int bytesRead = 0; - while(bytesRead >= 0) { - bytes.write(buffer, 0, bytesRead); - bytesRead = resourceFile.read(buffer, 0, buffer.length); - } - if (DEBUG) Log.d(TAG, "read " + bytes.size()); - icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size()); - if (icon == null) { - Log.w(TAG, "failed to decode pre-load icon for " + key); + public void cancel() { + mHandler.removeCallbacks(mRunnable); + } + } + + private static final class IconDB extends SQLiteOpenHelper { + private final static int DB_VERSION = 3; + + private final static String TABLE_NAME = "icons"; + private final static String COLUMN_ROWID = "rowid"; + private final static String COLUMN_COMPONENT = "componentName"; + private final static String COLUMN_USER = "profileId"; + private final static String COLUMN_LAST_UPDATED = "lastUpdated"; + private final static String COLUMN_VERSION = "version"; + private final static String COLUMN_ICON = "icon"; + private final static String COLUMN_ICON_LOW_RES = "icon_low_res"; + private final static String COLUMN_LABEL = "label"; + private final static String COLUMN_SYSTEM_STATE = "system_state"; + + public String mSystemState; + + public IconDB(Context context) { + super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION); + updateSystemStateString(context); + } + + public void updateSystemStateString(Context c) { + mSystemState = Locale.getDefault().toString() + "," + + c.getResources().getConfiguration().mcc; + } + + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + + COLUMN_COMPONENT + " TEXT NOT NULL, " + + COLUMN_USER + " INTEGER NOT NULL, " + + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_ICON + " BLOB, " + + COLUMN_ICON_LOW_RES + " BLOB, " + + COLUMN_LABEL + " TEXT, " + + COLUMN_SYSTEM_STATE + " TEXT, " + + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); } - } catch (FileNotFoundException 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 { - if(resourceFile != null) { - try { - resourceFile.close(); - } catch (IOException e) { - Log.d(TAG, "failed to manage pre-load icon file: " + key, e); - } + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); } } - return icon; - } + private void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } - /** - * Remove a pre-loaded icon from the persistent icon cache. - * - * @param componentName the component that should own the icon - */ - public void deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) { - // We don't keep icons for other profiles in persistent cache. - if (!user.equals(UserHandleCompat.myUserHandle()) || componentName == null) { - return; + public ContentValues newContentValues(Bitmap icon, String label) { + ContentValues values = new ContentValues(); + values.put(COLUMN_ICON, Utilities.flattenBitmap(icon)); + values.put(COLUMN_ICON_LOW_RES, Utilities.flattenBitmap( + Bitmap.createScaledBitmap(icon, + icon.getWidth() / LOW_RES_SCALE_FACTOR, + icon.getHeight() / LOW_RES_SCALE_FACTOR, true))); + values.put(COLUMN_LABEL, label); + values.put(COLUMN_SYSTEM_STATE, mSystemState); + return values; } - remove(componentName, user); - boolean success = mContext.deleteFile(getResourceFilename(componentName)); - if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache"); } - private static String getResourceFilename(ComponentName component) { - String resourceName = component.flattenToShortString(); - String filename = resourceName.replace(File.separatorChar, '_'); - return RESOURCE_FILE_PREFIX + filename; + private static Bitmap loadIconNoResize(Cursor c, int iconIndex) { + byte[] data = c.getBlob(iconIndex); + try { + return BitmapFactory.decodeByteArray(data, 0, data.length); + } catch (Exception e) { + return null; + } } } diff --git a/src/com/android/launcher3/InfoDropTarget.java b/src/com/android/launcher3/InfoDropTarget.java index 3c36361aa..e48640c93 100644 --- a/src/com/android/launcher3/InfoDropTarget.java +++ b/src/com/android/launcher3/InfoDropTarget.java @@ -18,21 +18,14 @@ package com.android.launcher3; import android.content.ComponentName; import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.drawable.TransitionDrawable; +import android.provider.Settings; import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; +import com.android.launcher3.R; import com.android.launcher3.compat.UserHandleCompat; public class InfoDropTarget extends ButtonDropTarget { - private ColorStateList mOriginalTextColor; - private TransitionDrawable mDrawable; - public InfoDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -44,43 +37,10 @@ public class InfoDropTarget extends ButtonDropTarget { @Override protected void onFinishInflate() { super.onFinishInflate(); - - mOriginalTextColor = getTextColors(); - // Get the hover color - Resources r = getResources(); - mHoverColor = r.getColor(R.color.info_target_hover_tint); - mDrawable = (TransitionDrawable) getCurrentDrawable(); - - if (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); - } - - // Remove the text in the Phone UI in landscape - int orientation = getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - if (!LauncherAppState.getInstance().isScreenLarge()) { - setText(""); - } - } - } - - @Override - public boolean acceptDrop(DragObject d) { - // acceptDrop is called just before onDrop. We do the work here, rather than - // in onDrop, because it allows us to reject the drop (by returning false) - // so that the object being dragged isn't removed from the drag source. + mHoverColor = getResources().getColor(R.color.info_target_hover_tint); - startDetailsActivityForInfo(d.dragInfo, mLauncher); - // There is no post-drop animation, so clean up the DragView now - d.deferDragViewCleanupPostAnimation = false; - return false; + setDrawable(R.drawable.info_target_selector); } public static void startDetailsActivityForInfo(Object info, Launcher launcher) { @@ -105,39 +65,14 @@ public class InfoDropTarget extends ButtonDropTarget { } @Override - public void onDragStart(DragSource source, Object info, int dragAction) { - boolean isVisible = true; - - // Hide this button unless we are dragging something from AllApps - if (!source.supportsAppInfoDropTarget()) { - isVisible = false; - } - - mActive = isVisible; - mDrawable.resetTransition(); - setTextColor(mOriginalTextColor); - ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); + protected boolean supportsDrop(DragSource source, Object info) { + return source.supportsAppInfoDropTarget() && + Settings.Global.getInt(getContext().getContentResolver(), + Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) == 1; } @Override - public void onDragEnd() { - super.onDragEnd(); - mActive = false; - } - - public void onDragEnter(DragObject d) { - super.onDragEnter(d); - - mDrawable.startTransition(mTransitionDuration); - setTextColor(mHoverColor); - } - - public void onDragExit(DragObject d) { - super.onDragExit(d); - - if (!d.dragComplete) { - mDrawable.resetTransition(); - setTextColor(mOriginalTextColor); - } + void completeDrop(DragObject d) { + startDetailsActivityForInfo(d.dragInfo, mLauncher); } } diff --git a/src/com/android/launcher3/Insettable.java b/src/com/android/launcher3/Insettable.java index 1d2356c65..3b8ef2f93 100644 --- a/src/com/android/launcher3/Insettable.java +++ b/src/com/android/launcher3/Insettable.java @@ -18,6 +18,10 @@ package com.android.launcher3; import android.graphics.Rect; +/** + * Allows the implementing {@link View} to not draw underneath system bars. + * e.g., notification bar on top and home key area on the bottom. + */ public interface Insettable { void setInsets(Rect insets); diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index 1ab308558..0c69154aa 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.text.TextUtils; @@ -32,6 +33,7 @@ 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 com.android.launcher3.util.Thunk; import org.json.JSONException; import org.json.JSONObject; @@ -146,6 +148,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { if (DBG) Log.d(TAG, "Got INSTALL_SHORTCUT: " + data.toUri(0)); PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); + info = convertToLauncherActivityIfPossible(info); queuePendingShortcutInfo(info, context); } @@ -186,11 +189,6 @@ public class InstallShortcutReceiver extends BroadcastReceiver { final PendingInstallShortcutInfo pendingInfo = iter.next(); final Intent intent = pendingInfo.launchIntent; - if (LauncherAppState.isDisableAllApps() && !isValidShortcutLaunchIntent(intent)) { - if (DBG) Log.d(TAG, "Ignoring shortcut with launchIntent:" + intent); - continue; - } - // If the intent specifies a package, make sure the package exists String packageName = pendingInfo.getTargetPackage(); if (!TextUtils.isEmpty(packageName)) { @@ -212,7 +210,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { // Add the new apps to the model and bind them if (!addShortcuts.isEmpty()) { LauncherAppState app = LauncherAppState.getInstance(); - app.getModel().addAndBindAddedWorkspaceApps(context, addShortcuts); + app.getModel().addAndBindAddedWorkspaceItems(context, addShortcuts); } } } @@ -245,7 +243,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { * Ensures that we have a valid, non-null name. If the provided name is null, we will return * the application name instead. */ - private static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) { + @Thunk static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) { if (name == null) { try { PackageManager pm = context.getPackageManager(); @@ -335,7 +333,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0)) .key(NAME_KEY).value(name); if (icon != null) { - byte[] iconByteArray = ItemInfo.flattenBitmap(icon); + byte[] iconByteArray = Utilities.flattenBitmap(icon); json = json.key(ICON_KEY).value( Base64.encodeToString( iconByteArray, 0, iconByteArray.length, Base64.DEFAULT)); @@ -354,16 +352,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { public ShortcutInfo getShortcutInfo() { if (activityInfo != null) { - final ShortcutInfo info = new ShortcutInfo(); - info.user = user; - info.title = label; - info.contentDescription = label; - info.customIcon = false; - info.intent = launchIntent; - info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; - info.flags = AppInfo.initFlags(activityInfo); - info.firstInstallTime = activityInfo.getFirstInstallTime(); - return info; + return ShortcutInfo.fromActivityInfo(activityInfo, mContext); } else { return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data); } @@ -377,6 +366,10 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } return packageName; } + + public boolean isLuncherActivity() { + return activityInfo != null; + } } private static PendingInstallShortcutInfo decode(String encoded, Context context) { @@ -424,4 +417,40 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } return null; } + + /** + * Tries to create a new PendingInstallShortcutInfo which represents the same target, + * but is an app target and not a shortcut. + * @return the newly created info or the original one. + */ + private static PendingInstallShortcutInfo convertToLauncherActivityIfPossible( + PendingInstallShortcutInfo original) { + if (original.isLuncherActivity()) { + // Already an activity target + return original; + } + if (isValidShortcutLaunchIntent(original.launchIntent) + || !original.user.equals(UserHandleCompat.myUserHandle())) { + // We can only convert shortcuts which point to a main activity in the current user. + return original; + } + + PackageManager pm = original.mContext.getPackageManager(); + ResolveInfo info = pm.resolveActivity(original.launchIntent, 0); + + if (info == null) { + return original; + } + + // Ignore any conflicts in the label name, as that can change based on locale. + LauncherActivityInfoCompat launcherInfo = LauncherActivityInfoCompat + .fromResolveInfo(info, original.mContext); + return new PendingInstallShortcutInfo(launcherInfo, original.mContext); + } + + public static boolean isLauncherActivity(Intent intent, Context context) { + Intent data = new Intent().putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent); + PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); + return convertToLauncherActivityIfPossible(info).isLuncherActivity(); + } } diff --git a/src/com/android/launcher3/InterruptibleInOutAnimator.java b/src/com/android/launcher3/InterruptibleInOutAnimator.java index 2898b347d..29df38bae 100644 --- a/src/com/android/launcher3/InterruptibleInOutAnimator.java +++ b/src/com/android/launcher3/InterruptibleInOutAnimator.java @@ -21,6 +21,8 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.view.View; +import com.android.launcher3.util.Thunk; + /** * A convenience class for two-way animations, e.g. a fadeIn/fadeOut animation. * With a regular ValueAnimator, if you call reverse to show the 'out' animation, you'll get @@ -43,7 +45,7 @@ public class InterruptibleInOutAnimator { private static final int OUT = 2; // TODO: This isn't really necessary, but is here to help diagnose a bug in the drag viz - private int mDirection = STOPPED; + @Thunk int mDirection = STOPPED; public InterruptibleInOutAnimator(View view, long duration, float fromValue, float toValue) { mAnimator = LauncherAnimUtils.ofFloat(view, fromValue, toValue).setDuration(duration); diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java index aff8323c2..f7e0ea488 100644 --- a/src/com/android/launcher3/ItemInfo.java +++ b/src/com/android/launcher3/ItemInfo.java @@ -20,13 +20,10 @@ 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; /** @@ -39,7 +36,7 @@ public class ItemInfo { */ static final String EXTRA_PROFILE = "profile"; - static final int NO_ID = -1; + public static final int NO_ID = -1; /** * The id in the settings database for this item @@ -85,7 +82,7 @@ public class ItemInfo { /** * Indicates the Y cell span. */ - int spanY = 1; + public int spanY = 1; /** * Indicates the minimum X cell span. @@ -110,21 +107,21 @@ public class ItemInfo { /** * Title of the item */ - CharSequence title; + public CharSequence title; /** * Content description of the item. */ - CharSequence contentDescription; + public CharSequence contentDescription; /** * The position of the item in a drag-and-drop operation. */ - int[] dropPos = null; + public int[] dropPos = null; - UserHandleCompat user; + public UserHandleCompat user; - ItemInfo() { + public ItemInfo() { user = UserHandleCompat.myUserHandle(); } @@ -177,25 +174,9 @@ public class ItemInfo { } } - static byte[] flattenBitmap(Bitmap bitmap) { - // Try go guesstimate how much space the icon will take when serialized - // to avoid unnecessary allocations/copies during the write. - int size = bitmap.getWidth() * bitmap.getHeight() * 4; - ByteArrayOutputStream out = new ByteArrayOutputStream(size); - try { - bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); - out.flush(); - out.close(); - return out.toByteArray(); - } catch (IOException e) { - Log.w("Favorite", "Could not write icon"); - return null; - } - } - static void writeBitmap(ContentValues values, Bitmap bitmap) { if (bitmap != null) { - byte[] data = flattenBitmap(bitmap); + byte[] data = Utilities.flattenBitmap(bitmap); values.put(LauncherSettings.Favorites.ICON, data); } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 5b8eaef50..2bfb29ef6 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -1,4 +1,3 @@ - /* * Copyright (C) 2008 The Android Open Source Project * @@ -22,7 +21,6 @@ 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.annotation.TargetApi; import android.app.Activity; @@ -49,7 +47,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; -import android.content.res.Resources; import android.database.ContentObserver; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; @@ -83,19 +80,17 @@ 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.ViewStub; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.Advanceable; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.TextView; import android.widget.Toast; import com.android.launcher3.DropTarget.DragObject; @@ -107,6 +102,9 @@ 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 com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.PendingAddWidgetInfo; +import com.android.launcher3.widget.WidgetsContainerView; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -116,7 +114,6 @@ 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; @@ -133,12 +130,13 @@ import java.util.concurrent.atomic.AtomicInteger; */ public class Launcher extends Activity implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, - View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener { + View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener, + LauncherStateTransitionAnimation.Callbacks { static final String TAG = "Launcher"; - static final boolean LOGD = false; + static final boolean LOGD = true; static final boolean PROFILE_STARTUP = false; - static final boolean DEBUG_WIDGETS = false; + static final boolean DEBUG_WIDGETS = true; static final boolean DEBUG_STRICT_MODE = false; static final boolean DEBUG_RESUME_TIME = false; static final boolean DEBUG_DUMP_LOG = false; @@ -147,7 +145,6 @@ 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_SHORTCUT = 7; private static final int REQUEST_PICK_APPWIDGET = 9; private static final int REQUEST_PICK_WALLPAPER = 10; @@ -167,7 +164,6 @@ public class Launcher extends Activity // To turn on these properties, type // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] static final String DUMP_STATE_PROPERTY = "launcher_dump_state"; - static final String DISABLE_ALL_APPS_PROPERTY = "launcher_noallapps"; // The Intent extra that defines whether to ignore the launch animation static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = @@ -216,9 +212,10 @@ public class Launcher extends Activity public static final String USER_HAS_MIGRATED = "launcher.user_migrated_from_old_data"; /** The different states that Launcher can be in. */ - private enum State { NONE, WORKSPACE, APPS_CUSTOMIZE, APPS_CUSTOMIZE_SPRING_LOADED }; - private State mState = State.WORKSPACE; - private AnimatorSet mStateAnimation; + enum State { NONE, WORKSPACE, APPS, APPS_SPRING_LOADED, WIDGETS, WIDGETS_SPRING_LOADED }; + @Thunk State mState = State.WORKSPACE; + @Thunk AnimatorSet mStateAnimation; + @Thunk LauncherStateTransitionAnimation mStateTransitionAnimation; private boolean mIsSafeModeEnabled; @@ -231,16 +228,13 @@ public class Launcher extends Activity 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 HashMap<Integer, Integer> mItemIdToViewId = new HashMap<Integer, Integer>(); private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); // How long to wait before the new-shortcut animation automatically pans the workspace 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; + @Thunk static int NEW_APPS_ANIMATION_DELAY = 500; private final BroadcastReceiver mCloseSystemDialogsReceiver = new CloseSystemDialogsIntentReceiver(); @@ -248,17 +242,17 @@ public class Launcher extends Activity private LayoutInflater mInflater; - private Workspace mWorkspace; + @Thunk Workspace mWorkspace; private View mLauncherView; private View mPageIndicators; - private DragLayer mDragLayer; + @Thunk DragLayer mDragLayer; private DragController mDragController; private View mWeightWatcher; private AppWidgetManagerCompat mAppWidgetManager; private LauncherAppWidgetHost mAppWidgetHost; - private ItemInfo mPendingAddInfo = new ItemInfo(); + @Thunk ItemInfo mPendingAddInfo = new ItemInfo(); private LauncherAppWidgetProviderInfo mPendingAddWidgetInfo; private int mPendingAddWidgetId = -1; @@ -272,8 +266,13 @@ public class Launcher extends Activity private View mAllAppsButton; private SearchDropTargetBar mSearchDropTargetBar; - private AppsCustomizeTabHost mAppsCustomizeTabHost; - private AppsCustomizePagedView mAppsCustomizeContent; + + // Main container view for the all apps screen. + @Thunk AppsContainerView mAppsView; + + // Main container view for the widget tray screen. + private WidgetsContainerView mWidgetsView; + private boolean mAutoAdvanceRunning = false; private AppWidgetHostView mQsb; @@ -285,7 +284,7 @@ public class Launcher extends Activity private SpannableStringBuilder mDefaultKeySsb = null; - private boolean mWorkspaceLoading = true; + @Thunk boolean mWorkspaceLoading = true; private boolean mPaused = true; private boolean mRestoring; @@ -299,34 +298,31 @@ public class Launcher extends Activity private LauncherModel mModel; private IconCache mIconCache; - private boolean mUserPresent = true; + @Thunk boolean mUserPresent = true; private boolean mVisible = false; private boolean mHasFocus = false; private boolean mAttached = false; - private static LocaleConfiguration sLocaleConfiguration = null; + @Thunk static LocaleConfiguration sLocaleConfiguration = null; private static HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>(); private View.OnTouchListener mHapticFeedbackTouchListener; - public static final int BUILD_LAYER = 0; - public static final int BUILD_AND_SET_LAYER = 1; - // Related to the auto-advancing of widgets private final int ADVANCE_MSG = 1; private final int mAdvanceInterval = 20000; private final int mAdvanceStagger = 250; private long mAutoAdvanceSentTime; private long mAutoAdvanceTimeLeft = -1; - private HashMap<View, AppWidgetProviderInfo> mWidgetsToAdvance = + @Thunk HashMap<View, AppWidgetProviderInfo> mWidgetsToAdvance = new HashMap<View, AppWidgetProviderInfo>(); // Determines how long to wait after a rotation before restoring the screen orientation to // match the sensor state. private final int mRestoreScreenOrientationDelay = 500; - private Drawable mWorkspaceBackgroundDrawable; + @Thunk Drawable mWorkspaceBackgroundDrawable; private final ArrayList<Integer> mSynchronouslyBoundPages = new ArrayList<Integer>(); private static final boolean DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE = false; @@ -342,11 +338,9 @@ public class Launcher extends Activity // it from the context. private SharedPreferences mSharedPrefs; - private static ArrayList<ComponentName> mIntentsOnWorkspaceFromUpgradePath = null; - // Holds the page that we need to animate to, and the icon views that we need to animate up // when we scroll to that page on resume. - private ImageView mFolderIconImageView; + @Thunk ImageView mFolderIconImageView; private Bitmap mFolderIconBitmap; private Canvas mFolderIconCanvas; private Rect mRectForFolderAnimation = new Rect(); @@ -363,7 +357,19 @@ public class Launcher extends Activity } } - private Runnable mBuildLayersRunnable = new Runnable() { + // TODO: remove this field and call method directly when Launcher3 can depend on M APIs + private static Method sClipRevealMethod = null; + static { + Class<?> activityOptionsClass = ActivityOptions.class; + try { + sClipRevealMethod = activityOptionsClass.getDeclaredMethod("makeClipRevealAnimation", + View.class, int.class, int.class, int.class, int.class); + } catch (Exception e) { + // Earlier version + } + } + + @Thunk Runnable mBuildLayersRunnable = new Runnable() { public void run() { if (mWorkspace != null) { mWorkspace.buildPageHardwareLayers(); @@ -373,7 +379,7 @@ public class Launcher extends Activity private static PendingAddArguments sPendingAddItem; - private static class PendingAddArguments { + @Thunk static class PendingAddArguments { int requestCode; Intent intent; long container; @@ -426,6 +432,7 @@ public class Launcher extends Activity mIconCache.flushInvalidIcons(grid); mDragController = new DragController(this); mInflater = getLayoutInflater(); + mStateTransitionAnimation = new LauncherStateTransitionAnimation(this, this); mStats = new Stats(this); @@ -513,6 +520,17 @@ public class Launcher extends Activity public boolean setLauncherCallbacks(LauncherCallbacks callbacks) { mLauncherCallbacks = callbacks; + mLauncherCallbacks.setLauncherAppsCallback(new Launcher.LauncherAppsCallbacks() { + @Override + public void onAllAppsBoundsChanged(Rect bounds) { + mAppsView.setFixedBounds(Launcher.this, bounds); + } + + @Override + public void dismissAllApps() { + showWorkspace(true); + } + }); return true; } @@ -561,7 +579,7 @@ public class Launcher extends Activity } } - private void checkForLocaleChange() { + @Thunk void checkForLocaleChange() { if (sLocaleConfiguration == null) { new AsyncTask<Void, Void, LocaleConfiguration>() { @Override @@ -610,13 +628,13 @@ public class Launcher extends Activity } } - private static class LocaleConfiguration { + @Thunk static class LocaleConfiguration { public String locale; public int mcc = -1; public int mnc = -1; } - private static void readConfiguration(Context context, LocaleConfiguration configuration) { + @Thunk static void readConfiguration(Context context, LocaleConfiguration configuration) { DataInputStream in = null; try { in = new DataInputStream(context.openFileInput(LauncherFiles.LAUNCHER_PREFERENCES)); @@ -638,7 +656,7 @@ public class Launcher extends Activity } } - private static void writeConfiguration(Context context, LocaleConfiguration configuration) { + @Thunk static void writeConfiguration(Context context, LocaleConfiguration configuration) { DataOutputStream out = null; try { out = new DataOutputStream(context.openFileOutput( @@ -671,7 +689,7 @@ public class Launcher extends Activity return mInflater; } - boolean isDraggingEnabled() { + public boolean isDraggingEnabled() { // We prevent dragging when we are loading the workspace as it is possible to pick up a view // that is subsequently removed from the workspace in startBinding(). return !mModel.isLoadingWorkspace(); @@ -915,7 +933,7 @@ public class Launcher extends Activity } } - private void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { + @Thunk void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { CellLayout cellLayout = (CellLayout) mWorkspace.getScreenWithId(mPendingAddInfo.screenId); Runnable onCompleteRunnable = null; @@ -987,8 +1005,10 @@ public class Launcher extends Activity // Restore the previous launcher state if (mOnResumeState == State.WORKSPACE) { showWorkspace(false); - } else if (mOnResumeState == State.APPS_CUSTOMIZE) { - showAllApps(false, mAppsCustomizeContent.getContentType(), false); + } else if (mOnResumeState == State.APPS) { + showAppsView(false /* animated */, false /* resetListToTop */); + } else if (mOnResumeState == State.WIDGETS) { + showWidgetsView(false, false); } mOnResumeState = State.NONE; @@ -1010,15 +1030,9 @@ public class Launcher extends Activity startTimeCallbacks = System.currentTimeMillis(); } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.setBulkBind(true); - } for (int i = 0; i < mBindOnResumeCallbacks.size(); i++) { mBindOnResumeCallbacks.get(i).run(); } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.setBulkBind(false); - } mBindOnResumeCallbacks.clear(); if (DEBUG_RESUME_TIME) { Log.d(TAG, "Time spent processing callbacks in onResume: " + @@ -1044,6 +1058,7 @@ public class Launcher extends Activity // (framework issue). On resuming, we ensure that any widgets are inflated for the current // orientation. getWorkspace().reinflateWidgetsIfNecessary(); + reinflateQSBIfNecessary(); // Process any items that were added while Launcher was away. InstallShortcutReceiver.disableAndFlushInstallQueue(this); @@ -1137,6 +1152,19 @@ public class Launcher extends Activity public void forceExitFullImmersion(); } + public interface LauncherAppsCallbacks { + /** + * Updates launcher to the available space that AllApps can take so as not to overlap with + * any other views. + */ + public void onAllAppsBoundsChanged(Rect bounds); + + /** + * Called to dismiss all apps if it is showing. + */ + public void dismissAllApps(); + } + public interface LauncherOverlayCallbacks { /** * This method indicates whether a call to {@link #enterFullImmersion()} will succeed, @@ -1209,9 +1237,8 @@ public class Launcher extends Activity if (mModel.isCurrentCallbacks(this)) { mModel.stopLoader(); } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.surrender(); - } + //TODO(hyunyoungs): stop the widgets loader when there is a rotation. + return Boolean.TRUE; } @@ -1297,8 +1324,8 @@ public class Launcher extends Activity } State state = intToState(savedState.getInt(RUNTIME_STATE, State.WORKSPACE.ordinal())); - if (state == State.APPS_CUSTOMIZE) { - mOnResumeState = State.APPS_CUSTOMIZE; + if (state == State.APPS || state == State.WIDGETS) { + mOnResumeState = state; } int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN, @@ -1332,19 +1359,6 @@ public class Launcher extends Activity mRestoring = true; } - // Restore the AppsCustomize tab - if (mAppsCustomizeTabHost != null) { - String curTab = savedState.getString("apps_customize_currentTab"); - if (curTab != null) { - mAppsCustomizeTabHost.setContentTypeImmediate( - mAppsCustomizeTabHost.getContentTypeForTabTag(curTab)); - mAppsCustomizeContent.loadAssociatedPages( - mAppsCustomizeContent.getCurrentPage()); - } - - int currentIndex = savedState.getInt("apps_customize_currentIndex"); - mAppsCustomizeContent.restorePageForIndex(currentIndex); - } mItemIdToViewId = (HashMap<Integer, Integer>) savedState.getSerializable(RUNTIME_STATE_VIEW_IDS); } @@ -1426,11 +1440,14 @@ public class Launcher extends Activity mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.search_drop_target_bar); + // Setup Apps + mAppsView = (AppsContainerView) findViewById(R.id.apps_view); + if (mLauncherCallbacks != null && mLauncherCallbacks.overrideAllAppsSearch()) { + mAppsView.hideSearchBar(); + } + // Setup AppsCustomize - mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane); - mAppsCustomizeContent = (AppsCustomizePagedView) - mAppsCustomizeTabHost.findViewById(R.id.apps_customize_pane_content); - mAppsCustomizeContent.setup(this, dragController); + mWidgetsView = (WidgetsContainerView) findViewById(R.id.widgets_view); // Setup the drag controller (drop targets have to be added in reverse order in priority) dragController.setDragScoller(mWorkspace); @@ -1439,7 +1456,7 @@ public class Launcher extends Activity dragController.addDropTarget(mWorkspace); if (mSearchDropTargetBar != null) { mSearchDropTargetBar.setup(this, dragController); - mSearchDropTargetBar.setQsbSearchBar(getQsbBar()); + mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); } if (getResources().getBoolean(R.bool.debug_memory_enabled)) { @@ -1548,7 +1565,7 @@ public class Launcher extends Activity return; } - LauncherModel.addItemToDatabase(this, info, container, screenId, cellXY[0], cellXY[1], false); + LauncherModel.addItemToDatabase(this, info, container, screenId, cellXY[0], cellXY[1]); if (!mRestoring) { mWorkspace.addInScreen(view, container, screenId, cellXY[0], cellXY[1], 1, 1, @@ -1588,7 +1605,7 @@ public class Launcher extends Activity * * @param appWidgetId The app widget id */ - private void completeAddAppWidget(int appWidgetId, long container, long screenId, + @Thunk void completeAddAppWidget(int appWidgetId, long container, long screenId, AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) { ItemInfo info = mPendingAddInfo; @@ -1610,7 +1627,7 @@ public class Launcher extends Activity launcherInfo.user = mAppWidgetManager.getUser(appWidgetInfo); LauncherModel.addItemToDatabase(this, launcherInfo, - container, screenId, info.cellX, info.cellY, false); + container, screenId, info.cellX, info.cellY); if (!mRestoring) { if (hostView == null) { @@ -1640,16 +1657,17 @@ public class Launcher extends Activity if (Intent.ACTION_SCREEN_OFF.equals(action)) { mUserPresent = false; mDragLayer.clearAllResizeFrames(); - updateRunning(); + updateAutoAdvanceState(); // Reset AllApps to its initial state only if we are not in the middle of // processing a multi-step drop - if (mAppsCustomizeTabHost != null && mPendingAddInfo.container == ItemInfo.NO_ID) { + if (mAppsView != null && mWidgetsView != null && + mPendingAddInfo.container == ItemInfo.NO_ID) { showWorkspace(false); } } else if (Intent.ACTION_USER_PRESENT.equals(action)) { mUserPresent = true; - updateRunning(); + updateAutoAdvanceState(); } else if (ENABLE_DEBUG_INTENTS && DebugIntents.DELETE_DATABASE.equals(action)) { mModel.resetLoadedState(false, true); mModel.startLoader(false, PagedView.INVALID_RESTORE_PAGE, @@ -1692,40 +1710,19 @@ public class Launcher extends Activity * Sets up transparent navigation and status bars in LMP. * This method is a no-op for other platform versions. */ - @TargetApi(19) + @TargetApi(Build.VERSION_CODES.LOLLIPOP) 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 {} + Window window = getWindow(); + window.getAttributes().systemUiVisibility |= + (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(Color.TRANSPARENT); + window.setNavigationBarColor(Color.TRANSPARENT); } } @@ -1738,17 +1735,16 @@ public class Launcher extends Activity unregisterReceiver(mReceiver); mAttached = false; } - updateRunning(); + updateAutoAdvanceState(); } public void onWindowVisibilityChanged(int visibility) { mVisible = visibility == View.VISIBLE; - updateRunning(); + updateAutoAdvanceState(); // The following code used to be in onResume, but it turns out onResume is called when // you're in All Apps and click home to go to the workspace. onWindowVisibilityChanged // is a more appropriate event to handle if (mVisible) { - mAppsCustomizeTabHost.onWindowVisible(); if (!mWorkspaceLoading) { final ViewTreeObserver observer = mWorkspace.getViewTreeObserver(); // We want to let Launcher draw itself at least once before we force it to build @@ -1783,14 +1779,14 @@ public class Launcher extends Activity } } - private void sendAdvanceMessage(long delay) { + @Thunk void sendAdvanceMessage(long delay) { mHandler.removeMessages(ADVANCE_MSG); Message msg = mHandler.obtainMessage(ADVANCE_MSG); mHandler.sendMessageDelayed(msg, delay); mAutoAdvanceSentTime = System.currentTimeMillis(); } - private void updateRunning() { + @Thunk void updateAutoAdvanceState() { boolean autoAdvanceRunning = mVisible && mUserPresent && !mWidgetsToAdvance.isEmpty(); if (autoAdvanceRunning != mAutoAdvanceRunning) { mAutoAdvanceRunning = autoAdvanceRunning; @@ -1836,14 +1832,14 @@ public class Launcher extends Activity if (v instanceof Advanceable) { mWidgetsToAdvance.put(hostView, appWidgetInfo); ((Advanceable) v).fyiWillBeAdvancedByHostKThx(); - updateRunning(); + updateAutoAdvanceState(); } } void removeWidgetToAutoAdvance(View hostView) { if (mWidgetsToAdvance.containsKey(hostView)) { mWidgetsToAdvance.remove(hostView); - updateRunning(); + updateAutoAdvanceState(); } } @@ -1852,19 +1848,23 @@ public class Launcher extends Activity launcherInfo.hostView = null; } - void showOutOfSpaceMessage(boolean isHotseatLayout) { + public void showOutOfSpaceMessage(boolean isHotseatLayout) { int strId = (isHotseatLayout ? R.string.hotseat_out_of_space : R.string.out_of_space); Toast.makeText(this, getString(strId), Toast.LENGTH_SHORT).show(); } - public ArrayList<AppInfo> getAllAppsList() { - return mAppsCustomizeContent.getApps(); - } - public DragLayer getDragLayer() { return mDragLayer; } + public AppsContainerView getAppsView() { + return mAppsView; + } + + public WidgetsContainerView getWidgetsView() { + return mWidgetsView; + } + public Workspace getWorkspace() { return mWorkspace; } @@ -1950,9 +1950,14 @@ public class Launcher extends Activity imm.hideSoftInputFromWindow(v.getWindowToken(), 0); } - // Reset the apps customize page - if (!alreadyOnHome && mAppsCustomizeTabHost != null) { - mAppsCustomizeTabHost.reset(); + // Reset the apps view + if (!alreadyOnHome && mAppsView != null) { + mAppsView.scrollToTop(); + } + + // Reset the widgets view + if (!alreadyOnHome && mWidgetsView != null) { + mWidgetsView.scrollToTop(); } if (mLauncherCallbacks != null) { @@ -2007,16 +2012,8 @@ public class Launcher extends Activity outState.putLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID, mFolderInfo.id); } - // Save the current AppsCustomize tab - if (mAppsCustomizeTabHost != null) { - AppsCustomizePagedView.ContentType type = mAppsCustomizeContent.getContentType(); - String currentTabTag = mAppsCustomizeTabHost.getTabTagForContentType(type); - if (currentTabTag != null) { - outState.putString("apps_customize_currentTab", currentTabTag); - } - int currentIndex = mAppsCustomizeContent.getSaveInstanceStateIndex(); - outState.putInt("apps_customize_currentIndex", currentIndex); - } + // Save the current widgets tray? + // TODO(hyunyoungs) outState.putSerializable(RUNTIME_STATE_VIEW_IDS, mItemIdToViewId); if (mLauncherCallbacks != null) { @@ -2414,8 +2411,8 @@ public class Launcher extends Activity folderInfo.title = getText(R.string.folder_name); // Update the model - LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screenId, cellX, cellY, - false); + LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screenId, + cellX, cellY); sFolders.put(folderInfo.id, folderInfo); // Create the view @@ -2433,13 +2430,6 @@ public class Launcher extends Activity sFolders.remove(folder.id); } - protected ComponentName getWallpaperPickerComponent() { - if (mLauncherCallbacks != null) { - return mLauncherCallbacks.getWallpaperPickerComponent(); - } - return new ComponentName(getPackageName(), LauncherWallpaperPickerActivity.class.getName()); - } - /** * Registers various content observers. The current implementation registers * only a favorites observer to keep track of the favorites applications. @@ -2479,13 +2469,16 @@ public class Launcher extends Activity return; } - if (isAllAppsVisible()) { - if (mAppsCustomizeContent.getContentType() == - AppsCustomizePagedView.ContentType.Applications) { - showWorkspace(true); - } else { - showOverviewMode(true); - } + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + if (delegate != null && delegate.onBackPressed()) { + return; + } + + if (isAppsViewVisible()) { + showWorkspace(true); + } else if (isWidgetsViewVisible()) { + showOverviewMode(true); } else if (mWorkspace.isInOverviewMode()) { mWorkspace.exitOverviewMode(true); } else if (mWorkspace.getOpenFolder() != null) { @@ -2506,7 +2499,7 @@ public class Launcher extends Activity /** * Re-listen when widgets are reset. */ - private void onAppWidgetReset() { + @Thunk void onAppWidgetReset() { if (mAppWidgetHost != null) { mAppWidgetHost.startListening(); } @@ -2616,13 +2609,10 @@ public class Launcher extends Activity */ protected void onClickAllAppsButton(View v) { if (LOGD) Log.d(TAG, "onClickAllAppsButton"); - if (isAllAppsVisible()) { + if (isAppsViewVisible()) { showWorkspace(true); } else { - showAllApps(true, AppsCustomizePagedView.ContentType.Applications, false); - } - if (mLauncherCallbacks != null) { - mLauncherCallbacks.onClickAllAppsButton(v); + showAppsView(true /* animated */, false /* resetListToTop */); } } @@ -2704,7 +2694,7 @@ public class Launcher extends Activity } } - private void startAppShortcutOrInfoActivity(View v) { + @Thunk void startAppShortcutOrInfoActivity(View v) { Object tag = v.getTag(); final ShortcutInfo shortcut; final Intent intent; @@ -2790,7 +2780,7 @@ public class Launcher extends Activity if (mIsSafeModeEnabled) { Toast.makeText(this, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show(); } else { - showAllApps(true, AppsCustomizePagedView.ContentType.Widgets, true); + showWidgetsView(true /* animated */, true /* resetPageToZero */); if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickAddWidgetButton(view); } @@ -2803,9 +2793,8 @@ public class Launcher extends Activity */ 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); + startActivityForResult(new Intent(Intent.ACTION_SET_WALLPAPER).setPackage(getPackageName()), + REQUEST_PICK_WALLPAPER); if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickWallpaperPicker(v); @@ -2939,12 +2928,39 @@ public class Launcher extends Activity } Bundle optsBundle = null; - if (useLaunchAnimation && !Utilities.isLmpOrAbove()) { - // On pre-L devices, we use the scale up transition. - // Otherwise we use system default. - ActivityOptions opts = - ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getMeasuredWidth(), v.getMeasuredHeight()); - optsBundle = opts.toBundle(); + if (useLaunchAnimation) { + ActivityOptions opts = null; + if (sClipRevealMethod != null) { + // TODO: call method directly when Launcher3 can depend on M APIs + int left = 0, top = 0; + int width = v.getMeasuredWidth(), height = v.getMeasuredHeight(); + if (v instanceof TextView) { + // Launch from center of icon, not entire view + Drawable icon = Workspace.getTextViewIcon((TextView) v); + if (icon != null) { + Rect bounds = icon.getBounds(); + left = (width - bounds.width()) / 2; + top = v.getPaddingTop(); + width = bounds.width(); + height = bounds.height(); + } + } + try { + opts = (ActivityOptions) sClipRevealMethod.invoke(null, v, + left, top, width, height); + } catch (IllegalAccessException e) { + Log.d(TAG, "Could not call makeClipRevealAnimation: " + e); + sClipRevealMethod = null; + } catch (InvocationTargetException e) { + Log.d(TAG, "Could not call makeClipRevealAnimation: " + e); + sClipRevealMethod = null; + } + } + if (opts == null && !Utilities.isLmpOrAbove()) { + opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, + v.getMeasuredWidth(), v.getMeasuredHeight()); + } + optsBundle = opts != null ? opts.toBundle() : null; } if (user == null || user.equals(UserHandleCompat.myUserHandle())) { @@ -3096,10 +3112,19 @@ public class Launcher extends Activity */ public void openFolder(FolderIcon folderIcon) { Folder folder = folderIcon.getFolder(); + Folder openFolder = mWorkspace != null ? mWorkspace.getOpenFolder() : null; + if (openFolder != null && openFolder != folder) { + // Close any open folder before opening a folder. + closeFolder(); + } + FolderInfo info = folder.mInfo; info.opened = true; + // While the folder is open, the position of the icon cannot change. + ((CellLayout.LayoutParams) folderIcon.getLayoutParams()).canReorder = false; + // Just verify that the folder hasn't already been added to the DragLayer. // There was a one-off crash where the folder had a parent already. if (folder.getParent() == null) { @@ -3135,6 +3160,9 @@ public class Launcher extends Activity if (parent != null) { FolderIcon fi = (FolderIcon) mWorkspace.getViewForTag(folder.mInfo); shrinkAndFadeInFolderIcon(fi); + if (fi != null) { + ((CellLayout.LayoutParams) fi.getLayoutParams()).canReorder = true; + } } folder.animateClosed(); @@ -3166,7 +3194,7 @@ public class Launcher extends Activity View itemUnderLongClick = null; if (v.getTag() instanceof ItemInfo) { ItemInfo info = (ItemInfo) v.getTag(); - longClickCellInfo = new CellLayout.CellInfo(v, info);; + longClickCellInfo = new CellLayout.CellInfo(v, info); itemUnderLongClick = longClickCellInfo.cell; resetAddInfo(); } @@ -3207,7 +3235,7 @@ public class Launcher extends Activity /** * Returns the CellLayout of the specified container at the specified screen. */ - CellLayout getCellLayout(long container, long screenId) { + public CellLayout getCellLayout(long container, long screenId) { if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { if (mHotseat != null) { return mHotseat.getLayout(); @@ -3215,12 +3243,23 @@ public class Launcher extends Activity return null; } } else { - return (CellLayout) mWorkspace.getScreenWithId(screenId); + return mWorkspace.getScreenWithId(screenId); } } + /** + * For overridden classes. + */ public boolean isAllAppsVisible() { - return (mState == State.APPS_CUSTOMIZE) || (mOnResumeState == State.APPS_CUSTOMIZE); + return isAppsViewVisible(); + } + + public boolean isAppsViewVisible() { + return (mState == State.APPS) || (mOnResumeState == State.APPS); + } + + public boolean isWidgetsViewVisible() { + return (mState == State.WIDGETS) || (mOnResumeState == State.WIDGETS); } private void setWorkspaceBackground(boolean workspace) { @@ -3238,579 +3277,6 @@ public class Launcher extends Activity setWorkspaceBackground(visible); } - private void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionPrepare(this, animated, toWorkspace); - } - } - - private void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionStart(this, animated, toWorkspace); - } - - // Update the workspace transition step as well - dispatchOnLauncherTransitionStep(v, 0f); - } - - private void dispatchOnLauncherTransitionStep(View v, float t) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionStep(this, t); - } - } - - private void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionEnd(this, animated, toWorkspace); - } - - // Update the workspace transition step as well - dispatchOnLauncherTransitionStep(v, 1f); - } - - /** - * Things to test when changing the following seven functions. - * - Home from workspace - * - from center screen - * - from other screens - * - Home from all apps - * - from center screen - * - from other screens - * - Back from all apps - * - from center screen - * - from other screens - * - Launch app from workspace and quit - * - with back - * - with home - * - Launch app from all apps and quit - * - with back - * - with home - * - Go to a screen that's not the default, then all - * apps, and launch and app, and go back - * - with back - * -with home - * - On workspace, long press power and go back - * - with back - * - with home - * - On all apps, long press power and go back - * - with back - * - with home - * - On workspace, power off - * - On all apps, power off - * - Launch an app and turn off the screen while in that app - * - Go back with home key - * - Go back with back key TODO: make this not go to workspace - * - From all apps - * - From workspace - * - Enter and exit car mode (becuase it causes an extra configuration changed) - * - From all apps - * - From the center workspace - * - From another workspace - */ - - /** - * Zoom the camera out from the workspace to reveal 'toView'. - * Assumes that the view to show is anchored at either the very top or very bottom - * of the screen. - */ - private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded) { - AppsCustomizePagedView.ContentType contentType = mAppsCustomizeContent.getContentType(); - showAppsCustomizeHelper(animated, springLoaded, contentType); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded, - final AppsCustomizePagedView.ContentType contentType) { - if (mStateAnimation != null) { - mStateAnimation.setDuration(0); - mStateAnimation.cancel(); - mStateAnimation = null; - } - - boolean material = Utilities.isLmpOrAbove(); - - final Resources res = getResources(); - - final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime); - final int itemsAlphaStagger = - res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); - - final View fromView = mWorkspace; - final AppsCustomizeTabHost toView = mAppsCustomizeTabHost; - - final HashMap<View, Integer> layerViews = new HashMap<View, Integer>(); - - Workspace.State workspaceState = contentType == AppsCustomizePagedView.ContentType.Widgets ? - Workspace.State.OVERVIEW_HIDDEN : Workspace.State.NORMAL_HIDDEN; - Animator workspaceAnim = - 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 for some reason our views aren't initialized, don't animate - boolean initialized = getAllAppsButton() != null; - - if (animated && initialized) { - mStateAnimation = LauncherAnimUtils.createAnimatorSet(); - 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 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; - - layerViews.put(revealView, BUILD_AND_SET_LAYER); - 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); - layerViews.put(page, BUILD_AND_SET_LAYER); - - 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 onAnimationEnd(Animator animation) { - dispatchOnLauncherTransitionEnd(fromView, animated, false); - dispatchOnLauncherTransitionEnd(toView, animated, false); - - revealView.setVisibility(View.INVISIBLE); - - for (View v : layerViews.keySet()) { - if (layerViews.get(v) == BUILD_AND_SET_LAYER) { - v.setLayerType(View.LAYER_TYPE_NONE, null); - } - } - content.setPageBackgroundsVisible(true); - - // Hide the search bar - if (mSearchDropTargetBar != null) { - mSearchDropTargetBar.hideSearchBar(false); - } - - // This can hold unnecessary references to views. - mStateAnimation = null; - } - - }); - - if (workspaceAnim != null) { - mStateAnimation.play(workspaceAnim); - } - - dispatchOnLauncherTransitionPrepare(fromView, animated, false); - dispatchOnLauncherTransitionPrepare(toView, animated, false); - 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); - - revealView.setAlpha(initAlpha); - - for (View v : layerViews.keySet()) { - if (layerViews.get(v) == BUILD_AND_SET_LAYER) { - v.setLayerType(View.LAYER_TYPE_HARDWARE, null); - } - } - - if (Utilities.isLmpOrAbove()) { - for (View v : layerViews.keySet()) { - if (Utilities.isViewAttachedToWindow(v)) v.buildLayer(); - } - } - mStateAnimation.start(); - } - }; - toView.bringToFront(); - toView.setVisibility(View.VISIBLE); - toView.post(startAnimRunnable); - } else { - toView.setTranslationX(0.0f); - toView.setTranslationY(0.0f); - toView.setScaleX(1.0f); - toView.setScaleY(1.0f); - toView.setVisibility(View.VISIBLE); - toView.bringToFront(); - - if (!springLoaded && !LauncherAppState.getInstance().isScreenLarge()) { - // Hide the search bar - if (mSearchDropTargetBar != null) { - mSearchDropTargetBar.hideSearchBar(false); - } - } - dispatchOnLauncherTransitionPrepare(fromView, animated, false); - dispatchOnLauncherTransitionStart(fromView, animated, false); - dispatchOnLauncherTransitionEnd(fromView, animated, false); - dispatchOnLauncherTransitionPrepare(toView, animated, false); - dispatchOnLauncherTransitionStart(toView, animated, false); - dispatchOnLauncherTransitionEnd(toView, animated, false); - } - } - - /** - * Zoom the camera back into the workspace, hiding 'fromView'. - * This is the opposite of showAppsCustomizeHelper. - * @param animated If true, the transition will be animated. - */ - private void hideAppsCustomizeHelper(Workspace.State toState, final boolean animated, - final boolean springLoaded, final Runnable onCompleteRunnable) { - - if (mStateAnimation != null) { - mStateAnimation.setDuration(0); - mStateAnimation.cancel(); - mStateAnimation = null; - } - - boolean material = Utilities.isLmpOrAbove(); - Resources res = getResources(); - - final int revealDuration = res.getInteger(R.integer.config_appsCustomizeConcealTime); - final int itemsAlphaStagger = - res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); - - final View fromView = mAppsCustomizeTabHost; - final View toView = mWorkspace; - Animator workspaceAnim = null; - final HashMap<View, Integer> layerViews = new HashMap<View, Integer>(); - - if (toState == Workspace.State.NORMAL) { - workspaceAnim = mWorkspace.getChangeStateAnimation( - toState, animated, layerViews); - } else if (toState == Workspace.State.SPRING_LOADED || - toState == Workspace.State.OVERVIEW) { - workspaceAnim = mWorkspace.getChangeStateAnimation( - 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); - } - - 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 = 2 * height / 3; - xDrift = 0; - } - - layerViews.put(revealView, BUILD_AND_SET_LAYER); - TimeInterpolator decelerateInterpolator = material ? - new LogDecelerateInterpolator(100, 0) : - new DecelerateInterpolator(1f); - - // 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(material ? revealDuration : 150); - panelAlpha.setInterpolator(decelerateInterpolator); - panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); - mStateAnimation.play(panelAlpha); - } - - if (page != null) { - layerViews.put(page, BUILD_AND_SET_LAYER); - - 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 - public void onAnimationEnd(Animator animation) { - fromView.setVisibility(View.GONE); - dispatchOnLauncherTransitionEnd(fromView, animated, true); - dispatchOnLauncherTransitionEnd(toView, animated, true); - if (onCompleteRunnable != null) { - onCompleteRunnable.run(); - } - - for (View v : layerViews.keySet()) { - if (layerViews.get(v) == BUILD_AND_SET_LAYER) { - v.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(); - - // This can hold unnecessary references to views. - mStateAnimation = null; - } - }); - - 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); - - for (View v : layerViews.keySet()) { - if (layerViews.get(v) == BUILD_AND_SET_LAYER) { - v.setLayerType(View.LAYER_TYPE_HARDWARE, null); - } - } - - if (Utilities.isLmpOrAbove()) { - for (View v : layerViews.keySet()) { - if (Utilities.isViewAttachedToWindow(v)) v.buildLayer(); - } - } - mStateAnimation.start(); - } - }; - fromView.post(startAnimRunnable); - } else { - fromView.setVisibility(View.GONE); - dispatchOnLauncherTransitionPrepare(fromView, animated, true); - dispatchOnLauncherTransitionStart(fromView, animated, true); - dispatchOnLauncherTransitionEnd(fromView, animated, true); - dispatchOnLauncherTransitionPrepare(toView, animated, true); - dispatchOnLauncherTransitionStart(toView, animated, true); - dispatchOnLauncherTransitionEnd(toView, animated, true); - } - } - @Override public void onTrimMemory(int level) { super.onTrimMemory(level); @@ -3820,28 +3286,33 @@ public class Launcher extends Activity SQLiteDatabase.releaseMemory(); // This clears all widget bitmaps from the widget tray - if (mAppsCustomizeTabHost != null) { - mAppsCustomizeTabHost.trimMemory(); - } + // TODO(hyunyoungs) } if (mLauncherCallbacks != null) { mLauncherCallbacks.onTrimMemory(level); } } - protected void showWorkspace(boolean animated) { - showWorkspace(animated, null); + @Override + public void onStateTransitionHideSearchBar() { + // Hide the search bar + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.hideSearchBar(false /* animated */); + } } - protected void showWorkspace() { - showWorkspace(true); + protected void showWorkspace(boolean animated) { + showWorkspace(animated, null); } void showWorkspace(boolean animated, Runnable onCompleteRunnable) { - if (mState != State.WORKSPACE || mWorkspace.getState() != Workspace.State.NORMAL) { + boolean changed = mState != State.WORKSPACE || + mWorkspace.getState() != Workspace.State.NORMAL; + if (changed) { boolean wasInSpringLoadedMode = (mState != State.WORKSPACE); mWorkspace.setVisibility(View.VISIBLE); - hideAppsCustomizeHelper(Workspace.State.NORMAL, animated, false, onCompleteRunnable); + mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.NORMAL, + animated, onCompleteRunnable); // Show the search bar (only animate if we were showing the drop target bar in spring // loaded mode) @@ -3860,18 +3331,21 @@ public class Launcher extends Activity // Resume the auto-advance of widgets mUserPresent = true; - updateRunning(); + updateAutoAdvanceState(); - // Send an accessibility event to announce the context change - getWindow().getDecorView() - .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + if (changed) { + // Send an accessibility event to announce the context change + getWindow().getDecorView() + .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - onWorkspaceShown(animated); + onWorkspaceShown(animated); + } } void showOverviewMode(boolean animated) { mWorkspace.setVisibility(View.VISIBLE); - hideAppsCustomizeHelper(Workspace.State.OVERVIEW, animated, false, null); + mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.OVERVIEW, + animated, null /* onCompleteRunnable */); mState = State.WORKSPACE; onWorkspaceShown(animated); } @@ -3879,28 +3353,56 @@ public class Launcher extends Activity public void onWorkspaceShown(boolean animated) { } - void showAllApps(boolean animated, AppsCustomizePagedView.ContentType contentType, - boolean resetPageToZero) { - if (mState != State.WORKSPACE) return; + /** + * Shows the apps view. + */ + void showAppsView(boolean animated, boolean resetListToTop) { + if (resetListToTop) { + mAppsView.scrollToTop(); + } + showAppsOrWidgets(animated, State.APPS); + } + /** + * Shows the widgets view. + */ + void showWidgetsView(boolean animated, boolean resetPageToZero) { + Log.d(TAG, "showWidgetsView:" + animated + " resetPageToZero:" + resetPageToZero); if (resetPageToZero) { - mAppsCustomizeTabHost.reset(); + mWidgetsView.scrollToTop(); } - showAppsCustomizeHelper(animated, false, contentType); - mAppsCustomizeTabHost.post(new Runnable() { + showAppsOrWidgets(animated, State.WIDGETS); + + mWidgetsView.post(new Runnable() { @Override public void run() { - // We post this in-case the all apps view isn't yet constructed. - mAppsCustomizeTabHost.requestFocus(); + mWidgetsView.requestFocus(); } }); + } + + /** + * Sets up the transition to show the apps/widgets view. + */ + private void showAppsOrWidgets(boolean animated, State toState) { + if (mState != State.WORKSPACE) return; + if (toState != State.APPS && toState != State.WIDGETS) return; + + if (toState == State.APPS) { + mStateTransitionAnimation.startAnimationToAllApps(animated); + if (mLauncherCallbacks != null) { + mLauncherCallbacks.onAllAppsShown(); + } + } else { + mStateTransitionAnimation.startAnimationToWidgets(animated); + } // Change the state *after* we've called all the transition code - mState = State.APPS_CUSTOMIZE; + mState = toState; // Pause the auto-advance of widgets until we are out of AllApps mUserPresent = false; - updateRunning(); + updateAutoAdvanceState(); closeFolder(); // Send an accessibility event to announce the context change @@ -3908,25 +3410,33 @@ public class Launcher extends Activity .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } - void enterSpringLoadedDragMode() { - if (isAllAppsVisible()) { - hideAppsCustomizeHelper(Workspace.State.SPRING_LOADED, true, true, null); - mState = State.APPS_CUSTOMIZE_SPRING_LOADED; + public void enterSpringLoadedDragMode() { + Log.d(TAG, String.format("enterSpringLoadedDragMode [mState=%s", + mState.name())); + if (mState == State.WORKSPACE || mState == State.APPS_SPRING_LOADED || + mState == State.WIDGETS_SPRING_LOADED) { + return; } + + mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.SPRING_LOADED, + true /* animated */, null /* onCompleteRunnable */); + mState = isAppsViewVisible() ? State.APPS_SPRING_LOADED : State.WIDGETS_SPRING_LOADED; } - void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, int delay, + public void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, int delay, final Runnable onCompleteRunnable) { - if (mState != State.APPS_CUSTOMIZE_SPRING_LOADED) return; + if (mState != State.APPS_SPRING_LOADED && mState != State.WIDGETS_SPRING_LOADED) return; mHandler.postDelayed(new Runnable() { @Override public void run() { if (successfulDrop) { + // TODO(hyunyoungs): verify if this hack is still needed, if not, delete. + // // Before we show workspace, hide all apps again because // exitSpringLoadedDragMode made it visible. This is a bit hacky; we should // clean up our state transition functions - mAppsCustomizeTabHost.setVisibility(View.GONE); + mWidgetsView.setVisibility(View.GONE); showWorkspace(true, onCompleteRunnable); } else { exitSpringLoadedDragMode(); @@ -3936,11 +3446,12 @@ public class Launcher extends Activity } void exitSpringLoadedDragMode() { - if (mState == State.APPS_CUSTOMIZE_SPRING_LOADED) { - final boolean animated = true; - final boolean springLoaded = true; - showAppsCustomizeHelper(animated, springLoaded); - mState = State.APPS_CUSTOMIZE; + if (mState == State.APPS_SPRING_LOADED) { + mStateTransitionAnimation.startAnimationToAllApps(true /* animated */); + mState = State.APPS; + } else if (mState == State.WIDGETS_SPRING_LOADED) { + mStateTransitionAnimation.startAnimationToWidgets(true /* animated */); + mState = State.WIDGETS; } // Otherwise, we are not in spring loaded mode, so don't do anything. } @@ -3957,7 +3468,7 @@ public class Launcher extends Activity // NO-OP } - public View getQsbBar() { + public View getOrCreateQsbBar() { if (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch()) { return mLauncherCallbacks.getQsbBar(); } @@ -4006,19 +3517,31 @@ public class Launcher extends Activity mQsb.updateAppWidgetOptions(opts); mQsb.setPadding(0, 0, 0, 0); mSearchDropTargetBar.addView(mQsb); + mSearchDropTargetBar.setQsbSearchBar(mQsb); } } return mQsb; } + private void reinflateQSBIfNecessary() { + if (mQsb instanceof LauncherAppWidgetHostView && + ((LauncherAppWidgetHostView) mQsb).isReinflateRequired()) { + mSearchDropTargetBar.removeView(mQsb); + mQsb = null; + mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); + } + } + @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { final boolean result = super.dispatchPopulateAccessibilityEvent(event); final List<CharSequence> text = event.getText(); text.clear(); // Populate event with a fake title based on the current state. - if (mState == State.APPS_CUSTOMIZE) { - text.add(mAppsCustomizeTabHost.getContentTag()); + if (mState == State.APPS) { + text.add("Apps"); + } else if (mState == State.WIDGETS) { + text.add("Widgets"); } else { text.add(getString(R.string.all_apps_home_button_label)); } @@ -4028,7 +3551,7 @@ public class Launcher extends Activity /** * Receives notifications when system dialogs are to be closed. */ - private class CloseSystemDialogsIntentReceiver extends BroadcastReceiver { + @Thunk class CloseSystemDialogsIntentReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { closeSystemDialogs(); @@ -4241,9 +3764,8 @@ public class Launcher extends Activity // Remove the extra empty screen mWorkspace.removeExtraEmptyScreen(false, false); - if (!LauncherAppState.isDisableAllApps() && - addedApps != null && mAppsCustomizeContent != null) { - mAppsCustomizeContent.addApps(addedApps); + if (addedApps != null && mAppsView != null) { + mAppsView.addApps(addedApps); } } @@ -4416,8 +3938,8 @@ public class Launcher extends Activity pendingInfo.spanY = item.spanY; pendingInfo.minSpanX = item.minSpanX; pendingInfo.minSpanY = item.minSpanY; - Bundle options = - AppsCustomizePagedView.getDefaultOptionsForWidget(this, pendingInfo); + Bundle options = null; + // AppsCustomizePagedView.getDefaultOptionsForWidget(this, pendingInfo); int newWidgetId = mAppWidgetHost.allocateAppWidgetId(); boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( @@ -4510,10 +4032,10 @@ public class Launcher extends Activity * * Implementation of the method from LauncherModel.Callbacks. */ - public void finishBindingItems(final boolean upgradePath) { + public void finishBindingItems() { Runnable r = new Runnable() { public void run() { - finishBindingItems(upgradePath); + finishBindingItems(); } }; if (waitUntilResume(r)) { @@ -4548,14 +4070,10 @@ public class Launcher extends Activity sPendingAddItem = null; } - if (upgradePath) { - mWorkspace.getUniqueComponents(true, null); - mIntentsOnWorkspaceFromUpgradePath = mWorkspace.getUniqueComponents(true, null); - } PackageInstallerCompat.getInstance(this).onFinishBind(); if (mLauncherCallbacks != null) { - mLauncherCallbacks.finishBindingItems(upgradePath); + mLauncherCallbacks.finishBindingItems(false); } } @@ -4612,7 +4130,7 @@ public class Launcher extends Activity mSearchDropTargetBar.removeView(mQsb); mQsb = null; } - mSearchDropTargetBar.setQsbSearchBar(getQsbBar()); + mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); } /** @@ -4621,24 +4139,12 @@ public class Launcher extends Activity * Implementation of the method from LauncherModel.Callbacks. */ public void bindAllApplications(final ArrayList<AppInfo> apps) { - if (LauncherAppState.isDisableAllApps()) { - if (mIntentsOnWorkspaceFromUpgradePath != null) { - if (LauncherModel.UPGRADE_USE_MORE_APPS_FOLDER) { - getHotseat().addAllAppsFolder(mIconCache, apps, - mIntentsOnWorkspaceFromUpgradePath, Launcher.this, mWorkspace); - } - mIntentsOnWorkspaceFromUpgradePath = null; - } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.onPackagesUpdated( - LauncherModel.getSortedWidgetsAndShortcuts(this, false /* refresh */)); - } - } else { - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.setApps(apps); - mAppsCustomizeContent.onPackagesUpdated( - LauncherModel.getSortedWidgetsAndShortcuts(this, false /* refresh */)); - } + if (mAppsView != null) { + mAppsView.setApps(apps); + } + if (mWidgetsView != null) { + mWidgetsView.addWidgets(LauncherModel.getSortedWidgetsAndShortcuts(this, false), + getPackageManager()); } if (mLauncherCallbacks != null) { mLauncherCallbacks.bindAllApplications(apps); @@ -4660,9 +4166,8 @@ public class Launcher extends Activity return; } - if (!LauncherAppState.isDisableAllApps() && - mAppsCustomizeContent != null) { - mAppsCustomizeContent.updateApps(apps); + if (mAppsView != null) { + mAppsView.updateApps(apps); } } @@ -4776,31 +4281,31 @@ public class Launcher extends Activity } // Update AllApps - if (!LauncherAppState.isDisableAllApps() && - mAppsCustomizeContent != null) { - mAppsCustomizeContent.removeApps(appInfos); + if (mAppsView != null) { + mAppsView.removeApps(appInfos); } } /** * A number of packages were updated. */ - private ArrayList<Object> mWidgetsAndShortcuts; + @Thunk ArrayList<Object> mWidgetsAndShortcuts; private Runnable mBindPackagesUpdatedRunnable = new Runnable() { public void run() { bindPackagesUpdated(mWidgetsAndShortcuts); mWidgetsAndShortcuts = null; } }; + public void bindPackagesUpdated(final ArrayList<Object> widgetsAndShortcuts) { if (waitUntilResume(mBindPackagesUpdatedRunnable, true)) { mWidgetsAndShortcuts = widgetsAndShortcuts; return; } - // Update the widgets pane - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.onPackagesUpdated(widgetsAndShortcuts); + if (mWidgetsView != null) { + mWidgetsView.addWidgets(LauncherModel.getSortedWidgetsAndShortcuts(this, false), + getPackageManager()); } } @@ -4910,6 +4415,17 @@ public class Launcher extends Activity return null; } + /** + * Returns whether the launcher callbacks overrides search in all apps. + * @return + */ + @Thunk boolean isAllAppsSearchOverridden() { + if (mLauncherCallbacks != null) { + return mLauncherCallbacks.overrideAllAppsSearch(); + } + return false; + } + private boolean shouldRunFirstRunActivity() { return !ActivityManager.isRunningInTestHarness() && !mSharedPrefs.getBoolean(FIRST_RUN_ACTIVITY_DISPLAYED, false); @@ -5010,7 +4526,7 @@ public class Launcher extends Activity editor.apply(); } - private void showFirstRunClings() { + @Thunk 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 @@ -5047,7 +4563,7 @@ public class Launcher extends Activity if (activityInfo == null) { return null; } - return new AppInfo(this, activityInfo, myUser, mIconCache, null); + return new AppInfo(this, activityInfo, myUser, mIconCache); } public ItemInfo createShortcutDragInfo(Intent shortcutIntent, CharSequence caption, @@ -5093,10 +4609,8 @@ public class Launcher extends Activity Log.d(TAG, "mSavedInstanceState=" + mSavedInstanceState); Log.d(TAG, "sFolders.size=" + sFolders.size()); mModel.dumpState(); + // TODO(hyunyoungs): add mWidgetsView.dumpState(); or mWidgetsModel.dumpState(); - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.dumpState(); - } Log.d(TAG, "END launcher3 dump state"); } diff --git a/src/com/android/launcher3/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/LauncherAccessibilityDelegate.java index c9e277e4c..8ba02ea5f 100644 --- a/src/com/android/launcher3/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/LauncherAccessibilityDelegate.java @@ -1,15 +1,21 @@ package com.android.launcher3; import android.annotation.TargetApi; +import android.content.res.Resources; +import android.graphics.Rect; import android.os.Build; import android.os.Bundle; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.util.SparseArray; import android.view.View; import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import com.android.launcher3.LauncherModel.ScreenPosProvider; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -20,10 +26,25 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { public static final int INFO = R.id.action_info; public static final int UNINSTALL = R.id.action_uninstall; public static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace; + public static final int MOVE = R.id.action_move; + + enum DragType { + ICON, + FOLDER, + WIDGET + } + + public static class DragInfo { + DragType dragType; + ItemInfo info; + View item; + } + + private DragInfo mDragInfo = null; private final SparseArray<AccessibilityAction> mActions = new SparseArray<AccessibilityAction>(); - private final Launcher mLauncher; + @Thunk final Launcher mLauncher; public LauncherAccessibilityDelegate(Launcher launcher) { mLauncher = launcher; @@ -36,6 +57,9 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { launcher.getText(R.string.delete_target_uninstall_label))); mActions.put(ADD_TO_WORKSPACE, new AccessibilityAction(ADD_TO_WORKSPACE, launcher.getText(R.string.action_add_to_workspace))); + mActions.put(MOVE, new AccessibilityAction(MOVE, + launcher.getText(R.string.action_move))); + } @Override @@ -49,6 +73,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { || (item instanceof FolderInfo)) { // Workspace shortcut / widget info.addAction(mActions.get(REMOVE)); + info.addAction(mActions.get(MOVE)); } else if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) { // App or Widget from customization tray if (item instanceof AppInfo) { @@ -69,14 +94,22 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { } public boolean performAction(View host, ItemInfo item, int action) { + Resources res = mLauncher.getResources(); if (action == REMOVE) { - return DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host); + if (DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host)) { + announceConfirmation(R.string.item_removed_from_workspace); + return true; + } + return false; } else if (action == INFO) { InfoDropTarget.startDetailsActivityForInfo(item, mLauncher); return true; } else if (action == UNINSTALL) { - DeleteDropTarget.uninstallApp(mLauncher, (AppInfo) item); + AppInfo info = (AppInfo) item; + mLauncher.startApplicationUninstallActivity(info.componentName, info.flags, info.user); return true; + } else if (action == MOVE) { + beginAccessibleDrag(host, item); } else if (action == ADD_TO_WORKSPACE) { final int preferredPage = mLauncher.getWorkspace().getCurrentPage(); final ScreenPosProvider screenProvider = new ScreenPosProvider() { @@ -90,20 +123,92 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate { final ArrayList<ItemInfo> addShortcuts = new ArrayList<ItemInfo>(); addShortcuts.add(((AppInfo) item).makeShortcut()); mLauncher.showWorkspace(true, new Runnable() { - @Override public void run() { - mLauncher.getModel().addAndBindAddedWorkspaceApps( + mLauncher.getModel().addAndBindAddedWorkspaceItems( mLauncher, addShortcuts, screenProvider, 0, true); + announceConfirmation(R.string.item_added_to_workspace); } }); return true; } else if (item instanceof PendingAddItemInfo) { mLauncher.getModel().addAndBindPendingItem( mLauncher, (PendingAddItemInfo) item, screenProvider, 0); + announceConfirmation(R.string.item_added_to_workspace); return true; } } return false; } + + @Thunk void announceConfirmation(int resId) { + announceConfirmation(mLauncher.getResources().getString(resId)); + } + + @Thunk void announceConfirmation(String confirmation) { + mLauncher.getDragLayer().announceForAccessibility(confirmation); + + } + + public boolean isInAccessibleDrag() { + return mDragInfo != null; + } + + public DragInfo getDragInfo() { + return mDragInfo; + } + + public void handleAccessibleDrop(CellLayout targetContainer, Rect dropLocation, + String confirmation) { + if (!isInAccessibleDrag()) return; + + int[] loc = new int[2]; + loc[0] = dropLocation.centerX(); + loc[1] = dropLocation.centerY(); + + mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(targetContainer, loc); + mLauncher.getDragController().completeAccessibleDrag(loc); + + endAccessibleDrag(); + announceConfirmation(confirmation); + } + + public void beginAccessibleDrag(View item, ItemInfo info) { + mDragInfo = new DragInfo(); + mDragInfo.info = info; + mDragInfo.item = item; + mDragInfo.dragType = DragType.ICON; + if (info instanceof FolderInfo) { + mDragInfo.dragType = DragType.FOLDER; + } else if (info instanceof LauncherAppWidgetInfo) { + mDragInfo.dragType = DragType.WIDGET; + } + + CellLayout.CellInfo cellInfo = new CellLayout.CellInfo(item, info); + + Rect pos = new Rect(); + mLauncher.getDragLayer().getDescendantRectRelativeToSelf(item, pos); + + mLauncher.getDragController().prepareAccessibleDrag(pos.centerX(), pos.centerY()); + mLauncher.getWorkspace().enableAccessibleDrag(true); + mLauncher.getWorkspace().startDrag(cellInfo, true); + } + + public boolean onBackPressed() { + if (isInAccessibleDrag()) { + cancelAccessibleDrag(); + return true; + } + return false; + } + + private void cancelAccessibleDrag() { + mLauncher.getDragController().cancelDrag(); + endAccessibleDrag(); + } + + private void endAccessibleDrag() { + mDragInfo = null; + mLauncher.getWorkspace().enableAccessibleDrag(false); + } } diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 87e9aae15..6e77d0628 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -19,44 +19,38 @@ package com.android.launcher3; import android.annotation.TargetApi; import android.app.SearchManager; 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.graphics.Point; import android.os.Build; -import android.os.Handler; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; -import android.view.View.AccessibilityDelegate; import android.view.WindowManager; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; +import com.android.launcher3.util.Thunk; import java.lang.ref.WeakReference; import java.util.ArrayList; public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { - private static final String TAG = "LauncherAppState"; - - private static final boolean DEBUG = false; private final AppFilter mAppFilter; private final BuildInfo mBuildInfo; - private final LauncherModel mModel; + @Thunk final LauncherModel mModel; private final IconCache mIconCache; + private final WidgetPreviewLoader mWidgetCache; private final boolean mIsScreenLarge; private final float mScreenDensity; private final int mLongPressTimeout = 300; - private WidgetPreviewLoader.CacheDb mWidgetPreviewCacheDb; private boolean mWallpaperChangedSinceLastCheck; private static WeakReference<LauncherProvider> sLauncherProvider; @@ -65,7 +59,7 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { private static LauncherAppState INSTANCE; private DynamicGrid mDynamicGrid; - private AccessibilityDelegate mAccessibilityDelegate; + private LauncherAccessibilityDelegate mAccessibilityDelegate; public static LauncherAppState getInstance() { if (INSTANCE == null) { @@ -103,39 +97,22 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { // set sIsScreenXLarge and mScreenDensity *before* creating icon cache mIsScreenLarge = isScreenLarge(sContext.getResources()); mScreenDensity = sContext.getResources().getDisplayMetrics().density; - - recreateWidgetPreviewDb(); mIconCache = new IconCache(sContext); + mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache); 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); + + LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel); // Register intent receivers IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_LOCALE_CHANGED); filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); - sContext.registerReceiver(mModel, filter); - filter = new IntentFilter(); filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); - sContext.registerReceiver(mModel, filter); - filter = new IntentFilter(); filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED); sContext.registerReceiver(mModel, filter); - - // Register for changes to the favorites - ContentResolver resolver = sContext.getContentResolver(); - resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true, - mFavoritesObserver); - } - - public void recreateWidgetPreviewDb() { - if (mWidgetPreviewCacheDb != null) { - mWidgetPreviewCacheDb.close(); - } - mWidgetPreviewCacheDb = new WidgetPreviewLoader.CacheDb(sContext); } /** @@ -146,23 +123,16 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(sContext); launcherApps.removeOnAppsChangedCallback(mModel); PackageInstallerCompat.getInstance(sContext).onStop(); - - ContentResolver resolver = sContext.getContentResolver(); - resolver.unregisterContentObserver(mFavoritesObserver); } /** - * Receives notifications whenever the user favorites have changed. + * Reloads the workspace items from the DB and re-binds the workspace. This should generally + * not be called as DB updates are automatically followed by UI update */ - private final ContentObserver mFavoritesObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange) { - // If the database has ever changed, then we really need to force a reload of the - // workspace on the next load - mModel.resetLoadedState(false, true); - mModel.startLoaderFromBackground(); - } - }; + public void reloadWorkspace() { + mModel.resetLoadedState(false, true); + mModel.startLoaderFromBackground(); + } LauncherModel setLauncher(Launcher launcher) { mModel.initialize(launcher); @@ -171,7 +141,7 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return mModel; } - AccessibilityDelegate getAccessibilityDelegate() { + public LauncherAccessibilityDelegate getAccessibilityDelegate() { return mAccessibilityDelegate; } @@ -179,7 +149,7 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return mIconCache; } - LauncherModel getModel() { + public LauncherModel getModel() { return mModel; } @@ -187,10 +157,6 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return mAppFilter == null || mAppFilter.shouldShowApp(componentName); } - WidgetPreviewLoader.CacheDb getWidgetPreviewCacheDb() { - return mWidgetPreviewCacheDb; - } - static void setLauncherProvider(LauncherProvider provider) { sLauncherProvider = new WeakReference<LauncherProvider>(provider); } @@ -246,6 +212,10 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return mDynamicGrid; } + public WidgetPreviewLoader getWidgetCache() { + return mWidgetCache; + } + public boolean isScreenLarge() { return mIsScreenLarge; } @@ -283,12 +253,6 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { Utilities.setIconSize(grid.iconSizePx); } - public static boolean isDisableAllApps() { - // Returns false on non-dogfood builds. - return getInstance().mBuildInfo.isDogfoodBuild() && - Utilities.isPropertyEnabled(Launcher.DISABLE_ALL_APPS_PROPERTY); - } - public static boolean isDogfoodBuild() { return getInstance().mBuildInfo.isDogfoodBuild(); } diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java index a28fd255a..583f85ad0 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHost.java +++ b/src/com/android/launcher3/LauncherAppWidgetHost.java @@ -84,8 +84,10 @@ public class LauncherAppWidgetHost extends AppWidgetHost { mLauncher.bindPackagesUpdated(LauncherModel.getSortedWidgetsAndShortcuts(mLauncher, true /* refresh */)); - for (Runnable callback : mProviderChangeListeners) { - callback.run(); + if (!mProviderChangeListeners.isEmpty()) { + for (Runnable callback : new ArrayList<>(mProviderChangeListeners)) { + callback.run(); + } } } diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java index e7f49b2ce..bb4580ce7 100644 --- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java @@ -7,8 +7,6 @@ import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Parcel; -import java.lang.reflect.Field; - /** * This class is a thin wrapper around the framework AppWidgetProviderInfo class. This class affords * a common object for describing both framework provided AppWidgets as well as custom widgets @@ -18,10 +16,10 @@ import java.lang.reflect.Field; public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { public boolean isCustomWidget = false; - int spanX = -1; - int spanY = -1; - int minSpanX = -1; - int minSpanY = -1; + public int spanX = -1; + public int spanY = -1; + public int minSpanX = -1; + public int minSpanY = -1; public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context, AppWidgetProviderInfo info) { @@ -80,10 +78,11 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { return super.loadIcon(context, cache.getFullResIconDpi()); } - public String toString() { + public String toString(PackageManager pm) { if (isCustomWidget) { - return "LauncherAppWidgetProviderInfo(" + provider + ")"; + return "WidgetProviderInfo(" + provider + ")"; } - return super.toString(); + return String.format("WidgetProviderInfo provider:%s package:%s short:%s label:%s span(%d, %d) minSpan(%d, %d)", + provider.toString(), provider.getPackageName(), provider.getShortClassName(), getLabel(pm), spanX, spanY, minSpanX, minSpanY); } } diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java index ddfd70d6b..5f7173fac 100644 --- a/src/com/android/launcher3/LauncherBackupAgentHelper.java +++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java @@ -78,7 +78,7 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { 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); + LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); hasData = c.moveToNext(); c.close(); } catch (Exception e) { diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index 31b434c42..064f4363a 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -19,13 +19,16 @@ import android.app.backup.BackupDataInputStream; import android.app.backup.BackupDataOutput; import android.app.backup.BackupHelper; import android.app.backup.BackupManager; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentResolver; 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.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.XmlResourceParser; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -51,6 +54,9 @@ import com.android.launcher3.compat.UserManagerCompat; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.protobuf.nano.MessageNano; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -58,7 +64,6 @@ import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.zip.CRC32; @@ -138,6 +143,8 @@ public class LauncherBackupHelper implements BackupHelper { private final Context mContext; private final HashSet<String> mExistingKeys; private final ArrayList<Key> mKeys; + private final ItemTypeMatcher[] mItemTypeMatchers; + private final long mUserSerial; private IconCache mIconCache; private BackupManager mBackupManager; @@ -154,6 +161,10 @@ public class LauncherBackupHelper implements BackupHelper { mExistingKeys = new HashSet<String>(); mKeys = new ArrayList<Key>(); restoreSuccessful = true; + mItemTypeMatchers = new ItemTypeMatcher[CommonAppTypeParser.SUPPORTED_TYPE_COUNT]; + + UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); + mUserSerial = userManager.getSerialNumberForUser(UserHandleCompat.myUserHandle()); } private void dataChanged() { @@ -290,6 +301,12 @@ public class LauncherBackupHelper implements BackupHelper { if (!restoreSuccessful) { return; } + if (!initializeIconCache()) { + // During restore we do not need an initialized instance of IconCache. We can create + // a temporary icon cache here, as the process will be rebooted after restore + // is complete. + mIconCache = new IconCache(mContext); + } int dataSize = data.size(); if (mBuffer.length < dataSize) { @@ -445,7 +462,7 @@ public class LauncherBackupHelper implements BackupHelper { ContentResolver cr = mContext.getContentResolver(); ContentValues values = unpackFavorite(buffer, dataSize); - cr.insert(Favorites.CONTENT_URI_NO_NOTIFICATION, values); + cr.insert(Favorites.CONTENT_URI, values); } /** @@ -594,7 +611,8 @@ public class LauncherBackupHelper implements BackupHelper { Log.w(TAG, "failed to unpack icon for " + key.name); } if (VERBOSE) Log.v(TAG, "saving restored icon as: " + key.name); - IconCache.preloadIcon(mContext, ComponentName.unflattenFromString(key.name), icon, res.dpi); + mIconCache.preloadIcon(ComponentName.unflattenFromString(key.name), icon, res.dpi, + "" /* label */, mUserSerial); } /** @@ -612,8 +630,7 @@ public class LauncherBackupHelper implements BackupHelper { return; } final ContentResolver cr = mContext.getContentResolver(); - final WidgetPreviewLoader previewLoader = new WidgetPreviewLoader(mContext); - final PagedViewCellLayout widgetSpacingLayout = new PagedViewCellLayout(mContext); + final WidgetPreviewLoader previewLoader = appState.getWidgetCache(); final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; final DeviceProfile profile = appState.getDynamicGrid().getDeviceProfile(); if (DEBUG) Log.d(TAG, "cellWidthPx: " + profile.cellWidthPx); @@ -629,7 +646,6 @@ public class LauncherBackupHelper implements BackupHelper { final long id = cursor.getLong(ID_INDEX); final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX); final int spanX = cursor.getInt(SPANX_INDEX); - final int spanY = cursor.getInt(SPANY_INDEX); final ComponentName provider = ComponentName.unflattenFromString(providerName); Key key = null; String backupKey = null; @@ -648,11 +664,10 @@ public class LauncherBackupHelper implements BackupHelper { if (DEBUG) Log.d(TAG, "I can count this high: " + backupWidgetCount); if (backupWidgetCount < MAX_WIDGETS_PER_PASS) { if (DEBUG) Log.d(TAG, "saving widget " + backupKey); - previewLoader.setPreviewSize(spanX * profile.cellWidthPx, - spanY * profile.cellHeightPx, widgetSpacingLayout); UserHandleCompat user = UserHandleCompat.myUserHandle(); writeRowToBackup(key, - packWidget(dpi, previewLoader, mIconCache, provider, user), + packWidget(dpi, previewLoader,spanX * profile.cellWidthPx, + mIconCache, provider, user), data); mKeys.add(key); backupWidgetCount ++; @@ -689,8 +704,8 @@ public class LauncherBackupHelper implements BackupHelper { 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); + mIconCache.preloadIcon(ComponentName.unflattenFromString(widget.provider), + icon, widget.icon.dpi, widget.label, mUserSerial); } } @@ -756,6 +771,17 @@ public class LauncherBackupHelper implements BackupHelper { return checksum.getValue(); } + /** + * @return true if its an hotseat item, that can be replaced during restore. + * TODO: Extend check for folders in hotseat. + */ + private boolean isReplaceableHotseatItem(Favorite favorite) { + return favorite.container == Favorites.CONTAINER_HOTSEAT + && favorite.intent != null + && (favorite.itemType == Favorites.ITEM_TYPE_APPLICATION + || favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT); + } + /** Serialize a Favorite for persistence, including a checksum wrapper. */ private Favorite packFavorite(Cursor c) { Favorite favorite = new Favorite(); @@ -788,9 +814,10 @@ public class LauncherBackupHelper implements BackupHelper { favorite.title = title; } String intentDescription = c.getString(INTENT_INDEX); + Intent intent = null; if (!TextUtils.isEmpty(intentDescription)) { try { - Intent intent = Intent.parseUri(intentDescription, 0); + intent = Intent.parseUri(intentDescription, 0); intent.removeExtra(ItemInfo.EXTRA_PROFILE); favorite.intent = intent.toUri(0); } catch (URISyntaxException e) { @@ -806,6 +833,31 @@ public class LauncherBackupHelper implements BackupHelper { } } + if (isReplaceableHotseatItem(favorite)) { + if (intent != null && intent.getComponent() != null) { + PackageManager pm = mContext.getPackageManager(); + ActivityInfo activity = null;; + try { + activity = pm.getActivityInfo(intent.getComponent(), 0); + } catch (NameNotFoundException e) { + Log.e(TAG, "Target not found", e); + } + if (activity == null) { + return favorite; + } + for (int i = 0; i < mItemTypeMatchers.length; i++) { + if (mItemTypeMatchers[i] == null) { + mItemTypeMatchers[i] = new ItemTypeMatcher( + CommonAppTypeParser.getResourceForItemType(i)); + } + if (mItemTypeMatchers[i].matches(activity, pm)) { + favorite.targetType = i; + break; + } + } + } + } + return favorite; } @@ -813,6 +865,7 @@ public class LauncherBackupHelper implements BackupHelper { private ContentValues unpackFavorite(byte[] buffer, int dataSize) throws IOException { Favorite favorite = unpackProto(new Favorite(), buffer, dataSize); + ContentValues values = new ContentValues(); values.put(Favorites._ID, favorite.id); values.put(Favorites.SCREEN, favorite.screen); @@ -863,8 +916,17 @@ public class LauncherBackupHelper implements BackupHelper { throw new InvalidBackupException("Widget not in screen bounds, aborting restore"); } } else { - // Let LauncherModel know we've been here. - values.put(LauncherSettings.Favorites.RESTORED, 1); + // Check if it is an hotseat item, that can be replaced. + if (isReplaceableHotseatItem(favorite) + && favorite.targetType != Favorite.TARGET_NONE + && favorite.targetType < CommonAppTypeParser.SUPPORTED_TYPE_COUNT) { + Log.e(TAG, "Added item type flag"); + values.put(LauncherSettings.Favorites.RESTORED, + 1 | CommonAppTypeParser.encodeItemTypeToFlag(favorite.targetType)); + } else { + // Let LauncherModel know we've been here. + values.put(LauncherSettings.Favorites.RESTORED, 1); + } // Verify placement if (favorite.container == Favorites.CONTAINER_HOTSEAT) { @@ -915,7 +977,8 @@ public class LauncherBackupHelper implements BackupHelper { } /** Serialize a widget for persistence, including a checksum wrapper. */ - private Widget packWidget(int dpi, WidgetPreviewLoader previewLoader, IconCache iconCache, + private Widget packWidget(int dpi, WidgetPreviewLoader previewLoader, + int previewWidth, IconCache iconCache, ComponentName provider, UserHandleCompat user) { final LauncherAppWidgetProviderInfo info = LauncherModel.getProviderInfo(mContext, provider, user); @@ -935,7 +998,7 @@ public class LauncherBackupHelper implements BackupHelper { } if (info.previewImage != 0) { widget.preview = new Resource(); - Bitmap preview = previewLoader.generateWidgetPreview(info, null); + Bitmap preview = previewLoader.generateWidgetPreview(info, previewWidth, null); ByteArrayOutputStream os = new ByteArrayOutputStream(); if (preview.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { widget.preview.data = os.toByteArray(); @@ -1094,9 +1157,11 @@ public class LauncherBackupHelper implements BackupHelper { final LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); if (appState == null) { - Throwable stackTrace = new Throwable(); - stackTrace.fillInStackTrace(); - Log.w(TAG, "Failed to get app state during backup/restore", stackTrace); + if (DEBUG) { + Throwable stackTrace = new Throwable(); + stackTrace.fillInStackTrace(); + Log.w(TAG, "Failed to get app state during backup/restore", stackTrace); + } return false; } mIconCache = appState.getIconCache(); @@ -1131,6 +1196,9 @@ public class LauncherBackupHelper implements BackupHelper { } private class InvalidBackupException extends IOException { + + private static final long serialVersionUID = 8931456637211665082L; + private InvalidBackupException(Throwable cause) { super(cause); } @@ -1139,4 +1207,54 @@ public class LauncherBackupHelper implements BackupHelper { super(reason); } } + + /** + * A class to check if an activity can handle one of the intents from a list of + * predefined intents. + */ + private class ItemTypeMatcher { + + private final ArrayList<Intent> mIntents; + + ItemTypeMatcher(int xml_res) { + mIntents = xml_res == 0 ? new ArrayList<Intent>() : parseIntents(xml_res); + } + + private ArrayList<Intent> parseIntents(int xml_res) { + ArrayList<Intent> intents = new ArrayList<Intent>(); + XmlResourceParser parser = mContext.getResources().getXml(xml_res); + try { + DefaultLayoutParser.beginDocument(parser, DefaultLayoutParser.TAG_RESOLVE); + final int depth = parser.getDepth(); + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } else if (DefaultLayoutParser.TAG_FAVORITE.equals(parser.getName())) { + final String uri = DefaultLayoutParser.getAttributeValue( + parser, DefaultLayoutParser.ATTR_URI); + intents.add(Intent.parseUri(uri, 0)); + } + } + } catch (URISyntaxException | XmlPullParserException | IOException e) { + Log.e(TAG, "Unable to parse " + xml_res, e); + } finally { + parser.close(); + } + return intents; + } + + public boolean matches(ActivityInfo activity, PackageManager pm) { + for (Intent intent : mIntents) { + intent.setPackage(activity.packageName); + ResolveInfo info = pm.resolveActivity(intent, 0); + if (info != null && (info.activityInfo.name.equals(activity.name) + || info.activityInfo.name.equals(activity.targetActivity))) { + return true; + } + } + return false; + } + } } diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java index d8128d6e5..2fee81c3d 100644 --- a/src/com/android/launcher3/LauncherCallbacks.java +++ b/src/com/android/launcher3/LauncherCallbacks.java @@ -50,6 +50,7 @@ public interface LauncherCallbacks { public void onLauncherProviderChange(); public void finishBindingItems(final boolean upgradePath); public void onClickAllAppsButton(View v); + public void onAllAppsShown(); public void bindAllApplications(ArrayList<AppInfo> apps); public void onClickFolderIcon(View v); public void onClickAppShortcut(View v); @@ -87,6 +88,7 @@ public interface LauncherCallbacks { public ComponentName getWallpaperPickerComponent(); public boolean overrideWallpaperDimensions(); public boolean isLauncherPreinstalled(); + public boolean overrideAllAppsSearch(); /** * Returning true will immediately result in a call to {@link #setLauncherOverlayView(ViewGroup, @@ -106,4 +108,12 @@ public interface LauncherCallbacks { public Launcher.LauncherOverlay setLauncherOverlayView(InsettableFrameLayout container, Launcher.LauncherOverlayCallbacks callbacks); + /** + * Sets the callbacks to allow any extensions to callback to the launcher. + * + * @param callbacks A set of callbacks to the Launcher, is actually a LauncherAppsCallback, but + * for implementation purposes is passed around as an object. + */ + public void setLauncherAppsCallback(Object callbacks); + } diff --git a/src/com/android/launcher3/LauncherClings.java b/src/com/android/launcher3/LauncherClings.java index ef8e8abcf..2ce8b1c59 100644 --- a/src/com/android/launcher3/LauncherClings.java +++ b/src/com/android/launcher3/LauncherClings.java @@ -35,6 +35,8 @@ import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.accessibility.AccessibilityManager; +import com.android.launcher3.util.Thunk; + class LauncherClings implements OnClickListener { private static final String MIGRATION_CLING_DISMISSED_KEY = "cling_gel.migration.dismissed"; private static final String WORKSPACE_CLING_DISMISSED_KEY = "cling_gel.workspace.dismissed"; @@ -49,7 +51,7 @@ class LauncherClings implements OnClickListener { // New Secure Setting in L private static final String SKIP_FIRST_USE_HINTS = "skip_first_use_hints"; - private Launcher mLauncher; + @Thunk Launcher mLauncher; private LayoutInflater mInflater; /** Ctor */ @@ -174,7 +176,7 @@ class LauncherClings implements OnClickListener { }); } - private void dismissLongPressCling() { + @Thunk void dismissLongPressCling() { Runnable dismissCb = new Runnable() { public void run() { dismissCling(mLauncher.findViewById(R.id.longpress_cling), null, @@ -185,7 +187,7 @@ class LauncherClings implements OnClickListener { } /** Hides the specified Cling */ - private void dismissCling(final View cling, final Runnable postAnimationCb, + @Thunk 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. diff --git a/src/com/android/launcher3/LauncherExtension.java b/src/com/android/launcher3/LauncherExtension.java index fe9bd6c23..e4fdbbc7c 100644 --- a/src/com/android/launcher3/LauncherExtension.java +++ b/src/com/android/launcher3/LauncherExtension.java @@ -124,6 +124,10 @@ public class LauncherExtension extends Launcher { } @Override + public void onAllAppsShown() { + } + + @Override public void bindAllApplications(ArrayList<AppInfo> apps) { } @@ -246,6 +250,11 @@ public class LauncherExtension extends Launcher { } @Override + public boolean overrideAllAppsSearch() { + return false; + } + + @Override public boolean isLauncherPreinstalled() { return false; } @@ -265,6 +274,11 @@ public class LauncherExtension extends Launcher { return mLauncherOverlay; } + @Override + public void setLauncherAppsCallback(Object callbacks) { + // Do nothing + } + class LauncherExtensionOverlay implements LauncherOverlay { LauncherOverlayCallbacks mLauncherOverlayCallbacks; ViewGroup mOverlayView; diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java index fa053650f..9dd8dc50c 100644 --- a/src/com/android/launcher3/LauncherFiles.java +++ b/src/com/android/launcher3/LauncherFiles.java @@ -18,23 +18,29 @@ public class LauncherFiles { public static final String DEFAULT_WALLPAPER_THUMBNAIL_OLD = "default_thumb.jpg"; public static final String LAUNCHER_DB = "launcher.db"; public static final String LAUNCHER_PREFERENCES = "launcher.preferences"; - public static final String LAUNCHES_LOG = "launches.log"; public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs"; - public static final String STATS_LOG = "stats.log"; public static final String WALLPAPER_CROP_PREFERENCES_KEY = - WallpaperCropActivity.class.getName(); + "com.android.launcher3.WallpaperCropActivity"; + public static final String MANAGED_USER_PREFERENCES_KEY = "com.android.launcher3.managedusers.prefs"; + public static final String WALLPAPER_IMAGES_DB = "saved_wallpaper_images.db"; public static final String WIDGET_PREVIEWS_DB = "widgetpreviews.db"; + public static final String APP_ICONS_DB = "app_icons.db"; public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList( DEFAULT_WALLPAPER_THUMBNAIL, DEFAULT_WALLPAPER_THUMBNAIL_OLD, LAUNCHER_DB, LAUNCHER_PREFERENCES, - LAUNCHES_LOG, SHARED_PREFERENCES_KEY + XML, - STATS_LOG, WALLPAPER_CROP_PREFERENCES_KEY + XML, WALLPAPER_IMAGES_DB, - WIDGET_PREVIEWS_DB)); + WIDGET_PREVIEWS_DB, + MANAGED_USER_PREFERENCES_KEY, + APP_ICONS_DB)); + + // TODO: Delete these files on upgrade + public static final List<String> OBSOLETE_FILES = Collections.unmodifiableList(Arrays.asList( + "launches.log", + "stats.log")); } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index b19496147..f7df6bc1a 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -36,7 +36,6 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.graphics.Rect; import android.net.Uri; import android.os.Environment; @@ -60,6 +59,8 @@ import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.ManagedProfileHeuristic; +import com.android.launcher3.util.Thunk; import java.lang.ref.WeakReference; import java.net.URISyntaxException; @@ -76,7 +77,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Set; -import java.util.TreeMap; /** * Maintains in-memory state of the Launcher. It is expected that there should be only one @@ -88,13 +88,9 @@ public class LauncherModel extends BroadcastReceiver static final boolean DEBUG_LOADERS = false; private static final boolean DEBUG_RECEIVER = false; private static final boolean REMOVE_UNRESTORED_ICONS = true; - private static final boolean ADD_MANAGED_PROFILE_SHORTCUTS = false; 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; @@ -102,19 +98,14 @@ public class LauncherModel extends BroadcastReceiver private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons private static final long INVALID_SCREEN_ID = -1L; - private final boolean mAppsCanBeOnRemoveableStorage; + @Thunk final boolean mAppsCanBeOnRemoveableStorage; private final boolean mOldContentProviderExists; - private final LauncherAppState mApp; - private final Object mLock = new Object(); - private DeferredHandler mHandler = new DeferredHandler(); - private LoaderTask mLoaderTask; - private boolean mIsLoaderTaskRunning; - - /** - * Maintain a set of packages per user, for which we added a shortcut on the workspace. - */ - private static final String INSTALLED_SHORTCUTS_SET_PREFIX = "installed_shortcuts_set_for_user_"; + @Thunk final LauncherAppState mApp; + @Thunk final Object mLock = new Object(); + @Thunk DeferredHandler mHandler = new DeferredHandler(); + @Thunk LoaderTask mLoaderTask; + @Thunk boolean mIsLoaderTaskRunning; // Specific runnable types that are run on the main thread deferred handler, this allows us to // clear all queued binding runnables when the Launcher activity is destroyed. @@ -123,17 +114,17 @@ public class LauncherModel extends BroadcastReceiver private static final String MIGRATE_AUTHORITY = "com.android.launcher2.settings"; - private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); + @Thunk static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); static { sWorkerThread.start(); } - private static final Handler sWorker = new Handler(sWorkerThread.getLooper()); + @Thunk static final Handler sWorker = new Handler(sWorkerThread.getLooper()); // We start off with everything not loaded. After that, we assume that // our monitoring of the package manager provides all updates and we never // need to do a requery. These are only ever touched from the loader thread. - private boolean mWorkspaceLoaded; - private boolean mAllAppsLoaded; + @Thunk boolean mWorkspaceLoaded; + @Thunk boolean mAllAppsLoaded; // When we are loading pages synchronously, we can't just post the binding of items on the side // pages as this delays the rotation process. Instead, we wait for a callback from the first @@ -141,7 +132,7 @@ public class LauncherModel extends BroadcastReceiver // a normal load, we also clear this set of Runnables. static final ArrayList<Runnable> mDeferredBindRunnables = new ArrayList<Runnable>(); - private WeakReference<Callbacks> mCallbacks; + @Thunk WeakReference<Callbacks> mCallbacks; // < only access in worker thread > AllAppsList mBgAllAppsList; @@ -168,9 +159,6 @@ public class LauncherModel extends BroadcastReceiver // sBgFolders is all FolderInfos created by LauncherModel. Passed to bindFolders() static final HashMap<Long, FolderInfo> sBgFolders = new HashMap<Long, FolderInfo>(); - // sBgDbIconCache is the set of ItemInfos that need to have their icons updated in the database - static final HashMap<Object, byte[]> sBgDbIconCache = new HashMap<Object, byte[]>(); - // sBgWorkspaceScreens is the ordered set of workspace screens. static final ArrayList<Long> sBgWorkspaceScreens = new ArrayList<Long>(); @@ -183,12 +171,12 @@ public class LauncherModel extends BroadcastReceiver // </ only access in worker thread > - private IconCache mIconCache; + @Thunk IconCache mIconCache; protected int mPreviousConfigMcc; - private final LauncherAppsCompat mLauncherApps; - private final UserManagerCompat mUserManager; + @Thunk final LauncherAppsCompat mLauncherApps; + @Thunk final UserManagerCompat mUserManager; public interface Callbacks { public boolean setLoadOnResume(); @@ -199,7 +187,7 @@ public class LauncherModel extends BroadcastReceiver public void bindScreens(ArrayList<Long> orderedScreenIds); public void bindAddScreens(ArrayList<Long> orderedScreenIds); public void bindFolders(HashMap<Long,FolderInfo> folders); - public void finishBindingItems(boolean upgradePath); + public void finishBindingItems(); public void bindAppWidget(LauncherAppWidgetInfo info); public void bindAllApplications(ArrayList<AppInfo> apps); public void bindAppsAdded(ArrayList<Long> newScreens, @@ -266,10 +254,10 @@ public class LauncherModel extends BroadcastReceiver /** Runs the specified runnable immediately if called from the main thread, otherwise it is * posted on the main thread handler. */ - private void runOnMainThread(Runnable r) { + @Thunk void runOnMainThread(Runnable r) { runOnMainThread(r, 0); } - private void runOnMainThread(Runnable r, int type) { + @Thunk void runOnMainThread(Runnable r, int type) { if (sWorkerThread.getThreadId() == Process.myTid()) { // If we are on the worker thread, post onto the main handler mHandler.post(r); @@ -345,9 +333,9 @@ public class LauncherModel extends BroadcastReceiver runOnWorkerThread(r); } - public void addAndBindAddedWorkspaceApps(final Context context, + public void addAndBindAddedWorkspaceItems(final Context context, final ArrayList<ItemInfo> workspaceApps) { - addAndBindAddedWorkspaceApps(context, workspaceApps, + addAndBindAddedWorkspaceItems(context, workspaceApps, new ScreenPosProvider() { @Override @@ -380,7 +368,7 @@ public class LauncherModel extends BroadcastReceiver * Find a position on the screen for the given size or adds a new screen. * @return screenId and the coordinates for the item. */ - private static Pair<Long, int[]> findSpaceForItem( + @Thunk static Pair<Long, int[]> findSpaceForItem( Context context, ScreenPosProvider preferredScreen, int fallbackStartScreen, @@ -491,13 +479,7 @@ public class LauncherModel extends BroadcastReceiver Runnable r = new Runnable() { public void run() { final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<Long>(); - - ArrayList<Long> workspaceScreens = new ArrayList<Long>(); - TreeMap<Integer, Long> orderedScreens = loadWorkspaceScreensDb(context); - for (Integer i : orderedScreens.keySet()) { - long screenId = orderedScreens.get(i); - workspaceScreens.add(screenId); - } + ArrayList<Long> workspaceScreens = loadWorkspaceScreensDb(context); // Find appropriate space for the item. Pair<Long, int[]> coords = findSpaceForItem(context, preferredScreen, @@ -531,7 +513,7 @@ public class LauncherModel extends BroadcastReceiver * @param fallbackStartScreen the screen to start search for empty space if * preferredScreen is not available. */ - public void addAndBindAddedWorkspaceApps(final Context context, + public void addAndBindAddedWorkspaceItems(final Context context, final ArrayList<ItemInfo> workspaceApps, final ScreenPosProvider preferredScreen, final int fallbackStartScreen, @@ -549,16 +531,10 @@ public class LauncherModel extends BroadcastReceiver // Get the list of workspace screens. We need to append to this list and // can not use sBgWorkspaceScreens because loadWorkspace() may not have been // called. - ArrayList<Long> workspaceScreens = new ArrayList<Long>(); - TreeMap<Integer, Long> orderedScreens = loadWorkspaceScreensDb(context); - for (Integer i : orderedScreens.keySet()) { - long screenId = orderedScreens.get(i); - workspaceScreens.add(screenId); - } - + ArrayList<Long> workspaceScreens = loadWorkspaceScreensDb(context); synchronized(sBgLock) { for (ItemInfo item : workspaceApps) { - if (!allowDuplicate) { + if (!allowDuplicate && item instanceof ShortcutInfo) { // Short-circuit this logic if the icon exists somewhere on the workspace if (shortcutExists(context, item.title.toString(), item.getIntent(), item.user)) { @@ -573,21 +549,21 @@ public class LauncherModel extends BroadcastReceiver long screenId = coords.first; int[] cordinates = coords.second; - ShortcutInfo shortcutInfo; - if (item instanceof ShortcutInfo) { - shortcutInfo = (ShortcutInfo) item; + ItemInfo itemInfo; + if (item instanceof ShortcutInfo || item instanceof FolderInfo) { + itemInfo = item; } else if (item instanceof AppInfo) { - shortcutInfo = ((AppInfo) item).makeShortcut(); + itemInfo = ((AppInfo) item).makeShortcut(); } else { throw new RuntimeException("Unexpected info type"); } // Add the shortcut to the db - addItemToDatabase(context, shortcutInfo, + addItemToDatabase(context, itemInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, - screenId, cordinates[0], cordinates[1], false); + screenId, cordinates[0], cordinates[1]); // Save the ShortcutInfo for binding in the workspace - addedShortcutsFinal.add(shortcutInfo); + addedShortcutsFinal.add(itemInfo); } } @@ -671,7 +647,7 @@ public class LauncherModel extends BroadcastReceiver long screenId, int cellX, int cellY) { if (item.container == ItemInfo.NO_ID) { // From all apps - addItemToDatabase(context, item, container, screenId, cellX, cellY, false); + addItemToDatabase(context, item, container, screenId, cellX, cellY); } else { // From somewhere else moveItemInDatabase(context, item, container, screenId, cellX, cellY); @@ -737,7 +713,7 @@ public class LauncherModel extends BroadcastReceiver static void updateItemInDatabaseHelper(Context context, final ContentValues values, final ItemInfo item, final String callingFunction) { final long itemId = item.id; - final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false); + final Uri uri = LauncherSettings.Favorites.getContentUri(itemId); final ContentResolver cr = context.getContentResolver(); final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); @@ -763,7 +739,7 @@ public class LauncherModel extends BroadcastReceiver for (int i = 0; i < count; i++) { ItemInfo item = items.get(i); final long itemId = item.id; - final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false); + final Uri uri = LauncherSettings.Favorites.getContentUri(itemId); ContentValues values = valuesList.get(i); ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); @@ -982,6 +958,7 @@ public class LauncherModel extends BroadcastReceiver final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); + final int optionsIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.OPTIONS); FolderInfo folderInfo = null; switch (c.getInt(itemTypeIndex)) { @@ -996,6 +973,7 @@ public class LauncherModel extends BroadcastReceiver folderInfo.screenId = c.getInt(screenIndex); folderInfo.cellX = c.getInt(cellXIndex); folderInfo.cellY = c.getInt(cellYIndex); + folderInfo.options = c.getInt(optionsIndex); return folderInfo; } @@ -1010,8 +988,8 @@ public class LauncherModel extends BroadcastReceiver * Add an item to the database in a specified container. Sets the container, screen, cellX and * cellY fields of the item. Also assigns an ID to the item. */ - static void addItemToDatabase(Context context, final ItemInfo item, final long container, - final long screenId, final int cellX, final int cellY, final boolean notify) { + public static void addItemToDatabase(Context context, final ItemInfo item, final long container, + final long screenId, final int cellX, final int cellY) { item.container = container; item.cellX = cellX; item.cellY = cellY; @@ -1034,8 +1012,7 @@ public class LauncherModel extends BroadcastReceiver final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); Runnable r = new Runnable() { public void run() { - cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI : - LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values); + cr.insert(LauncherSettings.Favorites.CONTENT_URI, values); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { @@ -1102,7 +1079,7 @@ public class LauncherModel extends BroadcastReceiver * @param context * @param item */ - static void deleteItemFromDatabase(Context context, final ItemInfo item) { + public static void deleteItemFromDatabase(Context context, final ItemInfo item) { ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); items.add(item); deleteItemsFromDatabase(context, items); @@ -1115,11 +1092,10 @@ public class LauncherModel extends BroadcastReceiver */ static void deleteItemsFromDatabase(Context context, final ArrayList<? extends ItemInfo> items) { final ContentResolver cr = context.getContentResolver(); - Runnable r = new Runnable() { public void run() { for (ItemInfo item : items) { - final Uri uri = LauncherSettings.Favorites.getContentUri(item.id, false); + final Uri uri = LauncherSettings.Favorites.getContentUri(item.id); cr.delete(uri, null, null); // Lock on mBgLock *after* the db operation @@ -1147,7 +1123,6 @@ public class LauncherModel extends BroadcastReceiver break; } sBgItemsIdMap.remove(item.id); - sBgDbIconCache.remove(item); } } } @@ -1210,27 +1185,25 @@ public class LauncherModel extends BroadcastReceiver /** * Remove the contents of the specified folder from the database */ - static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) { + public static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) { final ContentResolver cr = context.getContentResolver(); Runnable r = new Runnable() { public void run() { - cr.delete(LauncherSettings.Favorites.getContentUri(info.id, false), null, null); + cr.delete(LauncherSettings.Favorites.getContentUri(info.id), null, null); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { sBgItemsIdMap.remove(info.id); sBgFolders.remove(info.id); - sBgDbIconCache.remove(info); sBgWorkspaceItems.remove(info); } - cr.delete(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, + cr.delete(LauncherSettings.Favorites.CONTENT_URI, LauncherSettings.Favorites.CONTAINER + "=" + info.id, null); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { for (ItemInfo childInfo : info.contents) { sBgItemsIdMap.remove(childInfo.id); - sBgDbIconCache.remove(childInfo); } } } @@ -1443,40 +1416,31 @@ public class LauncherModel extends BroadcastReceiver } } - /** Loads the workspace screens db into a map of Rank -> ScreenId */ - private static TreeMap<Integer, Long> loadWorkspaceScreensDb(Context context) { + /** + * Loads the workspace screen ids in an ordered list. + */ + @Thunk static ArrayList<Long> loadWorkspaceScreensDb(Context context) { final ContentResolver contentResolver = context.getContentResolver(); final Uri screensUri = LauncherSettings.WorkspaceScreens.CONTENT_URI; - final Cursor sc = contentResolver.query(screensUri, null, null, null, null); - TreeMap<Integer, Long> orderedScreens = new TreeMap<Integer, Long>(); + // Get screens ordered by rank. + final Cursor sc = contentResolver.query(screensUri, null, null, null, + LauncherSettings.WorkspaceScreens.SCREEN_RANK); + ArrayList<Long> screenIds = new ArrayList<Long>(); try { - final int idIndex = sc.getColumnIndexOrThrow( - LauncherSettings.WorkspaceScreens._ID); - final int rankIndex = sc.getColumnIndexOrThrow( - LauncherSettings.WorkspaceScreens.SCREEN_RANK); + final int idIndex = sc.getColumnIndexOrThrow(LauncherSettings.WorkspaceScreens._ID); while (sc.moveToNext()) { try { - long screenId = sc.getLong(idIndex); - int rank = sc.getInt(rankIndex); - orderedScreens.put(rank, screenId); + screenIds.add(sc.getLong(idIndex)); } catch (Exception e) { - Launcher.addDumpLog(TAG, "Desktop items loading interrupted - invalid screens: " + e, true); + Launcher.addDumpLog(TAG, "Desktop items loading interrupted" + + " - invalid screens: " + e, true); } } } finally { sc.close(); } - - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - loadWorkspaceScreensDb()", true); - ArrayList<String> orderedScreensPairs= new ArrayList<String>(); - for (Integer i : orderedScreens.keySet()) { - orderedScreensPairs.add("{ " + i + ": " + orderedScreens.get(i) + " }"); - } - Launcher.addDumpLog(TAG, "11683562 - screens: " + - TextUtils.join(", ", orderedScreensPairs), true); - return orderedScreens; + return screenIds; } public boolean isAllAppsLoaded() { @@ -1501,17 +1465,14 @@ public class LauncherModel extends BroadcastReceiver private class LoaderTask implements Runnable { private Context mContext; private boolean mIsLaunching; - private boolean mIsLoadingAndBindingWorkspace; + @Thunk boolean mIsLoadingAndBindingWorkspace; private boolean mStopped; - private boolean mLoadAndBindStepFinished; + @Thunk boolean mLoadAndBindStepFinished; private int mFlags; - private HashMap<Object, CharSequence> mLabelCache; - LoaderTask(Context context, boolean isLaunching, int flags) { mContext = context; mIsLaunching = isLaunching; - mLabelCache = new HashMap<Object, CharSequence>(); mFlags = flags; } @@ -1523,8 +1484,7 @@ public class LauncherModel extends BroadcastReceiver return mIsLoadingAndBindingWorkspace; } - /** Returns whether this is an upgrade path */ - private boolean loadAndBindWorkspace() { + private void loadAndBindWorkspace() { mIsLoadingAndBindingWorkspace = true; // Load the workspace @@ -1532,20 +1492,18 @@ public class LauncherModel extends BroadcastReceiver Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded); } - boolean isUpgradePath = false; if (!mWorkspaceLoaded) { - isUpgradePath = loadWorkspace(); + loadWorkspace(); synchronized (LoaderTask.this) { if (mStopped) { - return isUpgradePath; + return; } mWorkspaceLoaded = true; } } // Bind the workspace - bindWorkspace(-1, isUpgradePath); - return isUpgradePath; + bindWorkspace(-1); } private void waitForIdle() { @@ -1614,15 +1572,13 @@ public class LauncherModel extends BroadcastReceiver // Divide the set of loaded items into those that we are binding synchronously, and // everything else that is to be bound normally (asynchronously). - bindWorkspace(synchronousBindPage, false); + bindWorkspace(synchronousBindPage); // XXX: For now, continue posting the binding of AllApps as there are other issues that // arise from that. onlyBindAllApps(); } public void run() { - boolean isUpgrade = false; - synchronized (mLock) { mIsLoaderTaskRunning = true; } @@ -1639,7 +1595,7 @@ public class LauncherModel extends BroadcastReceiver ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND); } if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace"); - isUpgrade = loadAndBindWorkspace(); + loadAndBindWorkspace(); if (mStopped) { break keep_running; @@ -1659,29 +1615,15 @@ public class LauncherModel extends BroadcastReceiver if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps"); loadAndBindAllApps(); + // Remove entries for packages which changed while the launcher was dead. + LauncherAppState.getInstance().getWidgetCache().removeObsoletePreviews(); + // Restore the default thread priority after we are done loading items synchronized (mLock) { android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); } } - // Update the saved icons if necessary - if (DEBUG_LOADERS) Log.d(TAG, "Comparing loaded icons to database icons"); - synchronized (sBgLock) { - for (Object key : sBgDbIconCache.keySet()) { - updateSavedIcon(mContext, (ShortcutInfo) key, sBgDbIconCache.get(key)); - } - sBgDbIconCache.clear(); - } - - if (LauncherAppState.isDisableAllApps()) { - // Ensure that all the applications that are in the system are - // represented on the home screen. - if (!UPGRADE_USE_MORE_APPS_FOLDER || !isUpgrade) { - verifyApplications(); - } - } - // Clear out this reference, otherwise we end up holding it until all of the // callback runnables are done. mContext = null; @@ -1732,28 +1674,6 @@ public class LauncherModel extends BroadcastReceiver } } - private void verifyApplications() { - final Context context = mApp.getContext(); - - // Cross reference all the applications in our apps list with items in the workspace - ArrayList<ItemInfo> tmpInfos; - ArrayList<ItemInfo> added = new ArrayList<ItemInfo>(); - synchronized (sBgLock) { - for (AppInfo app : mBgAllAppsList.data) { - tmpInfos = getItemInfoForComponentName(app.componentName, app.user); - if (tmpInfos.isEmpty()) { - // We are missing an application icon, so add this to the workspace - added.add(app); - // This is a rare event, so lets log it - Log.e(TAG, "Missing Application on load: " + app); - } - } - } - if (!added.isEmpty()) { - addAndBindAddedWorkspaceApps(context, added); - } - } - // check & update map of what's occupied; used to discard overlapping/invalid items private boolean checkItemPlacement(HashMap<Long, ItemInfo[][]> occupied, ItemInfo item) { LauncherAppState app = LauncherAppState.getInstance(); @@ -1851,13 +1771,11 @@ public class LauncherModel extends BroadcastReceiver sBgAppWidgets.clear(); sBgFolders.clear(); sBgItemsIdMap.clear(); - sBgDbIconCache.clear(); sBgWorkspaceScreens.clear(); } } - /** Returns whether this is an upgrade path */ - private boolean loadWorkspace() { + private void loadWorkspace() { // Log to disk Launcher.addDumpLog(TAG, "11683562 - loadWorkspace()", true); @@ -1891,12 +1809,6 @@ public class LauncherModel extends BroadcastReceiver LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary(); } - // 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 @@ -1904,7 +1816,7 @@ public class LauncherModel extends BroadcastReceiver final ArrayList<Long> itemsToRemove = new ArrayList<Long>(); final ArrayList<Long> restoredRows = new ArrayList<Long>(); - final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION; + final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI; if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri); final Cursor c = contentResolver.query(contentUri, null, null, null, null); @@ -1950,9 +1862,8 @@ public class LauncherModel extends BroadcastReceiver 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); + final int optionsIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.OPTIONS); ShortcutInfo info; String intentDescription; @@ -1978,6 +1889,7 @@ public class LauncherModel extends BroadcastReceiver user = mUserManager.getUserForSerialNumber(serialNumber); int promiseType = c.getInt(restoredIndex); int disabledState = 0; + boolean itemReplaced = false; if (user == null) { // User has been deleted remove the item. itemsToRemove.add(id); @@ -2009,9 +1921,7 @@ public class LauncherModel extends BroadcastReceiver 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); + updateItem(id, values); } } @@ -2041,10 +1951,27 @@ public class LauncherModel extends BroadcastReceiver 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); - + updateItem(id, values); + } else if ((promiseType & ShortcutInfo.FLAG_RESTORED_APP_TYPE) != 0) { + // This is a common app. Try to replace this. + int appType = CommonAppTypeParser.decodeItemTypeFromFlag(promiseType); + CommonAppTypeParser parser = new CommonAppTypeParser(id, appType, context); + if (parser.findDefaultApp()) { + // Default app found. Replace it. + intent = parser.parsedIntent; + cn = intent.getComponent(); + ContentValues values = parser.parsedValues; + values.put(LauncherSettings.Favorites.RESTORED, 0); + updateItem(id, values); + restored = false; + itemReplaced = true; + + } else if (REMOVE_UNRESTORED_ICONS) { + Launcher.addDumpLog(TAG, + "Unrestored package removed: " + cn, true); + itemsToRemove.add(id); + continue; + } } else if (REMOVE_UNRESTORED_ICONS) { Launcher.addDumpLog(TAG, "Unrestored package removed: " + cn, true); @@ -2090,12 +2017,26 @@ public class LauncherModel extends BroadcastReceiver continue; } - if (restored) { + container = c.getInt(containerIndex); + boolean useLowResIcon = container >= 0 && + c.getInt(rankIndex) >= FolderIcon.NUM_ITEMS_IN_PREVIEW; + + if (itemReplaced) { + if (user.equals(UserHandleCompat.myUserHandle())) { + info = getAppShortcutInfo(manager, intent, user, context, null, + iconIndex, titleIndex, false, useLowResIcon); + } else { + // Don't replace items for other profiles. + itemsToRemove.add(id); + continue; + } + } else if (restored) { if (user.equals(UserHandleCompat.myUserHandle())) { Launcher.addDumpLog(TAG, "constructing info for partially restored package", true); - info = getRestoredItemInfo(c, titleIndex, intent, promiseType); + info = getRestoredItemInfo(c, titleIndex, intent, + promiseType, useLowResIcon); intent = getRestoredItemIntent(c, context, intent); } else { // Don't restore items for other profiles. @@ -2104,8 +2045,8 @@ public class LauncherModel extends BroadcastReceiver } } else if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { - info = getShortcutInfo(manager, intent, user, context, c, - iconIndex, titleIndex, mLabelCache, allowMissingTarget); + info = getAppShortcutInfo(manager, intent, user, context, c, + iconIndex, titleIndex, allowMissingTarget, useLowResIcon); } else { info = getShortcutInfo(c, context, iconTypeIndex, iconPackageIndex, iconResourceIndex, iconIndex, @@ -2127,7 +2068,6 @@ public class LauncherModel extends BroadcastReceiver if (info != null) { info.id = id; info.intent = intent; - container = c.getInt(containerIndex); info.container = container; info.screenId = c.getInt(screenIndex); info.cellX = c.getInt(cellXIndex); @@ -2160,10 +2100,6 @@ public class LauncherModel extends BroadcastReceiver break; } sBgItemsIdMap.put(info.id, info); - - // now that we've loaded everthing re-save it with the - // icon in case it disappears somehow. - queueIconToBeChecked(sBgDbIconCache, info, c, iconIndex); } else { throw new RuntimeException("Unexpected null ShortcutInfo"); } @@ -2182,6 +2118,7 @@ public class LauncherModel extends BroadcastReceiver folderInfo.cellY = c.getInt(cellYIndex); folderInfo.spanX = 1; folderInfo.spanY = 1; + folderInfo.options = c.getInt(optionsIndex); // check & update map of what's occupied if (!checkItemPlacement(occupied, folderInfo)) { @@ -2326,9 +2263,7 @@ public class LauncherModel extends BroadcastReceiver providerName); values.put(LauncherSettings.Favorites.RESTORED, appWidgetInfo.restoreStatus); - String where = BaseColumns._ID + "= ?"; - String[] args = {Long.toString(id)}; - contentResolver.update(contentUri, values, where, args); + updateItem(id, values); } } sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); @@ -2349,7 +2284,7 @@ public class LauncherModel extends BroadcastReceiver // Break early if we've stopped loading if (mStopped) { clearSBgDataStructures(); - return false; + return; } if (itemsToRemove.size() > 0) { @@ -2362,8 +2297,7 @@ public class LauncherModel extends BroadcastReceiver } // Don't notify content observers try { - client.delete(LauncherSettings.Favorites.getContentUri(id, false), - null, null); + client.delete(LauncherSettings.Favorites.getContentUri(id), null, null); } catch (RemoteException e) { Log.w(TAG, "Could not remove id = " + id); } @@ -2382,7 +2316,7 @@ public class LauncherModel extends BroadcastReceiver selectionBuilder.append(")"); ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites.RESTORED, 0); - updater.update(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, + updater.update(LauncherSettings.Favorites.CONTENT_URI, values, selectionBuilder.toString(), null); } catch (RemoteException e) { Log.w(TAG, "Could not update restored rows"); @@ -2395,63 +2329,29 @@ public class LauncherModel extends BroadcastReceiver null, sWorker); } - if (loadedOldDb) { - long maxScreenId = 0; - // If we're importing we use the old screen order. - for (ItemInfo item: sBgItemsIdMap.values()) { - long screenId = item.screenId; - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && - !sBgWorkspaceScreens.contains(screenId)) { - sBgWorkspaceScreens.add(screenId); - if (screenId > maxScreenId) { - maxScreenId = screenId; - } - } - } - Collections.sort(sBgWorkspaceScreens); - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - maxScreenId: " + maxScreenId, true); - Launcher.addDumpLog(TAG, "11683562 - sBgWorkspaceScreens: " + - TextUtils.join(", ", sBgWorkspaceScreens), true); - - LauncherAppState.getLauncherProvider().updateMaxScreenId(maxScreenId); - updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); + sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext)); + // Log to disk + Launcher.addDumpLog(TAG, "11683562 - sBgWorkspaceScreens: " + + TextUtils.join(", ", sBgWorkspaceScreens), true); - // Update the max item id after we load an old db - long maxItemId = 0; - // If we're importing we use the old screen order. - for (ItemInfo item: sBgItemsIdMap.values()) { - maxItemId = Math.max(maxItemId, item.id); - } - LauncherAppState.getLauncherProvider().updateMaxItemId(maxItemId); - } else { - TreeMap<Integer, Long> orderedScreens = loadWorkspaceScreensDb(mContext); - for (Integer i : orderedScreens.keySet()) { - sBgWorkspaceScreens.add(orderedScreens.get(i)); - } - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - sBgWorkspaceScreens: " + - TextUtils.join(", ", sBgWorkspaceScreens), true); - - // Remove any empty screens - ArrayList<Long> unusedScreens = new ArrayList<Long>(sBgWorkspaceScreens); - for (ItemInfo item: sBgItemsIdMap.values()) { - long screenId = item.screenId; - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && - unusedScreens.contains(screenId)) { - unusedScreens.remove(screenId); - } + // Remove any empty screens + ArrayList<Long> unusedScreens = new ArrayList<Long>(sBgWorkspaceScreens); + for (ItemInfo item: sBgItemsIdMap.values()) { + long screenId = item.screenId; + if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && + unusedScreens.contains(screenId)) { + unusedScreens.remove(screenId); } + } - // If there are any empty screens remove them, and update. - if (unusedScreens.size() != 0) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - unusedScreens (to be removed): " + - TextUtils.join(", ", unusedScreens), true); + // If there are any empty screens remove them, and update. + if (unusedScreens.size() != 0) { + // Log to disk + Launcher.addDumpLog(TAG, "11683562 - unusedScreens (to be removed): " + + TextUtils.join(", ", unusedScreens), true); - sBgWorkspaceScreens.removeAll(unusedScreens); - updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); - } + sBgWorkspaceScreens.removeAll(unusedScreens); + updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); } if (DEBUG_LOADERS) { @@ -2480,7 +2380,17 @@ public class LauncherModel extends BroadcastReceiver } } } - return loadedOldDb; + } + + /** + * Partially updates the item without any notification. Must be called on the worker thread. + */ + private void updateItem(long itemId, ContentValues update) { + mContext.getContentResolver().update( + LauncherSettings.Favorites.CONTENT_URI, + update, + BaseColumns._ID + "= ?", + new String[]{Long.toString(itemId)}); } /** Filters the set of items who are directly or indirectly (via another container) on the @@ -2677,7 +2587,7 @@ public class LauncherModel extends BroadcastReceiver /** * Binds all loaded data to actual views on the main thread. */ - private void bindWorkspace(int synchronizeBindPage, final boolean isUpgradePath) { + private void bindWorkspace(int synchronizeBindPage) { final long t = SystemClock.uptimeMillis(); Runnable r; @@ -2781,7 +2691,7 @@ public class LauncherModel extends BroadcastReceiver public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { - callbacks.finishBindingItems(isUpgradePath); + callbacks.finishBindingItems(); } // If we're profiling, ensure this is the last thing in the queue. @@ -2885,39 +2795,55 @@ public class LauncherModel extends BroadcastReceiver 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"); + + // Update icon cache + HashSet<String> updatedPackages = mIconCache.updateDBIcons(user, apps); + + // If any package icon has changed (app was updated while launcher was dead), + // update the corresponding shortcuts. + if (!updatedPackages.isEmpty()) { + final ArrayList<ShortcutInfo> updates = new ArrayList<ShortcutInfo>(); + synchronized (sBgLock) { + for (ItemInfo info : sBgItemsIdMap.values()) { + if (info instanceof ShortcutInfo && user.equals(info.user) + && info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { + ShortcutInfo si = (ShortcutInfo) info; + ComponentName cn = si.getTargetComponent(); + if (cn != null && updatedPackages.contains(cn.getPackageName())) { + si.updateIcon(mIconCache); + updates.add(si); + } + } + } + } + + if (!updates.isEmpty()) { + final UserHandleCompat userFinal = user; + mHandler.post(new Runnable() { + + public void run() { + Callbacks cb = getCallback(); + if (cb != null) { + cb.bindShortcutsChanged( + updates, new ArrayList<ShortcutInfo>(), userFinal); + } + } + }); + } } // 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)); + mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache)); } - if (ADD_MANAGED_PROFILE_SHORTCUTS && !user.equals(UserHandleCompat.myUserHandle())) { - // Add shortcuts for packages which were installed while launcher was dead. - String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX - + mUserManager.getSerialNumberForUser(user); - Set<String> packagesAdded = prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET); - HashSet<String> newPackageSet = new HashSet<String>(); - - for (LauncherActivityInfoCompat info : apps) { - String packageName = info.getComponentName().getPackageName(); - if (!packagesAdded.contains(packageName) - && !newPackageSet.contains(packageName)) { - InstallShortcutReceiver.queueInstallShortcut(info, mContext); - } - newPackageSet.add(packageName); + if (!user.equals(UserHandleCompat.myUserHandle())) { + ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(mContext, user); + if (heuristic != null) { + heuristic.processUserApps(apps); } - - prefs.edit().putStringSet(shortcutsSetKey, newPackageSet).commit(); } } // Huh? Shouldn't this be inside the Runnable below? @@ -2940,6 +2866,8 @@ public class LauncherModel extends BroadcastReceiver } } }); + // Cleanup any data stored for a deleted user. + ManagedProfileHeuristic.processAllUsers(profiles, mContext); if (DEBUG_LOADERS) { Log.d(TAG, "Icons processed in " @@ -2962,7 +2890,7 @@ public class LauncherModel extends BroadcastReceiver sWorker.post(task); } - private class AppsAvailabilityCheck extends BroadcastReceiver { + @Thunk class AppsAvailabilityCheck extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -3027,68 +2955,43 @@ public class LauncherModel extends BroadcastReceiver final String[] packages = mPackages; final int N = packages.length; switch (mOp) { - case OP_ADD: + case OP_ADD: { for (int i=0; i<N; i++) { if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]); - mIconCache.remove(packages[i], mUser); + mIconCache.updateIconsForPkg(packages[i], mUser); mBgAllAppsList.addPackage(context, packages[i], mUser); } - // Auto add shortcuts for added packages. - if (ADD_MANAGED_PROFILE_SHORTCUTS - && !UserHandleCompat.myUserHandle().equals(mUser)) { - SharedPreferences prefs = context.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); - String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX - + mUserManager.getSerialNumberForUser(mUser); - Set<String> shortcutSet = new HashSet<String>( - prefs.getStringSet(shortcutsSetKey,Collections.EMPTY_SET)); - - for (int i=0; i<N; i++) { - if (!shortcutSet.contains(packages[i])) { - shortcutSet.add(packages[i]); - List<LauncherActivityInfoCompat> activities = - mLauncherApps.getActivityList(packages[i], mUser); - if (activities != null && !activities.isEmpty()) { - InstallShortcutReceiver.queueInstallShortcut( - activities.get(0), context); - } - } - } - - prefs.edit().putStringSet(shortcutsSetKey, shortcutSet).commit(); + ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser); + if (heuristic != null) { + heuristic.processPackageAdd(mPackages); } break; + } case OP_UPDATE: for (int i=0; i<N; i++) { if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]); + mIconCache.updateIconsForPkg(packages[i], mUser); mBgAllAppsList.updatePackage(context, packages[i], mUser); - WidgetPreviewLoader.removePackageFromDb( - mApp.getWidgetPreviewCacheDb(), packages[i]); + mApp.getWidgetCache().removePackage(packages[i], mUser); } break; - case OP_REMOVE: - // Remove the packageName for the set of auto-installed shortcuts. This - // will ensure that the shortcut when the app is installed again. - if (ADD_MANAGED_PROFILE_SHORTCUTS - && !UserHandleCompat.myUserHandle().equals(mUser)) { - SharedPreferences prefs = context.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); - String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX - + mUserManager.getSerialNumberForUser(mUser); - HashSet<String> shortcutSet = new HashSet<String>( - prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET)); - shortcutSet.removeAll(Arrays.asList(mPackages)); - prefs.edit().putStringSet(shortcutsSetKey, shortcutSet).commit(); + case OP_REMOVE: { + ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser); + if (heuristic != null) { + heuristic.processPackageRemoved(mPackages); + } + for (int i=0; i<N; i++) { + if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); + mIconCache.removeIconsForPkg(packages[i], mUser); } // Fall through + } case OP_UNAVAILABLE: - boolean clearCache = mOp == OP_REMOVE; for (int i=0; i<N; i++) { if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); - mBgAllAppsList.removePackage(packages[i], mUser, clearCache); - WidgetPreviewLoader.removePackageFromDb( - mApp.getWidgetPreviewCacheDb(), packages[i]); + mBgAllAppsList.removePackage(packages[i], mUser); + mApp.getWidgetCache().removePackage(packages[i], mUser); } break; } @@ -3120,13 +3023,7 @@ public class LauncherModel extends BroadcastReceiver new HashMap<ComponentName, AppInfo>(); if (added != null) { - // Ensure that we add all the workspace applications to the db - if (LauncherAppState.isDisableAllApps()) { - final ArrayList<ItemInfo> addedInfos = new ArrayList<ItemInfo>(added); - addAndBindAddedWorkspaceApps(context, addedInfos); - } else { - addAppsToAllApps(context, added); - } + addAppsToAllApps(context, added); for (AppInfo ai : added) { addedOrUpdatedApps.put(ai.componentName, ai); } @@ -3179,7 +3076,6 @@ public class LauncherModel extends BroadcastReceiver AppInfo appInfo = addedOrUpdatedApps.get(cn); if (si.isPromise()) { - mIconCache.deletePreloadedIcon(cn, mUser); if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) { // Auto install icon PackageManager pm = context.getPackageManager(); @@ -3210,6 +3106,9 @@ public class LauncherModel extends BroadcastReceiver si.status &= ~ShortcutInfo.FLAG_RESTORED_ICON & ~ShortcutInfo.FLAG_AUTOINTALL_ICON & ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; + if (appInfo != null) { + si.flags = appInfo.flags; + } infoUpdated = true; si.updateIcon(mIconCache); @@ -3392,7 +3291,7 @@ public class LauncherModel extends BroadcastReceiver return widgetsAndShortcuts; } - private static boolean isPackageDisabled(Context context, String packageName, + @Thunk static boolean isPackageDisabled(Context context, String packageName, UserHandleCompat user) { final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); return !launcherApps.isPackageEnabledForProfile(packageName, user); @@ -3424,10 +3323,10 @@ public class LauncherModel extends BroadcastReceiver * to a package that is not yet installed on the system. */ public ShortcutInfo getRestoredItemInfo(Cursor cursor, int titleIndex, Intent intent, - int promiseType) { + int promiseType, boolean useLowResIcon) { final ShortcutInfo info = new ShortcutInfo(); info.user = UserHandleCompat.myUserHandle(); - mIconCache.getTitleAndIcon(info, intent, info.user, true); + mIconCache.getTitleAndIcon(info, intent, info.user, useLowResIcon); if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) { String title = (cursor != null) ? cursor.getString(titleIndex) : null; @@ -3455,7 +3354,7 @@ public class LauncherModel extends BroadcastReceiver * Make an Intent object for a restored application or shortcut item that points * to the market page for the item. */ - private Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) { + @Thunk Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) { ComponentName componentName = intent.getComponent(); return getMarketIntent(componentName.getPackageName()); } @@ -3470,22 +3369,13 @@ public class LauncherModel extends BroadcastReceiver } /** - * 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, - UserHandleCompat user, Context context) { - return getShortcutInfo(manager, intent, user, context, null, -1, -1, null, false); - } - - /** * Make an ShortcutInfo object for a shortcut that is an application. * * If c is not null, then it will be used to fill in missing data like the title and icon. */ - public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, + public ShortcutInfo getAppShortcutInfo(PackageManager manager, Intent intent, UserHandleCompat user, Context context, Cursor c, int iconIndex, int titleIndex, - HashMap<Object, CharSequence> labelCache, boolean allowMissingTarget) { + boolean allowMissingTarget, boolean useLowResIcon) { if (user == null) { Log.d(TAG, "Null user found in getShortcutInfo"); return null; @@ -3507,52 +3397,29 @@ public class LauncherModel extends BroadcastReceiver } 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); - - // the db - if (icon == null) { - if (c != null) { - icon = getIconFromCursor(c, iconIndex, context); - } + mIconCache.getTitleAndIcon(info, componentName, lai, user, false, useLowResIcon); + if (mIconCache.isDefaultIcon(info.getIcon(mIconCache), user) && c != null) { + Bitmap icon = Utilities.createIconBitmap(c, iconIndex, context); + info.setIcon(icon == null ? mIconCache.getDefaultIcon(user) : icon); } - // the fallback icon - if (icon == null) { - 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 (info.title == null && lai != null) { - info.title = lai.getLabel(); - if (labelCache != null) { - labelCache.put(componentName, info.title); - } - } // from the db - if (info.title == null) { - if (c != null) { - info.title = c.getString(titleIndex); - } + if (TextUtils.isEmpty(info.title) && c != null) { + info.title = c.getString(titleIndex); } + // fall back to the class name of the activity if (info.title == null) { info.title = componentName.getClassName(); } + info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; info.user = user; info.contentDescription = mUserManager.getBadgedLabelForUser( info.title.toString(), info.user); + if (lai != null) { + info.flags = AppInfo.initFlags(lai); + } return info; } @@ -3585,7 +3452,7 @@ public class LauncherModel extends BroadcastReceiver return new ArrayList<ItemInfo>(filtered); } - private ArrayList<ItemInfo> getItemInfoForComponentName(final ComponentName cname, + @Thunk ArrayList<ItemInfo> getItemInfoForComponentName(final ComponentName cname, final UserHandleCompat user) { ItemInfoFilter filter = new ItemInfoFilter() { @Override @@ -3603,7 +3470,7 @@ public class LauncherModel extends BroadcastReceiver /** * Make an ShortcutInfo object for a shortcut that isn't an application. */ - private ShortcutInfo getShortcutInfo(Cursor c, Context context, + @Thunk ShortcutInfo getShortcutInfo(Cursor c, Context context, int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, int iconIndex, int titleIndex) { @@ -3627,7 +3494,7 @@ public class LauncherModel extends BroadcastReceiver icon = Utilities.createIconBitmap(packageName, resourceName, mIconCache, context); // the db if (icon == null) { - icon = getIconFromCursor(c, iconIndex, context); + icon = Utilities.createIconBitmap(c, iconIndex, context); } // the fallback icon if (icon == null) { @@ -3636,7 +3503,7 @@ public class LauncherModel extends BroadcastReceiver } break; case LauncherSettings.Favorites.ICON_TYPE_BITMAP: - icon = getIconFromCursor(c, iconIndex, context); + icon = Utilities.createIconBitmap(c, iconIndex, context); if (icon == null) { icon = mIconCache.getDefaultIcon(info.user); info.customIcon = false; @@ -3655,22 +3522,6 @@ public class LauncherModel extends BroadcastReceiver return info; } - Bitmap getIconFromCursor(Cursor c, int iconIndex, Context context) { - @SuppressWarnings("all") // suppress dead code warning - final boolean debug = false; - if (debug) { - Log.d(TAG, "getIconFromCursor app=" - + c.getString(c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE))); - } - byte[] data = c.getBlob(iconIndex); - try { - return Utilities.createIconBitmap( - BitmapFactory.decodeByteArray(data, 0, data.length), context); - } catch (Exception e) { - return null; - } - } - ShortcutInfo infoFromShortcutIntent(Context context, Intent data) { Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); @@ -3719,50 +3570,11 @@ public class LauncherModel extends BroadcastReceiver return info; } - boolean queueIconToBeChecked(HashMap<Object, byte[]> cache, ShortcutInfo info, Cursor c, - int iconIndex) { - // If apps can't be on SD, don't even bother. - if (!mAppsCanBeOnRemoveableStorage) { - return false; - } - // If this icon doesn't have a custom icon, check to see - // what's stored in the DB, and if it doesn't match what - // we're going to show, store what we are going to show back - // into the DB. We do this so when we're loading, if the - // package manager can't find an icon (for example because - // the app is on SD) then we can use that instead. - if (!info.customIcon && !info.usingFallbackIcon) { - cache.put(info, c.getBlob(iconIndex)); - return true; - } - return false; - } - void updateSavedIcon(Context context, ShortcutInfo info, byte[] data) { - boolean needSave = false; - try { - if (data != null) { - Bitmap saved = BitmapFactory.decodeByteArray(data, 0, data.length); - Bitmap loaded = info.getIcon(mIconCache); - needSave = !saved.sameAs(loaded); - } else { - needSave = true; - } - } catch (Exception e) { - needSave = true; - } - if (needSave) { - Log.d(TAG, "going to save icon bitmap for info=" + info); - // This is slower than is ideal, but this only happens once - // or when the app is updated with a new icon. - updateItemInDatabase(context, info); - } - } - /** * Return an existing FolderInfo object if we have encountered this ID previously, * or make a new one. */ - private static FolderInfo findOrMakeFolder(HashMap<Long, FolderInfo> folders, long id) { + @Thunk static FolderInfo findOrMakeFolder(HashMap<Long, FolderInfo> folders, long id) { // See if a placeholder was created for us already FolderInfo folderInfo = folders.get(id); if (folderInfo == null) { @@ -3773,79 +3585,13 @@ public class LauncherModel extends BroadcastReceiver return folderInfo; } - public static final Comparator<AppInfo> getAppNameComparator() { - final Collator collator = Collator.getInstance(); - return new Comparator<AppInfo>() { - public final int compare(AppInfo a, AppInfo b) { - 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()); - } - } - }; - } - public static final Comparator<AppInfo> APP_INSTALL_TIME_COMPARATOR - = new Comparator<AppInfo>() { - public final int compare(AppInfo a, AppInfo b) { - if (a.firstInstallTime < b.firstInstallTime) return 1; - if (a.firstInstallTime > b.firstInstallTime) return -1; - return 0; - } - }; - static ComponentName getComponentNameFromResolveInfo(ResolveInfo info) { - if (info.activityInfo != null) { - return new ComponentName(info.activityInfo.packageName, info.activityInfo.name); - } else { - return new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name); - } - } - public static class ShortcutNameComparator implements Comparator<LauncherActivityInfoCompat> { - private Collator mCollator; - private HashMap<Object, CharSequence> mLabelCache; - ShortcutNameComparator(PackageManager pm) { - mLabelCache = new HashMap<Object, CharSequence>(); - mCollator = Collator.getInstance(); - } - ShortcutNameComparator(HashMap<Object, CharSequence> labelCache) { - mLabelCache = labelCache; - mCollator = Collator.getInstance(); - } - 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).toString(); - } else { - labelA = a.getLabel().toString().trim(); - - mLabelCache.put(keyA, labelA); - } - if (mLabelCache.containsKey(keyB)) { - labelB = mLabelCache.get(keyB).toString(); - } else { - labelB = b.getLabel().toString().trim(); - - mLabelCache.put(keyB, labelB); - } - return mCollator.compare(labelA, labelB); - } - }; public static class WidgetAndShortcutNameComparator implements Comparator<Object> { private final AppWidgetManagerCompat mManager; private final PackageManager mPackageManager; private final HashMap<Object, String> mLabelCache; private final Collator mCollator; - WidgetAndShortcutNameComparator(Context context) { + public WidgetAndShortcutNameComparator(Context context) { mManager = AppWidgetManagerCompat.getInstance(context); mPackageManager = context.getPackageManager(); mLabelCache = new HashMap<Object, String>(); @@ -3894,4 +3640,13 @@ public class LauncherModel extends BroadcastReceiver public Callbacks getCallback() { return mCallbacks != null ? mCallbacks.get() : null; } + + /** + * @return {@link FolderInfo} if its already loaded. + */ + public FolderInfo findFolderById(Long folderId) { + synchronized (sBgLock) { + return sBgFolders.get(folderId); + } + } } diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 1040b1173..f9f5ae1bb 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -16,6 +16,7 @@ package com.android.launcher3; +import android.annotation.TargetApi; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; import android.content.ComponentName; @@ -29,14 +30,19 @@ import android.content.Context; import android.content.Intent; import android.content.OperationApplicationException; import android.content.SharedPreferences; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; +import android.database.sqlite.SQLiteStatement; import android.net.Uri; +import android.os.Build; +import android.os.Bundle; import android.os.StrictMode; +import android.os.UserManager; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; @@ -46,6 +52,7 @@ 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 com.android.launcher3.util.Thunk; import java.io.File; import java.net.URISyntaxException; @@ -57,20 +64,19 @@ public class LauncherProvider extends ContentProvider { private static final String TAG = "Launcher.LauncherProvider"; private static final boolean LOGD = false; - private static final int MIN_DATABASE_VERSION = 12; - private static final int DATABASE_VERSION = 22; + private static final int DATABASE_VERSION = 24; static final String OLD_AUTHORITY = "com.android.launcher2.settings"; static final String AUTHORITY = ProviderConfig.AUTHORITY; - static final String TABLE_FAVORITES = "favorites"; - static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens"; - static final String PARAMETER_NOTIFY = "notify"; - static final String UPGRADED_FROM_OLD_DATABASE = "UPGRADED_FROM_OLD_DATABASE"; + static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME; + static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME; static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd"; + private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name"; + private LauncherProviderChangeListener mListener; /** @@ -126,7 +132,7 @@ public class LauncherProvider extends ContentProvider { return result; } - private static long dbInsertAndCheck(DatabaseHelper helper, + @Thunk static long dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) { if (values == null) { throw new RuntimeException("Error: attempting to insert null values"); @@ -144,7 +150,8 @@ public class LauncherProvider extends ContentProvider { // 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)) { + final boolean isExternalAll = externalAdd != null && "true".equals(externalAdd); + if (isExternalAll) { if (!mOpenHelper.initializeExternalAdd(initialValues)) { return null; } @@ -156,7 +163,14 @@ public class LauncherProvider extends ContentProvider { if (rowId < 0) return null; uri = ContentUris.withAppendedId(uri, rowId); - sendNotify(uri); + notifyListeners(); + + if (isExternalAll) { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app != null) { + app.reloadWorkspace(); + } + } return uri; } @@ -181,7 +195,7 @@ public class LauncherProvider extends ContentProvider { db.endTransaction(); } - sendNotify(uri); + notifyListeners(); return values.length; } @@ -205,7 +219,7 @@ public class LauncherProvider extends ContentProvider { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.delete(args.table, args.where, args.args); - if (count > 0) sendNotify(uri); + if (count > 0) notifyListeners(); return count; } @@ -217,17 +231,12 @@ public class LauncherProvider extends ContentProvider { addModifiedTime(values); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.update(args.table, values, args.where, args.args); - if (count > 0) sendNotify(uri); + if (count > 0) notifyListeners(); return count; } - private void sendNotify(Uri uri) { - String notify = uri.getQueryParameter(PARAMETER_NOTIFY); - if (notify == null || "true".equals(notify)) { - getContext().getContentResolver().notifyChange(uri, null); - } - + private void notifyListeners() { // always notify the backup agent LauncherBackupAgentHelper.dataChanged(getContext()); if (mListener != null) { @@ -235,7 +244,7 @@ public class LauncherProvider extends ContentProvider { } } - private static void addModifiedTime(ContentValues values) { + @Thunk static void addModifiedTime(ContentValues values) { values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis()); } @@ -251,12 +260,6 @@ public class LauncherProvider extends ContentProvider { return mOpenHelper.generateNewScreenId(); } - // This is only required one time while loading the workspace during the - // upgrade path, and should never be called from anywhere else. - public void updateMaxScreenId(long maxScreenId) { - mOpenHelper.updateMaxScreenId(maxScreenId); - } - /** * Clears all the data for a fresh start. */ @@ -274,9 +277,10 @@ public class LauncherProvider extends ContentProvider { /** * 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 + * 1) From the app restrictions + * 2) From a package provided by play store + * 3) From a partner configuration APK, already in the system image + * 4) The default configuration for the particular device */ synchronized public void loadDefaultFavoritesIfNecessary() { String spKey = LauncherAppState.getSharedPreferencesKey(); @@ -285,9 +289,11 @@ public class LauncherProvider extends ContentProvider { if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) { Log.d(TAG, "loading default workspace"); - AutoInstallsLayout loader = AutoInstallsLayout.get(getContext(), - mOpenHelper.mAppWidgetHost, mOpenHelper); - + AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(); + if (loader == null) { + loader = AutoInstallsLayout.get(getContext(), + mOpenHelper.mAppWidgetHost, mOpenHelper); + } if (loader == null) { final Partner partner = Partner.get(getContext().getPackageManager()); if (partner != null && partner.hasDefaultLayout()) { @@ -321,6 +327,40 @@ public class LauncherProvider extends ContentProvider { } } + /** + * Creates workspace loader from an XML resource listed in the app restrictions. + * + * @return the loader if the restrictions are set and the resource exists; null otherwise. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction() { + // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18 + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + return null; + } + + Context ctx = getContext(); + UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE); + Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName()); + if (bundle == null) { + return null; + } + + String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME); + if (packageName != null) { + try { + Resources targetResources = ctx.getPackageManager() + .getResourcesForApplication(packageName); + return AutoInstallsLayout.get(ctx, packageName, targetResources, + mOpenHelper.mAppWidgetHost, mOpenHelper); + } catch (NameNotFoundException e) { + Log.e(TAG, "Target package for restricted profile not found", e); + return null; + } + } + return null; + } + private DefaultLayoutParser getDefaultLayoutParser() { int defaultLayout = LauncherAppState.getInstance() .getDynamicGrid().getDeviceProfile().defaultLayoutId; @@ -350,7 +390,7 @@ public class LauncherProvider extends ContentProvider { private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback { private final Context mContext; - private final AppWidgetHost mAppWidgetHost; + @Thunk final AppWidgetHost mAppWidgetHost; private long mMaxItemId = -1; private long mMaxScreenId = -1; @@ -421,7 +461,8 @@ public class LauncherProvider extends ContentProvider { "modified INTEGER NOT NULL DEFAULT 0," + "restored INTEGER NOT NULL DEFAULT 0," + "profileId INTEGER DEFAULT " + userSerialNumber + "," + - "rank INTEGER NOT NULL DEFAULT 0" + + "rank INTEGER NOT NULL DEFAULT 0," + + "options INTEGER NOT NULL DEFAULT 0" + ");"); addWorkspacesTable(db); @@ -478,142 +519,116 @@ public class LauncherProvider extends ContentProvider { private void setFlagJustLoadedOldDb() { String spKey = LauncherAppState.getSharedPreferencesKey(); SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, true); - editor.putBoolean(EMPTY_DATABASE_CREATED, false); - editor.commit(); + sp.edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit(); } private void setFlagEmptyDbCreated() { String spKey = LauncherAppState.getSharedPreferencesKey(); SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putBoolean(EMPTY_DATABASE_CREATED, true); - editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, false); - editor.commit(); + sp.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit(); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion); - - int version = oldVersion; - if (version < MIN_DATABASE_VERSION) { - // The version cannot be lower that this, as Launcher3 never supported a lower + switch (oldVersion) { + // The version cannot be lower that 12, as Launcher3 never supported a lower // version of the DB. - createEmptyDB(db); - version = DATABASE_VERSION; - } - - if (version < 13) { - // With the new shrink-wrapped and re-orderable workspaces, it makes sense - // to persist workspace screens and their relative order. - mMaxScreenId = 0; - - addWorkspacesTable(db); - version = 13; - } - - if (version < 14) { - db.beginTransaction(); - try { - // Insert new column for holding widget provider name - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN appWidgetProvider TEXT;"); - db.setTransactionSuccessful(); - version = 14; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); + case 12: { + // With the new shrink-wrapped and re-orderable workspaces, it makes sense + // to persist workspace screens and their relative order. + mMaxScreenId = 0; + addWorkspacesTable(db); } - } - - if (version < 15) { - db.beginTransaction(); - try { - // Insert new column for holding update timestamp - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); - db.execSQL("ALTER TABLE workspaceScreens " + - "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); - db.setTransactionSuccessful(); - version = 15; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); + case 13: { + db.beginTransaction(); + try { + // Insert new column for holding widget provider name + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN appWidgetProvider TEXT;"); + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + // Old version remains, which means we wipe old data + break; + } finally { + db.endTransaction(); + } } - } - - - if (version < 16) { - db.beginTransaction(); - try { - // Insert new column for holding restore status - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;"); - db.setTransactionSuccessful(); - version = 16; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); + case 14: { + db.beginTransaction(); + try { + // Insert new column for holding update timestamp + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); + db.execSQL("ALTER TABLE workspaceScreens " + + "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + // Old version remains, which means we wipe old data + break; + } finally { + db.endTransaction(); + } } - } - - if (version < 17) { - // We use the db version upgrade here to identify users who may not have seen - // clings yet (because they weren't available), but for whom the clings are now - // available (tablet users). Because one of the possible cling flows (migration) - // is very destructive (wipes out workspaces), we want to prevent this from showing - // until clear data. We do so by marking that the clings have been shown. - LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext); - 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; + case 15: { + if (!addIntegerColumn(db, Favorites.RESTORED, 0)) { + // Old version remains, which means we wipe old data + break; + } } - // else old version remains, which means we wipe old data - } - - if (version < 21) { - if (updateFolderItemsRank(db, true)) { - version = 21; + case 16: { + // We use the db version upgrade here to identify users who may not have seen + // clings yet (because they weren't available), but for whom the clings are now + // available (tablet users). Because one of the possible cling flows (migration) + // is very destructive (wipes out workspaces), we want to prevent this from showing + // until clear data. We do so by marking that the clings have been shown. + LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext); } - } - - if (version == 21) { - // Recreate workspace table with screen id a primary key - if (recreateWorkspaceTable(db)) { - version = 22; + case 17: { + // No-op + } + case 18: { + // 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); + } + case 19: { + // Add userId column + if (!addProfileColumn(db)) { + // Old version remains, which means we wipe old data + break; + } + } + case 20: + if (!updateFolderItemsRank(db, true)) { + break; + } + case 21: + // Recreate workspace table with screen id a primary key + if (!recreateWorkspaceTable(db)) { + break; + } + case 22: { + if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) { + // Old version remains, which means we wipe old data + break; + } + } + case 23: + convertShortcutsToLauncherActivities(db); + case 24: { + // DB Upgraded successfully + return; } } - if (version != DATABASE_VERSION) { - Log.w(TAG, "Destroying all old data."); - createEmptyDB(db); - } + // DB was not upgraded + Log.w(TAG, "Destroying all old data."); + createEmptyDB(db); } @Override @@ -624,7 +639,6 @@ public class LauncherProvider extends ContentProvider { createEmptyDB(db); } - /** * Clears all the data for a fresh start. */ @@ -635,6 +649,63 @@ public class LauncherProvider extends ContentProvider { } /** + * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid + * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}. + */ + private void convertShortcutsToLauncherActivities(SQLiteDatabase db) { + db.beginTransaction(); + Cursor c = null; + SQLiteStatement updateStmt = null; + + try { + // Only consider the primary user as other users can't have a shortcut. + long userSerial = UserManagerCompat.getInstance(mContext) + .getSerialNumberForUser(UserHandleCompat.myUserHandle()); + c = db.query(TABLE_FAVORITES, new String[] { + Favorites._ID, + Favorites.INTENT, + }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial, + null, null, null, null); + + updateStmt = db.compileStatement("UPDATE favorites SET itemType=" + + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?"); + + final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); + final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT); + + while (c.moveToNext()) { + String intentDescription = c.getString(intentIndex); + Intent intent; + try { + intent = Intent.parseUri(intentDescription, 0); + } catch (URISyntaxException e) { + Log.e(TAG, "Unable to parse intent", e); + continue; + } + + if (!InstallShortcutReceiver.isLauncherActivity(intent, mContext)) { + continue; + } + + long id = c.getLong(idIndex); + updateStmt.bindLong(1, id); + updateStmt.execute(); + } + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.w(TAG, "Error deduping shortcuts", ex); + } finally { + db.endTransaction(); + if (c != null) { + c.close(); + } + if (updateStmt != null) { + updateStmt.close(); + } + } + } + + /** * Recreates workspace table and migrates data to the new table. */ public boolean recreateWorkspaceTable(SQLiteDatabase db) { @@ -682,7 +753,7 @@ public class LauncherProvider extends ContentProvider { return true; } - private boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) { + @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) { db.beginTransaction(); try { if (addRankColumn) { @@ -715,20 +786,21 @@ public class LauncherProvider extends ContentProvider { } private boolean addProfileColumn(SQLiteDatabase db) { + UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); + // Default to the serial number of this user, for older + // shortcuts. + long userSerialNumber = userManager.getSerialNumberForUser( + UserHandleCompat.myUserHandle()); + return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber); + } + + private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) { 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.execSQL("ALTER TABLE favorites ADD COLUMN " + + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";"); db.setTransactionSuccessful(); } catch (SQLException ex) { - // Old version remains, which means we wipe old data Log.e(TAG, ex.getMessage(), ex); return false; } finally { @@ -770,23 +842,7 @@ public class LauncherProvider extends ContentProvider { } private long initializeMaxItemId(SQLiteDatabase db) { - Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null); - - // get the result - final int maxIdIndex = 0; - long id = -1; - if (c != null && c.moveToNext()) { - id = c.getLong(maxIdIndex); - } - if (c != null) { - c.close(); - } - - if (id == -1) { - throw new RuntimeException("Error: could not query max item id"); - } - - return id; + return getMaxId(db, TABLE_FAVORITES); } // Generates a new ID to use for an workspace screen in your database. This method @@ -804,35 +860,11 @@ public class LauncherProvider extends ContentProvider { return mMaxScreenId; } - public void updateMaxScreenId(long maxScreenId) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - updateMaxScreenId(): " + maxScreenId, true); - mMaxScreenId = maxScreenId; - } - private long initializeMaxScreenId(SQLiteDatabase db) { - Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null); - - // get the result - final int maxIdIndex = 0; - long id = -1; - if (c != null && c.moveToNext()) { - id = c.getLong(maxIdIndex); - } - if (c != null) { - c.close(); - } - - if (id == -1) { - throw new RuntimeException("Error: could not query max screen id"); - } - - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true); - return id; + return getMaxId(db, TABLE_WORKSPACE_SCREENS); } - private boolean initializeExternalAdd(ContentValues values) { + @Thunk boolean initializeExternalAdd(ContentValues values) { // 1. Ensure that externally added items have a valid item id long id = generateNewItemId(); values.put(LauncherSettings.Favorites._ID, id); @@ -919,7 +951,7 @@ public class LauncherProvider extends ContentProvider { return rank; } - private int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { + @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { ArrayList<Long> screenIds = new ArrayList<Long>(); // TODO: Use multiple loaders with fall-back and transaction. int count = loader.loadLayout(db, screenIds); @@ -946,7 +978,7 @@ public class LauncherProvider extends ContentProvider { return count; } - private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { + @Thunk void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { final ContentResolver resolver = mContext.getContentResolver(); Cursor c = null; int count = 0; @@ -1242,6 +1274,27 @@ public class LauncherProvider extends ContentProvider { } } + /** + * @return the max _id in the provided table. + */ + @Thunk static long getMaxId(SQLiteDatabase db, String table) { + Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null); + // get the result + long id = -1; + if (c != null && c.moveToNext()) { + id = c.getLong(0); + } + if (c != null) { + c.close(); + } + + if (id == -1) { + throw new RuntimeException("Error: could not query max id in " + table); + } + + return id; + } + static class SqlArguments { public final String table; public final String where; diff --git a/src/com/android/launcher3/LauncherScroller.java b/src/com/android/launcher3/LauncherScroller.java index 3bd0a78c4..a9b49556b 100644 --- a/src/com/android/launcher3/LauncherScroller.java +++ b/src/com/android/launcher3/LauncherScroller.java @@ -20,7 +20,6 @@ import android.animation.TimeInterpolator; import android.content.Context; import android.hardware.SensorManager; import android.os.Build; -import android.util.FloatMath; import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; @@ -409,7 +408,7 @@ public class LauncherScroller { float dx = (float) (mFinalX - mStartX); float dy = (float) (mFinalY - mStartY); - float hyp = FloatMath.sqrt(dx * dx + dy * dy); + float hyp = (float) Math.hypot(dx, dy); float ndx = dx / hyp; float ndy = dy / hyp; @@ -426,7 +425,7 @@ public class LauncherScroller { mMode = FLING_MODE; mFinished = false; - float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); + float velocity = (float) Math.hypot(velocityX, velocityY); mVelocity = velocity; mDuration = getSplineFlingDuration(velocity); diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 13fd7ee30..90e60e450 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -19,10 +19,12 @@ package com.android.launcher3; import android.net.Uri; import android.provider.BaseColumns; +import com.android.launcher3.config.ProviderConfig; + /** * Settings related utilities. */ -class LauncherSettings { +public class LauncherSettings { /** Columns required on table staht will be subject to backup and restore. */ static interface ChangeLogColumns extends BaseColumns { /** @@ -45,7 +47,7 @@ class LauncherSettings { * an Intent that can be launched. * <P>Type: TEXT</P> */ - static final String INTENT = "intent"; + public static final String INTENT = "intent"; /** * The type of the gesture @@ -104,72 +106,59 @@ class LauncherSettings { * * Tracks the order of workspace screens. */ - static final class WorkspaceScreens implements ChangeLogColumns { + public static final class WorkspaceScreens implements ChangeLogColumns { + + public static final String TABLE_NAME = "workspaceScreens"; + /** * The content:// style URL for this table */ static final Uri CONTENT_URI = Uri.parse("content://" + - LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_WORKSPACE_SCREENS + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); + ProviderConfig.AUTHORITY + "/" + TABLE_NAME); /** * The rank of this screen -- ie. how it is ordered relative to the other screens. * <P>Type: INTEGER</P> */ - static final String SCREEN_RANK = "screenRank"; + public static final String SCREEN_RANK = "screenRank"; } /** * Favorites. */ - static final class Favorites implements BaseLauncherColumns { - /** - * The content:// style URL for this table - */ - static final Uri CONTENT_URI = Uri.parse("content://" + - LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); + public static final class Favorites implements BaseLauncherColumns { - /** - * The content:// style URL for this table - */ - static final Uri OLD_CONTENT_URI = Uri.parse("content://" + - LauncherProvider.OLD_AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); + public static final String TABLE_NAME = "favorites"; /** - * The content:// style URL for this table. When this Uri is used, no notification is - * sent if the content changes. + * The content:// style URL for this table */ - static final Uri CONTENT_URI_NO_NOTIFICATION = Uri.parse("content://" + - LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=false"); + public static final Uri CONTENT_URI = Uri.parse("content://" + + ProviderConfig.AUTHORITY + "/" + TABLE_NAME); /** * The content:// style URL for a given row, identified by its id. * * @param id The row id. - * @param notify True to send a notification is the content changes. * * @return The unique content URL for the specified row. */ - static Uri getContentUri(long id, boolean notify) { - return Uri.parse("content://" + LauncherProvider.AUTHORITY + - "/" + LauncherProvider.TABLE_FAVORITES + "/" + id + "?" + - LauncherProvider.PARAMETER_NOTIFY + "=" + notify); + static Uri getContentUri(long id) { + return Uri.parse("content://" + ProviderConfig.AUTHORITY + + "/" + TABLE_NAME + "/" + id); } /** * The container holding the favorite * <P>Type: INTEGER</P> */ - static final String CONTAINER = "container"; + public static final String CONTAINER = "container"; /** * The icon is a resource identified by a package name and an integer id. */ - static final int CONTAINER_DESKTOP = -100; - static final int CONTAINER_HOTSEAT = -101; + public static final int CONTAINER_DESKTOP = -100; + public static final int CONTAINER_HOTSEAT = -101; static final String containerToString(int container) { switch (container) { @@ -183,7 +172,7 @@ class LauncherSettings { * The screen holding the favorite (if container is CONTAINER_DESKTOP) * <P>Type: INTEGER</P> */ - static final String SCREEN = "screen"; + public static final String SCREEN = "screen"; /** * The X coordinate of the cell holding the favorite @@ -236,12 +225,12 @@ class LauncherSettings { /** * The favorite is a widget */ - static final int ITEM_TYPE_APPWIDGET = 4; + public static final int ITEM_TYPE_APPWIDGET = 4; /** * The favorite is a custom widget provided by the launcher */ - static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5; + public static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5; /** * The favorite is a clock @@ -309,5 +298,11 @@ class LauncherSettings { * <p>Type: INTEGER</p> */ static final String RANK = "rank"; + + /** + * Stores general flag based options for {@link ItemInfo}s. + * <p>Type: INTEGER</p> + */ + static final String OPTIONS = "options"; } } diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java new file mode 100644 index 000000000..8ba5c60f3 --- /dev/null +++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java @@ -0,0 +1,783 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; +import android.content.res.Resources; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.WidgetsContainerView; + +import java.util.HashMap; + +/** + * TODO: figure out what kind of tests we can write for this + * + * Things to test when changing the following class. + * - Home from workspace + * - from center screen + * - from other screens + * - Home from all apps + * - from center screen + * - from other screens + * - Back from all apps + * - from center screen + * - from other screens + * - Launch app from workspace and quit + * - with back + * - with home + * - Launch app from all apps and quit + * - with back + * - with home + * - Go to a screen that's not the default, then all + * apps, and launch and app, and go back + * - with back + * -with home + * - On workspace, long press power and go back + * - with back + * - with home + * - On all apps, long press power and go back + * - with back + * - with home + * - On workspace, power off + * - On all apps, power off + * - Launch an app and turn off the screen while in that app + * - Go back with home key + * - Go back with back key TODO: make this not go to workspace + * - From all apps + * - From workspace + * - Enter and exit car mode (becuase it causes an extra configuration changed) + * - From all apps + * - From the center workspace + * - From another workspace + */ +public class LauncherStateTransitionAnimation { + + /** + * Callbacks made during the state transition + */ + interface Callbacks { + public void onStateTransitionHideSearchBar(); + } + + /** + * Private callbacks made during transition setup. + */ + static abstract class PrivateTransitionCallbacks { + void onRevealViewVisible(View revealView, View contentView, View allAppsButtonView) {} + void onAnimationComplete(View revealView, View contentView, View allAppsButtonView) {} + float getMaterialRevealViewFinalAlpha(View revealView) { + return 0; + } + float getMaterialRevealViewFinalXDrift(View revealView) { + return 0; + } + float getMaterialRevealViewFinalYDrift(View revealView) { + return 0; + } + float getMaterialRevealViewStartFinalRadius() { + return 0; + } + AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(View revealView, + View allAppsButtonView) { + return null; + } + } + + public static final String TAG = "LauncherStateTransitionAnimation"; + + // Flags to determine how to set the layers on views before the transition animation + public static final int BUILD_LAYER = 0; + public static final int BUILD_AND_SET_LAYER = 1; + public static final int SINGLE_FRAME_DELAY = 16; + + @Thunk Launcher mLauncher; + @Thunk Callbacks mCb; + @Thunk AnimatorSet mStateAnimation; + + public LauncherStateTransitionAnimation(Launcher l, Callbacks cb) { + mLauncher = l; + mCb = cb; + } + + /** + * Starts an animation to the apps view. + */ + public void startAnimationToAllApps(final boolean animated) { + final AppsContainerView toView = mLauncher.getAppsView(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + private int[] mAllAppsToPanelDelta; + + @Override + public void onRevealViewVisible(View revealView, View contentView, + View allAppsButtonView) { + toView.setBackground(null); + // Get the y delta between the center of the page and the center of the all apps + // button + mAllAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, + allAppsButtonView, null); + } + @Override + public float getMaterialRevealViewFinalAlpha(View revealView) { + return 1f; + } + @Override + public float getMaterialRevealViewFinalXDrift(View revealView) { + return mAllAppsToPanelDelta[0]; + } + @Override + public float getMaterialRevealViewFinalYDrift(View revealView) { + return mAllAppsToPanelDelta[1]; + } + @Override + public float getMaterialRevealViewStartFinalRadius() { + int allAppsButtonSize = LauncherAppState.getInstance(). + getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize; + return allAppsButtonSize / 2; + } + @Override + public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( + final View revealView, final View allAppsButtonView) { + return new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + allAppsButtonView.setVisibility(View.INVISIBLE); + } + public void onAnimationEnd(Animator animation) { + allAppsButtonView.setVisibility(View.VISIBLE); + } + }; + } + }; + startAnimationToOverlay(Workspace.State.NORMAL_HIDDEN, toView, toView.getContentView(), + toView.getRevealView(), animated, false /* hideSearchBar */, cb); + } + + /** + * Starts an animation to the widgets view. + */ + public void startAnimationToWidgets(final boolean animated) { + final WidgetsContainerView toView = mLauncher.getWidgetsView(); + final Resources res = mLauncher.getResources(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + public void onRevealViewVisible(View revealView, View contentView, + View allAppsButtonView) { + revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); + } + @Override + public float getMaterialRevealViewFinalAlpha(View revealView) { + return 0.3f; + } + @Override + public float getMaterialRevealViewFinalYDrift(View revealView) { + return revealView.getMeasuredHeight() / 2; + } + }; + startAnimationToOverlay(Workspace.State.OVERVIEW_HIDDEN, toView, + toView.getContentView(), toView.getRevealView(), animated, true /* hideSearchBar */, + cb); + } + + /** + * Starts and animation to the workspace from the current overlay view. + */ + public void startAnimationToWorkspace(final Launcher.State fromState, + final Workspace.State toWorkspaceState, final boolean animated, + final Runnable onCompleteRunnable) { + if (toWorkspaceState != Workspace.State.NORMAL && + toWorkspaceState != Workspace.State.SPRING_LOADED && + toWorkspaceState != Workspace.State.OVERVIEW) { + Log.e(TAG, "Unexpected call to startAnimationToWorkspace"); + } + + if (fromState == Launcher.State.APPS || fromState == Launcher.State.APPS_SPRING_LOADED) { + startAnimationToWorkspaceFromAllApps(fromState, toWorkspaceState, animated, + onCompleteRunnable); + } else { + startAnimationToWorkspaceFromWidgets(fromState, toWorkspaceState, animated, + onCompleteRunnable); + } + } + + /** + * Creates and starts a new animation to a particular overlay view. + */ + private void startAnimationToOverlay(final Workspace.State toWorkspaceState, final View toView, + final View contentView, final View revealView, final boolean animated, + final boolean hideSearchBar, final PrivateTransitionCallbacks pCb) { + final Resources res = mLauncher.getResources(); + final boolean material = Utilities.isLmpOrAbove(); + final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime); + final int itemsAlphaStagger = + res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); + + final View allAppsButtonView = mLauncher.getAllAppsButton(); + final View fromView = mLauncher.getWorkspace(); + + final HashMap<View, Integer> layerViews = new HashMap<>(); + + // If for some reason our views aren't initialized, don't animate + boolean initialized = allAppsButtonView != null; + + // Cancel the current animation + cancelAnimation(); + + // Create the workspace animation. + // NOTE: this call apparently also sets the state for the workspace if !animated + Animator workspaceAnim = mLauncher.getWorkspace().getChangeStateAnimation( + toWorkspaceState, animated, layerViews); + + if (animated && initialized) { + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + + // Setup the reveal view animation + int width = revealView.getMeasuredWidth(); + int height = revealView.getMeasuredHeight(); + float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4); + revealView.setVisibility(View.VISIBLE); + revealView.setAlpha(0f); + revealView.setTranslationY(0f); + revealView.setTranslationX(0f); + pCb.onRevealViewVisible(revealView, contentView, allAppsButtonView); + + // Calculate the final animation values + final float revealViewToAlpha; + final float revealViewToXDrift; + final float revealViewToYDrift; + if (material) { + revealViewToAlpha = pCb.getMaterialRevealViewFinalAlpha(revealView); + revealViewToYDrift = pCb.getMaterialRevealViewFinalYDrift(revealView); + revealViewToXDrift = pCb.getMaterialRevealViewFinalXDrift(revealView); + } else { + revealViewToAlpha = 0f; + revealViewToYDrift = 2 * height / 3; + revealViewToXDrift = 0; + } + + // Create the animators + PropertyValuesHolder panelAlpha = + PropertyValuesHolder.ofFloat("alpha", revealViewToAlpha, 1f); + PropertyValuesHolder panelDriftY = + PropertyValuesHolder.ofFloat("translationY", revealViewToYDrift, 0); + PropertyValuesHolder panelDriftX = + PropertyValuesHolder.ofFloat("translationX", revealViewToXDrift, 0); + ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView, + panelAlpha, panelDriftY, panelDriftX); + panelAlphaAndDrift.setDuration(revealDuration); + panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + + // Play the animation + layerViews.put(revealView, BUILD_AND_SET_LAYER); + mStateAnimation.play(panelAlphaAndDrift); + + // Setup the animation for the content view + contentView.setVisibility(View.VISIBLE); + contentView.setAlpha(0f); + contentView.setTranslationY(revealViewToYDrift); + layerViews.put(contentView, BUILD_AND_SET_LAYER); + + // Create the individual animators + ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY", + revealViewToYDrift, 0); + pageDrift.setDuration(revealDuration); + pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + pageDrift.setStartDelay(itemsAlphaStagger); + mStateAnimation.play(pageDrift); + + ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f); + itemsAlpha.setDuration(revealDuration); + itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); + itemsAlpha.setStartDelay(itemsAlphaStagger); + mStateAnimation.play(itemsAlpha); + + if (material) { + // Animate the all apps button + float startRadius = pCb.getMaterialRevealViewStartFinalRadius(); + AnimatorListenerAdapter listener = pCb.getMaterialRevealViewAnimatorListener( + revealView, allAppsButtonView); + Animator reveal = ViewAnimationUtils.createCircularReveal(revealView, width / 2, + height / 2, startRadius, revealRadius); + reveal.setDuration(revealDuration); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + if (listener != null) { + reveal.addListener(listener); + } + mStateAnimation.play(reveal); + } + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); + + // Hide the reveal view + revealView.setVisibility(View.INVISIBLE); + pCb.onAnimationComplete(revealView, contentView, allAppsButtonView); + + // Disable all necessary layers + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + if (hideSearchBar) { + mCb.onStateTransitionHideSearchBar(); + } + + // This can hold unnecessary references to views. + mStateAnimation = null; + } + + }); + + // Play the workspace animation + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + + // Dispatch the prepare transition signal + dispatchOnLauncherTransitionPrepare(fromView, animated, false); + dispatchOnLauncherTransitionPrepare(toView, animated, false); + + + 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); + + // Enable all necessary layers + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + if (Utilities.isViewAttachedToWindow(v)) { + v.buildLayer(); + } + } + + // Focus the new view + toView.requestFocus(); + + mStateAnimation.start(); + } + }; + toView.bringToFront(); + toView.setVisibility(View.VISIBLE); + toView.post(startAnimRunnable); + } else { + toView.setTranslationX(0.0f); + toView.setTranslationY(0.0f); + toView.setScaleX(1.0f); + toView.setScaleY(1.0f); + toView.setVisibility(View.VISIBLE); + toView.bringToFront(); + + // Show the content view + contentView.setVisibility(View.VISIBLE); + + if (hideSearchBar) { + mCb.onStateTransitionHideSearchBar(); + } + + dispatchOnLauncherTransitionPrepare(fromView, animated, false); + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionPrepare(toView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); + } + } + + /** + * Starts and animation to the workspace from the apps view. + */ + private void startAnimationToWorkspaceFromAllApps(final Launcher.State fromState, + final Workspace.State toWorkspaceState, final boolean animated, + final Runnable onCompleteRunnable) { + AppsContainerView appsView = mLauncher.getAppsView(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + int[] mAllAppsToPanelDelta; + + @Override + public void onRevealViewVisible(View revealView, View contentView, + View allAppsButtonView) { + // Get the y delta between the center of the page and the center of the all apps + // button + mAllAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, + allAppsButtonView, null); + } + @Override + public float getMaterialRevealViewFinalXDrift(View revealView) { + return mAllAppsToPanelDelta[0]; + } + @Override + public float getMaterialRevealViewFinalYDrift(View revealView) { + return mAllAppsToPanelDelta[1]; + } + @Override + float getMaterialRevealViewFinalAlpha(View revealView) { + // No alpha anim from all apps + return 1f; + } + @Override + float getMaterialRevealViewStartFinalRadius() { + int allAppsButtonSize = LauncherAppState.getInstance(). + getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize; + return allAppsButtonSize / 2; + } + @Override + public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( + final View revealView, final View allAppsButtonView) { + return new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + // We set the alpha instead of visibility to ensure that the focus does not + // get taken from the all apps view + allAppsButtonView.setVisibility(View.VISIBLE); + allAppsButtonView.setAlpha(0f); + } + public void onAnimationEnd(Animator animation) { + // Hide the reveal view + revealView.setVisibility(View.INVISIBLE); + + // Show the all apps button, and focus it + allAppsButtonView.setAlpha(1f); + } + }; + } + }; + startAnimationToWorkspaceFromOverlay(toWorkspaceState, appsView, appsView.getContentView(), + appsView.getRevealView(), animated, onCompleteRunnable, cb); + } + + /** + * Starts and animation to the workspace from the widgets view. + */ + private void startAnimationToWorkspaceFromWidgets(final Launcher.State fromState, + final Workspace.State toWorkspaceState, final boolean animated, + final Runnable onCompleteRunnable) { + final WidgetsContainerView widgetsView = mLauncher.getWidgetsView(); + final Resources res = mLauncher.getResources(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + public void onRevealViewVisible(View revealView, View contentView, + View allAppsButtonView) { + revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); + } + @Override + public float getMaterialRevealViewFinalYDrift(View revealView) { + return revealView.getMeasuredHeight() / 2; + } + @Override + float getMaterialRevealViewFinalAlpha(View revealView) { + return 0.4f; + } + @Override + public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( + final View revealView, final View allAppsButtonView) { + return new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + // Hide the reveal view + revealView.setVisibility(View.INVISIBLE); + } + }; + } + }; + startAnimationToWorkspaceFromOverlay(toWorkspaceState, widgetsView, + widgetsView.getContentView(), widgetsView.getRevealView(), animated, + onCompleteRunnable, cb); + } + + /** + * Creates and starts a new animation to the workspace. + */ + private void startAnimationToWorkspaceFromOverlay(final Workspace.State toWorkspaceState, + final View fromView, final View contentView, final View revealView, + final boolean animated, final Runnable onCompleteRunnable, + final PrivateTransitionCallbacks pCb) { + final Resources res = mLauncher.getResources(); + final boolean material = Utilities.isLmpOrAbove(); + final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime); + final int itemsAlphaStagger = + res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); + + final View allAppsButtonView = mLauncher.getAllAppsButton(); + final View toView = mLauncher.getWorkspace(); + + final HashMap<View, Integer> layerViews = new HashMap<>(); + + // If for some reason our views aren't initialized, don't animate + boolean initialized = allAppsButtonView != null; + + // Cancel the current animation + cancelAnimation(); + + // Create the workspace animation. + // NOTE: this call apparently also sets the state for the workspace if !animated + Animator workspaceAnim = mLauncher.getWorkspace().getChangeStateAnimation( + toWorkspaceState, animated, layerViews); + + if (animated && initialized) { + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + + // Play the workspace animation + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + + // 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) { + int width = revealView.getMeasuredWidth(); + int height = revealView.getMeasuredHeight(); + float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4); + revealView.setVisibility(View.VISIBLE); + revealView.setAlpha(1f); + revealView.setTranslationY(0); + layerViews.put(revealView, BUILD_AND_SET_LAYER); + pCb.onRevealViewVisible(revealView, contentView, allAppsButtonView); + + // Calculate the final animation values + final float revealViewToXDrift; + final float revealViewToYDrift; + if (material) { + revealViewToYDrift = pCb.getMaterialRevealViewFinalYDrift(revealView); + revealViewToXDrift = pCb.getMaterialRevealViewFinalXDrift(revealView); + } else { + revealViewToYDrift = 2 * height / 3; + revealViewToXDrift = 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 correspondingly + // shorten the duration so that the slide and conceal end at the same time. + TimeInterpolator decelerateInterpolator = material ? + new LogDecelerateInterpolator(100, 0) : + new DecelerateInterpolator(1f); + ObjectAnimator panelDriftY = LauncherAnimUtils.ofFloat(revealView, "translationY", + 0, revealViewToYDrift); + 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, revealViewToXDrift); + panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY); + panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelDriftX.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelDriftX); + + // Setup animation for the reveal panel alpha + final float revealViewToAlpha = !material ? 0f : + pCb.getMaterialRevealViewFinalAlpha(revealView); + if (revealViewToAlpha != 1f) { + ObjectAnimator panelAlpha = LauncherAnimUtils.ofFloat(revealView, "alpha", + 1f, revealViewToAlpha); + panelAlpha.setDuration(material ? revealDuration : 150); + panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelAlpha.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelAlpha); + } + + // Setup the animation for the content view + layerViews.put(contentView, BUILD_AND_SET_LAYER); + + // Create the individual animators + ObjectAnimator pageDrift = LauncherAnimUtils.ofFloat(contentView, "translationY", + 0, revealViewToYDrift); + contentView.setTranslationY(0); + pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY); + pageDrift.setInterpolator(decelerateInterpolator); + pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + mStateAnimation.play(pageDrift); + + contentView.setAlpha(1f); + ObjectAnimator itemsAlpha = LauncherAnimUtils.ofFloat(contentView, "alpha", 1f, 0f); + itemsAlpha.setDuration(100); + itemsAlpha.setInterpolator(decelerateInterpolator); + mStateAnimation.play(itemsAlpha); + + if (material) { + // Animate the all apps button + float finalRadius = pCb.getMaterialRevealViewStartFinalRadius(); + AnimatorListenerAdapter listener = + pCb.getMaterialRevealViewAnimatorListener(revealView, allAppsButtonView); + Animator reveal = + LauncherAnimUtils.createCircularReveal(revealView, width / 2, + height / 2, revealRadius, finalRadius); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + reveal.setDuration(revealDuration); + reveal.setStartDelay(itemsAlphaStagger); + if (listener != null) { + reveal.addListener(listener); + } + mStateAnimation.play(reveal); + } + + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + } + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + fromView.setVisibility(View.GONE); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); + + // Run any queued runnables + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + + // Animation complete callback + pCb.onAnimationComplete(revealView, contentView, allAppsButtonView); + + // Disable all necessary layers + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + // Reset page transforms + if (contentView != null) { + contentView.setTranslationX(0); + contentView.setTranslationY(0); + contentView.setAlpha(1); + } + + // This can hold unnecessary references to views. + mStateAnimation = null; + } + }); + + 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); + + // Enable all necessary layers + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + if (Utilities.isLmpOrAbove()) { + v.buildLayer(); + } + } + mStateAnimation.start(); + } + }; + fromView.post(startAnimRunnable); + } else { + fromView.setVisibility(View.GONE); + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionStart(fromView, animated, true); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + dispatchOnLauncherTransitionStart(toView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); + + // Run any queued runnables + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + } + + + /** + * Dispatches the prepare-transition event to suitable views. + */ + void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionPrepare(mLauncher, animated, + toWorkspace); + } + } + + /** + * Dispatches the start-transition event to suitable views. + */ + void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStart(mLauncher, animated, + toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 0f); + } + + /** + * Dispatches the step-transition event to suitable views. + */ + void dispatchOnLauncherTransitionStep(View v, float t) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStep(mLauncher, t); + } + } + + /** + * Dispatches the end-transition event to suitable views. + */ + void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionEnd(mLauncher, animated, + toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 1f); + } + + /** + * Cancels the current animation. + */ + private void cancelAnimation() { + if (mStateAnimation != null) { + mStateAnimation.setDuration(0); + mStateAnimation.cancel(); + mStateAnimation = null; + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/PackageChangedReceiver.java b/src/com/android/launcher3/PackageChangedReceiver.java deleted file mode 100644 index e59f6d81d..000000000 --- a/src/com/android/launcher3/PackageChangedReceiver.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.android.launcher3; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class PackageChangedReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, Intent intent) { - final String packageName = intent.getData().getSchemeSpecificPart(); - - if (packageName == null || packageName.length() == 0) { - // they sent us a bad intent - return; - } - // in rare cases the receiver races with the application to set up LauncherAppState - LauncherAppState.setApplicationContext(context.getApplicationContext()); - LauncherAppState app = LauncherAppState.getInstance(); - WidgetPreviewLoader.removePackageFromDb(app.getWidgetPreviewCacheDb(), packageName); - } -} diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 7d65f4686..88295c084 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -19,6 +19,7 @@ package com.android.launcher3; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.animation.LayoutTransition; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; @@ -51,6 +52,8 @@ import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; interface Page { @@ -124,7 +127,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected LauncherScroller mScroller; private Interpolator mDefaultInterpolator; private VelocityTracker mVelocityTracker; - private int mPageSpacing = 0; + @Thunk int mPageSpacing = 0; private float mParentDownMotionX; private float mParentDownMotionY; @@ -207,8 +210,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private boolean mWasInOverscroll = false; // Page Indicator - private int mPageIndicatorViewId; - private PageIndicator mPageIndicator; + @Thunk int mPageIndicatorViewId; + @Thunk PageIndicator mPageIndicator; private boolean mAllowPagedViewAnimations = true; // The viewport whether the pages are to be contained (the actual view may be larger than the @@ -227,7 +230,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected View mDragView; protected AnimatorSet mZoomInOutAnim; private Runnable mSidePageHoverRunnable; - private int mSidePageHoverIndex = -1; + @Thunk int mSidePageHoverIndex = -1; // This variable's scope is only for the duration of startReordering() and endReordering() private boolean mReorderingStarted = false; // This variable's scope is for the duration of startReordering() and after the zoomIn() @@ -246,14 +249,14 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private Rect mAltTmpRect = new Rect(); // Fling to delete - private int FLING_TO_DELETE_FADE_OUT_DURATION = 350; + @Thunk int FLING_TO_DELETE_FADE_OUT_DURATION = 350; private float FLING_TO_DELETE_FRICTION = 0.035f; // The degrees specifies how much deviation from the up vector to still consider a fling "up" private float FLING_TO_DELETE_MAX_FLING_DEGREES = 65f; protected int mFlingToDeleteThresholdVelocity = -1400; // Drag to delete - private boolean mDeferringForDelete = false; - private int DELETE_SLIDE_IN_SIDE_PAGE_DURATION = 250; + @Thunk boolean mDeferringForDelete = false; + @Thunk int DELETE_SLIDE_IN_SIDE_PAGE_DURATION = 250; private int DRAG_TO_DELETE_FADE_OUT_DURATION = 350; // Drop to delete @@ -473,13 +476,14 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc /** * Returns the index of the currently displayed page. - * - * @return The index of the currently displayed page. */ int getCurrentPage() { return mCurrentPage; } + /** + * Returns the index of page to be shown immediately afterwards. + */ int getNextPage() { return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; } @@ -959,8 +963,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc LayoutParams nextLp; int childLeft = offsetX + (lp.isFullScreenPage ? 0 : getPaddingLeft()); - if (mPageScrolls == null || getChildCount() != mChildCountOnLastLayout) { - mPageScrolls = new int[getChildCount()]; + if (mPageScrolls == null || childCount != mChildCountOnLastLayout) { + mPageScrolls = new int[childCount]; } for (int i = startIndex; i != endIndex; i += delta) { @@ -1007,19 +1011,36 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) { updateCurrentPageScroll(); mFirstLayout = false; } - if (childCount > 0) { - final int index = isLayoutRtl() ? 0 : childCount - 1; - mMaxScrollX = getScrollForPage(index); + final LayoutTransition transition = getLayoutTransition(); + // If the transition is running defer updating max scroll, as some empty pages could + // still be present, and a max scroll change could cause sudden jumps in scroll. + if (transition != null && transition.isRunning()) { + transition.addTransitionListener(new LayoutTransition.TransitionListener() { + + @Override + public void startTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType) { } + + @Override + public void endTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType) { + // Wait until all transitions are complete. + if (!transition.isRunning()) { + transition.removeTransitionListener(this); + updateMaxScrollX(); + } + } + }); } else { - mMaxScrollX = 0; + updateMaxScrollX(); } - if (mScroller.isFinished() && mChildCountOnLastLayout != getChildCount() && + if (mScroller.isFinished() && mChildCountOnLastLayout != childCount && !mDeferringForDelete) { if (mRestorePage != INVALID_RESTORE_PAGE) { setCurrentPage(mRestorePage); @@ -1028,13 +1049,23 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc setCurrentPage(getNextPage()); } } - mChildCountOnLastLayout = getChildCount(); + mChildCountOnLastLayout = childCount; if (isReordering(true)) { updateDragViewTranslationDuringDrag(); } } + private void updateMaxScrollX() { + int childCount = getChildCount(); + if (childCount > 0) { + final int index = isLayoutRtl() ? 0 : childCount - 1; + mMaxScrollX = getScrollForPage(index); + } else { + mMaxScrollX = 0; + } + } + public void setPageSpacing(int pageSpacing) { mPageSpacing = pageSpacing; requestLayout(); @@ -1700,7 +1731,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc setEnableOverscroll(!freeScroll); } - private void setEnableOverscroll(boolean enable) { + protected void setEnableOverscroll(boolean enable) { mAllowOverScroll = enable; } @@ -2356,7 +2387,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc super(superState); } - private SavedState(Parcel in) { + @Thunk SavedState(Parcel in) { super(in); currentPage = in.readInt(); } @@ -2514,7 +2545,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc invalidate(); } - private void onPostReorderingAnimationCompleted() { + @Thunk void onPostReorderingAnimationCompleted() { // Trigger the callback when reordering has settled --mPostReorderingPreZoomInRemainingAnimationCount; if (mPostReorderingPreZoomInRunnable != null && diff --git a/src/com/android/launcher3/PagedViewCellLayout.java b/src/com/android/launcher3/PagedViewCellLayout.java deleted file mode 100644 index 2d9e10b9d..000000000 --- a/src/com/android/launcher3/PagedViewCellLayout.java +++ /dev/null @@ -1,492 +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.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewDebug; -import android.view.ViewGroup; - -/** - * An abstraction of the original CellLayout which supports laying out items - * which span multiple cells into a grid-like layout. Also supports dimming - * to give a preview of its contents. - */ -public class PagedViewCellLayout extends ViewGroup implements Page { - static final String TAG = "PagedViewCellLayout"; - - private int mCellCountX; - private int mCellCountY; - private int mOriginalCellWidth; - private int mOriginalCellHeight; - private int mCellWidth; - private int mCellHeight; - private int mOriginalWidthGap; - private int mOriginalHeightGap; - private int mWidthGap; - private int mHeightGap; - protected PagedViewCellLayoutChildren mChildren; - - public PagedViewCellLayout(Context context) { - this(context, null); - } - - public PagedViewCellLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagedViewCellLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - setAlwaysDrawnWithCacheEnabled(false); - - // setup default cell parameters - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mOriginalCellWidth = mCellWidth = grid.cellWidthPx; - mOriginalCellHeight = mCellHeight = grid.cellHeightPx; - mCellCountX = (int) grid.numColumns; - mCellCountY = (int) grid.numRows; - mOriginalWidthGap = mOriginalHeightGap = mWidthGap = mHeightGap = -1; - - mChildren = new PagedViewCellLayoutChildren(context); - mChildren.setCellDimensions(mCellWidth, mCellHeight); - mChildren.setGap(mWidthGap, mHeightGap); - - addView(mChildren); - } - - public int getCellWidth() { - return mCellWidth; - } - - public int getCellHeight() { - return mCellHeight; - } - - @Override - public void cancelLongPress() { - super.cancelLongPress(); - - // Cancel long press for all children - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - child.cancelLongPress(); - } - } - - public boolean addViewToCellLayout(View child, int index, int childId, - PagedViewCellLayout.LayoutParams params) { - final PagedViewCellLayout.LayoutParams lp = params; - - // Generate an id for each view, this assumes we have at most 256x256 cells - // per workspace screen - if (lp.cellX >= 0 && lp.cellX <= (mCellCountX - 1) && - lp.cellY >= 0 && (lp.cellY <= mCellCountY - 1)) { - // If the horizontal or vertical span is set to -1, it is taken to - // mean that it spans the extent of the CellLayout - if (lp.cellHSpan < 0) lp.cellHSpan = mCellCountX; - if (lp.cellVSpan < 0) lp.cellVSpan = mCellCountY; - - child.setId(childId); - mChildren.addView(child, index, lp); - - return true; - } - return false; - } - - @Override - public void removeAllViewsOnPage() { - mChildren.removeAllViews(); - setLayerType(LAYER_TYPE_NONE, null); - } - - @Override - public void removeViewOnPageAt(int index) { - mChildren.removeViewAt(index); - } - - /** - * Clears all the key listeners for the individual icons. - */ - public void resetChildrenOnKeyListeners() { - int childCount = mChildren.getChildCount(); - for (int j = 0; j < childCount; ++j) { - mChildren.getChildAt(j).setOnKeyListener(null); - } - } - - @Override - public int getPageChildCount() { - return mChildren.getChildCount(); - } - - public PagedViewCellLayoutChildren getChildrenLayout() { - return mChildren; - } - - @Override - public View getChildOnPageAt(int i) { - return mChildren.getChildAt(i); - } - - @Override - public int indexOfChildOnPage(View v) { - return mChildren.indexOfChild(v); - } - - public int getCellCountX() { - return mCellCountX; - } - - public int getCellCountY() { - return mCellCountY; - } - - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { - throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); - } - - int numWidthGaps = mCellCountX - 1; - int numHeightGaps = mCellCountY - 1; - - if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) { - int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight(); - int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom(); - int hFreeSpace = hSpace - (mCellCountX * mOriginalCellWidth); - int vFreeSpace = vSpace - (mCellCountY * mOriginalCellHeight); - mWidthGap = numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0; - mHeightGap = numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0; - - mChildren.setGap(mWidthGap, mHeightGap); - } else { - mWidthGap = mOriginalWidthGap; - mHeightGap = mOriginalHeightGap; - } - - // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY - int newWidth = widthSpecSize; - int newHeight = heightSpecSize; - if (widthSpecMode == MeasureSpec.AT_MOST) { - newWidth = getPaddingLeft() + getPaddingRight() + (mCellCountX * mCellWidth) + - ((mCellCountX - 1) * mWidthGap); - newHeight = getPaddingTop() + getPaddingBottom() + (mCellCountY * mCellHeight) + - ((mCellCountY - 1) * mHeightGap); - setMeasuredDimension(newWidth, newHeight); - } - - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - int childWidthMeasureSpec = - MeasureSpec.makeMeasureSpec(newWidth - getPaddingLeft() - - getPaddingRight(), MeasureSpec.EXACTLY); - int childheightMeasureSpec = - MeasureSpec.makeMeasureSpec(newHeight - getPaddingTop() - - getPaddingBottom(), MeasureSpec.EXACTLY); - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - } - - setMeasuredDimension(newWidth, newHeight); - } - - int getContentWidth() { - return getWidthBeforeFirstLayout() + getPaddingLeft() + getPaddingRight(); - } - - int getContentHeight() { - if (mCellCountY > 0) { - return mCellCountY * mCellHeight + (mCellCountY - 1) * Math.max(0, mHeightGap); - } - return 0; - } - - int getWidthBeforeFirstLayout() { - if (mCellCountX > 0) { - return mCellCountX * mCellWidth + (mCellCountX - 1) * Math.max(0, mWidthGap); - } - return 0; - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - child.layout(getPaddingLeft(), getPaddingTop(), - r - l - getPaddingRight(), b - t - getPaddingBottom()); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - boolean result = super.onTouchEvent(event); - int count = getPageChildCount(); - if (count > 0) { - // We only intercept the touch if we are tapping in empty space after the final row - View child = getChildOnPageAt(count - 1); - int bottom = child.getBottom(); - int numRows = (int) Math.ceil((float) getPageChildCount() / getCellCountX()); - if (numRows < getCellCountY()) { - // Add a little bit of buffer if there is room for another row - bottom += mCellHeight / 2; - } - result = result || (event.getY() < bottom); - } - return result; - } - - public void enableCenteredContent(boolean enabled) { - mChildren.enableCenteredContent(enabled); - } - - @Override - protected void setChildrenDrawingCacheEnabled(boolean enabled) { - mChildren.setChildrenDrawingCacheEnabled(enabled); - } - - public void setCellCount(int xCount, int yCount) { - mCellCountX = xCount; - mCellCountY = yCount; - requestLayout(); - } - - public void setGap(int widthGap, int heightGap) { - mOriginalWidthGap = mWidthGap = widthGap; - mOriginalHeightGap = mHeightGap = heightGap; - mChildren.setGap(widthGap, heightGap); - } - - public int[] getCellCountForDimensions(int width, int height) { - // Always assume we're working with the smallest span to make sure we - // reserve enough space in both orientations - int smallerSize = Math.min(mCellWidth, mCellHeight); - - // Always round up to next largest cell - int spanX = (width + smallerSize) / smallerSize; - int spanY = (height + smallerSize) / smallerSize; - - return new int[] { spanX, spanY }; - } - - /** - * Start dragging the specified child - * - * @param child The child that is being dragged - */ - void onDragChild(View child) { - PagedViewCellLayout.LayoutParams lp = (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - lp.isDragging = true; - } - - /** - * Estimates the number of cells that the specified width would take up. - */ - public int estimateCellHSpan(int width) { - // We don't show the next/previous pages any more, so we use the full width, minus the - // padding - int availWidth = width - (getPaddingLeft() + getPaddingRight()); - - // We know that we have to fit N cells with N-1 width gaps, so we just juggle to solve for N - int n = Math.max(1, (availWidth + mWidthGap) / (mCellWidth + mWidthGap)); - - // We don't do anything fancy to determine if we squeeze another row in. - return n; - } - - /** - * Estimates the number of cells that the specified height would take up. - */ - public int estimateCellVSpan(int height) { - // The space for a page is the height - top padding (current page) - bottom padding (current - // page) - int availHeight = height - (getPaddingTop() + getPaddingBottom()); - - // We know that we have to fit N cells with N-1 height gaps, so we juggle to solve for N - int n = Math.max(1, (availHeight + mHeightGap) / (mCellHeight + mHeightGap)); - - // We don't do anything fancy to determine if we squeeze another row in. - return n; - } - - /** Returns an estimated center position of the cell at the specified index */ - public int[] estimateCellPosition(int x, int y) { - return new int[] { - getPaddingLeft() + (x * mCellWidth) + (x * mWidthGap) + (mCellWidth / 2), - getPaddingTop() + (y * mCellHeight) + (y * mHeightGap) + (mCellHeight / 2) - }; - } - - public void calculateCellCount(int width, int height, int maxCellCountX, int maxCellCountY) { - mCellCountX = Math.min(maxCellCountX, estimateCellHSpan(width)); - mCellCountY = Math.min(maxCellCountY, estimateCellVSpan(height)); - requestLayout(); - } - - /** - * Estimates the width that the number of hSpan cells will take up. - */ - public int estimateCellWidth(int hSpan) { - // TODO: we need to take widthGap into effect - return hSpan * mCellWidth; - } - - /** - * Estimates the height that the number of vSpan cells will take up. - */ - public int estimateCellHeight(int vSpan) { - // TODO: we need to take heightGap into effect - return vSpan * mCellHeight; - } - - @Override - public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { - return new PagedViewCellLayout.LayoutParams(getContext(), attrs); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof PagedViewCellLayout.LayoutParams; - } - - @Override - protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - return new PagedViewCellLayout.LayoutParams(p); - } - - public static class LayoutParams extends ViewGroup.MarginLayoutParams { - /** - * Horizontal location of the item in the grid. - */ - @ViewDebug.ExportedProperty - public int cellX; - - /** - * Vertical location of the item in the grid. - */ - @ViewDebug.ExportedProperty - public int cellY; - - /** - * Number of cells spanned horizontally by the item. - */ - @ViewDebug.ExportedProperty - public int cellHSpan; - - /** - * Number of cells spanned vertically by the item. - */ - @ViewDebug.ExportedProperty - public int cellVSpan; - - /** - * Is this item currently being dragged - */ - public boolean isDragging; - - // a data object that you can bind to this layout params - private Object mTag; - - // X coordinate of the view in the layout. - @ViewDebug.ExportedProperty - int x; - // Y coordinate of the view in the layout. - @ViewDebug.ExportedProperty - int y; - - public LayoutParams() { - super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - cellHSpan = 1; - cellVSpan = 1; - } - - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - cellHSpan = 1; - cellVSpan = 1; - } - - public LayoutParams(ViewGroup.LayoutParams source) { - super(source); - cellHSpan = 1; - cellVSpan = 1; - } - - public LayoutParams(LayoutParams source) { - super(source); - this.cellX = source.cellX; - this.cellY = source.cellY; - this.cellHSpan = source.cellHSpan; - this.cellVSpan = source.cellVSpan; - } - - public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) { - super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - this.cellX = cellX; - this.cellY = cellY; - this.cellHSpan = cellHSpan; - this.cellVSpan = cellVSpan; - } - - public void setup(Context context, - int cellWidth, int cellHeight, int widthGap, int heightGap, - int hStartPadding, int vStartPadding) { - - final int myCellHSpan = cellHSpan; - final int myCellVSpan = cellVSpan; - final int myCellX = cellX; - final int myCellY = cellY; - - width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - - leftMargin - rightMargin; - height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - - topMargin - bottomMargin; - - if (LauncherAppState.getInstance().isScreenLarge()) { - x = hStartPadding + myCellX * (cellWidth + widthGap) + leftMargin; - y = vStartPadding + myCellY * (cellHeight + heightGap) + topMargin; - } else { - x = myCellX * (cellWidth + widthGap) + leftMargin; - y = myCellY * (cellHeight + heightGap) + topMargin; - } - } - - public Object getTag() { - return mTag; - } - - public void setTag(Object tag) { - mTag = tag; - } - - public String toString() { - return "(" + this.cellX + ", " + this.cellY + ", " + - this.cellHSpan + ", " + this.cellVSpan + ")"; - } - } -}
\ No newline at end of file diff --git a/src/com/android/launcher3/PagedViewCellLayoutChildren.java b/src/com/android/launcher3/PagedViewCellLayoutChildren.java deleted file mode 100644 index 84d2b1dd3..000000000 --- a/src/com/android/launcher3/PagedViewCellLayoutChildren.java +++ /dev/null @@ -1,161 +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.Rect; -import android.view.View; -import android.view.ViewGroup; - -/** - * An abstraction of the original CellLayout which supports laying out items - * which span multiple cells into a grid-like layout. Also supports dimming - * to give a preview of its contents. - */ -public class PagedViewCellLayoutChildren extends ViewGroup { - static final String TAG = "PagedViewCellLayout"; - - private boolean mCenterContent; - - private int mCellWidth; - private int mCellHeight; - private int mWidthGap; - private int mHeightGap; - - public PagedViewCellLayoutChildren(Context context) { - super(context); - } - - @Override - public void cancelLongPress() { - super.cancelLongPress(); - - // Cancel long press for all children - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - child.cancelLongPress(); - } - } - - public void setGap(int widthGap, int heightGap) { - mWidthGap = widthGap; - mHeightGap = heightGap; - requestLayout(); - } - - public void setCellDimensions(int width, int height) { - mCellWidth = width; - mCellHeight = height; - requestLayout(); - } - - @Override - public void requestChildFocus(View child, View focused) { - super.requestChildFocus(child, focused); - if (child != null) { - Rect r = new Rect(); - child.getDrawingRect(r); - requestRectangleOnScreen(r); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { - throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); - } - - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - PagedViewCellLayout.LayoutParams lp = - (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - lp.setup(getContext(), - mCellWidth, mCellHeight, mWidthGap, mHeightGap, - getPaddingLeft(), - getPaddingTop()); - - int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, - MeasureSpec.EXACTLY); - int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, - MeasureSpec.EXACTLY); - - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - } - - setMeasuredDimension(widthSpecSize, heightSpecSize); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int count = getChildCount(); - - int offsetX = 0; - if (mCenterContent && count > 0) { - // determine the max width of all the rows and center accordingly - int maxRowX = 0; - int minRowX = Integer.MAX_VALUE; - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - if (child.getVisibility() != GONE) { - PagedViewCellLayout.LayoutParams lp = - (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - minRowX = Math.min(minRowX, lp.x); - maxRowX = Math.max(maxRowX, lp.x + lp.width); - } - } - int maxRowWidth = maxRowX - minRowX; - offsetX = (getMeasuredWidth() - maxRowWidth) / 2; - } - - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - if (child.getVisibility() != GONE) { - PagedViewCellLayout.LayoutParams lp = - (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - - int childLeft = offsetX + lp.x; - int childTop = lp.y; - child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height); - } - } - } - - public void enableCenteredContent(boolean enabled) { - mCenterContent = enabled; - } - - @Override - protected void setChildrenDrawingCacheEnabled(boolean enabled) { - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View view = getChildAt(i); - view.setDrawingCacheEnabled(enabled); - // Update the drawing caches - if (!view.isHardwareAccelerated()) { - view.buildDrawingCache(true); - } - } - } -} diff --git a/src/com/android/launcher3/PagedViewWithDraggableItems.java b/src/com/android/launcher3/PagedViewWithDraggableItems.java deleted file mode 100644 index 0e593698d..000000000 --- a/src/com/android/launcher3/PagedViewWithDraggableItems.java +++ /dev/null @@ -1,174 +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.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - - -/* Class that does most of the work of enabling dragging items out of a PagedView by performing a - * vertical drag. Used by both CustomizePagedView and AllAppsPagedView. - * Subclasses must do the following: - * * call setDragSlopeThreshold after making an instance of the PagedViewWithDraggableItems - * * call child.setOnLongClickListener(this) and child.setOnTouchListener(this) on all children - * (good place to do it is in syncPageItems) - * * override beginDragging(View) (but be careful to call super.beginDragging(View) - * - */ -public abstract class PagedViewWithDraggableItems extends PagedView - implements View.OnLongClickListener, View.OnTouchListener { - private View mLastTouchedItem; - private boolean mIsDragging; - private boolean mIsDragEnabled; - private float mDragSlopeThreshold; - private Launcher mLauncher; - - public PagedViewWithDraggableItems(Context context) { - this(context, null); - } - - public PagedViewWithDraggableItems(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagedViewWithDraggableItems(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mLauncher = (Launcher) context; - } - - protected boolean beginDragging(View v) { - boolean wasDragging = mIsDragging; - mIsDragging = true; - return !wasDragging; - } - - protected void cancelDragging() { - mIsDragging = false; - mLastTouchedItem = null; - mIsDragEnabled = false; - } - - private void handleTouchEvent(MotionEvent ev) { - final int action = ev.getAction(); - switch (action & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - cancelDragging(); - mIsDragEnabled = true; - break; - case MotionEvent.ACTION_MOVE: - if (mTouchState != TOUCH_STATE_SCROLLING && !mIsDragging && mIsDragEnabled) { - determineDraggingStart(ev); - } - break; - } - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - handleTouchEvent(ev); - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - handleTouchEvent(ev); - return super.onTouchEvent(ev); - } - - public void trimMemory() { - mLastTouchedItem = null; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - mLastTouchedItem = v; - mIsDragEnabled = true; - return false; - } - - @Override - public boolean onLongClick(View v) { - // Return early if this is not initiated from a touch - if (!v.isInTouchMode()) return false; - // Return early if we are still animating the pages - if (mNextPage != INVALID_PAGE) return false; - // When we have exited all apps or are in transition, disregard long clicks - if (!mLauncher.isAllAppsVisible() || - mLauncher.getWorkspace().isSwitchingState()) return false; - // Return if global dragging is not enabled - if (!mLauncher.isDraggingEnabled()) return false; - - return beginDragging(v); - } - - /* - * Determines if we should change the touch state to start scrolling after the - * user moves their touch point too far. - */ - protected void determineScrollingStart(MotionEvent ev) { - if (!mIsDragging) super.determineScrollingStart(ev); - } - - /* - * Determines if we should change the touch state to start dragging after the - * user moves their touch point far enough. - */ - protected void determineDraggingStart(MotionEvent ev) { - /* - * Locally do absolute value. mLastMotionX is set to the y value - * of the down event. - */ - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - final float x = ev.getX(pointerIndex); - final float y = ev.getY(pointerIndex); - final int xDiff = (int) Math.abs(x - mLastMotionX); - final int yDiff = (int) Math.abs(y - mLastMotionY); - - final int touchSlop = mTouchSlop; - boolean yMoved = yDiff > touchSlop; - boolean isUpwardMotion = (yDiff / (float) xDiff) > mDragSlopeThreshold; - - if (isUpwardMotion && yMoved && mLastTouchedItem != null) { - // Drag if the user moved far enough along the Y axis - beginDragging(mLastTouchedItem); - - // Cancel any pending long press - if (mAllowLongPress) { - mAllowLongPress = false; - // Try canceling the long press. It could also have been scheduled - // by a distant descendant, so use the mAllowLongPress flag to block - // everything - final View currentPage = getPageAt(mCurrentPage); - if (currentPage != null) { - currentPage.cancelLongPress(); - } - } - } - } - - public void setDragSlopeThreshold(float dragSlopeThreshold) { - mDragSlopeThreshold = dragSlopeThreshold; - } - - @Override - protected void onDetachedFromWindow() { - cancelDragging(); - super.onDetachedFromWindow(); - } -} diff --git a/src/com/android/launcher3/PendingAddItemInfo.java b/src/com/android/launcher3/PendingAddItemInfo.java index ac54a262f..1aaf85bbd 100644 --- a/src/com/android/launcher3/PendingAddItemInfo.java +++ b/src/com/android/launcher3/PendingAddItemInfo.java @@ -16,93 +16,17 @@ package com.android.launcher3; -import android.appwidget.AppWidgetHostView; import android.content.ComponentName; -import android.content.pm.ActivityInfo; -import android.os.Bundle; -import android.os.Parcelable; /** - * We pass this object with a drag from the customization tray + * Meta data that is used for deferred binding. + * e.g., this object is used to pass information on dragable targets when they are dropped onto + * the workspace from another container. */ -class PendingAddItemInfo extends ItemInfo { +public class PendingAddItemInfo extends ItemInfo { + /** * The component that will be created. */ - ComponentName componentName; -} - -class PendingAddShortcutInfo extends PendingAddItemInfo { - - ActivityInfo shortcutActivityInfo; - - public PendingAddShortcutInfo(ActivityInfo activityInfo) { - shortcutActivityInfo = activityInfo; - } - - @Override - public String toString() { - return "Shortcut: " + shortcutActivityInfo.packageName; - } -} - -class PendingAddWidgetInfo extends PendingAddItemInfo { - int minWidth; - int minHeight; - int minResizeWidth; - int minResizeHeight; - int previewImage; - int icon; - LauncherAppWidgetProviderInfo info; - AppWidgetHostView boundWidget; - Bundle bindOptions = null; - - public PendingAddWidgetInfo(LauncherAppWidgetProviderInfo i, Parcelable data) { - if (i.isCustomWidget) { - itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; - } else { - itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; - } - this.info = i; - componentName = i.provider; - minWidth = i.minWidth; - minHeight = i.minHeight; - minResizeWidth = i.minResizeWidth; - minResizeHeight = i.minResizeHeight; - previewImage = i.previewImage; - icon = i.icon; - - spanX = i.spanX; - spanY = i.spanY; - minSpanX = i.minSpanX; - minSpanY = i.minSpanY; - } - - public boolean isCustomWidget() { - return itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; - } - - // Copy constructor - public PendingAddWidgetInfo(PendingAddWidgetInfo copy) { - minWidth = copy.minWidth; - minHeight = copy.minHeight; - minResizeWidth = copy.minResizeWidth; - minResizeHeight = copy.minResizeHeight; - previewImage = copy.previewImage; - icon = copy.icon; - info = copy.info; - boundWidget = copy.boundWidget; - componentName = copy.componentName; - itemType = copy.itemType; - spanX = copy.spanX; - spanY = copy.spanY; - minSpanX = copy.minSpanX; - minSpanY = copy.minSpanY; - bindOptions = copy.bindOptions == null ? null : (Bundle) copy.bindOptions.clone(); - } - - @Override - public String toString() { - return "Widget: " + componentName.toShortString(); - } + public ComponentName componentName; } diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java index 99c2e0859..a8dcd0f06 100644 --- a/src/com/android/launcher3/SearchDropTargetBar.java +++ b/src/com/android/launcher3/SearchDropTargetBar.java @@ -44,11 +44,14 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D private boolean mIsSearchBarHidden; private View mQSBSearchBar; private View mDropTargetBar; - private ButtonDropTarget mInfoDropTarget; - private ButtonDropTarget mDeleteDropTarget; private int mBarHeight; private boolean mDeferOnDragEnd = false; + // Drop targets + private ButtonDropTarget mInfoDropTarget; + private ButtonDropTarget mDeleteDropTarget; + private ButtonDropTarget mUninstallDropTarget; + private boolean mEnableDropDownDropTargets; public SearchDropTargetBar(Context context, AttributeSet attrs) { @@ -61,13 +64,19 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D public void setup(Launcher launcher, DragController dragController) { dragController.addDragListener(this); + dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); + dragController.addDragListener(mInfoDropTarget); dragController.addDragListener(mDeleteDropTarget); + dragController.addDragListener(mUninstallDropTarget); + dragController.addDropTarget(mInfoDropTarget); dragController.addDropTarget(mDeleteDropTarget); - dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); + dragController.addDropTarget(mUninstallDropTarget); + mInfoDropTarget.setLauncher(launcher); mDeleteDropTarget.setLauncher(launcher); + mUninstallDropTarget.setLauncher(launcher); } public void setQsbSearchBar(View qsb) { @@ -116,9 +125,11 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D mDropTargetBar = findViewById(R.id.drag_target_bar); mInfoDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.info_target_text); mDeleteDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.delete_target_text); + mUninstallDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.uninstall_target_text); mInfoDropTarget.setSearchDropTargetBar(this); mDeleteDropTarget.setSearchDropTargetBar(this); + mUninstallDropTarget.setSearchDropTargetBar(this); mEnableDropDownDropTargets = getResources().getBoolean(R.bool.config_useDropTargetDownTransition); @@ -197,6 +208,10 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D */ @Override public void onDragStart(DragSource source, Object info, int dragAction) { + showDeleteTarget(); + } + + public void showDeleteTarget() { // Animate out the QSB search bar, and animate in the drop target bar prepareStartAnimation(mDropTargetBar); mDropTargetBarAnim.start(); @@ -206,6 +221,16 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D } } + public void hideDeleteTarget() { + // Restore the QSB search bar, and animate out the drop target bar + prepareStartAnimation(mDropTargetBar); + mDropTargetBarAnim.reverse(); + if (!mIsSearchBarHidden) { + prepareStartAnimation(mQSBSearchBar); + mQSBSearchBarAnim.reverse(); + } + } + public void deferOnDragEnd() { mDeferOnDragEnd = true; } @@ -213,13 +238,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D @Override public void onDragEnd() { if (!mDeferOnDragEnd) { - // Restore the QSB search bar, and animate out the drop target bar - prepareStartAnimation(mDropTargetBar); - mDropTargetBarAnim.reverse(); - if (!mIsSearchBarHidden) { - prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.reverse(); - } + hideDeleteTarget(); } else { mDeferOnDragEnd = false; } diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java index ff0604540..15b617683 100644 --- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java +++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java @@ -177,7 +177,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup { child.measure(childWidthMeasureSpec, childheightMeasureSpec); } - private boolean invertLayoutHorizontally() { + public boolean invertLayoutHorizontally() { return mInvertIfRtl && isLayoutRtl(); } diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 01f79314e..5bef845bb 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -23,7 +23,10 @@ import android.content.Intent; import android.graphics.Bitmap; import android.util.Log; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; import java.util.ArrayList; import java.util.Arrays; @@ -46,18 +49,24 @@ public class ShortcutInfo extends ItemInfo { * be present along with {@link #FLAG_RESTORED_ICON}, and is set during default layout * parsing. */ - public static final int FLAG_AUTOINTALL_ICON = 2; + public static final int FLAG_AUTOINTALL_ICON = 2; //0B10; /** * 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; + public static final int FLAG_INSTALL_SESSION_ACTIVE = 4; // 0B100; /** * Indicates that the widget restore has started. */ - public static final int FLAG_RESTORE_STARTED = 8; + public static final int FLAG_RESTORE_STARTED = 8; //0B1000; + + /** + * Indicates if it represents a common type mentioned in {@link CommonAppTypeParser}. + * Upto 15 different types supported. + */ + public static final int FLAG_RESTORED_APP_TYPE = 0B0011110000; /** * The intent used to start the application. @@ -77,6 +86,11 @@ public class ShortcutInfo extends ItemInfo { boolean usingFallbackIcon; /** + * Indicates whether we're using a low res icon + */ + boolean usingLowResIcon; + + /** * If isShortcut=true and customIcon=false, this contains a reference to the * shortcut icon as an application's resource. */ @@ -184,8 +198,10 @@ public class ShortcutInfo extends ItemInfo { } public void updateIcon(IconCache iconCache) { - mIcon = iconCache.getIcon(promisedIntent != null ? promisedIntent : intent, user); - usingFallbackIcon = iconCache.isDefaultIcon(mIcon, user); + if (itemType == Favorites.ITEM_TYPE_APPLICATION) { + iconCache.getTitleAndIcon(this, promisedIntent != null ? promisedIntent : intent, user, + shouldUseLowResIcon()); + } } @Override @@ -207,9 +223,9 @@ public class ShortcutInfo extends ItemInfo { if (!usingFallbackIcon) { writeBitmap(values, mIcon); } - values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, - LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE); if (iconResource != null) { + values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, + LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE); values.put(LauncherSettings.BaseLauncherColumns.ICON_PACKAGE, iconResource.packageName); values.put(LauncherSettings.BaseLauncherColumns.ICON_RESOURCE, @@ -256,5 +272,23 @@ public class ShortcutInfo extends ItemInfo { mInstallProgress = progress; status |= FLAG_INSTALL_SESSION_ACTIVE; } + + public boolean shouldUseLowResIcon() { + return usingLowResIcon && container >= 0 && rank >= FolderIcon.NUM_ITEMS_IN_PREVIEW; + } + + public static ShortcutInfo fromActivityInfo(LauncherActivityInfoCompat info, Context context) { + final ShortcutInfo shortcut = new ShortcutInfo(); + shortcut.user = info.getUser(); + shortcut.title = info.getLabel().toString(); + shortcut.contentDescription = UserManagerCompat.getInstance(context) + .getBadgedLabelForUser(info.getLabel(), info.getUser()); + shortcut.customIcon = false; + shortcut.intent = AppInfo.makeLaunchIntent(context, info, info.getUser()); + shortcut.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; + shortcut.flags = AppInfo.initFlags(info); + shortcut.firstInstallTime = info.getFirstInstallTime(); + return shortcut; + } } diff --git a/src/com/android/launcher3/Stats.java b/src/com/android/launcher3/Stats.java index a87986562..9d06f755f 100644 --- a/src/com/android/launcher3/Stats.java +++ b/src/com/android/launcher3/Stats.java @@ -22,14 +22,8 @@ import android.content.Intent; import android.content.IntentFilter; import android.util.Log; -import java.io.*; -import java.util.ArrayList; - public class Stats { private static final boolean DEBUG_BROADCASTS = false; - private static final String TAG = "Launcher3/Stats"; - - private static final boolean LOCAL_LAUNCH_LOG = true; public static final String ACTION_LAUNCH = "com.android.launcher3.action.LAUNCH"; public static final String EXTRA_INTENT = "intent"; @@ -38,54 +32,20 @@ public class Stats { public static final String EXTRA_CELLX = "cellX"; public static final String EXTRA_CELLY = "cellY"; - private static final int LOG_VERSION = 1; - private static final int LOG_TAG_VERSION = 0x1; - private static final int LOG_TAG_LAUNCH = 0x1000; - - private static final int STATS_VERSION = 1; - private static final int INITIAL_STATS_SIZE = 100; - - // TODO: delayed/batched writes - private static final boolean FLUSH_IMMEDIATELY = true; - private final Launcher mLauncher; - private final String mLaunchBroadcastPermission; - DataOutputStream mLog; - - ArrayList<String> mIntents; - ArrayList<Integer> mHistogram; - public Stats(Launcher launcher) { mLauncher = launcher; - mLaunchBroadcastPermission = launcher.getResources().getString(R.string.receive_launch_broadcasts_permission); - loadStats(); - - if (LOCAL_LAUNCH_LOG) { - try { - mLog = new DataOutputStream(mLauncher.openFileOutput( - LauncherFiles.LAUNCHES_LOG, Context.MODE_APPEND)); - mLog.writeInt(LOG_TAG_VERSION); - mLog.writeInt(LOG_VERSION); - } catch (FileNotFoundException e) { - Log.e(TAG, "unable to create stats log: " + e); - mLog = null; - } catch (IOException e) { - Log.e(TAG, "unable to write to stats log: " + e); - mLog = null; - } - } - if (DEBUG_BROADCASTS) { launcher.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - android.util.Log.v("Stats", "got broadcast: " + intent + " for launched intent: " + Log.v("Stats", "got broadcast: " + intent + " for launched intent: " + intent.getStringExtra(EXTRA_INTENT)); } }, @@ -96,16 +56,6 @@ public class Stats { } } - public void incrementLaunch(String intentStr) { - int pos = mIntents.indexOf(intentStr); - if (pos < 0) { - mIntents.add(intentStr); - mHistogram.add(1); - } else { - mHistogram.set(pos, mHistogram.get(pos) + 1); - } - } - public void recordLaunch(Intent intent) { recordLaunch(intent, null); } @@ -115,7 +65,6 @@ public class Stats { intent.setSourceBounds(null); final String flat = intent.toUri(0); - Intent broadcastIntent = new Intent(ACTION_LAUNCH).putExtra(EXTRA_INTENT, flat); if (shortcut != null) { broadcastIntent.putExtra(EXTRA_CONTAINER, shortcut.container) @@ -124,94 +73,5 @@ public class Stats { .putExtra(EXTRA_CELLY, shortcut.cellY); } mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission); - - incrementLaunch(flat); - - if (FLUSH_IMMEDIATELY) { - saveStats(); - } - - if (LOCAL_LAUNCH_LOG && mLog != null) { - try { - mLog.writeInt(LOG_TAG_LAUNCH); - mLog.writeLong(System.currentTimeMillis()); - if (shortcut == null) { - mLog.writeShort(0); - mLog.writeShort(0); - mLog.writeShort(0); - mLog.writeShort(0); - } else { - mLog.writeShort((short) shortcut.container); - mLog.writeShort((short) shortcut.screenId); - mLog.writeShort((short) shortcut.cellX); - mLog.writeShort((short) shortcut.cellY); - } - mLog.writeUTF(flat); - if (FLUSH_IMMEDIATELY) { - mLog.flush(); // TODO: delayed writes - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - private void saveStats() { - DataOutputStream stats = null; - try { - stats = new DataOutputStream(mLauncher.openFileOutput( - LauncherFiles.STATS_LOG + ".tmp", Context.MODE_PRIVATE)); - stats.writeInt(STATS_VERSION); - final int N = mHistogram.size(); - stats.writeInt(N); - for (int i=0; i<N; i++) { - stats.writeUTF(mIntents.get(i)); - stats.writeInt(mHistogram.get(i)); - } - stats.close(); - stats = null; - mLauncher.getFileStreamPath(LauncherFiles.STATS_LOG + ".tmp") - .renameTo(mLauncher.getFileStreamPath(LauncherFiles.STATS_LOG)); - } catch (FileNotFoundException e) { - Log.e(TAG, "unable to create stats data: " + e); - } catch (IOException e) { - Log.e(TAG, "unable to write to stats data: " + e); - } finally { - if (stats != null) { - try { - stats.close(); - } catch (IOException e) { } - } - } - } - - private void loadStats() { - mIntents = new ArrayList<String>(INITIAL_STATS_SIZE); - mHistogram = new ArrayList<Integer>(INITIAL_STATS_SIZE); - DataInputStream stats = null; - try { - stats = new DataInputStream(mLauncher.openFileInput(LauncherFiles.STATS_LOG)); - final int version = stats.readInt(); - if (version == STATS_VERSION) { - final int N = stats.readInt(); - for (int i=0; i<N; i++) { - final String pkg = stats.readUTF(); - final int count = stats.readInt(); - mIntents.add(pkg); - mHistogram.add(count); - } - } - } catch (FileNotFoundException e) { - // not a problem - } catch (IOException e) { - // more of a problem - - } finally { - if (stats != null) { - try { - stats.close(); - } catch (IOException e) { } - } - } } } diff --git a/src/com/android/launcher3/UninstallDropTarget.java b/src/com/android/launcher3/UninstallDropTarget.java new file mode 100644 index 000000000..4a7fffeb2 --- /dev/null +++ b/src/com/android/launcher3/UninstallDropTarget.java @@ -0,0 +1,122 @@ +package com.android.launcher3; + +import android.content.ComponentName; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.UserManager; +import android.util.AttributeSet; +import android.util.Pair; + +import com.android.launcher3.R; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.Thunk; + +public class UninstallDropTarget extends ButtonDropTarget { + + public UninstallDropTarget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public UninstallDropTarget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + // Get the hover color + mHoverColor = getResources().getColor(R.color.delete_target_hover_tint); + + setDrawable(R.drawable.uninstall_target_selector); + } + + @Override + protected boolean supportsDrop(DragSource source, Object info) { + 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)) { + return false; + } + } + + Pair<ComponentName, Integer> componentInfo = getAppInfoFlags(info); + return componentInfo != null && (componentInfo.second & AppInfo.DOWNLOADED_FLAG) != 0; + } + + /** + * @return the component name and flags if {@param info} is an AppInfo or an app shortcut. + */ + private static Pair<ComponentName, Integer> getAppInfoFlags(Object item) { + if (item instanceof AppInfo) { + AppInfo info = (AppInfo) item; + return Pair.create(info.componentName, info.flags); + } else if (item instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) item; + ComponentName component = info.getTargetComponent(); + if (info.itemType == LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION + && component != null) { + return Pair.create(component, info.flags); + } + } + return null; + } + + @Override + public void onDrop(DragObject d) { + // Differ item deletion + if (d.dragSource instanceof UninstallSource) { + ((UninstallSource) d.dragSource).deferCompleteDropAfterUninstallActivity(); + } + super.onDrop(d); + } + + @Override + void completeDrop(final DragObject d) { + final Pair<ComponentName, Integer> componentInfo = getAppInfoFlags(d.dragInfo); + final UserHandleCompat user = ((ItemInfo) d.dragInfo).user; + if (mLauncher.startApplicationUninstallActivity( + componentInfo.first, componentInfo.second, user)) { + + final Runnable checkIfUninstallWasSuccess = new Runnable() { + @Override + public void run() { + String packageName = componentInfo.first.getPackageName(); + boolean uninstallSuccessful = !AllAppsList.packageHasActivities( + getContext(), packageName, user); + sendUninstallResult(d.dragSource, uninstallSuccessful); + } + }; + mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess); + } else { + sendUninstallResult(d.dragSource, false); + } + } + + @Thunk void sendUninstallResult(DragSource target, boolean result) { + if (target instanceof UninstallSource) { + ((UninstallSource) target).onUninstallActivityReturned(result); + } + } + + /** + * Interface defining an object that can provide uninstallable drag objects. + */ + public static interface UninstallSource { + + /** + * A pending uninstall operation was complete. + * @param result true if uninstall was successful, false otherwise. + */ + void onUninstallActivityReturned(boolean result); + + /** + * Indicates that an uninstall request are made and the actual result may come + * after some time. + */ + void deferCompleteDropAfterUninstallActivity(); + } +} diff --git a/src/com/android/launcher3/UninstallShortcutReceiver.java b/src/com/android/launcher3/UninstallShortcutReceiver.java deleted file mode 100644 index c9d0bb5f5..000000000 --- a/src/com/android/launcher3/UninstallShortcutReceiver.java +++ /dev/null @@ -1,133 +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.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.widget.Toast; - -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Iterator; - -public class UninstallShortcutReceiver extends BroadcastReceiver { - private static final String ACTION_UNINSTALL_SHORTCUT = - "com.android.launcher.action.UNINSTALL_SHORTCUT"; - - // The set of shortcuts that are pending uninstall - private static ArrayList<PendingUninstallShortcutInfo> mUninstallQueue = - new ArrayList<PendingUninstallShortcutInfo>(); - - // Determines whether to defer uninstalling shortcuts immediately until - // disableAndFlushUninstallQueue() is called. - private static boolean mUseUninstallQueue = false; - - private static class PendingUninstallShortcutInfo { - Intent data; - - public PendingUninstallShortcutInfo(Intent rawData) { - data = rawData; - } - } - - public void onReceive(Context context, Intent data) { - if (!ACTION_UNINSTALL_SHORTCUT.equals(data.getAction())) { - return; - } - - PendingUninstallShortcutInfo info = new PendingUninstallShortcutInfo(data); - if (mUseUninstallQueue) { - mUninstallQueue.add(info); - } else { - processUninstallShortcut(context, info); - } - } - - static void enableUninstallQueue() { - mUseUninstallQueue = true; - } - - static void disableAndFlushUninstallQueue(Context context) { - mUseUninstallQueue = false; - Iterator<PendingUninstallShortcutInfo> iter = mUninstallQueue.iterator(); - while (iter.hasNext()) { - processUninstallShortcut(context, iter.next()); - iter.remove(); - } - } - - private static void processUninstallShortcut(Context context, - PendingUninstallShortcutInfo pendingInfo) { - final Intent data = pendingInfo.data; - - LauncherAppState.setApplicationContext(context.getApplicationContext()); - LauncherAppState app = LauncherAppState.getInstance(); - synchronized (app) { // TODO: make removeShortcut internally threadsafe - removeShortcut(context, data); - } - } - - private static void removeShortcut(Context context, Intent data) { - Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); - String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); - boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); - - if (intent != null && name != null) { - final ContentResolver cr = context.getContentResolver(); - Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, - new String[] { LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT }, - LauncherSettings.Favorites.TITLE + "=?", new String[] { name }, null); - - final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); - final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); - - boolean changed = false; - - try { - while (c.moveToNext()) { - try { - String intentStr = c.getString(intentIndex); - if (intentStr != null - && intent.filterEquals(Intent.parseUri(intentStr, 0))) { - final long id = c.getLong(idIndex); - final Uri uri = LauncherSettings.Favorites.getContentUri(id, false); - cr.delete(uri, null, null); - changed = true; - if (!duplicate) { - break; - } - } - } catch (URISyntaxException e) { - // Ignore - } - } - } finally { - c.close(); - } - - if (changed) { - cr.notifyChange(LauncherSettings.Favorites.CONTENT_URI, null); - Toast.makeText(context, context.getString(R.string.shortcut_uninstalled, name), - Toast.LENGTH_SHORT).show(); - } - } - } -} diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 1a9b9a16c..22677c8ea 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -31,7 +31,9 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.Resources; +import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; @@ -48,6 +50,8 @@ import android.util.SparseArray; import android.view.View; import android.widget.Toast; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; @@ -112,6 +116,15 @@ public final class Utilities { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } + static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) { + byte[] data = c.getBlob(iconIndex); + try { + return createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), context); + } catch (Exception e) { + return null; + } + } + /** * Returns a bitmap suitable for the all apps view. If the package or the resource do not * exist, it returns null. @@ -544,6 +557,25 @@ public final class Utilities { return defaultWidgetForSearchPackage; } + /** + * Compresses the bitmap to a byte array for serialization. + */ + public static byte[] flattenBitmap(Bitmap bitmap) { + // Try go guesstimate how much space the icon will take when serialized + // to avoid unnecessary allocations/copies during the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w(TAG, "Could not write bitmap"); + return null; + } + } + public static final Comparator<ItemInfo> RANK_COMPARATOR = new Comparator<ItemInfo>() { @Override diff --git a/src/com/android/launcher3/WeightWatcher.java b/src/com/android/launcher3/WeightWatcher.java index 70b8afea8..75684797f 100644 --- a/src/com/android/launcher3/WeightWatcher.java +++ b/src/com/android/launcher3/WeightWatcher.java @@ -34,6 +34,8 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.launcher3.util.Thunk; + public class WeightWatcher extends LinearLayout { private static final int RAM_GRAPH_RSS_COLOR = 0xFF990000; private static final int RAM_GRAPH_PSS_COLOR = 0xFF99CC00; @@ -81,7 +83,7 @@ public class WeightWatcher extends LinearLayout { } } }; - private MemoryTracker mMemoryService; + @Thunk MemoryTracker mMemoryService; public WeightWatcher(Context context, AttributeSet attrs) { super(context, attrs); @@ -134,7 +136,7 @@ public class WeightWatcher extends LinearLayout { GraphView mRamGraph; TextView mText; int mPid; - private MemoryTracker.ProcessMemInfo mMemInfo; + @Thunk MemoryTracker.ProcessMemInfo mMemInfo; public ProcessWatcher(Context context) { this(context, null); diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index d963f2db9..5c3ed9272 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -1,524 +1,361 @@ package com.android.launcher3; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; -import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.database.Cursor; -import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteDiskIOException; import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteReadOnlyDatabaseException; 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; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.Rect; -import android.graphics.Shader; +import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; -import android.os.Build; import android.util.Log; +import android.util.LongSparseArray; + import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.WidgetCell; -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.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; public class WidgetPreviewLoader { - private static abstract class SoftReferenceThreadLocal<T> { - private ThreadLocal<SoftReference<T>> mThreadLocal; - public SoftReferenceThreadLocal() { - mThreadLocal = new ThreadLocal<SoftReference<T>>(); - } - - abstract T initialValue(); - - public void set(T t) { - mThreadLocal.set(new SoftReference<T>(t)); - } - - public T get() { - SoftReference<T> reference = mThreadLocal.get(); - T obj; - if (reference == null) { - obj = initialValue(); - mThreadLocal.set(new SoftReference<T>(obj)); - return obj; - } else { - obj = reference.get(); - if (obj == null) { - obj = initialValue(); - mThreadLocal.set(new SoftReference<T>(obj)); - } - return obj; - } - } - } - - private static class CanvasCache extends SoftReferenceThreadLocal<Canvas> { - @Override - protected Canvas initialValue() { - return new Canvas(); - } - } - - private static class PaintCache extends SoftReferenceThreadLocal<Paint> { - @Override - protected Paint initialValue() { - return null; - } - } - - private static class BitmapCache extends SoftReferenceThreadLocal<Bitmap> { - @Override - protected Bitmap initialValue() { - return null; - } - } - - private static class RectCache extends SoftReferenceThreadLocal<Rect> { - @Override - protected Rect initialValue() { - return new Rect(); - } - } - - private static class BitmapFactoryOptionsCache extends - SoftReferenceThreadLocal<BitmapFactory.Options> { - @Override - protected BitmapFactory.Options initialValue() { - return new BitmapFactory.Options(); - } - } - private static final String TAG = "WidgetPreviewLoader"; - private static final String ANDROID_INCREMENTAL_VERSION_NAME_KEY = "android.incremental.version"; + private static final boolean DEBUG = false; 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<String, WeakReference<Bitmap>>(); - private final ArrayList<SoftReference<Bitmap>> mUnusedBitmaps = new ArrayList<SoftReference<Bitmap>>(); + private final HashMap<String, long[]> mPackageVersions = new HashMap<>(); + private final HashMap<WidgetCacheKey, WeakReference<Bitmap>> mLoadedPreviews = new HashMap<>(); + private Set<Bitmap> mUnusedBitmaps = Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>()); private final Context mContext; - private final int mAppIconSize; private final IconCache mIconCache; + private final UserManagerCompat mUserManager; private final AppWidgetManagerCompat mManager; - - private int mPreviewBitmapWidth; - private int mPreviewBitmapHeight; - private String mSize; - private PagedViewCellLayout mWidgetSpacingLayout; - - private String mCachedSelectQuery; - - - private CacheDb mDb; + private final CacheDb mDb; private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); - public WidgetPreviewLoader(Context context) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - + public WidgetPreviewLoader(Context context, IconCache iconCache) { mContext = context; - mAppIconSize = grid.iconSizePx; - mIconCache = app.getIconCache(); + mIconCache = iconCache; mManager = AppWidgetManagerCompat.getInstance(context); - - mDb = app.getWidgetPreviewCacheDb(); - - SharedPreferences sp = context.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); - final String lastVersionName = sp.getString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, null); - final String versionName = android.os.Build.VERSION.INCREMENTAL; - final boolean isLollipopOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - if (!versionName.equals(lastVersionName)) { - try { - // clear all the previews whenever the system version changes, to ensure that - // previews are up-to-date for any apps that might have been updated with the system - clearDb(); - } catch (SQLiteReadOnlyDatabaseException e) { - if (isLollipopOrGreater) { - // Workaround for Bug. 18554839, if we fail to clear the db due to the read-only - // issue, then ignore this error and leave the old previews - } else { - throw e; - } - } finally { - SharedPreferences.Editor editor = sp.edit(); - editor.putString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, versionName); - editor.commit(); - } - } - } - - public void recreateDb() { - LauncherAppState app = LauncherAppState.getInstance(); - app.recreateWidgetPreviewDb(); - mDb = app.getWidgetPreviewCacheDb(); - } - - public void setPreviewSize(int previewWidth, int previewHeight, - PagedViewCellLayout widgetSpacingLayout) { - mPreviewBitmapWidth = previewWidth; - mPreviewBitmapHeight = previewHeight; - mSize = previewWidth + "x" + previewHeight; - mWidgetSpacingLayout = widgetSpacingLayout; + mUserManager = UserManagerCompat.getInstance(context); + mDb = new CacheDb(context); } - public Bitmap getPreview(final Object o) { - final String name = getObjectName(o); - final String packageName = getObjectPackage(o); - // check if the package is valid - synchronized(sInvalidPackages) { - boolean packageValid = !sInvalidPackages.contains(packageName); - if (!packageValid) { - return null; - } - } - 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; - } - } - } - - Bitmap unusedBitmap = null; - synchronized(mUnusedBitmaps) { - // not in cache; we need to load it from the db - 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(); - c.setBitmap(unusedBitmap); - c.drawColor(0, PorterDuff.Mode.CLEAR); - c.setBitmap(null); - } - } - - if (unusedBitmap == null) { - unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight, - Bitmap.Config.ARGB_8888); - } - Bitmap preview = readFromDb(name, unusedBitmap); - - if (preview != null) { - synchronized(mLoadedPreviews) { - mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); - } - return preview; - } else { - // it's not in the db... we need to generate it - final Bitmap generatedPreview = generatePreview(o, unusedBitmap); - preview = generatedPreview; - if (preview != unusedBitmap) { - throw new RuntimeException("generatePreview is not recycling the bitmap " + o); - } - - synchronized(mLoadedPreviews) { - mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); - } - - // write to db on a thread pool... this can be done lazily and improves the performance - // of the first time widget previews are loaded - new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - writeToDb(o, generatedPreview); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - - return preview; - } - } + /** + * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be + * called on UI thread + * + * @param o either {@link LauncherAppWidgetProviderInfo} or {@link ResolveInfo} + * @param immediateResult A bitmap array of size 1. If the result is already cached, it is + * set to the final result. + * @return a request id which can be used to cancel the request. + */ + public PreviewLoadRequest getPreview(final Object o, int previewWidth, int previewHeight, + WidgetCell caller, Bitmap[] immediateResult) { + String size = previewWidth + "x" + previewHeight; + WidgetCacheKey key = getObjectKey(o, size); - public void recycleBitmap(Object o, Bitmap bitmapToRecycle) { - String name = getObjectName(o); + // Check if we have the preview loaded or not. synchronized (mLoadedPreviews) { - if (mLoadedPreviews.containsKey(name)) { - Bitmap b = mLoadedPreviews.get(name).get(); - if (b == bitmapToRecycle) { - mLoadedPreviews.remove(name); - if (bitmapToRecycle.isMutable()) { - synchronized (mUnusedBitmaps) { - mUnusedBitmaps.add(new SoftReference<Bitmap>(b)); - } - } - } else { - throw new RuntimeException("Bitmap passed in doesn't match up"); - } + WeakReference<Bitmap> ref = mLoadedPreviews.get(key); + if (ref != null && ref.get() != null) { + immediateResult[0] = ref.get(); + return new PreviewLoadRequest(null, key); } } + + PreviewLoadTask task = new PreviewLoadTask(key, o, previewWidth, previewHeight, caller); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return new PreviewLoadRequest(task, key); } - static class CacheDb extends SQLiteOpenHelper { - final static int DB_VERSION = 2; - final static String TABLE_NAME = "shortcut_and_widget_previews"; - final static String COLUMN_NAME = "name"; - final static String COLUMN_SIZE = "size"; - final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; - Context mContext; + /** + * The DB holds the generated previews for various components. Previews can also have different + * sizes (landscape vs portrait). + */ + private static class CacheDb extends SQLiteOpenHelper { + private static final int DB_VERSION = 3; + + private static final String TABLE_NAME = "shortcut_and_widget_previews"; + private static final String COLUMN_COMPONENT = "componentName"; + private static final String COLUMN_USER = "profileId"; + private static final String COLUMN_SIZE = "size"; + private static final String COLUMN_PACKAGE = "packageName"; + private static final String COLUMN_LAST_UPDATED = "lastUpdated"; + private static final String COLUMN_VERSION = "version"; + private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; public CacheDb(Context context) { - super(context, new File(context.getCacheDir(), - LauncherFiles.WIDGET_PREVIEWS_DB).getPath(), null, DB_VERSION); - // Store the context for later use - mContext = context; + super(context, LauncherFiles.WIDGET_PREVIEWS_DB, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase database) { database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + - COLUMN_NAME + " TEXT NOT NULL, " + + COLUMN_COMPONENT + " TEXT NOT NULL, " + + COLUMN_USER + " INTEGER NOT NULL, " + COLUMN_SIZE + " TEXT NOT NULL, " + - COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " + - "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " + + COLUMN_PACKAGE + " TEXT NOT NULL, " + + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_PREVIEW_BITMAP + " BLOB, " + + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " + ");"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion != newVersion) { - // Delete all the records; they'll be repopulated as this is a cache - db.execSQL("DELETE FROM " + TABLE_NAME); + clearDB(db); } } - } - private static final String WIDGET_PREFIX = "Widget:"; - private static final String SHORTCUT_PREFIX = "Shortcut:"; + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } + } - private static String getObjectName(Object o) { - // should cache the string builder - StringBuilder sb = new StringBuilder(); - String output; - if (o instanceof AppWidgetProviderInfo) { - sb.append(WIDGET_PREFIX); - sb.append(((AppWidgetProviderInfo) o).toString()); - output = sb.toString(); - sb.setLength(0); - } else { - sb.append(SHORTCUT_PREFIX); - ResolveInfo info = (ResolveInfo) o; - sb.append(new ComponentName(info.activityInfo.packageName, - info.activityInfo.name).flattenToString()); - output = sb.toString(); - sb.setLength(0); + private void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); } - return output; } - private String getObjectPackage(Object o) { - if (o instanceof AppWidgetProviderInfo) { - return ((AppWidgetProviderInfo) o).provider.getPackageName(); + private WidgetCacheKey getObjectKey(Object o, String size) { + // should cache the string builder + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) o; + return new WidgetCacheKey(info.provider, mManager.getUser(info), size); } else { ResolveInfo info = (ResolveInfo) o; - return info.activityInfo.packageName; + return new WidgetCacheKey( + new ComponentName(info.activityInfo.packageName, info.activityInfo.name), + UserHandleCompat.myUserHandle(), size); } } - private void writeToDb(Object o, Bitmap preview) { - String name = getObjectName(o); - SQLiteDatabase db = mDb.getWritableDatabase(); + @Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) { ContentValues values = new ContentValues(); + values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString()); + values.put(CacheDb.COLUMN_USER, mUserManager.getSerialNumberForUser(key.user)); + values.put(CacheDb.COLUMN_SIZE, key.size); + values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName()); + values.put(CacheDb.COLUMN_VERSION, versions[0]); + values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]); + values.put(CacheDb.COLUMN_PREVIEW_BITMAP, Utilities.flattenBitmap(preview)); - values.put(CacheDb.COLUMN_NAME, name); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - preview.compress(Bitmap.CompressFormat.PNG, 100, stream); - values.put(CacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray()); - values.put(CacheDb.COLUMN_SIZE, mSize); try { - db.insert(CacheDb.TABLE_NAME, null, values); - } catch (SQLiteDiskIOException e) { - recreateDb(); - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + mDb.getWritableDatabase().insertWithOnConflict(CacheDb.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } catch (SQLException e) { + Log.e(TAG, "Error saving image to DB", e); } } - private void clearDb() { - SQLiteDatabase db = mDb.getWritableDatabase(); - // Delete everything + public void removePackage(String packageName, UserHandleCompat user) { + removePackage(packageName, user, mUserManager.getSerialNumberForUser(user)); + } + + private void removePackage(String packageName, UserHandleCompat user, long userSerial) { + synchronized(mPackageVersions) { + mPackageVersions.remove(packageName); + } + + synchronized (mLoadedPreviews) { + Set<WidgetCacheKey> keysToRemove = new HashSet<>(); + for (WidgetCacheKey key : mLoadedPreviews.keySet()) { + if (key.componentName.getPackageName().equals(packageName) && key.user.equals(user)) { + keysToRemove.add(key); + } + } + + for (WidgetCacheKey key : keysToRemove) { + WeakReference<Bitmap> req = mLoadedPreviews.remove(key); + if (req != null && req.get() != null) { + mUnusedBitmaps.add(req.get()); + } + } + } + try { - db.delete(CacheDb.TABLE_NAME, null, null); - } catch (SQLiteDiskIOException e) { - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + mDb.getWritableDatabase().delete(CacheDb.TABLE_NAME, + CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?", + new String[] {packageName, Long.toString(userSerial)}); + } catch (SQLException e) { + Log.e(TAG, "Unable to delete items from DB", e); } } - public static void removePackageFromDb(final CacheDb cacheDb, final String packageName) { - synchronized(sInvalidPackages) { - sInvalidPackages.add(packageName); + /** + * Updates the persistent DB: + * 1. Any preview generated for an old package version is removed + * 2. Any preview for an absent package is removed + * This ensures that we remove entries for packages which changed while the launcher was dead. + */ + public void removeObsoletePreviews() { + LongSparseArray<UserHandleCompat> userIdCache = new LongSparseArray<>(); + LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>(); + + for (Object obj : LauncherModel.getSortedWidgetsAndShortcuts(mContext, false)) { + final UserHandleCompat user; + final String pkg; + if (obj instanceof ResolveInfo) { + user = UserHandleCompat.myUserHandle(); + pkg = ((ResolveInfo) obj).activityInfo.packageName; + } else { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) obj; + user = mManager.getUser(info); + pkg = info.provider.getPackageName(); + } + + int userIdIndex = userIdCache.indexOfValue(user); + final long userId; + if (userIdIndex < 0) { + userId = mUserManager.getSerialNumberForUser(user); + userIdCache.put(userId, user); + } else { + userId = userIdCache.keyAt(userIdIndex); + } + + HashSet<String> packages = validPackages.get(userId); + if (packages == null) { + packages = new HashSet<>(); + validPackages.put(userId, packages); + } + packages.add(pkg); } - new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - SQLiteDatabase db = cacheDb.getWritableDatabase(); - try { - db.delete(CacheDb.TABLE_NAME, - CacheDb.COLUMN_NAME + " LIKE ? OR " + - CacheDb.COLUMN_NAME + " LIKE ?", // SELECT query - new String[] { - WIDGET_PREFIX + packageName + "/%", - SHORTCUT_PREFIX + packageName + "/%" - } // args to SELECT query - ); - } catch (SQLiteDiskIOException e) { - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + + LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>(); + Cursor c = null; + try { + c = mDb.getReadableDatabase().query(CacheDb.TABLE_NAME, + new String[] {CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE, + CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION}, + null, null, null, null, null); + while (c.moveToNext()) { + long userId = c.getLong(0); + String pkg = c.getString(1); + long lastUpdated = c.getLong(2); + long version = c.getLong(3); + + HashSet<String> packages = validPackages.get(userId); + if (packages != null && packages.contains(pkg)) { + long[] versions = getPackageVersion(pkg); + if (versions[0] == version && versions[1] == lastUpdated) { + // Every thing checks out + continue; + } } - synchronized(sInvalidPackages) { - sInvalidPackages.remove(packageName); + + // We need to delete this package. + packages = packagesToDelete.get(userId); + if (packages == null) { + packages = new HashSet<>(); + packagesToDelete.put(userId, packages); } - return null; + packages.add(pkg); } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - } - private static void removeItemFromDb(final CacheDb cacheDb, final String objectName) { - new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - SQLiteDatabase db = cacheDb.getWritableDatabase(); - try { - db.delete(CacheDb.TABLE_NAME, - CacheDb.COLUMN_NAME + " = ? ", // SELECT query - new String[] { objectName }); // args to SELECT query - } catch (SQLiteDiskIOException e) { - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + for (int i = 0; i < packagesToDelete.size(); i++) { + long userId = packagesToDelete.keyAt(i); + UserHandleCompat user = mUserManager.getUserForSerialNumber(userId); + for (String pkg : packagesToDelete.valueAt(i)) { + removePackage(pkg, user, userId); } - return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + } catch (SQLException e) { + Log.e(TAG, "Error updatating widget previews", e); + } finally { + if (c != null) { + c.close(); + } + } } - private Bitmap readFromDb(String name, Bitmap b) { - if (mCachedSelectQuery == null) { - mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " + - CacheDb.COLUMN_SIZE + " = ?"; - } - SQLiteDatabase db = mDb.getReadableDatabase(); - Cursor result; + private Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle) { + Cursor cursor = null; try { - result = db.query(CacheDb.TABLE_NAME, - new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return - mCachedSelectQuery, // select query - new String[] { name, mSize }, // args to select query - null, - null, - null, - null); - } catch (SQLiteDiskIOException e) { - recreateDb(); - return null; - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; - } - if (result.getCount() > 0) { - result.moveToFirst(); - byte[] blob = result.getBlob(0); - result.close(); - final BitmapFactory.Options opts = mCachedBitmapFactoryOptions.get(); - opts.inBitmap = b; - opts.inSampleSize = 1; - try { - return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); - } catch (IllegalArgumentException e) { - removeItemFromDb(mDb, name); - return null; + cursor = mDb.getReadableDatabase().query( + CacheDb.TABLE_NAME, + new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, + CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND " + CacheDb.COLUMN_SIZE + " = ?", + new String[] { + key.componentName.flattenToString(), + Long.toString(mUserManager.getSerialNumberForUser(key.user)), + key.size + }, + null, null, null); + if (cursor.moveToNext()) { + byte[] blob = cursor.getBlob(0); + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inBitmap = recycle; + try { + return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); + } catch (Exception e) { + return null; + } + } + } catch (SQLException e) { + Log.w(TAG, "Error loading preview from DB", e); + } finally { + if (cursor != null) { + cursor.close(); } - } else { - result.close(); - return null; } + return null; } - private Bitmap generatePreview(Object info, Bitmap preview) { - if (preview != null && - (preview.getWidth() != mPreviewBitmapWidth || - preview.getHeight() != mPreviewBitmapHeight)) { - throw new RuntimeException("Improperly sized bitmap passed as argument"); - } + private Bitmap generatePreview(Object info, Bitmap recycle, int previewWidth, int previewHeight) { if (info instanceof LauncherAppWidgetProviderInfo) { - return generateWidgetPreview((LauncherAppWidgetProviderInfo) info, preview); + return generateWidgetPreview((LauncherAppWidgetProviderInfo) info, previewWidth, recycle); } else { return generateShortcutPreview( - (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview); + (ResolveInfo) info, previewWidth, previewHeight, recycle); } } - public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info, Bitmap preview) { - int maxWidth = maxWidthForWidgetPreview(info.spanX); - int maxHeight = maxHeightForWidgetPreview(info.spanY); - return generateWidgetPreview(info, info.spanX, info.spanY, maxWidth, - maxHeight, preview, null); - } - - public int maxWidthForWidgetPreview(int spanX) { - return Math.min(mPreviewBitmapWidth, - mWidgetSpacingLayout.estimateCellWidth(spanX)); - } - - public int maxHeightForWidgetPreview(int spanY) { - return Math.min(mPreviewBitmapHeight, - mWidgetSpacingLayout.estimateCellHeight(spanY)); + public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info, + int previewWidth, Bitmap preview) { + int maxWidth = Math.min(previewWidth, info.spanX + * LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile().cellWidthPx); + return generateWidgetPreview(info, maxWidth, preview, null); } - public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info, int cellHSpan, int cellVSpan, - int maxPreviewWidth, int maxPreviewHeight, Bitmap preview, int[] preScaledWidthOut) { + public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info, + int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) { // Load the preview image if possible if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; - if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE; Drawable drawable = null; if (info.previewImage != 0) { @@ -531,61 +368,23 @@ public class WidgetPreviewLoader { } } + final boolean widgetPreviewExists = (drawable != null); + final int spanX = info.spanX < 1 ? 1 : info.spanX; + final int spanY = info.spanY < 1 ? 1 : info.spanY; + int previewWidth; int previewHeight; - Bitmap defaultPreview = null; - boolean widgetPreviewExists = (drawable != null); + Bitmap tileBitmap = null; + if (widgetPreviewExists) { previewWidth = drawable.getIntrinsicWidth(); previewHeight = drawable.getIntrinsicHeight(); } else { // Generate a preview image if we couldn't load one - if (cellHSpan < 1) cellHSpan = 1; - if (cellVSpan < 1) cellVSpan = 1; - - // 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 - .getIntrinsicWidth(); - final int previewDrawableHeight = previewDrawable - .getIntrinsicHeight(); - previewWidth = previewDrawableWidth * cellHSpan; - previewHeight = previewDrawableHeight * cellVSpan; - - defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); - final Canvas c = mCachedAppWidgetPreviewCanvas.get(); - c.setBitmap(defaultPreview); - 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 * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); - int smallestSide = Math.min(previewWidth, previewHeight); - float iconScale = Math.min((float) smallestSide - / (mAppIconSize + 2 * minOffset), 1f); - - try { - 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)); - } - } catch (Resources.NotFoundException e) { - } + tileBitmap = ((BitmapDrawable) mContext.getResources().getDrawable( + R.drawable.widget_tile)).getBitmap(); + previewWidth = tileBitmap.getWidth() * spanX; + previewHeight = tileBitmap.getHeight() * spanY; } // Scale to fit width only - let the widget preview be clipped in the @@ -603,30 +402,60 @@ public class WidgetPreviewLoader { } // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size + final Canvas c = new Canvas(); if (preview == null) { preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); + c.setBitmap(preview); + } else { + // Reusing bitmap. Clear it. + c.setBitmap(preview); + c.drawColor(0, PorterDuff.Mode.CLEAR); } // Draw the scaled preview into the final bitmap int x = (preview.getWidth() - previewWidth) / 2; if (widgetPreviewExists) { - renderDrawableToBitmap(drawable, preview, x, 0, previewWidth, - previewHeight); + drawable.setBounds(x, 0, x + previewWidth, previewHeight); + drawable.draw(c); } else { - final Canvas c = mCachedAppWidgetPreviewCanvas.get(); - final Rect src = mCachedAppWidgetPreviewSrcRect.get(); - final Rect dest = mCachedAppWidgetPreviewDestRect.get(); - c.setBitmap(preview); - src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight()); - dest.set(x, 0, x + previewWidth, previewHeight); - - Paint p = mCachedAppWidgetPreviewPaint.get(); - if (p == null) { - p = new Paint(); - p.setFilterBitmap(true); - mCachedAppWidgetPreviewPaint.set(p); + final Paint p = new Paint(); + p.setFilterBitmap(true); + int appIconSize = LauncherAppState.getInstance().getDynamicGrid() + .getDeviceProfile().iconSizePx; + + // draw the spanX x spanY tiles + final Rect src = new Rect(0, 0, tileBitmap.getWidth(), tileBitmap.getHeight()); + + float tileW = scale * tileBitmap.getWidth(); + float tileH = scale * tileBitmap.getHeight(); + final RectF dst = new RectF(0, 0, tileW, tileH); + + float tx = x; + for (int i = 0; i < spanX; i++, tx += tileW) { + float ty = 0; + for (int j = 0; j < spanY; j++, ty += tileH) { + dst.offsetTo(tx, ty); + c.drawBitmap(tileBitmap, src, dst, p); + } } - c.drawBitmap(defaultPreview, src, dest, p); + + // Draw the icon in the top left corner + // TODO: use top right for RTL + int minOffset = (int) (appIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); + int smallestSide = Math.min(previewWidth, previewHeight); + float iconScale = Math.min((float) smallestSide / (appIconSize + 2 * minOffset), scale); + + try { + Drawable icon = mutateOnMainThread(mManager.loadIcon(info, mIconCache)); + if (icon != null) { + int hoffset = (int) ((tileW - appIconSize * iconScale) / 2) + x; + int yoffset = (int) ((tileH - appIconSize * iconScale) / 2); + icon.setBounds(hoffset, yoffset, + hoffset + (int) (appIconSize * iconScale), + yoffset + (int) (appIconSize * iconScale)); + icon.draw(c); + } + } catch (Resources.NotFoundException e) { } c.setBitmap(null); } return mManager.getBadgeBitmap(info, preview); @@ -634,71 +463,49 @@ public class WidgetPreviewLoader { private Bitmap generateShortcutPreview( ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { - Bitmap tempBitmap = mCachedShortcutPreviewBitmap.get(); - final Canvas c = mCachedShortcutPreviewCanvas.get(); - if (tempBitmap == null || - tempBitmap.getWidth() != maxWidth || - tempBitmap.getHeight() != maxHeight) { - tempBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); - mCachedShortcutPreviewBitmap.set(tempBitmap); + final Canvas c = new Canvas(); + if (preview == null) { + preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); + c.setBitmap(preview); + } else if (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight) { + throw new RuntimeException("Improperly sized bitmap passed as argument"); } else { - c.setBitmap(tempBitmap); + // Reusing bitmap. Clear it. + c.setBitmap(preview); c.drawColor(0, PorterDuff.Mode.CLEAR); - c.setBitmap(null); } - // Render the icon - Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo)); - int paddingTop = mContext. - getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); - int paddingLeft = mContext. - getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); - int paddingRight = mContext. - getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); + Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo)); + icon.setFilterBitmap(true); + // Draw a desaturated/scaled version of the icon in the background as a watermark + ColorMatrix colorMatrix = new ColorMatrix(); + colorMatrix.setSaturation(0); + icon.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); + icon.setAlpha((int) (255 * 0.06f)); + + Resources res = mContext.getResources(); + int paddingTop = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); + int paddingLeft = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); + int paddingRight = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); int scaledIconWidth = (maxWidth - paddingLeft - paddingRight); + icon.setBounds(paddingLeft, paddingTop, + paddingLeft + scaledIconWidth, paddingTop + scaledIconWidth); + icon.draw(c); + + // Draw the final icon at top left corner. + // TODO: use top right for RTL + int appIconSize = LauncherAppState.getInstance().getDynamicGrid() + .getDeviceProfile().iconSizePx; + icon.setAlpha(255); + icon.setColorFilter(null); + icon.setBounds(0, 0, appIconSize, appIconSize); + icon.draw(c); - renderDrawableToBitmap( - icon, tempBitmap, paddingLeft, paddingTop, scaledIconWidth, scaledIconWidth); - - if (preview != null && - (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight)) { - throw new RuntimeException("Improperly sized bitmap passed as argument"); - } else if (preview == null) { - preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); - } - - c.setBitmap(preview); - // Draw a desaturated/scaled version of the icon in the background as a watermark - Paint p = mCachedShortcutPreviewPaint.get(); - if (p == null) { - p = new Paint(); - ColorMatrix colorMatrix = new ColorMatrix(); - colorMatrix.setSaturation(0); - p.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); - p.setAlpha((int) (255 * 0.06f)); - mCachedShortcutPreviewPaint.set(p); - } - c.drawBitmap(tempBitmap, 0, 0, p); c.setBitmap(null); - - renderDrawableToBitmap(icon, preview, 0, 0, mAppIconSize, mAppIconSize); - return preview; } - private static void renderDrawableToBitmap( - Drawable d, Bitmap bitmap, int x, int y, int w, int h) { - if (bitmap != null) { - Canvas c = new Canvas(bitmap); - Rect oldBounds = d.copyBounds(); - d.setBounds(x, y, x + w, y + h); - d.draw(c); - d.setBounds(oldBounds); // Restore the bounds - c.setBitmap(null); - } - } - private Drawable mutateOnMainThread(final Drawable drawable) { try { return mMainThreadExecutor.submit(new Callable<Drawable>() { @@ -715,82 +522,148 @@ public class WidgetPreviewLoader { } } - 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. + * @return an array of containing versionCode and lastUpdatedTime for the package. */ - 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; + private long[] getPackageVersion(String packageName) { + synchronized (mPackageVersions) { + long[] versions = mPackageVersions.get(packageName); + if (versions == null) { + versions = new long[2]; 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]++; + PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0); + versions[0] = info.versionCode; + versions[1] = info.lastUpdateTime; + } catch (NameNotFoundException e) { + Log.e(TAG, "PackageInfo not found", e); + } + mPackageVersions.put(packageName, versions); + } + return versions; + } + } + + /** + * A request Id which can be used by the client to cancel any request. + */ + public class PreviewLoadRequest { + + private final PreviewLoadTask mTask; + private final WidgetCacheKey mKey; + + public PreviewLoadRequest(PreviewLoadTask task, WidgetCacheKey key) { + mTask = task; + mKey = key; + } + + public void cancel(boolean recycleImage) { + if (mTask != null) { + mTask.cancel(true); + } + + if (recycleImage) { + synchronized(mLoadedPreviews) { + WeakReference<Bitmap> result = mLoadedPreviews.remove(mKey); + if (result != null && result.get() != null) { + mUnusedBitmaps.add(result.get()); } - files.add(resolved); - if (total % SAMPLE_RATE == 0) { - Log.i(TAG, " fd " + i + ": " + resolved - + " (" + types.get(type) + ")"); + } + } + } + } + + public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap> { + + private final WidgetCacheKey mKey; + private final Object mInfo; + private final int mPreviewHeight; + private final int mPreviewWidth; + private final WidgetCell mCaller; + + PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth, + int previewHeight, WidgetCell caller) { + mKey = key; + mInfo = info; + mPreviewHeight = previewHeight; + mPreviewWidth = previewWidth; + mCaller = caller; + if (DEBUG) { + Log.d(TAG, String.format("%s, %s, %d, %d", + mKey, mInfo, mPreviewHeight, mPreviewWidth)); + } + } + + @Override + protected Bitmap doInBackground(Void... params) { + Bitmap unusedBitmap = null; + + // TODO(hyunyoungs): Figure out why this path causes concurrency issue. + synchronized (mUnusedBitmaps) { + // Check if we can use a bitmap + for (Bitmap candidate : mUnusedBitmaps) { + if (candidate != null && candidate.isMutable() && + candidate.getWidth() == mPreviewWidth && + candidate.getHeight() == mPreviewHeight) { + unusedBitmap = candidate; + break; } - } catch (IOException e) { - // Ignoring exceptions for non-existing file descriptors. + } + + if (unusedBitmap == null) { + unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888); + } else { + mUnusedBitmaps.remove(unusedBitmap); } } - 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])); + if (isCancelled()) { + return null; } - } 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); + Bitmap preview = readFromDb(mKey, unusedBitmap); + if (!isCancelled() && preview == null) { + // Fetch the version info before we generate the preview, so that, in-case the + // app was updated while we are generating the preview, we use the old version info, + // which would gets re-written next time. + long[] versions = getPackageVersion(mKey.componentName.getPackageName()); + + // it's not in the db... we need to generate it + preview = generatePreview(mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight); + + if (!isCancelled()) { + writeToDb(mKey, versions, preview); + } + } + + return preview; + } + + @Override + protected void onPostExecute(Bitmap result) { + synchronized(mLoadedPreviews) { + mLoadedPreviews.put(mKey, new WeakReference<Bitmap>(result)); + } + + mCaller.applyPreview(result); + } + } + + private static final class WidgetCacheKey extends ComponentKey { + + // TODO: remove dependency on size + private final String size; + + public WidgetCacheKey(ComponentName componentName, UserHandleCompat user, String size) { + super(componentName, user); + this.size = size; + } + + @Override + public int hashCode() { + return super.hashCode() ^ size.hashCode(); + } + + @Override + public boolean equals(Object o) { + return super.equals(o) && ((WidgetCacheKey) o).size.equals(size); } } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 95215402c..6b03e3100 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -26,6 +26,7 @@ import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.app.WallpaperManager; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; @@ -43,12 +44,11 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region.Op; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Parcelable; -import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -66,9 +66,14 @@ import com.android.launcher3.FolderIcon.FolderRingAnimator; import com.android.launcher3.Launcher.CustomContentCallbacks; import com.android.launcher3.Launcher.LauncherOverlay; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.UninstallDropTarget.UninstallSource; import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.util.WallpaperUtils; +import com.android.launcher3.widget.PendingAddShortcutInfo; +import com.android.launcher3.widget.PendingAddWidgetInfo; import java.util.ArrayList; import java.util.HashMap; @@ -84,7 +89,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class Workspace extends SmoothPagedView implements DropTarget, DragSource, DragScroller, View.OnTouchListener, DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener, - Insettable { + Insettable, UninstallSource { private static final String TAG = "Launcher.Workspace"; private static final int CHILDREN_OUTLINE_FADE_OUT_DELAY = 0; @@ -117,24 +122,24 @@ public class Workspace extends SmoothPagedView private long mCustomContentShowTime = -1; private LayoutTransition mLayoutTransition; - private final WallpaperManager mWallpaperManager; - private IBinder mWindowToken; + @Thunk final WallpaperManager mWallpaperManager; + @Thunk IBinder mWindowToken; private int mOriginalDefaultPage; private int mDefaultPage; private ShortcutAndWidgetContainer mDragSourceInternal; - private static boolean sAccessibilityEnabled; + @Thunk static boolean sAccessibilityEnabled; // The screen id used for the empty screen always present to the right. 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>(); + @Thunk HashMap<Long, CellLayout> mWorkspaceScreens = new HashMap<Long, CellLayout>(); + @Thunk ArrayList<Long> mScreenOrder = new ArrayList<Long>(); - private Runnable mRemoveEmptyScreenRunnable; - private boolean mDeferRemoveExtraEmptyScreen = false; + @Thunk Runnable mRemoveEmptyScreenRunnable; + @Thunk boolean mDeferRemoveExtraEmptyScreen = false; /** * CellInfo for the cell that is currently being dragged @@ -144,7 +149,7 @@ public class Workspace extends SmoothPagedView /** * Target drop area calculated during last acceptDrop call. */ - private int[] mTargetCell = new int[2]; + @Thunk int[] mTargetCell = new int[2]; private int mDragOverX = -1; private int mDragOverY = -1; @@ -159,7 +164,7 @@ public class Workspace extends SmoothPagedView /** * The CellLayout that is currently being dragged over */ - private CellLayout mDragTargetLayout = null; + @Thunk CellLayout mDragTargetLayout = null; /** * The CellLayout that we will show as glowing */ @@ -170,16 +175,16 @@ public class Workspace extends SmoothPagedView */ private CellLayout mDropToLayout = null; - private Launcher mLauncher; - private IconCache mIconCache; - private DragController mDragController; + @Thunk Launcher mLauncher; + @Thunk IconCache mIconCache; + @Thunk DragController mDragController; // These are temporary variables to prevent having to allocate a new object just to // return an (x, y) value from helper functions. Do NOT use them to maintain other state. private int[] mTempCell = new int[2]; private int[] mTempPt = new int[2]; private int[] mTempEstimate = new int[2]; - private float[] mDragViewVisualCenter = new float[2]; + @Thunk float[] mDragViewVisualCenter = new float[2]; private float[] mTempCellLayoutCenterCoordinates = new float[2]; private Matrix mTempInverseMatrix = new Matrix(); @@ -204,7 +209,7 @@ public class Workspace extends SmoothPagedView private boolean mInScrollArea = false; private HolographicOutlineHelper mOutlineHelper; - private Bitmap mDragOutline = null; + @Thunk Bitmap mDragOutline = null; private static final Rect sTempRect = new Rect(); private final int[] mTempXY = new int[2]; private int[] mTempVisiblePagesRange = new int[2]; @@ -213,11 +218,11 @@ public class Workspace extends SmoothPagedView private boolean mWorkspaceFadeInAdjacentScreens; WallpaperOffsetInterpolator mWallpaperOffset; - private boolean mWallpaperIsLiveWallpaper; - private int mNumPagesForWallpaperParallax; - private float mLastSetWallpaperOffsetSteps = 0; + @Thunk boolean mWallpaperIsLiveWallpaper; + @Thunk int mNumPagesForWallpaperParallax; + @Thunk float mLastSetWallpaperOffsetSteps = 0; - private Runnable mDelayedResizeRunnable; + @Thunk Runnable mDelayedResizeRunnable; private Runnable mDelayedSnapToPageRunnable; private Point mDisplaySize = new Point(); @@ -226,7 +231,7 @@ public class Workspace extends SmoothPagedView public static final int REORDER_TIMEOUT = 350; private final Alarm mFolderCreationAlarm = new Alarm(); private final Alarm mReorderAlarm = new Alarm(); - private FolderRingAnimator mDragFolderRingAnimator = null; + @Thunk FolderRingAnimator mDragFolderRingAnimator = null; private FolderIcon mDragOverFolderIcon = null; private boolean mCreateUserFolderOnDrop = false; private boolean mAddToExistingFolderOnDrop = false; @@ -255,8 +260,8 @@ public class Workspace extends SmoothPagedView private static final int DRAG_MODE_ADD_TO_FOLDER = 2; private static final int DRAG_MODE_REORDER = 3; private int mDragMode = DRAG_MODE_NONE; - private int mLastReorderX = -1; - private int mLastReorderY = -1; + @Thunk int mLastReorderX = -1; + @Thunk int mLastReorderY = -1; private SparseArray<Parcelable> mSavedStates; private final ArrayList<Integer> mRestoredPages = new ArrayList<Integer>(); @@ -268,17 +273,17 @@ public class Workspace extends SmoothPagedView private float mCurrentScale; private float mNewScale; - private float[] mOldBackgroundAlphas; + @Thunk float[] mOldBackgroundAlphas; private float[] mOldAlphas; - private float[] mNewBackgroundAlphas; + @Thunk float[] mNewBackgroundAlphas; private float[] mNewAlphas; private int mLastChildCount = -1; private float mTransitionProgress; - private Animator mStateAnimator = null; + @Thunk Animator mStateAnimator = null; float mOverScrollEffect = 0f; - private Runnable mDeferredAction; + @Thunk Runnable mDeferredAction; private boolean mDeferDropAfterUninstall; private boolean mUninstallSuccessful; @@ -365,13 +370,12 @@ public class Workspace extends SmoothPagedView // estimate the size of a widget with spans hSpan, vSpan. return MAX_VALUE for each // dimension if unsuccessful - public int[] estimateItemSize(int hSpan, int vSpan, - ItemInfo itemInfo, boolean springLoaded) { + public int[] estimateItemSize(ItemInfo itemInfo, boolean springLoaded) { int[] size = new int[2]; if (getChildCount() > 0) { // Use the first non-custom page to estimate the child position CellLayout cl = (CellLayout) getChildAt(numCustomPages()); - Rect r = estimateItemPosition(cl, itemInfo, 0, 0, hSpan, vSpan); + Rect r = estimateItemPosition(cl, itemInfo, 0, 0, itemInfo.spanX, itemInfo.spanY); size[0] = r.width(); size[1] = r.height(); if (springLoaded) { @@ -401,7 +405,6 @@ public class Workspace extends SmoothPagedView setChildrenBackgroundAlphaMultipliers(1f); // Prevent any Un/InstallShortcutReceivers from updating the db while we are dragging InstallShortcutReceiver.enableInstallQueue(); - UninstallShortcutReceiver.enableUninstallQueue(); post(new Runnable() { @Override public void run() { @@ -429,7 +432,6 @@ public class Workspace extends SmoothPagedView // Re-enable any Un/InstallShortcutReceiver and now process any queued items InstallShortcutReceiver.disableAndFlushInstallQueue(getContext()); - UninstallShortcutReceiver.disableAndFlushUninstallQueue(getContext()); mDragSourceInternal = null; mLauncher.onInteractionEnd(); @@ -492,7 +494,7 @@ public class Workspace extends SmoothPagedView CellLayout cl = ((CellLayout) child); cl.setOnInterceptTouchListener(this); cl.setClickable(true); - cl.setImportantForAccessibility(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO); + cl.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); super.onChildViewAdded(parent, child); } @@ -561,10 +563,6 @@ public class Workspace extends SmoothPagedView } public long insertNewWorkspaceScreen(long screenId, int insertIndex) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - insertNewWorkspaceScreen(): " + screenId + - " at index: " + insertIndex, true); - if (mWorkspaceScreens.containsKey(screenId)) { throw new RuntimeException("Screen id " + screenId + " already exists!"); } @@ -578,6 +576,12 @@ public class Workspace extends SmoothPagedView mWorkspaceScreens.put(screenId, newScreen); mScreenOrder.add(insertIndex, screenId); addView(newScreen, insertIndex); + + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + if (delegate != null && delegate.isInAccessibleDrag()) { + newScreen.enableAccessibleDrag(true); + } return screenId; } @@ -664,9 +668,6 @@ public class Workspace extends SmoothPagedView } public void addExtraEmptyScreenOnDrag() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - addExtraEmptyScreenOnDrag()", true); - boolean lastChildOnScreen = false; boolean childOnFinalScreen = false; @@ -693,9 +694,6 @@ public class Workspace extends SmoothPagedView } public boolean addExtraEmptyScreen() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - addExtraEmptyScreen()", true); - if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) { insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID); return true; @@ -704,9 +702,6 @@ public class Workspace extends SmoothPagedView } private void convertFinalScreenToEmptyScreenIfNecessary() { - // 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); @@ -731,7 +726,6 @@ public class Workspace extends SmoothPagedView // Update the model if we have changed any screens mLauncher.getModel().updateWorkspaceScreenOrder(mLauncher, mScreenOrder); - Launcher.addDumpLog(TAG, "11683562 - extra empty screen: " + finalScreenId, true); } } @@ -741,8 +735,6 @@ public class Workspace extends SmoothPagedView 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); @@ -784,9 +776,7 @@ public class Workspace extends SmoothPagedView private void fadeAndRemoveEmptyScreen(int delay, int duration, final Runnable onComplete, final boolean stripEmptyScreens) { - // Log to disk // XXX: Do we need to update LM workspace screens below? - Launcher.addDumpLog(TAG, "11683562 - fadeAndRemoveEmptyScreen()", true); PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f); PropertyValuesHolder bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", 0f); @@ -830,8 +820,6 @@ 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); @@ -890,9 +878,6 @@ public class Workspace extends SmoothPagedView } public void stripEmptyScreens() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - stripEmptyScreens()", true); - if (mLauncher.isWorkspaceLoading()) { // Don't strip empty screens if the workspace is still loading. // This is dangerous and can result in data loss. @@ -920,7 +905,6 @@ public class Workspace extends SmoothPagedView int pageShift = 0; for (Long id: removeScreens) { - Launcher.addDumpLog(TAG, "11683562 - removing id: " + id, true); CellLayout cl = mWorkspaceScreens.get(id); mWorkspaceScreens.remove(id); mScreenOrder.remove(id); @@ -1334,10 +1318,10 @@ public class Workspace extends SmoothPagedView protected void setWallpaperDimension() { new AsyncTask<Void, Void, Void>() { public Void doInBackground(Void ... args) { - String spKey = WallpaperCropActivity.getSharedPreferencesKey(); + String spKey = LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; SharedPreferences sp = mLauncher.getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); - LauncherWallpaperPickerActivity.suggestWallpaperDimension(mLauncher.getResources(), + WallpaperUtils.suggestWallpaperDimension(mLauncher.getResources(), sp, mLauncher.getWindowManager(), mWallpaperManager, mLauncher.overrideWallpaperDimensions()); return null; @@ -1427,7 +1411,22 @@ public class Workspace extends SmoothPagedView } private float wallpaperOffsetForCurrentScroll() { + // TODO: do different behavior if it's a live wallpaper? + // Don't use up all the wallpaper parallax until you have at least + // MIN_PARALLAX_PAGE_SPAN pages + int numScrollingPages = getNumScreensExcludingEmptyAndCustom(); + int parallaxPageSpan; + if (mWallpaperIsLiveWallpaper) { + parallaxPageSpan = numScrollingPages - 1; + } else { + parallaxPageSpan = Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages - 1); + } + mNumPagesForWallpaperParallax = parallaxPageSpan; + if (getChildCount() <= 1) { + if (isLayoutRtl()) { + return 1 - 1.0f/mNumPagesForWallpaperParallax; + } return 0; } @@ -1447,28 +1446,20 @@ public class Workspace extends SmoothPagedView if (scrollRange == 0) { return 0; } else { - // TODO: do different behavior if it's a live wallpaper? // Sometimes the left parameter of the pages is animated during a layout transition; // this parameter offsets it to keep the wallpaper from animating as well int adjustedScroll = getScrollX() - firstPageScrollX - getLayoutTransitionOffsetForPage(0); float offset = Math.min(1, adjustedScroll / (float) scrollRange); offset = Math.max(0, offset); - // Don't use up all the wallpaper parallax until you have at least - // MIN_PARALLAX_PAGE_SPAN pages - int numScrollingPages = getNumScreensExcludingEmptyAndCustom(); - int parallaxPageSpan; - if (mWallpaperIsLiveWallpaper) { - parallaxPageSpan = numScrollingPages - 1; - } else { - parallaxPageSpan = Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages - 1); - } - mNumPagesForWallpaperParallax = parallaxPageSpan; // On RTL devices, push the wallpaper offset to the right if we don't have enough // pages (ie if numScrollingPages < MIN_PARALLAX_PAGE_SPAN) - int padding = isLayoutRtl() ? parallaxPageSpan - numScrollingPages + 1 : 0; - return offset * (padding + numScrollingPages - 1) / parallaxPageSpan; + if (!mWallpaperIsLiveWallpaper && numScrollingPages < MIN_PARALLAX_PAGE_SPAN + && isLayoutRtl()) { + return offset * (parallaxPageSpan - numScrollingPages + 1) / parallaxPageSpan; + } + return offset * (numScrollingPages - 1) / parallaxPageSpan; } } @@ -1548,7 +1539,7 @@ public class Workspace extends SmoothPagedView @Override public void announceForAccessibility(CharSequence text) { // Don't announce if apps is on top of us. - if (!mLauncher.isAllAppsVisible()) { + if (!mLauncher.isAppsViewVisible()) { super.announceForAccessibility(text); } } @@ -1647,7 +1638,6 @@ 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); } } } @@ -1660,6 +1650,23 @@ public class Workspace extends SmoothPagedView } } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void enableAccessibleDrag(boolean enable) { + for (int i = 0; i < getChildCount(); i++) { + CellLayout child = (CellLayout) getChildAt(i); + child.enableAccessibleDrag(enable); + } + + if (enable) { + // We need to allow our individual children to become click handlers in this case + setOnClickListener(null); + } else { + // Reset our click listener + setOnClickListener(mLauncher); + } + mLauncher.getHotseat().getLayout().enableAccessibleDrag(enable); + } + public boolean hasCustomContent() { return (mScreenOrder.size() > 0 && mScreenOrder.get(0) == CUSTOM_CONTENT_SCREEN_ID); } @@ -1701,7 +1708,11 @@ public class Workspace extends SmoothPagedView mLastCustomContentScrollProgress = progress; - mLauncher.getDragLayer().setBackgroundAlpha(progress * 0.8f); + // We should only update the drag layer background alpha if we are not in all apps or the + // widgets tray + if (mState == State.NORMAL) { + mLauncher.getDragLayer().setBackgroundAlpha(progress * 0.8f); + } if (mLauncher.getHotseat() != null) { mLauncher.getHotseat().setTranslationX(translationX); @@ -1820,7 +1831,7 @@ public class Workspace extends SmoothPagedView @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { - if (!mLauncher.isAllAppsVisible()) { + if (!mLauncher.isAppsViewVisible()) { final Folder openFolder = getOpenFolder(); if (openFolder != null) { return openFolder.requestFocus(direction, previouslyFocusedRect); @@ -1841,7 +1852,7 @@ public class Workspace extends SmoothPagedView @Override public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { - if (!mLauncher.isAllAppsVisible()) { + if (!mLauncher.isAppsViewVisible()) { final Folder openFolder = getOpenFolder(); if (openFolder != null) { openFolder.addFocusables(views, direction); @@ -1886,7 +1897,7 @@ public class Workspace extends SmoothPagedView } } - private void updateChildrenLayersEnabled(boolean force) { + @Thunk void updateChildrenLayersEnabled(boolean force) { boolean small = mState == State.OVERVIEW || mIsSwitchingState; boolean enableChildrenLayers = force || small || mAnimatingViewIntoPlace || isPageMoving(); @@ -2050,8 +2061,7 @@ public class Workspace extends SmoothPagedView // If this is a text view, use its drawable instead if (v instanceof TextView) { - TextView tv = (TextView) v; - Drawable d = tv.getCompoundDrawables()[1]; + Drawable d = getTextViewIcon((TextView) v); Rect bounds = getDrawableBounds(d); bmpWidth = bounds.width(); bmpHeight = bounds.height(); @@ -2069,7 +2079,7 @@ public class Workspace extends SmoothPagedView } public void onDragStartedWithItem(PendingAddItemInfo info, Bitmap b, boolean clipAlpha) { - int[] size = estimateItemSize(info.spanX, info.spanY, info, false); + int[] size = estimateItemSize(info, false); // The outline is used to visualize where the item will land if dropped mDragOutline = createDragOutline(b, DRAG_BITMAP_PADDING, size[0], size[1], clipAlpha); @@ -2210,8 +2220,8 @@ public class Workspace extends SmoothPagedView private void updateAccessibilityFlags() { int accessible = mState == State.NORMAL ? - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES : - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; + IMPORTANT_FOR_ACCESSIBILITY_NO : + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; setImportantForAccessibility(accessible); } @@ -2254,7 +2264,10 @@ public class Workspace extends SmoothPagedView float finalBackgroundAlpha = (stateIsSpringLoaded || stateIsOverview) ? 1.0f : 0f; float finalHotseatAndPageIndicatorAlpha = (stateIsNormal || stateIsSpringLoaded) ? 1f : 0f; float finalOverviewPanelAlpha = stateIsOverview ? 1f : 0f; - float finalSearchBarAlpha = !stateIsNormal ? 0f : 1f; + // We keep the search bar visible on the workspace and in AllApps now + boolean showSearchBar = stateIsNormal || + (mLauncher.isAllAppsSearchOverridden() && stateIsNormalHidden); + float finalSearchBarAlpha = showSearchBar ? 1f : 0f; float finalWorkspaceTranslationY = stateIsOverview || stateIsOverviewHidden ? getOverviewModeTranslationY() : 0; @@ -2331,7 +2344,7 @@ public class Workspace extends SmoothPagedView } } - final View searchBar = mLauncher.getQsbBar(); + final View searchBar = mLauncher.getOrCreateQsbBar(); final View overviewPanel = mLauncher.getOverviewPanel(); final View hotseat = mLauncher.getHotseat(); final View pageIndicator = getPageIndicator(); @@ -2352,7 +2365,7 @@ public class Workspace extends SmoothPagedView cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); } else { if (layerViews != null) { - layerViews.put(cl, Launcher.BUILD_LAYER); + layerViews.put(cl, LauncherStateTransitionAnimation.BUILD_LAYER); } if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) { LauncherViewPropertyAnimator alphaAnim = @@ -2404,8 +2417,8 @@ public class Workspace extends SmoothPagedView if (layerViews != null) { // If layerViews is not null, we add these views, and indicate that // the caller can manage layer state. - layerViews.put(hotseat, Launcher.BUILD_AND_SET_LAYER); - layerViews.put(overviewPanel, Launcher.BUILD_AND_SET_LAYER); + layerViews.put(hotseat, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); + layerViews.put(overviewPanel, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); } else { // Otherwise let the animator handle layer management. hotseatAlpha.withLayer(); @@ -2434,7 +2447,7 @@ public class Workspace extends SmoothPagedView if (layerViews != null) { // If layerViews is not null, we add these views, and indicate that // the caller can manage layer state. - layerViews.put(searchBar, Launcher.BUILD_AND_SET_LAYER); + layerViews.put(searchBar, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); } else { // Otherwise let the animator handle layer management. searchBarAlpha.withLayer(); @@ -2576,7 +2589,7 @@ public class Workspace extends SmoothPagedView } } - private void onTransitionEnd() { + @Thunk void onTransitionEnd() { mIsSwitchingState = false; updateChildrenLayersEnabled(false); showCustomContentIfNecessary(); @@ -2588,6 +2601,19 @@ public class Workspace extends SmoothPagedView } /** + * Returns the drawable for the given text view. + */ + public static Drawable getTextViewIcon(TextView tv) { + final Drawable[] drawables = tv.getCompoundDrawables(); + for (int i = 0; i < drawables.length; i++) { + if (drawables[i] != null) { + return drawables[i]; + } + } + return null; + } + + /** * Draw the View v into the given Canvas. * * @param v the view to draw @@ -2602,7 +2628,7 @@ public class Workspace extends SmoothPagedView destCanvas.save(); if (v instanceof TextView) { - Drawable d = ((TextView) v).getCompoundDrawables()[1]; + Drawable d = getTextViewIcon((TextView) v); Rect bounds = getDrawableBounds(d); clipRect.set(0, 0, bounds.width() + padding, bounds.height() + padding); destCanvas.translate(padding / 2 - bounds.left, padding / 2 - bounds.top); @@ -2639,7 +2665,7 @@ public class Workspace extends SmoothPagedView int padding = expectedPadding.get(); if (v instanceof TextView) { - Drawable d = ((TextView) v).getCompoundDrawables()[1]; + Drawable d = getTextViewIcon((TextView) v); Rect bounds = getDrawableBounds(d); b = Bitmap.createBitmap(bounds.width() + padding, bounds.height() + padding, Bitmap.Config.ARGB_8888); @@ -2700,7 +2726,11 @@ public class Workspace extends SmoothPagedView return b; } - void startDrag(CellLayout.CellInfo cellInfo) { + public void startDrag(CellLayout.CellInfo cellInfo) { + startDrag(cellInfo, false); + } + + public void startDrag(CellLayout.CellInfo cellInfo, boolean accessible) { View child = cellInfo.cell; // Make sure the drag was started by a long press as opposed to a long click. @@ -2713,10 +2743,15 @@ public class Workspace extends SmoothPagedView CellLayout layout = (CellLayout) child.getParent().getParent(); layout.prepareChildForDrag(child); - beginDragShared(child, this); + beginDragShared(child, this, accessible); } - public void beginDragShared(View child, DragSource source) { + public void beginDragShared(View child, DragSource source, boolean accessible) { + beginDragShared(child, new Point(), source, accessible); + } + + public void beginDragShared(View child, Point relativeTouchPos, DragSource source, + boolean accessible) { child.clearFocus(); child.setPressed(false); @@ -2741,11 +2776,23 @@ public class Workspace extends SmoothPagedView Point dragVisualizeOffset = null; Rect dragRect = null; if (child instanceof BubbleTextView) { + BubbleTextView icon = (BubbleTextView) child; int iconSize = grid.iconSizePx; int top = child.getPaddingTop(); int left = (bmpWidth - iconSize) / 2; int right = left + iconSize; int bottom = top + iconSize; + if (icon.isLayoutHorizontal()) { + // If the layout is horizontal, then if we are just picking up the icon, then just + // use the child position since the icon is top-left aligned. Otherwise, offset + // the drag layer position horizontally so that the icon is under the current + // touch position. + if (icon.getIcon().getBounds().contains(relativeTouchPos.x, relativeTouchPos.y)) { + dragLayerX = Math.round(mTempXY[0]); + } else { + dragLayerX = Math.round(mTempXY[0] + relativeTouchPos.x - (bmpWidth / 2)); + } + } 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. @@ -2770,7 +2817,7 @@ public class Workspace extends SmoothPagedView } DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), - DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible); dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor()); if (child.getParent() instanceof ShortcutAndWidgetContainer) { @@ -2820,25 +2867,13 @@ public class Workspace extends SmoothPagedView // Start the drag DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), - DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, false); 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); - - final int[] cellXY = new int[2]; - target.findCellForSpanThatIntersects(cellXY, 1, 1, intersectX, intersectY); - addInScreen(view, container, screenId, cellXY[0], cellXY[1], 1, 1, insertAtFirst); - - LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screenId, cellXY[0], - cellXY[1]); - } - public boolean transitionStateShouldAllowDrop() { return ((!isSwitchingState() || mTransitionProgress > 0.5f) && (mState == State.NORMAL || mState == State.SPRING_LOADED)); @@ -2857,8 +2892,7 @@ public class Workspace extends SmoothPagedView } if (!transitionStateShouldAllowDrop()) return false; - mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, - d.dragView, mDragViewVisualCenter); + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); // We want the point to be mapped to the dragTarget. if (mLauncher.isHotseatLayout(dropTargetLayout)) { @@ -3060,9 +3094,7 @@ public class Workspace extends SmoothPagedView } public void onDrop(final DragObject d) { - mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, - mDragViewVisualCenter); - + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); CellLayout dropTargetLayout = mDropToLayout; // We want the point to be mapped to the dragTarget. @@ -3178,7 +3210,8 @@ public class Workspace extends SmoothPagedView final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell; AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo(); - if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) { + if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE + && !d.accessibleDrag) { final Runnable addResizeFrame = new Runnable() { public void run() { DragLayer dragLayer = mLauncher.getDragLayer(); @@ -3568,38 +3601,6 @@ public class Workspace extends SmoothPagedView return bestMatchingScreen; } - // This is used to compute the visual center of the dragView. This point is then - // used to visualize drop locations and determine where to drop an item. The idea is that - // the visual center represents the user's interpretation of where the item is, and hence - // is the appropriate point to use when determining drop location. - private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, - DragView dragView, float[] recycle) { - float res[]; - if (recycle == null) { - res = new float[2]; - } else { - res = recycle; - } - - // First off, the drag view has been shifted in a way that is not represented in the - // x and y values or the x/yOffsets. Here we account for that shift. - x += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetX); - y += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY); - - // These represent the visual top and left of drag view if a dragRect was provided. - // If a dragRect was not provided, then they correspond to the actual view left and - // top, as the dragRect is in that case taken to be the entire dragView. - // R.dimen.dragViewOffsetY. - int left = x - xOffset; - int top = y - yOffset; - - // In order to find the visual center, we shift by half the dragRect - res[0] = left + dragView.getDragRegion().width() / 2; - res[1] = top + dragView.getDragRegion().height() / 2; - - return res; - } - private boolean isDragWidget(DragObject d) { return (d.dragInfo instanceof LauncherAppWidgetInfo || d.dragInfo instanceof PendingAddWidgetInfo); @@ -3624,8 +3625,7 @@ public class Workspace extends SmoothPagedView // Ensure that we have proper spans for the item that we are dropping if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found"); - mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, - d.dragView, mDragViewVisualCenter); + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); final View child = (mDragInfo == null) ? null : mDragInfo.cell; // Identify whether we have dragged over a side page @@ -3700,7 +3700,7 @@ public class Workspace extends SmoothPagedView mTargetCell[1]); manageFolderFeedback(info, mDragTargetLayout, mTargetCell, - targetCellDistance, dragOverView); + targetCellDistance, dragOverView, d.accessibleDrag); boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX, @@ -3738,15 +3738,21 @@ public class Workspace extends SmoothPagedView } private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout, - int[] targetCell, float distance, View dragOverView) { + int[] targetCell, float distance, View dragOverView, boolean accessibleDrag) { boolean userFolderPending = willCreateUserFolder(info, targetLayout, targetCell, distance, false); - if (mDragMode == DRAG_MODE_NONE && userFolderPending && !mFolderCreationAlarm.alarmPending()) { - mFolderCreationAlarm.setOnAlarmListener(new - FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1])); - mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); + + FolderCreationAlarmListener listener = new + FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]); + + if (!accessibleDrag) { + mFolderCreationAlarm.setOnAlarmListener(listener); + mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); + } else { + listener.onAlarm(mFolderCreationAlarm); + } return; } @@ -4043,8 +4049,7 @@ public class Workspace extends SmoothPagedView } public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) { - int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo.spanX, - widgetInfo.spanY, widgetInfo, false); + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo, false); int visibility = layout.getVisibility(); layout.setVisibility(VISIBLE); @@ -4115,7 +4120,6 @@ public class Workspace extends SmoothPagedView // In the case where we've prebound the widget, we remove it from the DragLayer if (finalView instanceof AppWidgetHostView && external) { - Log.d(TAG, "6557954 Animate widget drop, final view is appWidgetHostView"); mLauncher.getDragLayer().removeView(finalView); } @@ -4199,7 +4203,7 @@ public class Workspace extends SmoothPagedView * * pixelX and pixelY should be in the coordinate system of layout */ - private int[] findNearestArea(int pixelX, int pixelY, + @Thunk int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, CellLayout layout, int[] recycle) { return layout.findNearestArea( pixelX, pixelY, spanX, spanY, recycle); @@ -4236,19 +4240,14 @@ public class Workspace extends SmoothPagedView removeWorkspaceItem(mDragInfo.cell); } } else if (mDragInfo != null) { - CellLayout cellLayout; - if (mLauncher.isHotseatLayout(target)) { - cellLayout = mLauncher.getHotseat().getLayout(); - } else { - cellLayout = getScreenWithId(mDragInfo.screenId); - } - if (cellLayout == null && LauncherAppState.isDogfoodBuild()) { - throw new RuntimeException("Invalid state: cellLayout == null in " - + "Workspace#onDropCompleted. Please file a bug. "); - } + final CellLayout cellLayout = mLauncher.getCellLayout( + mDragInfo.container, mDragInfo.screenId); if (cellLayout != null) { cellLayout.onDropChild(mDragInfo.cell); - } + } else if (LauncherAppState.isDogfoodBuild()) { + throw new RuntimeException("Invalid state: cellLayout == null in " + + "Workspace#onDropCompleted. Please file a bug. "); + }; } if ((d.cancelled || (beingCalledAfterUninstall && !mUninstallSuccessful)) && mDragInfo.cell != null) { @@ -4258,6 +4257,9 @@ public class Workspace extends SmoothPagedView mDragInfo = null; } + /** + * For opposite operation. See {@link #addInScreen}. + */ public void removeWorkspaceItem(View v) { CellLayout parentCell = getParentCellLayoutForView(v); if (parentCell != null) { @@ -4270,11 +4272,13 @@ public class Workspace extends SmoothPagedView } } + @Override public void deferCompleteDropAfterUninstallActivity() { mDeferDropAfterUninstall = true; } /// maybe move this into a smaller part + @Override public void onUninstallActivityReturned(boolean success) { mDeferDropAfterUninstall = false; mUninstallSuccessful = success; @@ -4306,88 +4310,6 @@ public class Workspace extends SmoothPagedView } } - ArrayList<ComponentName> getUniqueComponents(boolean stripDuplicates, ArrayList<ComponentName> duplicates) { - ArrayList<ComponentName> uniqueIntents = new ArrayList<ComponentName>(); - getUniqueIntents((CellLayout) mLauncher.getHotseat().getLayout(), uniqueIntents, duplicates, false); - int count = getChildCount(); - for (int i = 0; i < count; i++) { - CellLayout cl = (CellLayout) getChildAt(i); - getUniqueIntents(cl, uniqueIntents, duplicates, false); - } - return uniqueIntents; - } - - void getUniqueIntents(CellLayout cl, ArrayList<ComponentName> uniqueIntents, - ArrayList<ComponentName> duplicates, boolean stripDuplicates) { - int count = cl.getShortcutsAndWidgets().getChildCount(); - - ArrayList<View> children = new ArrayList<View>(); - for (int i = 0; i < count; i++) { - View v = cl.getShortcutsAndWidgets().getChildAt(i); - children.add(v); - } - - for (int i = 0; i < count; i++) { - View v = children.get(i); - ItemInfo info = (ItemInfo) v.getTag(); - // Null check required as the AllApps button doesn't have an item info - if (info instanceof ShortcutInfo) { - ShortcutInfo si = (ShortcutInfo) info; - ComponentName cn = si.intent.getComponent(); - - Uri dataUri = si.intent.getData(); - // If dataUri is not null / empty or if this component isn't one that would - // have previously showed up in the AllApps list, then this is a widget-type - // shortcut, so ignore it. - if (dataUri != null && !dataUri.equals(Uri.EMPTY)) { - continue; - } - - if (!uniqueIntents.contains(cn)) { - uniqueIntents.add(cn); - } else { - if (stripDuplicates) { - cl.removeViewInLayout(v); - LauncherModel.deleteItemFromDatabase(mLauncher, si); - } - if (duplicates != null) { - duplicates.add(cn); - } - } - } - if (v instanceof FolderIcon) { - FolderIcon fi = (FolderIcon) v; - ArrayList<View> items = fi.getFolder().getItemsInReadingOrder(); - for (int j = 0; j < items.size(); j++) { - if (items.get(j).getTag() instanceof ShortcutInfo) { - ShortcutInfo si = (ShortcutInfo) items.get(j).getTag(); - ComponentName cn = si.intent.getComponent(); - - Uri dataUri = si.intent.getData(); - // If dataUri is not null / empty or if this component isn't one that would - // have previously showed up in the AllApps list, then this is a widget-type - // shortcut, so ignore it. - if (dataUri != null && !dataUri.equals(Uri.EMPTY)) { - continue; - } - - if (!uniqueIntents.contains(cn)) { - uniqueIntents.add(cn); - } else { - if (stripDuplicates) { - fi.getFolderInfo().remove(si); - LauncherModel.deleteItemFromDatabase(mLauncher, si); - } - if (duplicates != null) { - duplicates.add(cn); - } - } - } - } - } - } - } - void saveWorkspaceToDb() { saveWorkspaceScreenToDb((CellLayout) mLauncher.getHotseat().getLayout()); int count = getChildCount(); @@ -4420,8 +4342,7 @@ public class Workspace extends SmoothPagedView cellX = hotseat.getCellXFromOrder((int) info.screenId); cellY = hotseat.getCellYFromOrder((int) info.screenId); } - LauncherModel.addItemToDatabase(mLauncher, info, container, screenId, cellX, - cellY, false); + LauncherModel.addItemToDatabase(mLauncher, info, container, screenId, cellX, cellY); } if (v instanceof FolderIcon) { FolderIcon fi = (FolderIcon) v; @@ -4879,7 +4800,7 @@ public class Workspace extends SmoothPagedView updates.contains(info)) { ShortcutInfo si = (ShortcutInfo) info; BubbleTextView shortcut = (BubbleTextView) v; - boolean oldPromiseState = shortcut.getCompoundDrawables()[1] + boolean oldPromiseState = getTextViewIcon(shortcut) instanceof PreloadIconDrawable; shortcut.applyFromShortcutInfo(si, mIconCache, true, si.isPromise() != oldPromiseState); @@ -4914,7 +4835,8 @@ public class Workspace extends SmoothPagedView 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); + shortcutInfo.promisedIntent, user, + shortcutInfo.shouldUseLowResIcon()); } else { // Only update the icon for restored apps. shortcutInfo.updateIcon(mIconCache); diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java new file mode 100644 index 000000000..f890706ff --- /dev/null +++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java @@ -0,0 +1,149 @@ +package com.android.launcher3.compat; + +import android.content.Context; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * Fallback class to support Alphabetic indexing if not supported by the framework. + * TODO(winsonc): disable for non-english locales + */ +class BaseAlphabeticIndex { + + private static final String BUCKETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-"; + private static final int UNKNOWN_BUCKET_INDEX = BUCKETS.length() - 1; + + public BaseAlphabeticIndex() {} + + /** + * Sets the max number of the label buckets in this index. + */ + public void setMaxLabelCount(int count) { + // Not currently supported + } + + /** + * Returns the index of the bucket in which the given string should appear. + */ + protected int getBucketIndex(String s) { + if (s.isEmpty()) { + return UNKNOWN_BUCKET_INDEX; + } + int index = BUCKETS.indexOf(s.substring(0, 1).toUpperCase()); + if (index != -1) { + return index; + } + return UNKNOWN_BUCKET_INDEX; + } + + /** + * Returns the label for the bucket at the given index (as returned by getBucketIndex). + */ + protected String getBucketLabel(int index) { + return BUCKETS.substring(index, index + 1); + } +} + +/** + * Reflected libcore.icu.AlphabeticIndex implementation, falls back to the base alphabetic index. + */ +public class AlphabeticIndexCompat extends BaseAlphabeticIndex { + + private Object mAlphabeticIndex; + private Method mAddLabelsMethod; + private Method mSetMaxLabelCountMethod; + private Method mGetBucketIndexMethod; + private Method mGetBucketLabelMethod; + private boolean mHasValidAlphabeticIndex; + + public AlphabeticIndexCompat(Context context) { + super(); + try { + Locale curLocale = context.getResources().getConfiguration().locale; + Class clazz = Class.forName("libcore.icu.AlphabeticIndex"); + Constructor ctor = clazz.getConstructor(Locale.class); + mAddLabelsMethod = clazz.getDeclaredMethod("addLabels", Locale.class); + mSetMaxLabelCountMethod = clazz.getDeclaredMethod("setMaxLabelCount", int.class); + mGetBucketIndexMethod = clazz.getDeclaredMethod("getBucketIndex", String.class); + mGetBucketLabelMethod = clazz.getDeclaredMethod("getBucketLabel", int.class); + mAlphabeticIndex = ctor.newInstance(curLocale); + try { + // Ensure we always have some base English locale buckets + if (!curLocale.getLanguage().equals(new Locale("en").getLanguage())) { + mAddLabelsMethod.invoke(mAlphabeticIndex, Locale.ENGLISH); + } + } catch (Exception e) { + e.printStackTrace(); + } + mHasValidAlphabeticIndex = true; + } catch (Exception e) { + mHasValidAlphabeticIndex = false; + } + } + + /** + * Sets the max number of the label buckets in this index. + * (ICU 51 default is 99) + */ + public void setMaxLabelCount(int count) { + if (mHasValidAlphabeticIndex) { + try { + mSetMaxLabelCountMethod.invoke(mAlphabeticIndex, count); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + super.setMaxLabelCount(count); + } + } + + /** + * Computes the section name for an given string {@param s}. + */ + public String computeSectionName(String s) { + String sectionName = getBucketLabel(getBucketIndex(s)); + if (sectionName.trim().isEmpty() && s.length() > 0) { + boolean startsWithDigit = Character.isDigit(Character.codePointAt(s.trim(), 0)); + if (startsWithDigit) { + // Digit section + return "#"; + } else { + // Unknown section + return "\u2022"; + } + } + return sectionName; + } + + /** + * Returns the index of the bucket in which {@param s} should appear. + * Function is synchronized because underlying routine walks an iterator + * whose state is maintained inside the index object. + */ + protected int getBucketIndex(String s) { + if (mHasValidAlphabeticIndex) { + try { + return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s); + } catch (Exception e) { + e.printStackTrace(); + } + } + return super.getBucketIndex(s); + } + + /** + * Returns the label for the bucket at the given index (as returned by getBucketIndex). + */ + protected String getBucketLabel(int index) { + if (mHasValidAlphabeticIndex) { + try { + return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index); + } catch (Exception e) { + e.printStackTrace(); + } + } + return super.getBucketLabel(index); + } +} diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java index 90a4d1a1f..07ef0efb7 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java @@ -17,7 +17,9 @@ package com.android.launcher3.compat; import android.content.ComponentName; +import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; public abstract class LauncherActivityInfoCompat { @@ -32,4 +34,11 @@ public abstract class LauncherActivityInfoCompat { public abstract ApplicationInfo getApplicationInfo(); public abstract long getFirstInstallTime(); public abstract Drawable getBadgedIcon(int density); + + /** + * Creates a LauncherActivityInfoCompat for the primary user. + */ + public static LauncherActivityInfoCompat fromResolveInfo(ResolveInfo info, Context context) { + return new LauncherActivityInfoCompatV16(context, info); + } } diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java index 1d41a6ff6..ea51aace8 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java @@ -29,13 +29,15 @@ import android.graphics.drawable.Drawable; public class LauncherActivityInfoCompatV16 extends LauncherActivityInfoCompat { - private ActivityInfo mActivityInfo; - private ComponentName mComponentName; - private PackageManager mPm; + private final ResolveInfo mResolveInfo; + private final ActivityInfo mActivityInfo; + private final ComponentName mComponentName; + private final PackageManager mPm; LauncherActivityInfoCompatV16(Context context, ResolveInfo info) { super(); - this.mActivityInfo = info.activityInfo; + mResolveInfo = info; + mActivityInfo = info.activityInfo; mComponentName = new ComponentName(mActivityInfo.packageName, mActivityInfo.name); mPm = context.getPackageManager(); } @@ -49,31 +51,30 @@ public class LauncherActivityInfoCompatV16 extends LauncherActivityInfoCompat { } public CharSequence getLabel() { - return mActivityInfo.loadLabel(mPm); + return mResolveInfo.loadLabel(mPm); } public Drawable getIcon(int density) { - Drawable d = null; - if (mActivityInfo.getIconResource() != 0) { - Resources resources; + int iconRes = mResolveInfo.getIconResource(); + Resources resources = null; + Drawable icon = null; + // Get the preferred density icon from the app's resources + if (density != 0 && iconRes != 0) { 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. - } + resources = mPm.getResourcesForApplication(mActivityInfo.applicationInfo); + icon = resources.getDrawableForDensity(iconRes, density); + } catch (NameNotFoundException | Resources.NotFoundException exc) { } } - if (d == null) { - Resources resources = Resources.getSystem(); - d = resources.getDrawableForDensity(android.R.mipmap.sym_def_app_icon, density); + // Get the default density icon + if (icon == null) { + icon = mResolveInfo.loadIcon(mPm); + } + if (icon == null) { + resources = Resources.getSystem(); + icon = resources.getDrawableForDensity(android.R.mipmap.sym_def_app_icon, density); } - return d; + return icon; } public ApplicationInfo getApplicationInfo() { diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java index e47b9a58d..ac3d252f5 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java @@ -31,6 +31,8 @@ import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; import java.util.List; @@ -139,11 +141,11 @@ public class LauncherAppsCompatV16 extends LauncherAppsCompat { mContext.registerReceiver(mPackageMonitor, filter); } - private synchronized List<OnAppsChangedCallbackCompat> getCallbacks() { + @Thunk synchronized List<OnAppsChangedCallbackCompat> getCallbacks() { return new ArrayList<OnAppsChangedCallbackCompat>(mCallbacks); } - private class PackageMonitor extends BroadcastReceiver { + @Thunk class PackageMonitor extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final UserHandleCompat user = UserHandleCompat.myUserHandle(); diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java index 601f04cea..d6d4b8287 100644 --- a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java +++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java @@ -26,6 +26,7 @@ import android.util.SparseArray; import com.android.launcher3.IconCache; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; import java.util.HashSet; @@ -36,10 +37,10 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements private static final boolean DEBUG = false; // All updates to these sets must happen on the {@link #mWorker} thread. - private final SparseArray<SessionInfo> mPendingReplays = new SparseArray<SessionInfo>(); - private final HashSet<String> mPendingBadgeUpdates = new HashSet<String>(); + @Thunk final SparseArray<SessionInfo> mPendingReplays = new SparseArray<SessionInfo>(); + @Thunk final HashSet<String> mPendingBadgeUpdates = new HashSet<String>(); - private final PackageInstaller mInstaller; + @Thunk final PackageInstaller mInstaller; private final IconCache mCache; private final Handler mWorker; @@ -82,7 +83,7 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements return activePackages; } - private void addSessionInfoToCahce(SessionInfo info, UserHandleCompat user) { + @Thunk void addSessionInfoToCahce(SessionInfo info, UserHandleCompat user) { String packageName = info.getAppPackageName(); if (packageName != null) { mCache.cachePackageInstallInfo(packageName, user, info.getAppIcon(), @@ -123,7 +124,7 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements replayUpdates(null); } - private void replayUpdates(PackageInstallInfo newInfo) { + @Thunk void replayUpdates(PackageInstallInfo newInfo) { if (DEBUG) Log.d(TAG, "updates resumed"); if (!mResumed || !mBound) { // Not yet ready diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java index 1374b4e49..a79d94646 100644 --- a/src/com/android/launcher3/compat/UserManagerCompat.java +++ b/src/com/android/launcher3/compat/UserManagerCompat.java @@ -43,4 +43,5 @@ public abstract class UserManagerCompat { public abstract UserHandleCompat getUserForSerialNumber(long serialNumber); public abstract Drawable getBadgedDrawableForUser(Drawable unbadged, UserHandleCompat user); public abstract CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user); + public abstract long getUserCreationTime(UserHandleCompat user); } diff --git a/src/com/android/launcher3/compat/UserManagerCompatV16.java b/src/com/android/launcher3/compat/UserManagerCompatV16.java index 32f972e85..ffe698c8b 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatV16.java +++ b/src/com/android/launcher3/compat/UserManagerCompatV16.java @@ -48,4 +48,9 @@ public class UserManagerCompatV16 extends UserManagerCompat { public CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user) { return label; } + + @Override + public long getUserCreationTime(UserHandleCompat user) { + return 0; + } } diff --git a/src/com/android/launcher3/compat/UserManagerCompatVL.java b/src/com/android/launcher3/compat/UserManagerCompatVL.java index 19eeabdcf..884d6fe2a 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatVL.java +++ b/src/com/android/launcher3/compat/UserManagerCompatVL.java @@ -18,21 +18,27 @@ package com.android.launcher3.compat; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.UserHandle; -import android.os.UserManager; + +import com.android.launcher3.LauncherAppState; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class UserManagerCompatVL extends UserManagerCompatV17 { + private static final String USER_CREATION_TIME_KEY = "user_creation_time_"; + private final PackageManager mPm; + private final Context mContext; UserManagerCompatVL(Context context) { super(context); mPm = context.getPackageManager(); + mContext = context; } @Override @@ -61,5 +67,17 @@ public class UserManagerCompatVL extends UserManagerCompatV17 { } return mPm.getUserBadgedLabel(label, user.getUser()); } + + @Override + public long getUserCreationTime(UserHandleCompat user) { + // TODO: Use system API once available. + SharedPreferences prefs = mContext.getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); + String key = USER_CREATION_TIME_KEY + getSerialNumberForUser(user); + if (!prefs.contains(key)) { + prefs.edit().putLong(key, System.currentTimeMillis()).apply(); + } + return prefs.getLong(key, 0); + } } diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java new file mode 100644 index 000000000..a84e7df03 --- /dev/null +++ b/src/com/android/launcher3/util/FocusLogic.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2015 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.util; + +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.ShortcutAndWidgetContainer; + +import java.util.Arrays; + +/** + * Calculates the next item that a {@link KeyEvent} should change the focus to. + *<p> + * Note, this utility class calculates everything regards to icon index and its (x,y) coordinates. + * Currently supports: + * <ul> + * <li> full matrix of cells that are 1x1 + * <li> sparse matrix of cells that are 1x1 + * [ 1][ ][ 2][ ] + * [ ][ ][ 3][ ] + * [ ][ 4][ ][ ] + * [ ][ 5][ 6][ 7] + * </ul> + * *<p> + * For testing, one can use a BT keyboard, or use following adb command. + * ex. $ adb shell input keyevent 20 // KEYCODE_DPAD_LEFT + */ +public class FocusLogic { + + private static final String TAG = "FocusLogic"; + private static final boolean DEBUG = false; + + // Item and page index related constant used by {@link #handleKeyEvent}. + public static final int NOOP = -1; + + public static final int PREVIOUS_PAGE_RIGHT_COLUMN = -2; + public static final int PREVIOUS_PAGE_FIRST_ITEM = -3; + public static final int PREVIOUS_PAGE_LAST_ITEM = -4; + public static final int PREVIOUS_PAGE_LEFT_COLUMN = -5; + + public static final int CURRENT_PAGE_FIRST_ITEM = -6; + public static final int CURRENT_PAGE_LAST_ITEM = -7; + + public static final int NEXT_PAGE_FIRST_ITEM = -8; + public static final int NEXT_PAGE_LEFT_COLUMN = -9; + public static final int NEXT_PAGE_RIGHT_COLUMN = -10; + + // Matrix related constant. + public static final int EMPTY = -1; + public static final int PIVOT = 100; + + /** + * Returns true only if this utility class handles the key code. + */ + public static boolean shouldConsume(int keyCode) { + return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || + keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || + keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END || + keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN || + keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL); + } + + public static int handleKeyEvent(int keyCode, int cntX, int cntY, int [][] map, + int iconIdx, int pageIndex, int pageCount) { + + if (DEBUG) { + Log.v(TAG, String.format( + "handleKeyEvent START: cntX=%d, cntY=%d, iconIdx=%d, pageIdx=%d, pageCnt=%d", + cntX, cntY, iconIdx, pageIndex, pageCount)); + } + + DeviceProfile profile = LauncherAppState.getInstance().getDynamicGrid() + .getDeviceProfile(); + int newIndex = NOOP; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/); + if (!profile.isLayoutRtl && newIndex == NOOP && pageIndex > 0) { + newIndex = PREVIOUS_PAGE_RIGHT_COLUMN; + } else if (profile.isLayoutRtl && newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = NEXT_PAGE_RIGHT_COLUMN; + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/); + if (!profile.isLayoutRtl && newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = NEXT_PAGE_LEFT_COLUMN; + } else if (profile.isLayoutRtl && newIndex == NOOP && pageIndex > 0) { + newIndex = PREVIOUS_PAGE_LEFT_COLUMN; + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, 1 /*increment*/); + break; + case KeyEvent.KEYCODE_DPAD_UP: + newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, -1 /*increment*/); + break; + case KeyEvent.KEYCODE_MOVE_HOME: + newIndex = handleMoveHome(); + break; + case KeyEvent.KEYCODE_MOVE_END: + newIndex = handleMoveEnd(); + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + newIndex = handlePageDown(pageIndex, pageCount); + break; + case KeyEvent.KEYCODE_PAGE_UP: + newIndex = handlePageUp(pageIndex); + break; + default: + break; + } + + if (DEBUG) { + Log.v(TAG, String.format("handleKeyEvent FINISH: index [%d -> %s]", + iconIdx, getStringIndex(newIndex))); + } + return newIndex; + } + + /** + * Returns a matrix of size (m x n) that has been initialized with {@link #EMPTY}. + * + * @param m number of columns in the matrix + * @param n number of rows in the matrix + */ + // TODO: get rid of dynamic matrix creation. + private static int[][] createFullMatrix(int m, int n) { + int[][] matrix = new int [m][n]; + + for (int i=0; i < m;i++) { + Arrays.fill(matrix[i], EMPTY); + } + return matrix; + } + + /** + * Returns a matrix of size same as the {@link CellLayout} dimension that is initialized with the + * index of the child view. + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout layout) { + ShortcutAndWidgetContainer parent = layout.getShortcutsAndWidgets(); + final int m = layout.getCountX(); + final int n = layout.getCountY(); + final boolean invert = parent.invertLayoutHorizontally(); + + int[][] matrix = createFullMatrix(m, n); + + // Iterate thru the children. + for (int i = 0; i < parent.getChildCount(); i++ ) { + int cx = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellY; + matrix[invert ? (m - cx - 1) : cx][cy] = i; + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + /** + * Creates a sparse matrix that merges the icon and hotseat view group using the cell layout. + * The size of the returning matrix is [icon column count x (icon + hotseat row count)] + * in portrait orientation. In landscape, [(icon + hotseat) column count x (icon row count)] + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout iconLayout, CellLayout hotseatLayout, + boolean isHorizontal, int allappsiconRank, boolean includeAllappsicon) { + + ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets(); + + int m, n; + if (isHorizontal) { + m = iconLayout.getCountX(); + n = iconLayout.getCountY() + hotseatLayout.getCountY(); + } else { + m = iconLayout.getCountX() + hotseatLayout.getCountX(); + n = iconLayout.getCountY(); + } + int[][] matrix = createFullMatrix(m, n); + + // Iterate thru the children of the top parent. + for (int i = 0; i < iconParent.getChildCount(); i++) { + int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + matrix[cx][cy] = i; + } + + // Iterate thru the children of the bottom parent + // The hotseat view group contains one more item than iconLayout column count. + // If {@param allappsiconRank} not negative, then the last icon in the hotseat + // is truncated. If it is negative, then all apps icon index is not inserted. + for(int i = hotseatParent.getChildCount() - 1; i >= (includeAllappsicon ? 0 : 1); i--) { + int delta = 0; + if (isHorizontal) { + int cx = ((CellLayout.LayoutParams) + hotseatParent.getChildAt(i).getLayoutParams()).cellX; + if ((includeAllappsicon && cx >= allappsiconRank) || + (!includeAllappsicon && cx > allappsiconRank)) { + delta = -1; + } + matrix[cx + delta][iconLayout.getCountY()] = iconParent.getChildCount() + i; + } else { + int cy = ((CellLayout.LayoutParams) + hotseatParent.getChildAt(i).getLayoutParams()).cellY; + if ((includeAllappsicon && cy >= allappsiconRank) || + (!includeAllappsicon && cy > allappsiconRank)) { + delta = -1; + } + matrix[iconLayout.getCountX()][cy + delta] = iconParent.getChildCount() + i; + } + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + /** + * Creates a sparse matrix that merges the icon of previous/next page and last column of + * current page. When left key is triggered on the leftmost column, sparse matrix is created + * that combines previous page matrix and an extra column on the right. Likewise, when right + * key is triggered on the rightmost column, sparse matrix is created that combines this column + * on the 0th column and the next page matrix. + * + * @param pivotX x coordinate of the focused item in the current page + * @param pivotY y coordinate of the focused item in the current page + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout iconLayout, int pivotX, int pivotY) { + + ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + + int[][] matrix = createFullMatrix(iconLayout.getCountX() + 1, iconLayout.getCountY()); + + // Iterate thru the children of the top parent. + for (int i = 0; i < iconParent.getChildCount(); i++) { + int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + if (pivotX < 0) { + matrix[cx - pivotX][cy] = i; + } else { + matrix[cx][cy] = i; + } + } + + if (pivotX < 0) { + matrix[0][pivotY] = PIVOT; + } else { + matrix[pivotX][pivotY] = PIVOT; + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + // + // key event handling methods. + // + + /** + * Calculates icon that has is closest to the horizontal axis in reference to the cur icon. + * + * Example of the check order for KEYCODE_DPAD_RIGHT: + * [ ][ ][13][14][15] + * [ ][ 6][ 8][10][12] + * [ X][ 1][ 2][ 3][ 4] + * [ ][ 5][ 7][ 9][11] + */ + // TODO: add unit tests to verify all permutation. + private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY, + int[][] matrix, int increment) { + if(matrix == null) { + throw new IllegalStateException("Dpad navigation requires a matrix."); + } + int newIconIndex = NOOP; + + int xPos = -1; + int yPos = -1; + // Figure out the location of the icon. + for (int i = 0; i < cntX; i++) { + for (int j = 0; j < cntY; j++) { + if (matrix[i][j] == iconIdx) { + xPos = i; + yPos = j; + } + } + } + if (DEBUG) { + Log.v(TAG, String.format("\thandleDpadHorizontal: \t[x, y]=[%d, %d] iconIndex=%d", + xPos, yPos, iconIdx)); + } + + // Rule1: check first in the horizontal direction + for (int i = xPos + increment; 0 <= i && i < cntX; i = i + increment) { + if ((newIconIndex = inspectMatrix(i, yPos, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + + // Rule2: check (x1-n, yPos + increment), (x1-n, yPos - increment) + // (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment) + int nextYPos1; + int nextYPos2; + int i = -1; + for (int coeff = 1; coeff < cntY; coeff++) { + nextYPos1 = yPos + coeff * increment; + nextYPos2 = yPos - coeff * increment; + for (i = xPos + increment * coeff; 0 <= i && i < cntX; i = i + increment) { + if ((newIconIndex = inspectMatrix(i, nextYPos1, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + if ((newIconIndex = inspectMatrix(i, nextYPos2, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + } + return newIconIndex; + } + + /** + * Calculates icon that is closest to the vertical axis in reference to the current icon. + * + * Example of the check order for KEYCODE_DPAD_DOWN: + * [ ][ ][ ][ X][ ][ ][ ] + * [ ][ ][ 5][ 1][ 4][ ][ ] + * [ ][10][ 7][ 2][ 6][ 9][ ] + * [14][12][ 9][ 3][ 8][11][13] + */ + // TODO: add unit tests to verify all permutation. + private static int handleDpadVertical(int iconIndex, int cntX, int cntY, + int [][] matrix, int increment) { + int newIconIndex = NOOP; + if(matrix == null) { + throw new IllegalStateException("Dpad navigation requires a matrix."); + } + + int xPos = -1; + int yPos = -1; + // Figure out the location of the icon. + for (int i = 0; i< cntX; i++) { + for (int j = 0; j < cntY; j++) { + if (matrix[i][j] == iconIndex) { + xPos = i; + yPos = j; + } + } + } + + if (DEBUG) { + Log.v(TAG, String.format("\thandleDpadVertical: \t[x, y]=[%d, %d] iconIndex=%d", + xPos, yPos, iconIndex)); + } + + // Rule1: check first in the dpad direction + for (int j = yPos + increment; 0 <= j && j <cntY && 0 <= j; j = j + increment) { + if ((newIconIndex = inspectMatrix(xPos, j, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + + // Rule2: check (xPos + increment, y_(1-n)), (xPos - increment, y_(1-n)) + // (xPos + 2*increment, y_(2-n))), (xPos - 2*increment, y_(2-n)) + int nextXPos1; + int nextXPos2; + int j = -1; + for (int coeff = 1; coeff < cntX; coeff++) { + nextXPos1 = xPos + coeff * increment; + nextXPos2 = xPos - coeff * increment; + for (j = yPos + increment * coeff; 0 <= j && j < cntY; j = j + increment) { + if ((newIconIndex = inspectMatrix(nextXPos1, j, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + if ((newIconIndex = inspectMatrix(nextXPos2, j, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + } + return newIconIndex; + } + + private static int handleMoveHome() { + return CURRENT_PAGE_FIRST_ITEM; + } + + private static int handleMoveEnd() { + return CURRENT_PAGE_LAST_ITEM; + } + + private static int handlePageDown(int pageIndex, int pageCount) { + if (pageIndex < pageCount -1) { + return NEXT_PAGE_FIRST_ITEM; + } + return CURRENT_PAGE_LAST_ITEM; + } + + private static int handlePageUp(int pageIndex) { + if (pageIndex > 0) { + return PREVIOUS_PAGE_FIRST_ITEM; + } else { + return CURRENT_PAGE_FIRST_ITEM; + } + } + + // + // Helper methods. + // + + private static boolean isValid(int xPos, int yPos, int countX, int countY) { + return (0 <= xPos && xPos < countX && 0 <= yPos && yPos < countY); + } + + private static int inspectMatrix(int x, int y, int cntX, int cntY, int[][] matrix) { + int newIconIndex = NOOP; + if (isValid(x, y, cntX, cntY)) { + if (matrix[x][y] != -1) { + newIconIndex = matrix[x][y]; + if (DEBUG) { + Log.v(TAG, String.format("\t\tinspect: \t[x, y]=[%d, %d] %d", + x, y, matrix[x][y])); + } + return newIconIndex; + } + } + return newIconIndex; + } + + /** + * Only used for debugging. + */ + private static String getStringIndex(int index) { + switch(index) { + case NOOP: return "NOOP"; + case PREVIOUS_PAGE_FIRST_ITEM: return "PREVIOUS_PAGE_FIRST"; + case PREVIOUS_PAGE_LAST_ITEM: return "PREVIOUS_PAGE_LAST"; + case PREVIOUS_PAGE_RIGHT_COLUMN:return "PREVIOUS_PAGE_RIGHT_COLUMN"; + case CURRENT_PAGE_FIRST_ITEM: return "CURRENT_PAGE_FIRST"; + case CURRENT_PAGE_LAST_ITEM: return "CURRENT_PAGE_LAST"; + case NEXT_PAGE_FIRST_ITEM: return "NEXT_PAGE_FIRST"; + case NEXT_PAGE_LEFT_COLUMN: return "NEXT_PAGE_LEFT_COLUMN"; + default: + return Integer.toString(index); + } + } + + /** + * Only used for debugging. + */ + private static void printMatrix(int[][] matrix) { + Log.v(TAG, "\tprintMap:"); + int m = matrix.length; + int n = matrix[0].length; + + for (int j=0; j < n; j++) { + String colY = "\t\t"; + for (int i=0; i < m; i++) { + colY += String.format("%3d",matrix[i][j]); + } + Log.v(TAG, colY); + } + } + + /** + * @param edgeColumn the column of the new icon. either {@link #NEXT_PAGE_LEFT_COLUMN} or + * {@link #NEXT_PAGE_RIGHT_COLUMN} + * @return the view adjacent to {@param oldView} in the {@param nextPage}. + */ + public static View getAdjacentChildInNextPage( + ShortcutAndWidgetContainer nextPage, View oldView, int edgeColumn) { + final int newRow = ((CellLayout.LayoutParams) oldView.getLayoutParams()).cellY; + + int column = (edgeColumn == NEXT_PAGE_LEFT_COLUMN) ^ nextPage.invertLayoutHorizontally() + ? 0 : (((CellLayout) nextPage.getParent()).getCountX() - 1); + + for (; column >= 0; column--) { + for (int row = newRow; row >= 0; row--) { + View newView = nextPage.getChildAt(column, row); + if (newView != null) { + return newView; + } + } + } + return null; + } +} diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java new file mode 100644 index 000000000..cefa71c39 --- /dev/null +++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java @@ -0,0 +1,277 @@ +package com.android.launcher3.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; + +import com.android.launcher3.FolderInfo; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherFiles; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.Utilities; +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.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +/** + * Handles addition of app shortcuts for managed profiles. + * Methods of class should only be called on {@link LauncherModel#sWorkerThread}. + */ +public class ManagedProfileHeuristic { + + private static final String TAG = "ManagedProfileHeuristic"; + + /** + * Maintain a set of packages installed per user. + */ + private static final String INSTALLED_PACKAGES_PREFIX = "installed_packages_for_user_"; + + private static final String USER_FOLDER_ID_PREFIX = "user_folder_"; + + /** + * Duration (in milliseconds) for which app shortcuts will be added to work folder. + */ + private static final long AUTO_ADD_TO_FOLDER_DURATION = 8 * 60 * 60 * 1000; + + public static ManagedProfileHeuristic get(Context context, UserHandleCompat user) { + if (Utilities.isLmpOrAbove() && !UserHandleCompat.myUserHandle().equals(user)) { + return new ManagedProfileHeuristic(context, user); + } + return null; + } + + private final Context mContext; + private final UserHandleCompat mUser; + private final LauncherModel mModel; + + private final SharedPreferences mPrefs; + private final long mUserSerial; + private final long mUserCreationTime; + private final String mPackageSetKey; + + private ArrayList<ItemInfo> mHomescreenApps; + private ArrayList<ItemInfo> mWorkFolderApps; + + private ManagedProfileHeuristic(Context context, UserHandleCompat user) { + mContext = context; + mUser = user; + mModel = LauncherAppState.getInstance().getModel(); + + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + mUserSerial = userManager.getSerialNumberForUser(user); + mUserCreationTime = userManager.getUserCreationTime(user); + mPackageSetKey = INSTALLED_PACKAGES_PREFIX + mUserSerial; + + mPrefs = mContext.getSharedPreferences(LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + } + + /** + * Checks the list of user apps and adds icons for newly installed apps on the homescreen or + * workfolder. + */ + public void processUserApps(List<LauncherActivityInfoCompat> apps) { + mHomescreenApps = new ArrayList<ItemInfo>(); + mWorkFolderApps = new ArrayList<ItemInfo>(); + HashSet<String> packageSet = getPackageSet(); + boolean newPackageAdded = false; + + for (LauncherActivityInfoCompat info : apps) { + String packageName = info.getComponentName().getPackageName(); + if (!packageSet.contains(packageName)) { + packageSet.add(packageName); + newPackageAdded = true; + + try { + PackageInfo pkgInfo = mContext.getPackageManager() + .getPackageInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + markForAddition(info, pkgInfo.firstInstallTime); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unknown package " + packageName, e); + } + } + } + + if (newPackageAdded) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + finalizeAdditions(); + } + } + + private void markForAddition(LauncherActivityInfoCompat info, long installTime) { + ArrayList<ItemInfo> targetList = + (installTime <= mUserCreationTime + AUTO_ADD_TO_FOLDER_DURATION) ? + mWorkFolderApps : mHomescreenApps; + targetList.add(ShortcutInfo.fromActivityInfo(info, mContext)); + } + + /** + * Adds and binds shortcuts marked to be added to the work folder. + */ + private void finalizeWorkFolder() { + if (mWorkFolderApps.isEmpty()) { + return; + } + + // Try to get a work folder. + String folderIdKey = USER_FOLDER_ID_PREFIX + mUserSerial; + if (mPrefs.contains(folderIdKey)) { + long folderId = mPrefs.getLong(folderIdKey, 0); + final FolderInfo workFolder = mModel.findFolderById(folderId); + + if (workFolder == null || !workFolder.hasOption(FolderInfo.FLAG_WORK_FOLDER)) { + // Could not get a work folder. Add all the icons to homescreen. + mHomescreenApps.addAll(mWorkFolderApps); + return; + } + saveWorkFolderShortcuts(folderId, workFolder.contents.size()); + + final ArrayList<ItemInfo> shortcuts = mWorkFolderApps; + // FolderInfo could already be bound. We need to add shortcuts on the UI thread. + new MainThreadExecutor().execute(new Runnable() { + + @Override + public void run() { + for (ItemInfo info : shortcuts) { + workFolder.add((ShortcutInfo) info); + } + } + }); + } else { + // Create a new folder. + final FolderInfo workFolder = new FolderInfo(); + workFolder.title = mContext.getText(R.string.work_folder_name); + workFolder.setOption(FolderInfo.FLAG_WORK_FOLDER, true, null); + + // Add all shortcuts before adding it to the UI, as an empty folder might get deleted. + for (ItemInfo info : mWorkFolderApps) { + workFolder.add((ShortcutInfo) info); + } + + // Add the item to home screen and DB. This also generates an item id synchronously. + ArrayList<ItemInfo> itemList = new ArrayList<ItemInfo>(1); + itemList.add(workFolder); + mModel.addAndBindAddedWorkspaceItems(mContext, itemList); + mPrefs.edit().putLong(USER_FOLDER_ID_PREFIX + mUserSerial, workFolder.id).apply(); + + saveWorkFolderShortcuts(workFolder.id, 0); + } + } + + /** + * Add work folder shortcuts to the DB. + */ + private void saveWorkFolderShortcuts(long workFolderId, int startingRank) { + for (ItemInfo info : mWorkFolderApps) { + info.rank = startingRank++; + LauncherModel.addItemToDatabase(mContext, info, workFolderId, 0, 0, 0); + } + } + + /** + * Adds and binds all shortcuts marked for addition. + */ + private void finalizeAdditions() { + finalizeWorkFolder(); + + if (!mHomescreenApps.isEmpty()) { + mModel.addAndBindAddedWorkspaceItems(mContext, mHomescreenApps); + } + } + + /** + * Updates the list of installed apps and adds any new icons on homescreen or work folder. + */ + public void processPackageAdd(String[] packages) { + mHomescreenApps = new ArrayList<ItemInfo>(); + mWorkFolderApps = new ArrayList<ItemInfo>(); + HashSet<String> packageSet = getPackageSet(); + boolean newPackageAdded = false; + long installTime = System.currentTimeMillis(); + LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mContext); + + for (String packageName : packages) { + if (!packageSet.contains(packageName)) { + packageSet.add(packageName); + newPackageAdded = true; + + List<LauncherActivityInfoCompat> activities = + launcherApps.getActivityList(packageName, mUser); + if (!activities.isEmpty()) { + markForAddition(activities.get(0), installTime); + } + } + } + + if (newPackageAdded) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + finalizeAdditions(); + } + } + + /** + * Updates the list of installed packages for the user. + */ + public void processPackageRemoved(String[] packages) { + HashSet<String> packageSet = getPackageSet(); + boolean packageRemoved = false; + + for (String packageName : packages) { + if (packageSet.remove(packageName)) { + packageRemoved = true; + } + } + + if (packageRemoved) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + } + } + + @SuppressWarnings("unchecked") + private HashSet<String> getPackageSet() { + return new HashSet<String>(mPrefs.getStringSet(mPackageSetKey, Collections.EMPTY_SET)); + } + + /** + * Verifies that entries corresponding to {@param users} exist and removes all invalid entries. + */ + public static void processAllUsers(List<UserHandleCompat> users, Context context) { + if (!Utilities.isLmpOrAbove()) { + return; + } + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + HashSet<String> validKeys = new HashSet<String>(); + for (UserHandleCompat user : users) { + addAllUserKeys(userManager.getSerialNumberForUser(user), validKeys); + } + + SharedPreferences prefs = context.getSharedPreferences( + LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + for (String key : prefs.getAll().keySet()) { + if (!validKeys.contains(key)) { + editor.remove(key); + } + } + editor.apply(); + } + + private static void addAllUserKeys(long userSerial, HashSet<String> keysOut) { + keysOut.add(INSTALLED_PACKAGES_PREFIX + userSerial); + keysOut.add(USER_FOLDER_ID_PREFIX + userSerial); + } +} diff --git a/src/com/android/launcher3/util/Thunk.java b/src/com/android/launcher3/util/Thunk.java new file mode 100644 index 000000000..de350b068 --- /dev/null +++ b/src/com/android/launcher3/util/Thunk.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 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.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the given field or method has package visibility solely to prevent the creation + * of a synthetic method. In practice, you should treat this field/method as if it were private. + * <p> + * + * When a private method is called from an inner class, the Java compiler generates a simple + * package private shim method that the class generated from the inner class can call. This results + * in unnecessary bloat and runtime method call overhead. It also gets us closer to the dex method + * count limit. + * <p> + * + * If you'd like to see warnings for these synthetic methods in eclipse, turn on: + * Window > Preferences > Java > Compiler > Errors/Warnings > "Access to a non-accessible member + * of an enclosing type". + * <p> + * + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface Thunk { }
\ No newline at end of file diff --git a/src/com/android/launcher3/util/WallpaperUtils.java b/src/com/android/launcher3/util/WallpaperUtils.java new file mode 100644 index 000000000..53b2acd84 --- /dev/null +++ b/src/com/android/launcher3/util/WallpaperUtils.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2015 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.util; + +import android.annotation.TargetApi; +import android.app.WallpaperManager; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Point; +import android.os.Build; +import android.view.WindowManager; + +/** + * Utility methods for wallpaper management. + */ +public final class WallpaperUtils { + + public static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; + public static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; + public static final float WALLPAPER_SCREENS_SPAN = 2f; + + public static void suggestWallpaperDimension(Resources res, + final SharedPreferences sharedPrefs, + WindowManager windowManager, + final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { + final Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(res, windowManager); + // If we have saved a wallpaper width/height, use that instead + + int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); + int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1); + + if (savedWidth == -1 || savedHeight == -1) { + if (!fallBackToDefaults) { + return; + } else { + savedWidth = defaultWallpaperSize.x; + savedHeight = defaultWallpaperSize.y; + } + } + + if (savedWidth != wallpaperManager.getDesiredMinimumWidth() || + savedHeight != wallpaperManager.getDesiredMinimumHeight()) { + wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight); + } + } + + /** + * As a ratio of screen height, the total distance we want the parallax effect to span + * horizontally + */ + public static float wallpaperTravelToScreenWidthRatio(int width, int height) { + float aspectRatio = width / (float) height; + + // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width + // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width + // We will use these two data points to extrapolate how much the wallpaper parallax effect + // to span (ie travel) at any aspect ratio: + + final float ASPECT_RATIO_LANDSCAPE = 16/10f; + final float ASPECT_RATIO_PORTRAIT = 10/16f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; + + // To find out the desired width at different aspect ratios, we use the following two + // formulas, where the coefficient on x is the aspect ratio (width/height): + // (16/10)x + y = 1.5 + // (10/16)x + y = 1.2 + // We solve for x and y and end up with a final formula: + final float x = + (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / + (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); + final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; + return x * aspectRatio + y; + } + + private static Point sDefaultWallpaperSize; + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) { + if (sDefaultWallpaperSize == null) { + Point minDims = new Point(); + Point maxDims = new Point(); + windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); + + int maxDim = Math.max(maxDims.x, maxDims.y); + int minDim = Math.max(minDims.x, minDims.y); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + Point realSize = new Point(); + windowManager.getDefaultDisplay().getRealSize(realSize); + maxDim = Math.max(realSize.x, realSize.y); + minDim = Math.min(realSize.x, realSize.y); + } + + // We need to ensure that there is enough extra space in the wallpaper + // for the intended parallax effects + final int defaultWidth, defaultHeight; + if (res.getConfiguration().smallestScreenWidthDp >= 720) { + defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); + defaultHeight = maxDim; + } else { + defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); + defaultHeight = maxDim; + } + sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); + } + return sDefaultWallpaperSize; + } +} diff --git a/src/com/android/launcher3/widget/PackageItemInfo.java b/src/com/android/launcher3/widget/PackageItemInfo.java new file mode 100644 index 000000000..1a1de55c2 --- /dev/null +++ b/src/com/android/launcher3/widget/PackageItemInfo.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 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.widget; + +import android.content.ComponentName; +import android.graphics.Bitmap; + +import com.android.launcher3.ItemInfo; + +import java.util.Arrays; + +/** + * Represents a {@link Package} in the widget tray section. + */ +public class PackageItemInfo extends ItemInfo { + private static final String TAG = "PackageInfo"; + + /** + * A bitmap version of the application icon. + */ + public Bitmap iconBitmap; + + /** + * Indicates whether we're using a low res icon + */ + public boolean usingLowResIcon; + + public String packageName; + + int flags = 0; + + PackageItemInfo(String packageName) { + this.packageName = packageName; + } + + @Override + public String toString() { + return "PackageItemInfo(title=" + title.toString() + " id=" + this.id + + " type=" + this.itemType + " container=" + this.container + + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + + " user=" + user + ")"; + } +} diff --git a/src/com/android/launcher3/widget/PendingAddShortcutInfo.java b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java new file mode 100644 index 000000000..a56985083 --- /dev/null +++ b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 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.widget; + +import android.content.ComponentName; +import android.content.pm.ActivityInfo; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; + +/** + * Meta data used for late binding of the short cuts. + * + * @see {@link PendingAddItemInfo} + */ +public class PendingAddShortcutInfo extends PendingAddItemInfo { + + ActivityInfo activityInfo; + + public PendingAddShortcutInfo(ActivityInfo activityInfo) { + this.activityInfo = activityInfo; + componentName = new ComponentName(activityInfo.packageName, activityInfo.name); + itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + } + + @Override + public String toString() { + return String.format("PendingAddShortcutInfo package=%s, name=%s", + activityInfo.packageName, activityInfo.name); + } +} diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java new file mode 100644 index 000000000..db1699818 --- /dev/null +++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2015 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.widget; + +import android.appwidget.AppWidgetHostView; +import android.os.Bundle; +import android.os.Parcelable; + +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; + +/** + * Meta data used for late binding of {@link LauncherAppWidgetProviderInfo}. + * + * @see {@link PendingAddItemInfo} + */ +public class PendingAddWidgetInfo extends PendingAddItemInfo { + public int minWidth; + public int minHeight; + public int minResizeWidth; + public int minResizeHeight; + public int previewImage; + public int icon; + public LauncherAppWidgetProviderInfo info; + public AppWidgetHostView boundWidget; + public Bundle bindOptions = null; + + public PendingAddWidgetInfo(LauncherAppWidgetProviderInfo i, Parcelable data) { + if (i.isCustomWidget) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } else { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + } + this.info = i; + componentName = i.provider; + minWidth = i.minWidth; + minHeight = i.minHeight; + minResizeWidth = i.minResizeWidth; + minResizeHeight = i.minResizeHeight; + previewImage = i.previewImage; + icon = i.icon; + + spanX = i.spanX; + spanY = i.spanY; + minSpanX = i.minSpanX; + minSpanY = i.minSpanY; + } + + public boolean isCustomWidget() { + return itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } + + // Copy constructor + public PendingAddWidgetInfo(PendingAddWidgetInfo copy) { + minWidth = copy.minWidth; + minHeight = copy.minHeight; + minResizeWidth = copy.minResizeWidth; + minResizeHeight = copy.minResizeHeight; + previewImage = copy.previewImage; + icon = copy.icon; + info = copy.info; + boundWidget = copy.boundWidget; + componentName = copy.componentName; + itemType = copy.itemType; + spanX = copy.spanX; + spanY = copy.spanY; + minSpanX = copy.minSpanX; + minSpanY = copy.minSpanY; + bindOptions = copy.bindOptions == null ? null : (Bundle) copy.bindOptions.clone(); + } + + @Override + public String toString() { + return String.format("PendingAddWidgetInfo package=%s, name=%s", + componentName.getPackageName(), componentName.getShortClassName()); + } +} diff --git a/src/com/android/launcher3/PagedViewWidget.java b/src/com/android/launcher3/widget/WidgetCell.java index 107069b78..1ae75c3cc 100644 --- a/src/com/android/launcher3/PagedViewWidget.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2015 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. @@ -14,59 +14,78 @@ * limitations under the License. */ -package com.android.launcher3; +package com.android.launcher3.widget; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; +import android.graphics.Bitmap; import android.graphics.Rect; import android.util.AttributeSet; +import android.util.Log; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; +import android.view.View.OnLayoutChangeListener; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.FastBitmapDrawable; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.WidgetPreviewLoader.PreviewLoadRequest; import com.android.launcher3.compat.AppWidgetManagerCompat; /** - * The linear layout used strictly for the widget/wallpaper tab of the customization tray + * The linear layout used strictly for the widget tray. */ -public class PagedViewWidget extends LinearLayout { - static final String TAG = "PagedViewWidgetLayout"; +public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { - private static boolean sDeletePreviewsWhenDetachedFromWindow = true; - private static boolean sRecyclePreviewsWhenDetachedFromWindow = true; + private static final String TAG = "WidgetCell"; + private static final boolean DEBUG = false; + + private static final int FADE_IN_DURATION_MS = 70; + private int mPresetPreviewSize; + + private static WidgetCell sShortpressTarget = null; - private String mDimensionsFormatString; - CheckForShortPress mPendingCheckForShortPress = null; - ShortPressListener mShortPressListener = null; - boolean mShortPressTriggered = false; - static PagedViewWidget sShortpressTarget = null; - boolean mIsAppWidget; private final Rect mOriginalImagePadding = new Rect(); + + private String mDimensionsFormatString; + private CheckForShortPress mPendingCheckForShortPress = null; + private ShortPressListener mShortPressListener = null; + private boolean mShortPressTriggered = false; + private boolean mIsAppWidget; private Object mInfo; + private WidgetPreviewLoader mWidgetPreviewLoader; + private PreviewLoadRequest mActiveRequest; - public PagedViewWidget(Context context) { + public WidgetCell(Context context) { this(context, null); } - public PagedViewWidget(Context context, AttributeSet attrs) { + public WidgetCell(Context context, AttributeSet attrs) { this(context, attrs, 0); } - public PagedViewWidget(Context context, AttributeSet attrs, int defStyle) { + public WidgetCell(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final Resources r = context.getResources(); mDimensionsFormatString = r.getString(R.string.widget_dims_format); + mPresetPreviewSize = r.getDimensionPixelSize(R.dimen.widget_preview_size); setWillNotDraw(false); setClipToPadding(false); setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); + } @Override @@ -92,29 +111,36 @@ public class PagedViewWidget extends LinearLayout { } } - public static void setDeletePreviewsWhenDetachedFromWindow(boolean value) { - sDeletePreviewsWhenDetachedFromWindow = value; - } - - public static void setRecyclePreviewsWhenDetachedFromWindow(boolean value) { - sRecyclePreviewsWhenDetachedFromWindow = value; - } - @Override protected void onDetachedFromWindow() { + if (DEBUG) { + Log.d(TAG, String.format("[tag=%s] onDetachedFromWindow", getTagToString())); + } super.onDetachedFromWindow(); + deletePreview(false); + } - if (sDeletePreviewsWhenDetachedFromWindow) { + public void reset() { + ImageView image = (ImageView) findViewById(R.id.widget_preview); + final TextView name = (TextView) findViewById(R.id.widget_name); + final TextView dims = (TextView) findViewById(R.id.widget_dims); + image.setImageDrawable(null); + name.setText(null); + dims.setText(null); + } + + public void deletePreview(boolean recycleImage) { + if (recycleImage) { final ImageView image = (ImageView) findViewById(R.id.widget_preview); if (image != null) { - FastBitmapDrawable preview = (FastBitmapDrawable) image.getDrawable(); - if (sRecyclePreviewsWhenDetachedFromWindow && - mInfo != null && preview != null && preview.getBitmap() != null) { - mWidgetPreviewLoader.recycleBitmap(mInfo, preview.getBitmap()); - } image.setImageDrawable(null); } } + + if (mActiveRequest != null) { + mActiveRequest.cancel(recycleImage); + mActiveRequest = null; + } } public void applyFromAppWidgetProviderInfo(LauncherAppWidgetProviderInfo info, @@ -156,14 +182,19 @@ public class PagedViewWidget extends LinearLayout { public int[] getPreviewSize() { final ImageView i = (ImageView) findViewById(R.id.widget_preview); int[] maxSize = new int[2]; - maxSize[0] = i.getWidth() - mOriginalImagePadding.left - mOriginalImagePadding.right; - maxSize[1] = i.getHeight() - mOriginalImagePadding.top; + maxSize[0] = mPresetPreviewSize; + maxSize[1] = mPresetPreviewSize; return maxSize; } - void applyPreview(FastBitmapDrawable preview, int index) { - final PagedViewWidgetImageView image = - (PagedViewWidgetImageView) findViewById(R.id.widget_preview); + public void applyPreview(Bitmap bitmap) { + FastBitmapDrawable preview = new FastBitmapDrawable(bitmap); + final WidgetImageView image = + (WidgetImageView) findViewById(R.id.widget_preview); + if (DEBUG) { + Log.d(TAG, String.format("[tag=%s] applyPreview preview: %s", + getTagToString(), preview)); + } if (preview != null) { image.mAllowRequestLayout = false; image.setImageDrawable(preview); @@ -176,8 +207,10 @@ public class PagedViewWidget extends LinearLayout { mOriginalImagePadding.right, mOriginalImagePadding.bottom); } - image.setAlpha(1f); + image.setAlpha(0f); + image.animate().alpha(1.0f).setDuration(FADE_IN_DURATION_MS); image.mAllowRequestLayout = true; + image.requestLayout(); } } @@ -194,8 +227,8 @@ public class PagedViewWidget extends LinearLayout { public void run() { if (sShortpressTarget != null) return; if (mShortPressListener != null) { - mShortPressListener.onShortPress(PagedViewWidget.this); - sShortpressTarget = PagedViewWidget.this; + mShortPressListener.onShortPress(WidgetCell.this); + sShortpressTarget = WidgetCell.this; } mShortPressTriggered = true; } @@ -222,7 +255,7 @@ public class PagedViewWidget extends LinearLayout { removeShortPressCallback(); if (mShortPressTriggered) { if (mShortPressListener != null) { - mShortPressListener.cleanUpShortPress(PagedViewWidget.this); + mShortPressListener.cleanUpShortPress(WidgetCell.this); } mShortPressTriggered = false; } @@ -259,4 +292,54 @@ public class PagedViewWidget extends LinearLayout { // we just always mark the touch event as handled. return true; } + + public void ensurePreview() { + if (mActiveRequest != null) { + return; + } + int[] size = getPreviewSize(); + if (DEBUG) { + Log.d(TAG, String.format("[tag=%s] ensurePreview (%d, %d):", + getTagToString(), size[0], size[1])); + } + + if (size[0] <= 0 || size[1] <= 0) { + addOnLayoutChangeListener(this); + return; + } + Bitmap[] immediateResult = new Bitmap[1]; + mActiveRequest = mWidgetPreviewLoader.getPreview(mInfo, size[0], size[1], this, + immediateResult); + if (immediateResult[0] != null) { + applyPreview(immediateResult[0]); + } + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, + int oldTop, int oldRight, int oldBottom) { + removeOnLayoutChangeListener(this); + ensurePreview(); + } + + public int getActualItemWidth() { + ItemInfo info = (ItemInfo) getTag(); + int[] size = getPreviewSize(); + int cellWidth = LauncherAppState.getInstance() + .getDynamicGrid().getDeviceProfile().cellWidthPx; + + return Math.min(size[0], info.spanX * cellWidth); + } + + /** + * Helper method to get the string info of the tag. + */ + private String getTagToString() { + if (getTag() instanceof PendingAddWidgetInfo) { + return ((PendingAddWidgetInfo)getTag()).toString(); + } else if (getTag() instanceof PendingAddShortcutInfo) { + return ((PendingAddShortcutInfo)getTag()).toString(); + } + return ""; + } } diff --git a/src/com/android/launcher3/PagedViewWidgetImageView.java b/src/com/android/launcher3/widget/WidgetImageView.java index 7d8279547..75167bc7d 100644 --- a/src/com/android/launcher3/PagedViewWidgetImageView.java +++ b/src/com/android/launcher3/widget/WidgetImageView.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package com.android.launcher3; +package com.android.launcher3.widget; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.widget.ImageView; -public class PagedViewWidgetImageView extends ImageView { +public class WidgetImageView extends ImageView { public boolean mAllowRequestLayout = true; - public PagedViewWidgetImageView(Context context, AttributeSet attrs) { + public WidgetImageView(Context context, AttributeSet attrs) { super(context, attrs); } @@ -44,6 +44,5 @@ public class PagedViewWidgetImageView extends ImageView { super.onDraw(canvas); canvas.restore(); - } } diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java new file mode 100644 index 000000000..292a5de20 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2015 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.widget; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeleteDropTarget; +import com.android.launcher3.DragController; +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.FastBitmapDrawable; +import com.android.launcher3.Folder; +import com.android.launcher3.IconCache; +import com.android.launcher3.Insettable; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.PendingAddItemInfo; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.Workspace; + +import java.util.ArrayList; + +/** + * The widgets list view container. + */ +public class WidgetsContainerView extends FrameLayout implements Insettable, View.OnTouchListener, + View.OnLongClickListener, DragSource{ + + private static final String TAG = "WidgetContainerView"; + private static final boolean DEBUG = false; + + /* {@link RecyclerView} will keep following # of views in cache, before recycling. */ + private static final int WIDGET_CACHE_SIZE = 2; + + /* Global instances that are used inside this container. */ + private Launcher mLauncher; + private DragController mDragController; + private IconCache mIconCache; + + /* Data model for the widget */ + private WidgetsModel mWidgets; + + /* Recycler view related member variables */ + private RecyclerView mView; + private WidgetsListAdapter mAdapter; + + /* Dragging related. */ + private boolean mDraggingWidget = false; // TODO(hyunyoungs): seems not needed? check! + private Point mLastTouchDownPos = new Point(); + + /* Rendering related. */ + private WidgetPreviewLoader mWidgetPreviewLoader; + private Rect mPadding = new Rect(); + + public WidgetsContainerView(Context context) { + this(context, null); + } + + public WidgetsContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = (Launcher) context; + mDragController = mLauncher.getDragController(); + + mAdapter = new WidgetsListAdapter(context, this, mLauncher, this, mLauncher); + mWidgets = new WidgetsModel(context, mAdapter); + mAdapter.setWidgetsModel(mWidgets); + mIconCache = (LauncherAppState.getInstance()).getIconCache(); + + if (DEBUG) { + Log.d(TAG, "WidgetsContainerView constructor"); + } + } + + @Override + protected void onFinishInflate() { + if (DEBUG) { + Log.d(TAG, String.format("onFinishInflate [widgets size=%d]", + mWidgets.getPackageSize())); + } + mView = (RecyclerView) findViewById(R.id.widgets_list_view); + mView.setAdapter(mAdapter); + mView.setLayoutManager(new LinearLayoutManager(getContext())); + mView.setItemViewCacheSize(WIDGET_CACHE_SIZE); + + mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), + getPaddingBottom()); + } + + // + // Returns views used for launcher transitions. + // + + public View getContentView() { + return findViewById(R.id.widgets_content); + } + + public View getRevealView() { + // TODO(hyunyoungs): temporarily use apps view transition. + return findViewById(R.id.widgets_reveal_view); + } + + public void scrollToTop() { + mView.scrollToPosition(0); + if (DEBUG) { + Log.d(TAG, String.format("scrollToTop, [widgets size=%d]", + mWidgets.getPackageSize())); + } + } + + // + // Touch related handling. + // + + @Override + public boolean onLongClick(View v) { + if (DEBUG) { + Log.d(TAG, String.format("onLonglick [v=%s]", v)); + } + + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return false; + // When we have exited all apps or are in transition, disregard long clicks + if (!mLauncher.isWidgetsViewVisible() || + mLauncher.getWorkspace().isSwitchingState()) return false; + // Return if global dragging is not enabled + Log.d(TAG, String.format("onLonglick dragging enabled?.", v)); + if (!mLauncher.isDraggingEnabled()) return false; + + return beginDragging(v); + } + + private boolean beginDragging(View v) { + if (v instanceof WidgetCell) { + if (!beginDraggingWidget((WidgetCell) v)) { + return false; + } + } else { + Log.e(TAG, "Unexpected dragging view: " + v); + } + + // We delay entering spring-loaded mode slightly to make sure the UI + // thready is free of any work. + postDelayed(new Runnable() { + @Override + public void run() { + // We don't enter spring-loaded mode if the drag has been cancelled + if (mLauncher.getDragController().isDragging()) { + // Go into spring loaded mode (must happen before we startDrag()) + mLauncher.enterSpringLoadedDragMode(); + } + } + }, 150); + + return true; + } + + private boolean beginDraggingWidget(WidgetCell v) { + mDraggingWidget = true; + // Get the widget preview as the drag representation + ImageView image = (ImageView) v.findViewById(R.id.widget_preview); + PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); + + // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and + // we abort the drag. + if (image.getDrawable() == null) { + mDraggingWidget = false; + return false; + } + + // Compose the drag image + Bitmap preview; + Bitmap outline; + float scale = 1f; + Point previewPadding = null; + + if (createItemInfo instanceof PendingAddWidgetInfo) { + // This can happen in some weird cases involving multi-touch. We can't start dragging + // the widget if this is null, so we break out. + + PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) createItemInfo; + int[] size = mLauncher.getWorkspace().estimateItemSize(createWidgetInfo, true); + + FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable(); + float minScale = 1.25f; + int maxWidth = Math.min((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]); + + int[] previewSizeBeforeScale = new int[1]; + preview = getWidgetPreviewLoader().generateWidgetPreview(createWidgetInfo.info, + maxWidth, null, previewSizeBeforeScale); + // Compare the size of the drag preview to the preview in the AppsCustomize tray + int previewWidthInAppsCustomize = Math.min(previewSizeBeforeScale[0], + v.getActualItemWidth()); + scale = previewWidthInAppsCustomize / (float) preview.getWidth(); + + // The bitmap in the AppsCustomize tray is always the the same size, so there + // might be extra pixels around the preview itself - this accounts for that + if (previewWidthInAppsCustomize < previewDrawable.getIntrinsicWidth()) { + int padding = + (previewDrawable.getIntrinsicWidth() - previewWidthInAppsCustomize) / 2; + previewPadding = new Point(padding, 0); + } + } else { + PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); + Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.activityInfo); + preview = Utilities.createIconBitmap(icon, mLauncher); + createItemInfo.spanX = createItemInfo.spanY = 1; + } + + // Don't clip alpha values for the drag outline if we're using the default widget preview + boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo && + (((PendingAddWidgetInfo) createItemInfo).previewImage == 0)); + + // Save the preview for the outline generation, then dim the preview + outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(), + false); + + // Start the drag + mLauncher.lockScreenOrientation(); + mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, clipAlpha); + mDragController.startDrag(image, preview, this, createItemInfo, + DragController.DRAG_ACTION_COPY, previewPadding, scale); + outline.recycle(); + preview.recycle(); + return true; + } + + /* + * @see android.view.View.OnTouchListener#onTouch(android.view.View, android.view.MotionEvent) + */ + @Override + public boolean onTouch(View v, MotionEvent ev) { + Log.d(TAG, String.format("onTouch [MotionEvent=%s]", ev)); + if (ev.getAction() == MotionEvent.ACTION_DOWN || + ev.getAction() == MotionEvent.ACTION_MOVE) { + mLastTouchDownPos.set((int) ev.getX(), (int) ev.getY()); + } + return false; + } + + // + // Drag related handling methods that implement {@link DragSource} interface. + // + + @Override + public boolean supportsFlingToDelete() { + return false; + } + + @Override + public boolean supportsAppInfoDropTarget() { + return true; + } + + /* + * Both this method and {@link #supportsFlingToDelete} has to return {@code false} for the + * {@link DeleteDropTarget} to be invisible.) + */ + @Override + public boolean supportsDeleteDropTarget() { + return false; + } + + @Override + public float getIntrinsicIconScaleFactor() { + return 0; + } + + @Override + public void onFlingToDeleteCompleted() { + // We just dismiss the drag when we fling, so cleanup here + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + mLauncher.unlockScreenOrientation(false); + } + + @Override + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && + !(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.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + } + mLauncher.unlockScreenOrientation(false); + + // Display an error message if the drag failed due to there not being enough space on the + // target layout we were dropping on. + if (!success) { + boolean showOutOfSpaceMessage = false; + if (target instanceof Workspace) { + int currentScreen = mLauncher.getCurrentWorkspaceScreen(); + Workspace workspace = (Workspace) target; + CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); + ItemInfo itemInfo = (ItemInfo) d.dragInfo; + if (layout != null) { + layout.calculateSpans(itemInfo); + showOutOfSpaceMessage = + !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); + } + } + if (showOutOfSpaceMessage) { + mLauncher.showOutOfSpaceMessage(false); + } + d.deferDragViewCleanupPostAnimation = false; + } + } + + // + // Container rendering related. + // + + /* + * @see Insettable#setInsets(Rect) + */ + @Override + public void setInsets(Rect insets) { + setPadding(mPadding.left + insets.left, mPadding.top + insets.top, + mPadding.right + insets.right, mPadding.bottom + insets.bottom); + } + + /** + * Initialize the widget data model. + */ + public void addWidgets(ArrayList<Object> widgetsShortcuts, PackageManager pm) { + mWidgets.addWidgetsAndShortcuts(widgetsShortcuts, pm); + } + + private WidgetPreviewLoader getWidgetPreviewLoader() { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); + } + return mWidgetPreviewLoader; + } + +}
\ No newline at end of file diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java new file mode 100644 index 000000000..afeb2d385 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2015 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.widget; + +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.support.v7.widget.RecyclerView.Adapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.launcher3.IconCache; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.compat.UserHandleCompat; + +import java.util.List; + +/** + * List view adapter for the widget tray. + * + * <p>Memory vs. Performance: + * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling + * happens and less memory is consumed. {@link #getItemViewType} was not overridden as there is + * only a single type of view. + */ +public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { + + private static final String TAG = "WidgetsListAdapter"; + private static final boolean DEBUG = false; + + private Context mContext; + private Launcher mLauncher; + private LayoutInflater mLayoutInflater; + private IconCache mIconCache; + + private WidgetsModel mWidgetsModel; + private WidgetPreviewLoader mWidgetPreviewLoader; + + private View.OnTouchListener mTouchListener; + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + + + public WidgetsListAdapter(Context context, + View.OnTouchListener touchListener, + View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener, + Launcher launcher) { + mLayoutInflater = LayoutInflater.from(context); + mContext = context; + + mTouchListener = touchListener; + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + + mLauncher = launcher; + mIconCache = LauncherAppState.getInstance().getIconCache(); + } + + public void setWidgetsModel(WidgetsModel w) { + mWidgetsModel = w; + } + + @Override + public int getItemCount() { + return mWidgetsModel.getPackageSize(); + } + + @Override + public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) { + List<Object> infoList = mWidgetsModel.getSortedWidgets(pos); + + ViewGroup row = ((ViewGroup) holder.getContent().findViewById(R.id.widgets_cell_list)); + if (DEBUG) { + Log.d(TAG, String.format( + "onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]", + pos, infoList.size(), row.getChildCount())); + } + + // Add more views. + // if there are too many, hide them. + int diff = infoList.size() - row.getChildCount(); + if (diff > 0) { + for (int i = 0; i < diff; i++) { + WidgetCell widget = new WidgetCell(mContext); + widget = (WidgetCell) mLayoutInflater.inflate( + R.layout.widget_cell, row, false); + + // set up touch. + widget.setOnClickListener(mIconClickListener); + widget.setOnLongClickListener(mIconLongClickListener); + widget.setOnTouchListener(mTouchListener); + row.addView(widget); + } + } else if (diff < 0) { + for (int i=infoList.size() ; i < row.getChildCount(); i++) { + row.getChildAt(i).setVisibility(View.GONE); + } + } + + // Bind the views in the application info section. + PackageItemInfo infoOut = mWidgetsModel.getPackageItemInfo(pos); + if (infoOut.usingLowResIcon) { + // TODO(hyunyoungs): call this in none UI thread in the same way as BubbleTextView. + mIconCache.getTitleAndIconForApp(infoOut.packageName, + UserHandleCompat.myUserHandle(), false /* useLowResIcon */, infoOut); + } + ((TextView) holder.getContent().findViewById(R.id.section)).setText(infoOut.title); + ImageView iv = (ImageView) holder.getContent().findViewById(R.id.section_image); + iv.setImageBitmap(infoOut.iconBitmap); + + // Bind the view in the widget horizontal tray region. + for (int i=0; i < infoList.size(); i++) { + WidgetCell widget = (WidgetCell) row.getChildAt(i); + widget.reset(); + if (getWidgetPreviewLoader() == null) { + return; + } + if (infoList.get(i) instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) infoList.get(i); + PendingAddWidgetInfo pawi = new PendingAddWidgetInfo(info, null); + widget.setTag(pawi); + widget.applyFromAppWidgetProviderInfo(info, -1, mWidgetPreviewLoader); + } else if (infoList.get(i) instanceof ResolveInfo) { + ResolveInfo info = (ResolveInfo) infoList.get(i); + PendingAddShortcutInfo pasi = new PendingAddShortcutInfo(info.activityInfo); + widget.setTag(pasi); + widget.applyFromResolveInfo(mLauncher.getPackageManager(), info, mWidgetPreviewLoader); + } + widget.setVisibility(View.VISIBLE); + widget.ensurePreview(); + } + } + + @Override + public WidgetsRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (DEBUG) { + Log.v(TAG, String.format("\nonCreateViewHolder, [widget#=%d]", viewType)); + } + + ViewGroup container = (ViewGroup) mLayoutInflater.inflate( + R.layout.widgets_list_row_view, parent, false); + return new WidgetsRowViewHolder(container); + } + + @Override + public long getItemId(int pos) { + return pos; + } + + private WidgetPreviewLoader getWidgetPreviewLoader() { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); + } + return mWidgetPreviewLoader; + } +} diff --git a/src/com/android/launcher3/widget/WidgetsModel.java b/src/com/android/launcher3/widget/WidgetsModel.java new file mode 100644 index 000000000..71a7b9446 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsModel.java @@ -0,0 +1,128 @@ + +package com.android.launcher3.widget; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.support.v7.widget.RecyclerView; +import android.util.Log; + +import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.LauncherModel.WidgetAndShortcutNameComparator; +import com.android.launcher3.compat.UserHandleCompat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Widgets data model that is used by the adapters of the widget views and controllers. + * + * <p> The widgets and shortcuts are organized using package name as its index. + */ +public class WidgetsModel { + + private static final String TAG = "WidgetsModel"; + private static final boolean DEBUG = false; + + /* List of packages that is tracked by this model. */ + private List<PackageItemInfo> mPackageItemInfos = new ArrayList<>(); + + /* Map of widgets and shortcuts that are tracked per package. */ + private Map<PackageItemInfo, ArrayList<Object>> mWidgetsList = new HashMap<>(); + + /* Notifies the adapter when data changes. */ + private RecyclerView.Adapter mAdapter; + + private Comparator mWidgetAndShortcutNameComparator; + + private IconCache mIconCache; + + public WidgetsModel(Context context, RecyclerView.Adapter adapter) { + mAdapter = adapter; + mWidgetAndShortcutNameComparator = new WidgetAndShortcutNameComparator(context); + mIconCache = LauncherAppState.getInstance().getIconCache(); + } + + // Access methods that may be deleted if the private fields are made package-private. + public int getPackageSize() { + return mPackageItemInfos.size(); + } + + // Access methods that may be deleted if the private fields are made package-private. + public PackageItemInfo getPackageItemInfo(int pos) { + return mPackageItemInfos.get(pos); + } + + public List<Object> getSortedWidgets(int pos) { + return mWidgetsList.get(mPackageItemInfos.get(pos)); + } + + public void addWidgetsAndShortcuts(ArrayList<Object> widgetsShortcuts, PackageManager pm) { + if (DEBUG) { + Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + widgetsShortcuts.size()); + } + + // Temporary list for {@link PackageItemInfos} to avoid having to go through + // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList} + HashMap<String, PackageItemInfo> tmpPackageItemInfos = new HashMap<>(); + + // clear the lists. + mWidgetsList.clear(); + mPackageItemInfos.clear(); + + // add and update. + for (Object o: widgetsShortcuts) { + String packageName = ""; + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o; + packageName = widgetInfo.provider.getPackageName(); + } else if (o instanceof ResolveInfo) { + ResolveInfo resolveInfo = (ResolveInfo) o; + packageName = resolveInfo.activityInfo.packageName; + } else { + Log.e(TAG, String.format("addWidgetsAndShortcuts, nothing added for class=%s", + o.getClass().toString())); + } + + PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName); + ArrayList<Object> widgetsShortcutsList = mWidgetsList.get(pInfo); + if (widgetsShortcutsList != null) { + widgetsShortcutsList.add(o); + } else { + widgetsShortcutsList = new ArrayList<Object>(); + widgetsShortcutsList.add(o); + + pInfo = new PackageItemInfo(packageName); + mIconCache.getTitleAndIconForApp(packageName, UserHandleCompat.myUserHandle(), + true /* useLowResIcon */, pInfo); + mWidgetsList.put(pInfo, widgetsShortcutsList); + tmpPackageItemInfos.put(packageName, pInfo); + mPackageItemInfos.add(pInfo); + } + } + + // sort. + sortPackageItemInfos(); + for (PackageItemInfo p: mPackageItemInfos) { + Collections.sort(mWidgetsList.get(p), mWidgetAndShortcutNameComparator); + } + + // notify. + mAdapter.notifyDataSetChanged(); + } + + private void sortPackageItemInfos() { + Collections.sort(mPackageItemInfos, new Comparator<PackageItemInfo>() { + @Override + public int compare(PackageItemInfo lhs, PackageItemInfo rhs) { + return lhs.title.toString().compareTo(rhs.title.toString()); + } + }); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/PagedViewGridLayout.java b/src/com/android/launcher3/widget/WidgetsRowView.java index f69fa562d..54667384b 100644 --- a/src/com/android/launcher3/PagedViewGridLayout.java +++ b/src/com/android/launcher3/widget/WidgetsRowView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2015 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. @@ -14,36 +14,31 @@ * limitations under the License. */ -package com.android.launcher3; +package com.android.launcher3.widget; import android.content.Context; import android.view.MotionEvent; -import android.view.View; import android.widget.FrameLayout; -import android.widget.GridLayout; +import android.widget.HorizontalScrollView; +import android.widget.TextView; + +import com.android.launcher3.R; /** - * The grid based layout used strictly for the widget/wallpaper tab of the AppsCustomize pane + * Layout used for widget tray rows for each app. For performance, this view can be replaced with + * a {@link RecyclerView} in the future if we settle on scrollable single row for the widgets. + * If we decide on collapsable grid, then HorizontalScrollView can be replaced with a + * {@link GridLayout}. */ -public class PagedViewGridLayout extends GridLayout implements Page { - static final String TAG = "PagedViewGridLayout"; +public class WidgetsRowView extends HorizontalScrollView { + static final String TAG = "WidgetsRow"; - private int mCellCountX; - private int mCellCountY; private Runnable mOnLayoutListener; + private String mAppName; - public PagedViewGridLayout(Context context, int cellCountX, int cellCountY) { + public WidgetsRowView(Context context, String appName) { super(context, null, 0); - mCellCountX = cellCountX; - mCellCountY = cellCountY; - } - - int getCellCountX() { - return mCellCountX; - } - - int getCellCountY() { - return mCellCountY; + mAppName = appName; } /** @@ -57,6 +52,13 @@ public class PagedViewGridLayout extends GridLayout implements Page { } @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + TextView tv = (TextView) findViewById(R.id.widget_name); + tv.setText(mAppName); + } + + @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mOnLayoutListener = null; @@ -66,6 +68,7 @@ public class PagedViewGridLayout extends GridLayout implements Page { mOnLayoutListener = r; } + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mOnLayoutListener != null) { @@ -76,43 +79,9 @@ public class PagedViewGridLayout extends GridLayout implements Page { @Override public boolean onTouchEvent(MotionEvent event) { boolean result = super.onTouchEvent(event); - int count = getPageChildCount(); - if (count > 0) { - // We only intercept the touch if we are tapping in empty space after the final row - View child = getChildOnPageAt(count - 1); - int bottom = child.getBottom(); - result = result || (event.getY() < bottom); - } return result; } - @Override - public void removeAllViewsOnPage() { - removeAllViews(); - mOnLayoutListener = null; - setLayerType(LAYER_TYPE_NONE, null); - } - - @Override - public void removeViewOnPageAt(int index) { - removeViewAt(index); - } - - @Override - public int getPageChildCount() { - return getChildCount(); - } - - @Override - public View getChildOnPageAt(int i) { - return getChildAt(i); - } - - @Override - public int indexOfChildOnPage(View v) { - return indexOfChild(v); - } - public static class LayoutParams extends FrameLayout.LayoutParams { public LayoutParams(int width, int height) { super(width, height); diff --git a/src/com/android/launcher3/UserInitializeReceiver.java b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java index d8e17b12f..99a192c89 100644 --- a/src/com/android/launcher3/UserInitializeReceiver.java +++ b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * Copyright (C) 2015 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. @@ -13,20 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.launcher3.widget; -package com.android.launcher3; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; +public class WidgetsRowViewHolder extends ViewHolder { -/** - * Takes care of setting initial wallpaper for a user, by selecting the - * first wallpaper that is not in use by another user. - */ -public class UserInitializeReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - // TODO: initial wallpaper now that wallpapers are owned by another app + ViewGroup mContent; + + public WidgetsRowViewHolder(ViewGroup v) { + super(v); + mContent = v; + } + + ViewGroup getContent() { + return mContent; } } diff --git a/tests/Android.mk b/tests/Android.mk index 762a52b8d..eba4ade48 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011 The Android Open Source Project +# Copyright (C) 2015 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. @@ -12,5 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. # -#LOCAL_PATH := $(call my-dir) -#include $(call all-makefiles-under,$(LOCAL_PATH)) + +LOCAL_PATH := $(call my-dir) + +src_dirs := src +res_dirs := res + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_STATIC_JAVA_LIBRARIES := android-support-test + +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) +LOCAL_AAPT_FLAGS := --auto-add-overlay + +LOCAL_SDK_VERSION := 21 + +LOCAL_PACKAGE_NAME := Launcher3Tests + +LOCAL_INSTRUMENTATION_FOR := Launcher3 + +include $(BUILD_PACKAGE) diff --git a/tests/stress/AndroidManifest.xml b/tests/AndroidManifest.xml index bcca1ff53..42ae5a38a 100644 --- a/tests/stress/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -1,5 +1,5 @@ <?xml version="2.0" encoding="utf-8"?> -<!-- Copyright (C) 2010 The Android Open Source Project +<!-- Copyright (C) 2015 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. @@ -15,7 +15,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.launcher3.stress.launcherrotation"> + package="com.android.launcher3.tests"> <application> <uses-library android:name="android.test.runner" /> @@ -24,6 +24,6 @@ <instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="com.android.launcher3" - android:label="Rotation stress test using Launcher2"> + android:label="Unit tests for Launcher3"> </instrumentation> </manifest> diff --git a/tests/res/values/string.xml b/tests/res/values/string.xml new file mode 100644 index 000000000..3c1ec5c61 --- /dev/null +++ b/tests/res/values/string.xml @@ -0,0 +1,21 @@ +<!-- Copyright (C) 2015 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <!-- Dummy string for tests. [DO NOT TRANSLATE] --> + <string name="dummy" >Dummy string for tests.</string> + +</resources> diff --git a/tests/src/com/android/launcher3/util/FocusLogicTest.java b/tests/src/com/android/launcher3/util/FocusLogicTest.java new file mode 100644 index 000000000..ea87014e9 --- /dev/null +++ b/tests/src/com/android/launcher3/util/FocusLogicTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015 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.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.KeyEvent; + +import com.android.launcher3.util.FocusLogic; + +/** + * Tests the {@link FocusLogic} class that handles key event based focus handling. + */ +@SmallTest +public final class FocusLogicTest extends AndroidTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + // Nothing to set up as this class only tests static methods. + } + + @Override + protected void tearDown() throws Exception { + // Nothing to tear down as this class only tests static methods. + } + + public void testShouldConsume() { + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_LEFT)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_RIGHT)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_UP)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_DOWN)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_MOVE_HOME)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_MOVE_END)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_PAGE_UP)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_PAGE_DOWN)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DEL)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_FORWARD_DEL)); + } + + public void testCreateSparseMatrix() { + // Either, 1) create a helper method to generate/instantiate all possible cell layout that + // may get created in real world to test this method. OR 2) Move all the matrix + // management routine to celllayout and write tests for them. + } +} diff --git a/tests/stress/Android.mk b/tests/stress/Android.mk deleted file mode 100644 index 68289bd3e..000000000 --- a/tests/stress/Android.mk +++ /dev/null @@ -1,31 +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. -# -LOCAL_PATH := $(call my-dir) -include $(CLEAR_VARS) - -# We only want this apk build for tests. -LOCAL_MODULE_TAGS := tests - -LOCAL_JAVA_LIBRARIES := android.test.runner - -LOCAL_SRC_FILES := $(call all-java-files-under, src) - -LOCAL_PACKAGE_NAME := LauncherRotationStressTest - -LOCAL_CERTIFICATE := shared - -LOCAL_INSTRUMENTATION_FOR := Launcher2 - -include $(BUILD_PACKAGE) diff --git a/tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java b/tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java deleted file mode 100644 index a5b85eb19..000000000 --- a/tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java +++ /dev/null @@ -1,63 +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.stress; - - -import com.android.launcher3.Launcher; - -import android.content.pm.ActivityInfo; -import android.os.SystemClock; -import android.test.ActivityInstrumentationTestCase2; -import android.test.RepetitiveTest; -import android.util.Log; - -/** - * Run rotation stress test using Launcher2 for 50 iterations. - */ -public class LauncherRotationStressTest extends ActivityInstrumentationTestCase2<Launcher> { - - private static final int NUM_ITERATIONS = 50; - private static final int WAIT_TIME_MS = 500; - private static final String LOG_TAG = "LauncherRotationStressTest"; - - public LauncherRotationStressTest() { - super(Launcher.class); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - } - - @Override - protected void tearDown() throws Exception { - super.tearDown(); - } - - @RepetitiveTest(numIterations=NUM_ITERATIONS) - public void testLauncherRotationStress() throws Exception { - Launcher launcher = getActivity(); - getInstrumentation().waitForIdleSync(); - SystemClock.sleep(WAIT_TIME_MS); - launcher.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - getInstrumentation().waitForIdleSync(); - SystemClock.sleep(WAIT_TIME_MS); - launcher.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - } -} |