diff options
116 files changed, 8235 insertions, 5232 deletions
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/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/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..347001783 100644 --- a/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java +++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java @@ -16,87 +16,32 @@ package com.android.gallery3d.common; +import android.annotation.TargetApi; +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.Canvas; -import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Point; +import android.net.Uri; import android.os.Build; -import android.util.FloatMath; import android.util.Log; +import android.view.WindowManager; -import java.io.ByteArrayOutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import com.android.gallery3d.exif.ExifInterface; +import com.android.launcher3.WallpaperCropActivity; -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,15 +49,6 @@ 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); @@ -136,125 +72,104 @@ public class BitmapUtils { return config; } - 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); + /** + * 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); + } - 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; + // 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 * WallpaperCropActivity.WALLPAPER_SCREENS_SPAN), maxDim); + defaultHeight = maxDim; + } + sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); + } + return sDefaultWallpaperSize; } - 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 int getRotationFromExif(Context context, Uri uri) { + return BitmapUtils.getRotationFromExifHelper(null, 0, context, uri); } - 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 int getRotationFromExif(Resources res, int resId) { + return BitmapUtils.getRotationFromExifHelper(res, resId, null, null); } - 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/launcher3/LiveWallpaperListAdapter.java b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java index 88f4461bf..72f2d7e49 100644 --- a/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java +++ b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java @@ -90,8 +90,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); @@ -122,8 +120,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/ThirdPartyWallpaperPickerListAdapter.java b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java index 7a4d48ca9..27e65aa31 100644 --- a/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java +++ b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java @@ -126,8 +126,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..a3a3c537b 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,39 @@ 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.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 Activity 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; + /** * 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 +67,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 = 2f; - 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; + private LoadRequest mCurrentLoadRequest; + private byte[] mTempStorageForDecoding = new byte[16 * 1024]; + // A weak-set of reusable bitmaps + private Set<Bitmap> mReusableBitmaps = + Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>()); + @Override protected 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 +102,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(); @@ -130,7 +143,7 @@ public class WallpaperCropActivity extends Activity { } } }; - setCropViewTileSource(bitmapSource, true, false, onLoad); + setCropViewTileSource(bitmapSource, true, false, null, onLoad); } @Override @@ -138,65 +151,140 @@ public class WallpaperCropActivity extends Activity { 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; + /** + * 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; } + }); + } 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; } - 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(); - } + + req.result = new BitmapRegionTileSource(this, req.src, mTempStorageForDecoding); + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (req == mCurrentLoadRequest) { + onLoadRequestComplete(req, + req.src.getLoadingState() == BitmapSource.State.LOADED); + } else { + addReusableBitmap(req.result); } } - if (postExecute != null) { - postExecute.run(); + }); + return true; + } + return false; + } + + private 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); } } - }; + } + } + + 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)); + } + + // 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(); + } + addReusableBitmap(oldSrc); + } + if (req.postExecute != null) { + req.postExecute.run(); + } + } + + 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; + + // Remove any pending requests + mLoaderHandler.removeMessages(MSG_LOAD_IMAGE); + Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget(); + // 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() { + mProgressView.postDelayed(new Runnable() { public void run() { - if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) { - progressView.setVisibility(View.VISIBLE); + if (mCurrentLoadRequest == req) { + mProgressView.setVisibility(View.VISIBLE); } } }, 1000); - loadBitmapTask.execute(); } + public boolean enableRotation() { return getResources().getBoolean(R.bool.allow_rotation); } @@ -205,111 +293,8 @@ public class WallpaperCropActivity extends Activity { return LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; } - // 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; - } - - 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); - } - - // 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; - } - sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); - } - return sDefaultWallpaperSize; - } - - public static int getRotationFromExif(String path) { - return getRotationFromExifHelper(path, null, 0, null, null); - } - - public static int getRotationFromExif(Context context, Uri uri) { - return getRotationFromExifHelper(null, null, 0, context, uri); - } - - public static int getRotationFromExif(Resources res, int resId) { - return getRotationFromExifHelper(null, res, resId, null, null); - } - - 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; - } - protected void setWallpaper(Uri uri, final boolean finishActivityWhenDone) { - int rotation = getRotationFromExif(this, uri); + int rotation = BitmapUtils.getRotationFromExif(this, uri); BitmapCropTask cropTask = new BitmapCropTask( this, uri, null, rotation, 0, 0, true, false, null); final Point bounds = cropTask.getImageBounds(); @@ -331,11 +316,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 = BitmapUtils.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() { @@ -353,13 +338,9 @@ public class WallpaperCropActivity extends Activity { 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 +351,7 @@ public class WallpaperCropActivity extends Activity { d.getSize(displaySize); boolean isPortrait = displaySize.x < displaySize.y; - Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(), + Point defaultWallpaperSize = BitmapUtils.getDefaultWallpaperSize(getResources(), getWindowManager()); // Get the crop RectF cropRect = mCropView.getCrop(); @@ -452,372 +433,6 @@ 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); @@ -835,11 +450,11 @@ public class WallpaperCropActivity extends Activity { sp, getWindowManager(), WallpaperManager.getInstance(this), true); } - static public void suggestWallpaperDimension(Resources res, + public static void suggestWallpaperDimension(Resources res, final SharedPreferences sharedPrefs, WindowManager windowManager, final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { - final Point defaultWallpaperSize = getDefaultWallpaperSize(res, windowManager); + final Point defaultWallpaperSize = BitmapUtils.getDefaultWallpaperSize(res, windowManager); // If we have saved a wallpaper width/height, use that instead int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); @@ -860,39 +475,17 @@ public class WallpaperCropActivity extends Activity { } } - 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..d16fc31e5 100644 --- a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java +++ b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java @@ -20,7 +20,6 @@ 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.Context; import android.content.Intent; @@ -35,11 +34,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 +67,12 @@ 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.photos.BitmapRegionTileSource; import com.android.photos.BitmapRegionTileSource.BitmapSource; +import com.android.photos.views.TiledImageRenderer.TileSource; import java.io.File; import java.io.FileOutputStream; @@ -83,7 +84,6 @@ 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; @@ -93,7 +93,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { private OnClickListener mThumbnailOnClickListener; private LinearLayout mWallpapersView; - private View mWallpaperStrip; + private HorizontalScrollView mWallpaperScrollContainer; private ActionMode.Callback mActionModeCallback; private ActionMode mActionMode; @@ -102,9 +102,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { ArrayList<Uri> mTempWallpaperTiles = new ArrayList<Uri>(); private SavedWallpaperImages mSavedImages; - private WallpaperInfo mLiveWallpaperInfoOnPickerLaunch; private int mSelectedIndex = -1; - private WallpaperInfo mLastClickedLiveWallpaperInfo; public static abstract class WallpaperTileInfo { protected View mView; @@ -169,12 +167,12 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } mBitmapSource = new BitmapRegionTileSource.UriBitmapSource( a, mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE); - a.setCropViewTileSource(mBitmapSource, true, false, onLoad); + a.setCropViewTileSource(mBitmapSource, true, false, null, 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 @@ -206,7 +204,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { public void onClick(WallpaperPickerActivity a) { BitmapRegionTileSource.UriBitmapSource bitmapSource = new BitmapRegionTileSource.UriBitmapSource(a, Uri.fromFile(mFile), 1024); - a.setCropViewTileSource(bitmapSource, false, true, null); + a.setCropViewTileSource(bitmapSource, false, true, null, null); } @Override public void onSave(WallpaperPickerActivity a) { @@ -232,22 +230,22 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { mThumb = thumb; } @Override - public void onClick(WallpaperPickerActivity a) { + public void onClick(final WallpaperPickerActivity a) { 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 = BitmapUtils.getDefaultWallpaperSize( + a.getResources(), a.getWindowManager()); + RectF crop = Utils.getMaxCropRect( + src.getImageWidth(), src.getImageHeight(), + wallpaperSize.x, wallpaperSize.y, false); + return wallpaperSize.x / crop.width(); + } + }, null); } @Override public void onSave(WallpaperPickerActivity a) { @@ -272,21 +270,26 @@ 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); - 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, defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE); + a.onLoadRequestComplete(req, true); } @Override public void onSave(WallpaperPickerActivity a) { @@ -308,10 +311,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 @@ -349,24 +348,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 +362,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 +371,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 +397,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)); @@ -492,7 +479,6 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { 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 +486,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(); @@ -665,17 +650,14 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } 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(); + 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); } }); } @@ -702,10 +684,10 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { protected 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); } } @@ -812,7 +794,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,7 +811,6 @@ 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 @@ -839,7 +820,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { 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()) { @@ -885,33 +866,13 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { Uri uri = data.getData(); addTemporaryWallpaperTile(uri, false); } - } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY) { + } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY && resultCode == RESULT_OK) { + // Something was set on the third-party activity. setResult(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); } @@ -1024,7 +985,7 @@ 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); if (thumb != null) { @@ -1110,25 +1071,6 @@ 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; @@ -1156,8 +1098,6 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { view = convertView; } - setWallpaperItemPaddingToZero((FrameLayout) view); - ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); if (thumb != null) { 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/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/android/util/Pools.java b/WallpaperPicker/src/com/android/photos/views/Pools.java index 40bab1eae..c60f2f013 100644 --- a/WallpaperPicker/src/android/util/Pools.java +++ b/WallpaperPicker/src/com/android/photos/views/Pools.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.util; +package com.android.photos.views; /** * Helper class for crating pools of objects. An example use looks like this: diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java index c4e493b34..f9b7ab473 100644 --- a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java +++ b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java @@ -20,11 +20,9 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.RectF; -import android.support.v4.util.LongSparseArray; 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 +30,8 @@ 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.photos.views.Pools.Pool; +import com.android.photos.views.Pools.SynchronizedPool; /** * Handles laying out, decoding, and drawing of tiles in GL diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageView.java b/WallpaperPicker/src/com/android/photos/views/TiledImageView.java index 94063b027..56ee7a658 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,11 +26,9 @@ 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; @@ -43,18 +39,10 @@ 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; private FrameCallback mFrameCallback; @@ -79,35 +67,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 +89,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 +105,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 +125,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 +149,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 +171,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); 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-xxhdpi/ic_pageindicator_current_dark.png b/res/drawable-xxhdpi/ic_pageindicator_current_dark.png Binary files differnew file mode 100644 index 000000000..d5c4c8d4e --- /dev/null +++ b/res/drawable-xxhdpi/ic_pageindicator_current_dark.png diff --git a/res/drawable-xxhdpi/ic_pageindicator_default_dark.png b/res/drawable-xxhdpi/ic_pageindicator_default_dark.png Binary files differnew file mode 100644 index 000000000..79d307b07 --- /dev/null +++ b/res/drawable-xxhdpi/ic_pageindicator_default_dark.png diff --git a/res/drawable/apps_list_bg.xml b/res/drawable/apps_list_bg.xml new file mode 100644 index 000000000..64177c16b --- /dev/null +++ b/res/drawable/apps_list_bg.xml @@ -0,0 +1,20 @@ +<?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. +--> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/apps_list_bg_inset" + android:insetLeft="@dimen/apps_container_inset" + android:insetRight="@dimen/apps_container_inset" />
\ No newline at end of file diff --git a/res/drawable/apps_list_bg_inset.xml b/res/drawable/apps_list_bg_inset.xml new file mode 100644 index 000000000..5ea78952f --- /dev/null +++ b/res/drawable/apps_list_bg_inset.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="3dp" + android:bottomRightRadius="3dp" /> +</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..59383a5bb --- /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_size" /> +</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..eda33a918 --- /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:topLeftRadius="3dp" + android:topRightRadius="3dp" /> +</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..47c608f85 --- /dev/null +++ b/res/drawable/apps_reveal_bg.xml @@ -0,0 +1,20 @@ +<?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. +--> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/apps_reveal_bg_inset" + android:insetLeft="@dimen/apps_container_inset" + android:insetRight="@dimen/apps_container_inset" />
\ No newline at end of file diff --git a/res/drawable/apps_reveal_bg_inset.xml b/res/drawable/apps_reveal_bg_inset.xml new file mode 100644 index 000000000..61f1c083a --- /dev/null +++ b/res/drawable/apps_reveal_bg_inset.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="3dp" /> +</shape>
\ No newline at end of file diff --git a/res/layout-land/launcher.xml b/res/layout-land/launcher.xml index 6f95bd506..b13984a26 100644 --- a/res/layout-land/launcher.xml +++ b/res/layout-land/launcher.xml @@ -62,6 +62,12 @@ 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" /> </com.android.launcher3.DragLayer> <ViewStub diff --git a/res/layout-port/launcher.xml b/res/layout-port/launcher.xml index af30a32e5..3cb338efe 100644 --- a/res/layout-port/launcher.xml +++ b/res/layout-port/launcher.xml @@ -71,6 +71,12 @@ 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" /> </com.android.launcher3.DragLayer> <ViewStub diff --git a/res/layout-sw600dp/apps_view.xml b/res/layout-sw600dp/apps_view.xml new file mode 100644 index 000000000..6f22460fa --- /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:background="#22000000" + android:descendantFocusability="afterDescendants"> + <include + layout="@layout/apps_reveal_view" + android:layout_width="@dimen/apps_container_width" + android:layout_height="540dp" + android:layout_gravity="center" /> + <include + layout="@layout/apps_list_view" + android:layout_width="@dimen/apps_container_width" + android:layout_height="540dp" + 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..a3d502cf4 100644 --- a/res/layout-sw720dp/launcher.xml +++ b/res/layout-sw720dp/launcher.xml @@ -71,6 +71,12 @@ 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" /> </com.android.launcher3.DragLayer> <ViewStub 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..83c175bb8 --- /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>
\ No newline at end of file diff --git a/res/layout/apps_list_view.xml b/res/layout/apps_list_view.xml new file mode 100644 index 000000000..59c04103f --- /dev/null +++ b/res/layout/apps_list_view.xml @@ -0,0 +1,56 @@ +<?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"> + <EditText + android:id="@+id/app_search_box" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/apps_container_inset" + android:layout_marginRight="@dimen/apps_container_inset" + 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="flagNoExtractUi" + android:background="@drawable/apps_list_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:fadeScrollbars="false" + android:scrollbars="vertical" + android:scrollbarThumbVertical="@drawable/apps_list_scrollbar_thumb" + android:focusable="true" + android:descendantFocusability="afterDescendants" + android:background="@drawable/apps_list_bg" /> +</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..bc93359c1 --- /dev/null +++ b/res/layout/apps_reveal_view.xml @@ -0,0 +1,25 @@ +<?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:background="@drawable/apps_reveal_bg" + 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..86d67e15f --- /dev/null +++ b/res/layout/apps_view.xml @@ -0,0 +1,38 @@ +<?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:paddingTop="@dimen/apps_container_inset" + android:paddingBottom="@dimen/apps_container_inset" + android:background="@drawable/apps_customize_bg" + 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/user_folder.xml b/res/layout/user_folder.xml index ed8d43e46..7a4d5e881 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) 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. @@ -14,42 +15,49 @@ 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> + 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.FolderCellLayout + android:id="@+id/folder_content" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:cacheColorHint="#ff333333" + android:hapticFeedbackEnabled="false" /> + </FrameLayout> <com.android.launcher3.FolderEditText android:id="@+id/folder_name" 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:fontFamily="sans-serif-condensed" + android:gravity="center_horizontal" android:hint="@string/folder_hint_text" - android:textSize="14sp" + android:imeOptions="flagNoExtractUi" + android:paddingBottom="@dimen/folder_name_padding" + android:paddingTop="@dimen/folder_name_padding" + android:singleLine="true" android:textColor="#ff777777" - android:textColorHint="#ff808080" android:textColorHighlight="#ffCCCCCC" + android:textColorHint="#ff808080" android:textCursorDrawable="@null" - android:gravity="center_horizontal" - android:singleLine="true" - android:imeOptions="flagNoExtractUi" - android:fontFamily="sans-serif-condensed"/> -</com.android.launcher3.Folder> + android:textSize="14sp" /> + +</com.android.launcher3.Folder>
\ No newline at end of file diff --git a/res/layout/user_folder_scroll.xml b/res/layout/user_folder_scroll.xml new file mode 100644 index 000000000..421e426cb --- /dev/null +++ b/res/layout/user_folder_scroll.xml @@ -0,0 +1,78 @@ +<?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.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:background="@drawable/quantum_panel" + android:orientation="vertical" > + + <FrameLayout + android:id="@+id/folder_content_wrapper" + android:layout_width="match_parent" + 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:orientation="horizontal" > + + <com.android.launcher3.FolderEditText + android:id="@+id/folder_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + 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" /> + + <include + android:id="@+id/folder_page_indicator" + android:layout_width="wrap_content" + android:layout_height="12dp" + android:layout_gravity="center_vertical" + layout="@layout/page_indicator" /> + </LinearLayout> + +</com.android.launcher3.Folder>
\ No newline at end of file diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml index 28679be2e..d9075872a 100644 --- a/res/values-sw600dp/dimens.xml +++ b/res/values-sw600dp/dimens.xml @@ -17,6 +17,10 @@ <resources> <dimen name="app_icon_size">64dp</dimen> +<!-- Apps view --> + <dimen name="apps_container_width">480dp</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> 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/attrs.xml b/res/values/attrs.xml index 3331cdec4..845b18230 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -18,6 +18,15 @@ <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" /> + </declare-styleable> + <!-- Page Indicator specific attributes. --> <declare-styleable name="PageIndicator"> <attr name="windowSize" format="integer" /> diff --git a/res/values/colors.xml b/res/values/colors.xml index 2daf9fe12..590a8872b 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -36,4 +36,8 @@ <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> + </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..b9b9a2412 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -46,6 +46,17 @@ <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_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_size">6dp</dimen> + <dimen name="apps_view_fast_scroll_gutter_size">40dp</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 --> @@ -68,10 +79,6 @@ 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> diff --git a/res/values/strings.xml b/res/values/strings.xml index 8b7e6c199..a1e460183 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -71,6 +71,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 --> @@ -303,6 +311,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/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..5ed7a629a 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); 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..c1d2738da --- /dev/null +++ b/src/com/android/launcher3/AlphabeticalAppsList.java @@ -0,0 +1,247 @@ +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 java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + + +/** + * The alphabetically sorted list of applications. + */ +public class AlphabeticalAppsList { + + /** + * Info about a section in the alphabetic list + */ + public class SectionInfo { + public String sectionName; + public int numAppsInSection; + } + + /** + * A filter interface to limit the set of applications in the apps list. + */ + public interface Filter { + public boolean retainApp(AppInfo info); + } + + // Hack to force RecyclerView to break sections + public static final AppInfo SECTION_BREAK_INFO = null; + + private List<AppInfo> mApps = new ArrayList<>(); + private List<AppInfo> mFilteredApps = new ArrayList<>(); + private List<AppInfo> mSectionedFilteredApps = new ArrayList<>(); + private List<SectionInfo> mSections = new ArrayList<>(); + private RecyclerView.Adapter mAdapter; + private Filter mFilter; + private AlphabeticIndexCompat mIndexer; + + public AlphabeticalAppsList(Context context) { + mIndexer = new AlphabeticIndexCompat(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<AppInfo> getApps() { + return mSectionedFilteredApps; + } + + /** + * Returns the current filtered list of applications. + */ + public List<AppInfo> getAppsWithoutSectionBreaks() { + return mFilteredApps; + } + + /** + * Returns the section name for the application. + */ + public String getSectionNameForApp(AppInfo info) { + return mIndexer.computeSectionName(info.title.toString().trim()); + } + + /** + * Returns the indexer for this locale. + */ + public AlphabeticIndexCompat getIndexer() { + return mIndexer; + } + + /** + * 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, LauncherModel.getAppNameComparator()); + 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) { + int sectionedIndex = mSectionedFilteredApps.indexOf(info); + int numAppsInSection = numAppsInSection(info); + mApps.remove(removeIndex); + onAppsUpdated(); + if (numAppsInSection == 1) { + // Remove the section and the icon + mAdapter.notifyItemRemoved(sectionedIndex - 1); + mAdapter.notifyItemRemoved(sectionedIndex - 1); + } else { + mAdapter.notifyItemRemoved(sectionedIndex); + } + } + } + } + + /** + * 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) { + Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator(); + int index = Collections.binarySearch(mApps, info, appNameComparator); + if (index < 0) { + mApps.add(-(index + 1), info); + onAppsUpdated(); + + int sectionedIndex = mSectionedFilteredApps.indexOf(info); + int numAppsInSection = numAppsInSection(info); + if (numAppsInSection == 1) { + // New section added along with icon + mAdapter.notifyItemInserted(sectionedIndex - 1); + mAdapter.notifyItemInserted(sectionedIndex - 1); + } else { + mAdapter.notifyItemInserted(sectionedIndex); + } + } + } + + /** + * Returns the number of apps in the section that the given info is in. + */ + private int numAppsInSection(AppInfo info) { + int appIndex = mFilteredApps.indexOf(info); + int appCount = 0; + for (SectionInfo section : mSections) { + if (appCount + section.numAppsInSection > appIndex) { + return section.numAppsInSection; + } + appCount += section.numAppsInSection; + } + return 1; + } + + /** + * Updates internals when the set of apps are updated. + */ + private void onAppsUpdated() { + // Recreate the filtered apps + mFilteredApps.clear(); + for (AppInfo info : mApps) { + if (mFilter == null || mFilter.retainApp(info)) { + mFilteredApps.add(info); + } + } + + // Section the apps (for convenience for the grid layout) + mSections.clear(); + mSectionedFilteredApps.clear(); + SectionInfo lastSectionInfo = null; + for (AppInfo info : mFilteredApps) { + String sectionName = getSectionNameForApp(info); + if (lastSectionInfo == null || !lastSectionInfo.sectionName.equals(sectionName)) { + lastSectionInfo = new SectionInfo(); + lastSectionInfo.sectionName = sectionName; + mSectionedFilteredApps.add(SECTION_BREAK_INFO); + mSections.add(lastSectionInfo); + } + lastSectionInfo.numAppsInSection++; + mSectionedFilteredApps.add(info); + } + } +} diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index a66bac08a..455c6d16d 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. @@ -77,13 +73,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); intent = makeLaunchIntent(context, info, user); this.user = user; } diff --git a/src/com/android/launcher3/AppsContainerRecyclerView.java b/src/com/android/launcher3/AppsContainerRecyclerView.java new file mode 100644 index 000000000..c5a508c9c --- /dev/null +++ b/src/com/android/launcher3/AppsContainerRecyclerView.java @@ -0,0 +1,273 @@ +/* + * 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.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +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 mFastScrollerBg; + 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 mGutterSize; + + 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); + 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)); + mGutterSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_gutter_size); + 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; + } + + @Override + protected void onFinishInflate() { + addOnItemTouchListener(this); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mFastScrollAlpha > 0f) { + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + Rect bgBounds = mFastScrollerBg.getBounds(); + int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + int x; + if (isRtl) { + x = getPaddingLeft() + getScrollBarSize(); + } else { + x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width(); + } + int y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height()); + y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() - + bgBounds.height())); + 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); + } + } + + /** + * We intercept the touch handling only to support fast scrolling when initiated from the + * gutter. 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 + boolean isRtl = (getResources().getConfiguration().getLayoutDirection() == + LAYOUT_DIRECTION_RTL); + boolean isInGutter; + if (isRtl) { + isInGutter = mDownX < mGutterSize; + } else { + isInGutter = mDownX >= (getWidth() - mGutterSize); + } + if (!mDraggingFastScroller && isInGutter && + 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(); + } + + /** + * 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(); + // Get the total number of rows + int rowCount = 0; + for (AlphabeticalAppsList.SectionInfo info : sections) { + int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow); + rowCount += numRowsInSection; + } + + // Find the index of the first app in that row and scroll to that position + int rowAtProgress = (int) (progress * rowCount); + int appIndex = 0; + rowCount = 0; + for (AlphabeticalAppsList.SectionInfo info : sections) { + int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow); + if (rowCount + numRowsInSection > rowAtProgress) { + appIndex += (rowAtProgress - rowCount) * mNumAppsPerRow; + break; + } + rowCount += numRowsInSection; + appIndex += info.numAppsInSection; + } + appIndex = Math.max(0, Math.min(mApps.getAppsWithoutSectionBreaks().size() - 1, appIndex)); + AppInfo appInfo = mApps.getAppsWithoutSectionBreaks().get(appIndex); + int sectionedAppIndex = mApps.getApps().indexOf(appInfo); + scrollToPosition(sectionedAppIndex); + + // Returns the section name of the row + return mApps.getSectionNameForApp(appInfo); + } +} diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java new file mode 100644 index 000000000..ce092bfe4 --- /dev/null +++ b/src/com/android/launcher3/AppsContainerView.java @@ -0,0 +1,378 @@ +/* + * 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.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.TextView; + +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; + + private Launcher mLauncher; + private AlphabeticalAppsList mApps; + private RecyclerView.Adapter mAdapter; + private RecyclerView.LayoutManager mLayoutManager; + private RecyclerView.ItemDecoration mItemDecoration; + private AppsContainerRecyclerView mAppsListView; + private EditText mSearchBar; + private int mNumAppsPerRow; + private Point mLastTouchDownPos = new Point(); + private Rect mPadding = new Rect(); + private int mContentMarginStart; + + 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) { + this(context, attrs, defStyleAttr, 0); + } + + public AppsContainerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + Resources res = context.getResources(); + + 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); + } + + /** + * Scrolls this list view to the top. + */ + public void scrollToTop() { + mAppsListView.scrollToPosition(0); + } + + /** + * Returns the content view used for the launcher transitions. + */ + public View getContentView() { + return findViewById(R.id.apps_list); + } + + /** + * 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); + } + mSearchBar = (EditText) findViewById(R.id.app_search_box); + mSearchBar.addTextChangedListener(this); + mSearchBar.setOnEditorActionListener(this); + mAppsListView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view); + mAppsListView.setApps(mApps); + mAppsListView.setNumAppsPerRow(mNumAppsPerRow); + mAppsListView.setLayoutManager(mLayoutManager); + mAppsListView.setAdapter(mAdapter); + mAppsListView.setHasFixedSize(true); + if (isRtl) { + mAppsListView.setPadding(mAppsListView.getPaddingLeft(), mAppsListView.getPaddingTop(), + mAppsListView.getPaddingRight() + mContentMarginStart, + mAppsListView.getPaddingBottom()); + } else { + mAppsListView.setPadding(mAppsListView.getPaddingLeft() + mContentMarginStart, + mAppsListView.getPaddingTop(), mAppsListView.getPaddingRight(), + mAppsListView.getPaddingBottom()); + } + if (mItemDecoration != null) { + mAppsListView.addItemDecoration(mItemDecoration); + } + mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), + getPaddingBottom()); + } + + @Override + public void setInsets(Rect insets) { + setPadding(mPadding.left + insets.left, mPadding.top + insets.top, + mPadding.right + insets.right, mPadding.bottom + insets.bottom); + } + + @Override + public boolean onTouch(View v, MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN || + ev.getAction() == MotionEvent.ACTION_MOVE) { + mLastTouchDownPos.set((int) ev.getX(), (int) ev.getY()); + } + 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, mLastTouchDownPos, 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 true; + } + + @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 title = info.title.toString(); + String sectionName = mApps.getSectionNameForApp(info); + 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) { + List<AppInfo> appsWithoutSections = mApps.getAppsWithoutSectionBreaks(); + List<AppInfo> apps = mApps.getApps(); + if (appsWithoutSections.size() == 1) { + mAppsListView.getChildAt(apps.indexOf(appsWithoutSections.get(0))).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) { + if (!toWorkspace) { + // Disable the focus so that the search bar doesn't get focus + mSearchBar.setFocusableInTouchMode(false); + } + } + + @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 (toWorkspace) { + // Clear the search bar + mSearchBar.setText(""); + } else { + mSearchBar.setFocusableInTouchMode(true); + } + } +} diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java index 9e7e523e0..bf368125f 100644 --- a/src/com/android/launcher3/AppsCustomizePagedView.java +++ b/src/com/android/launcher3/AppsCustomizePagedView.java @@ -16,8 +16,6 @@ package com.android.launcher3; -import android.animation.AnimatorSet; -import android.animation.ValueAnimator; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; @@ -38,16 +36,15 @@ 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.FocusHelper.PagedViewKeyListener; import com.android.launcher3.compat.AppWidgetManagerCompat; import java.util.ArrayList; @@ -142,7 +139,7 @@ class AppsCustomizeAsyncTask extends AsyncTask<AsyncTaskPageData, Void, AsyncTas * The Apps/Customize page that displays all the applications, widgets, and shortcuts. */ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implements - View.OnClickListener, View.OnKeyListener, DragSource, + View.OnClickListener, DragSource, PagedViewWidget.ShortPressListener, LauncherTransitionable { static final String TAG = "AppsCustomizePagedView"; @@ -152,10 +149,9 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen * The different content types that this paged view can show. */ public enum ContentType { - Applications, Widgets } - private ContentType mContentType = ContentType.Applications; + private ContentType mContentType = ContentType.Widgets; // Refs private Launcher mLauncher; @@ -167,7 +163,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen private int mSaveInstanceStateItemIndex = -1; // Content - private ArrayList<AppInfo> mApps; private ArrayList<Object> mWidgets; // Caching @@ -177,14 +172,14 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen 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 final PagedViewKeyListener mKeyListener = new PagedViewKeyListener(); + private Runnable mInflateWidgetRunnable = null; private Runnable mBindWidgetRunnable = null; static final int WIDGET_NO_CLEANUP_REQUIRED = -1; @@ -215,10 +210,9 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen super(context, attrs); mLayoutInflater = LayoutInflater.from(context); mPackageManager = context.getPackageManager(); - mApps = new ArrayList<AppInfo>(); - mWidgets = new ArrayList<Object>(); + mWidgets = new ArrayList<>(); mIconCache = (LauncherAppState.getInstance()).getIconCache(); - mRunningTasks = new ArrayList<AppsCustomizeAsyncTask>(); + mRunningTasks = new ArrayList<>(); // Save the default widget preview background TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0); @@ -257,10 +251,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen grid.edgeMarginPx, 2 * grid.edgeMarginPx); } - void setAllAppsPadding(Rect r) { - mAllAppsPadding.set(r); - } - void setWidgetsPageIndicatorPadding(int pageIndicatorHeight) { setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), pageIndicatorHeight); } @@ -278,22 +268,12 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen 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(); + if (mContentType == ContentType.Widgets) { PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(currentPage); int numItemsPerPage = mWidgetCountX * mWidgetCountY; int childCount = layout.getChildCount(); if (childCount > 0) { - i = numApps + - (currentPage * numItemsPerPage) + (childCount / 2); + i = (currentPage * numItemsPerPage) + (childCount / 2); } } else { throw new RuntimeException("Invalid ContentType"); @@ -315,13 +295,8 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen 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; - } + int numItemsPerPage = mWidgetCountX * mWidgetCountY; + return index / numItemsPerPage; } /** Restores the page for an item at the specified index */ @@ -333,16 +308,9 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen 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 @@ -361,7 +329,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen super.onLayout(changed, l, t, r, b); if (!isDataReady()) { - if ((LauncherAppState.isDisableAllApps() || !mApps.isEmpty()) && !mWidgets.isEmpty()) { + if (!mWidgets.isEmpty()) { post(new Runnable() { // This code triggers requestLayout so must be posted outside of the // layout pass. @@ -439,7 +407,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen @Override public void onClick(View v) { // When we have exited all apps or are in transition, disregard clicks - if (!mLauncher.isAllAppsVisible() + if (!mLauncher.isWidgetsViewVisible() || mLauncher.getWorkspace().isSwitchingState() || !(v instanceof PagedViewWidget)) return; @@ -450,22 +418,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen 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); } /* @@ -473,11 +425,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen */ @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) { @@ -698,12 +645,12 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen protected boolean beginDragging(final View v) { if (!super.beginDragging(v)) return false; - if (v instanceof BubbleTextView) { - beginDraggingApplication(v); - } else if (v instanceof PagedViewWidget) { + if (v instanceof PagedViewWidget) { if (!beginDraggingWidget(v)) { return false; } + } else { + Log.e(TAG, "Unexpected dragging view: " + v); } // We delay entering spring-loaded mode slightly to make sure the UI @@ -869,7 +816,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // Clean up all the async tasks Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + AppsCustomizeAsyncTask task = iter.next(); task.cancel(false); iter.remove(); mDirtyPageContent.set(task.page, true); @@ -903,7 +850,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // Update the thread priorities given the direction lookahead Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + AppsCustomizeAsyncTask task = iter.next(); int pageIndex = task.page; if ((mNextPage > mCurrentPage && pageIndex >= mCurrentPage) || (mNextPage < mCurrentPage && pageIndex <= mCurrentPage)) { @@ -914,36 +861,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } } - /* - * 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(); @@ -955,43 +872,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } } - 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. */ @@ -1008,7 +888,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); int minPageDiff = Integer.MAX_VALUE; while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + AppsCustomizeAsyncTask task = iter.next(); minPageDiff = Math.abs(task.page - toPage); } @@ -1043,7 +923,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen // Prune all tasks that are no longer needed Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator(); while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); + AppsCustomizeAsyncTask task = iter.next(); int taskPage = task.page; if (taskPage < getAssociatedLowerPageBound(mCurrentPage) || taskPage > getAssociatedUpperPageBound(mCurrentPage)) { @@ -1156,7 +1036,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen widget.setOnClickListener(this); widget.setOnLongClickListener(this); widget.setOnTouchListener(this); - widget.setOnKeyListener(this); + widget.setOnKeyListener(mKeyListener); // Layout each widget int ix = i % mWidgetCountX; @@ -1281,14 +1161,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen 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) { + if (mContentType == ContentType.Widgets) { for (int j = 0; j < mNumWidgetPages; ++j) { PagedViewGridLayout layout = new PagedViewGridLayout(context, mWidgetCountX, mWidgetCountY); @@ -1308,7 +1181,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen if (mContentType == ContentType.Widgets) { syncWidgetPageItems(page, immediate); } else { - syncAppsPageItems(page, immediate); + Log.e(TAG, "Unexpected ContentType"); } } @@ -1400,7 +1273,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } /** - * We should call thise method whenever the core data changes (mApps, mWidgets) so that we can + * We should call thise method whenever the core data changes (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. @@ -1416,81 +1289,12 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen } } - 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 (mContentType != ContentType.Widgets) { + setContentType(ContentType.Widgets); } if (mCurrentPage != 0) { @@ -1504,7 +1308,6 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen public void dumpState() { // TODO: Dump information related to current list of Applications, Widgets, etc. - AppInfo.dumpApplicationInfoList(TAG, "mApps", mApps); dumpAppWidgetProviderInfoList(TAG, "mWidgets", mWidgets); } @@ -1559,10 +1362,7 @@ public class AppsCustomizePagedView extends PagedViewWithDraggableItems implemen 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) { + if (mContentType == ContentType.Widgets) { stringId = R.string.apps_customize_widgets_scroll_format; count = mNumWidgetPages; } else { diff --git a/src/com/android/launcher3/AppsCustomizeTabHost.java b/src/com/android/launcher3/AppsCustomizeTabHost.java index a2717126d..5e2f05c61 100644 --- a/src/com/android/launcher3/AppsCustomizeTabHost.java +++ b/src/com/android/launcher3/AppsCustomizeTabHost.java @@ -27,7 +27,6 @@ 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; @@ -50,10 +49,6 @@ public class AppsCustomizeTabHost extends FrameLayout implements LauncherTransit mPagedView.setContentType(type); } - public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) { - setContentTypeImmediate(type); - } - @Override public void setInsets(Rect insets) { mInsets.set(insets); @@ -79,27 +74,38 @@ public class AppsCustomizeTabHost extends FrameLayout implements LauncherTransit } /** + * Returns the content view used for the launcher transitions. + */ + public View getContentView() { + return findViewById(R.id.apps_customize_pane_content); + } + + /** + * Returns the reveal view used for the launcher transitions. + */ + public View getRevealView() { + return findViewById(R.id.fake_page); + } + + /** + * Returns the page indicators view. + */ + public View getPageIndicators() { + return findViewById(R.id.apps_customize_page_indicator); + } + + /** * 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; + return AppsCustomizePagedView.ContentType.Widgets; } /** * 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; + return WIDGETS_TAB_TAG; } /** @@ -199,6 +205,7 @@ public class AppsCustomizeTabHost extends FrameLayout implements LauncherTransit ViewGroup parent = (ViewGroup) getParent(); if (parent == null) return; + View appsView = ((Launcher) getContext()).getAppsView(); View overviewPanel = ((Launcher) getContext()).getOverviewPanel(); final int count = parent.getChildCount(); if (!isChildrenDrawingOrderEnabled()) { @@ -207,7 +214,8 @@ public class AppsCustomizeTabHost extends FrameLayout implements LauncherTransit if (child == this) { break; } else { - if (child.getVisibility() == GONE || child == overviewPanel) { + if (child.getVisibility() == GONE || child == overviewPanel || + child == appsView) { continue; } child.setVisibility(visibility); diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java new file mode 100644 index 000000000..6a4495e0e --- /dev/null +++ b/src/com/android/launcher3/AppsGridAdapter.java @@ -0,0 +1,242 @@ +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.compat.AlphabeticIndexCompat; + + +/** + * 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; + } + + AppInfo info = mApps.getApps().get(position); + if (info == AlphabeticalAppsList.SECTION_BREAK_INFO) { + // 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) { + 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 (mApps.getApps().get(holder.getPosition() - 1) == + AlphabeticalAppsList.SECTION_BREAK_INFO) { + // Draw at the parent + AppInfo info = mApps.getApps().get(holder.getPosition()); + String section = mApps.getSectionNameForApp(info); + 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; + private AlphabeticalAppsList mApps; + private GridSpanSizer mGridSizer; + private GridItemDecoration mItemDecoration; + private View.OnTouchListener mTouchListener; + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + private int mAppsPerRow; + private boolean mIsRtl; + private String mEmptySearchText; + + // Section drawing + private int mPaddingStart; + private int mStartMargin; + private Paint mSectionTextPaint; + private 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(); + 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 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) { + GridLayoutManager layoutMgr = new GridLayoutManager(context, mAppsPerRow, + GridLayoutManager.VERTICAL, false); + layoutMgr.setSpanSizeLookup(mGridSizer); + return layoutMgr; + } + + /** + * 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.getApps().get(position); + 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.getApps().size(); + } + + @Override + public int getItemViewType(int position) { + if (mApps.hasNoFilteredResults()) { + return EMPTY_VIEW_TYPE; + } else if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) { + 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..e1f4d3578 --- /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; +import com.android.launcher3.compat.AlphabeticIndexCompat; + +/** + * 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: + AppInfo info = mApps.getApps().get(position); + ViewGroup content = (ViewGroup) holder.mContent; + String sectionDescription = mApps.getSectionNameForApp(info); + + // Bind the section header + boolean showSectionHeader = true; + if (position > 0) { + AppInfo prevInfo = mApps.getApps().get(position - 1); + showSectionHeader = (prevInfo == AlphabeticalAppsList.SECTION_BREAK_INFO); + } + 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(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.getApps().size(); + } + + @Override + public int getItemViewType(int position) { + if (mApps.hasNoFilteredResults()) { + return EMPTY_VIEW_TYPE; + } else if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) { + 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..908bd3d79 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -116,7 +116,7 @@ public class AutoInstallsLayout { private final Context mContext; private final AppWidgetHost mAppWidgetHost; - private final LayoutParserCallback mCallback; + protected final LayoutParserCallback mCallback; protected final PackageManager mPackageManager; protected final Resources mSourceRes; @@ -126,13 +126,20 @@ public class AutoInstallsLayout { private final long[] mTemp = new long[2]; private final ContentValues mValues; - private final String mRootTag; + 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 +150,7 @@ public class AutoInstallsLayout { mSourceRes = res; mLayoutId = layoutId; - mHotseatAllAppsRank = LauncherAppState.getInstance() - .getDynamicGrid().getDeviceProfile().hotseatAllAppsRank; + mHotseatAllAppsRank = hotseatAllAppsRank; } /** diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index f9255e6bd..8ef234bb0 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -28,6 +28,7 @@ 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; @@ -49,7 +50,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,9 +63,13 @@ 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; @@ -79,10 +84,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 +108,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 +128,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 +139,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,21 +156,15 @@ 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); } - @Override protected boolean setFrame(int left, int top, int right, int bottom) { if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) { @@ -186,10 +195,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 +222,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 +251,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 +347,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,11 +379,6 @@ public class BubbleTextView extends TextView { } else { super.setTextColor(res.getColor(android.R.color.transparent)); } - mIsTextVisible = visible; - } - - public boolean isTextVisible() { - return mIsTextVisible; } @Override @@ -385,15 +401,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 +431,23 @@ 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; + } } diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index e6865b2e6..c57090d7c 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,25 +34,34 @@ 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; import android.view.MotionEvent; import android.view.View; +import android.view.View.OnClickListener; 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 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 { @@ -169,6 +179,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; + private int mDownX = 0; + private int mDownY = 0; + public CellLayout(Context context) { this(context, null); } @@ -294,6 +312,282 @@ 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(); + LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo(); + + int y = id % mCountY; + int x = id / mCountY; + + 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) { + if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { + String confirmation = getConfirmationForIconDrop(viewId); + LauncherAppState.getInstance().getAccessibilityDelegate() + .handleAccessibleDrop(CellLayout.this, getItemBounds(viewId), confirmation); + return true; + } + return false; + } + + @Override + public void onClick(View arg0) { + int viewId = getViewIdAt(mDownX, mDownY); + + String confirmation = getConfirmationForIconDrop(viewId); + LauncherAppState.getInstance().getAccessibilityDelegate() + .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(); + LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo(); + + int y = id % mCountY; + int x = id / mCountY; + + 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(); + LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo(); + + int y = id % mCountY; + int x = id / mCountY; + + 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 +872,11 @@ public class CellLayout extends ViewGroup { mInterceptTouchListener = listener; } - int getCountX() { + public int getCountX() { return mCountX; } - int getCountY() { + public int getCountY() { return mCountY; } @@ -613,11 +907,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 +973,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 diff --git a/src/com/android/launcher3/CommonAppTypeParser.java b/src/com/android/launcher3/CommonAppTypeParser.java new file mode 100644 index 000000000..fe2fbd75f --- /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(mContext, null, 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..6c3008b6e 100644 --- a/src/com/android/launcher3/DefaultLayoutParser.java +++ b/src/com/android/launcher3/DefaultLayoutParser.java @@ -28,15 +28,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 +44,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 @@ -196,7 +201,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(); diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index ebe874f38..1ada1a912 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -144,13 +144,11 @@ public class DeleteDropTarget extends ButtonDropTarget { return true; } - if (!LauncherAppState.isDisableAllApps() && - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { + if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { return true; } - if (!LauncherAppState.isDisableAllApps() && - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && + if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && item instanceof AppInfo) { AppInfo appInfo = (AppInfo) info; return (appInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; @@ -158,12 +156,7 @@ public class DeleteDropTarget extends ButtonDropTarget { 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 true; } } return false; @@ -173,8 +166,7 @@ public class DeleteDropTarget extends ButtonDropTarget { @Override public void onDragStart(DragSource source, Object info, int dragAction) { boolean isVisible = true; - boolean useUninstallLabel = !LauncherAppState.isDisableAllApps() && - isAllAppsApplication(source, info); + boolean useUninstallLabel = isAllAppsApplication(source, info); boolean useDeleteLabel = !useUninstallLabel && source.supportsDeleteDropTarget(); // If we are dragging an application from AppsCustomize, only show the control if we can @@ -277,12 +269,6 @@ public class DeleteDropTarget extends ButtonDropTarget { } 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; } diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 34e1f3c5f..ddd300257 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -122,6 +122,7 @@ public class DeviceProfile { int hotseatAllAppsRank; int allAppsNumRows; int allAppsNumCols; + int appsViewNumCols; int searchBarSpaceWidthPx; int searchBarSpaceHeightPx; int pageIndicatorHeightPx; @@ -137,7 +138,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 +364,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 +373,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 +383,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 +401,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 +416,17 @@ 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 appsContainerViewPx = res.getDimensionPixelSize(R.dimen.apps_container_width); + int appsViewLeftMarginPx = + res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin); + int availableAppsWidthPx = (appsContainerViewPx > 0) ? appsContainerViewPx : + availableWidthPx; + appsViewNumCols = (availableAppsWidthPx - appsViewLeftMarginPx) / + (allAppsCellWidthPx + allAppsCellPaddingPx); } void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx, @@ -766,8 +774,7 @@ public class DeviceProfile { lp.gravity = Gravity.BOTTOM; lp.width = LayoutParams.MATCH_PARENT; lp.height = hotseatBarHeightPx; - hotseat.findViewById(R.id.layout).setPadding(2 * edgeMarginPx, 0, - 2 * edgeMarginPx, 0); + hotseat.setPadding(2 * edgeMarginPx, 0, 2 * edgeMarginPx, 0); } hotseat.setLayoutParams(lp); diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java index 480dce999..8dc6e185c 100644 --- a/src/com/android/launcher3/DragController.java +++ b/src/com/android/launcher3/DragController.java @@ -49,8 +49,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; @@ -73,6 +73,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; @@ -182,7 +185,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 +205,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 +232,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 +362,7 @@ public class DragController { private void endDrag() { if (mDragging) { mDragging = false; + mIsAccessibleDrag = false; clearScrollRunnable(); boolean isDeferred = false; if (mDragObject.dragView != null) { @@ -421,6 +435,10 @@ public class DragController { + mDragging); } + if (mIsAccessibleDrag) { + return false; + } + // Update the velocity tracker acquireVelocityTrackerAndAddMovement(ev); @@ -560,7 +578,7 @@ public class DragController { * Call this from a drag source view. */ public boolean onTouchEvent(MotionEvent ev) { - if (!mDragging) { + if (!mDragging || mIsAccessibleDrag) { return false; } @@ -617,6 +635,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/DragView.java b/src/com/android/launcher3/DragView.java index ea34e46f9..78d72b30f 100644 --- a/src/com/android/launcher3/DragView.java +++ b/src/com/android/launcher3/DragView.java @@ -70,8 +70,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 +85,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..94ae82b82 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 { 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/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java index e60704718..b090a7c3f 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. @@ -17,20 +17,20 @@ 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.FocusHelper.PagedViewKeyListener; +import com.android.launcher3.util.FocusLogic; /** * 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); } @@ -40,6 +40,7 @@ class IconKeyEventListener implements View.OnKeyListener { * A keyboard listener we set on all the workspace icons. */ class FolderKeyEventListener implements View.OnKeyListener { + @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return FocusHelper.handleFolderKeyEvent(v, keyCode, event); } @@ -49,30 +50,58 @@ class FolderKeyEventListener implements View.OnKeyListener { * 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 { + private static final String TAG = "FocusHelper"; + private static final boolean DEBUG = false; + + // + // Key code handling methods. + // + /** - * Returns the Viewgroup containing page contents for the page at the index specified. + * A keyboard listener for scrollable folders */ - 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(); + public static class PagedFolderKeyEventListener extends PagedViewKeyListener { + + private final Folder mFolder; + + public PagedFolderKeyEventListener(Folder folder) { + mFolder = folder; + } + + @Override + public void handleNoopKey(int keyCode, View v) { + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mFolder.mFolderName.requestFocus(); + playSoundEffect(keyCode, v); + } } - return page; } /** - * Handles key events in a PageViewCellLayout containing PagedViewIcons. + * Handles key events in the all apps screen. */ - static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) { + public static class PagedViewKeyListener implements View.OnKeyListener { + + @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 APPS keyevent=[%s].", + KeyEvent.keyCodeToString(keyCode))); + } + + // Initialize variables. ViewGroup parentLayout; ViewGroup itemContainer; int countX; @@ -83,591 +112,417 @@ public class FocusHelper { countX = ((CellLayout) parentLayout).getCountX(); countY = ((CellLayout) parentLayout).getCountY(); } else { - itemContainer = parentLayout = (ViewGroup) v.getParent(); - countX = ((PagedViewGridLayout) parentLayout).getCellCountX(); - countY = ((PagedViewGridLayout) parentLayout).getCellCountY(); + if (LauncherAppState.isDogfoodBuild()) { + throw new IllegalStateException("Parent of the focused item is not supported."); + } else { + return false; + } } - // 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 PagedView container = (PagedView) parentLayout.getParent(); + final int pageIndex = 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); - } - } - } - } + // TODO(hyunyoungs): this matrix is not applicable on the last page. + int[][] matrix = FocusLogic.createFullMatrix(countX, countY, true); + + // Process focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix, + iconIndex, pageIndex, pageCount); + if (newIconIndex == FocusLogic.NOOP) { + handleNoopKey(keyCode, v); + return consume; + } + switch (newIconIndex) { + case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: + newParent = getAppsCustomizePage(container, pageIndex -1); + if (newParent != null) { + int row = FocusLogic.findRow(matrix, iconIndex); + container.snapToPage(pageIndex - 1); + // no need to create a new matrix. + child = newParent.getChildAt(matrix[countX-1][row]); } - 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); - } - } - } - } + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + newParent = getAppsCustomizePage(container, pageIndex - 1); + if (newParent != null) { + container.snapToPage(pageIndex - 1); + child = newParent.getChildAt(0); } - 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); - } + case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: + newParent = getAppsCustomizePage(container, pageIndex - 1); + if (newParent != null) { + container.snapToPage(pageIndex - 1); + child = newParent.getChildAt(newParent.getChildCount() - 1); } - 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); - } - } + case FocusLogic.NEXT_PAGE_FIRST_ITEM: + newParent = getAppsCustomizePage(container, pageIndex + 1); + if (newParent != null) { + container.snapToPage(pageIndex + 1); + child = newParent.getChildAt(0); } - 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); - } + case FocusLogic.NEXT_PAGE_LEFT_COLUMN: + newParent = getAppsCustomizePage(container, pageIndex + 1); + if (newParent != null) { + container.snapToPage(pageIndex + 1); + int row = FocusLogic.findRow(matrix, iconIndex); + child = newParent.getChildAt(matrix[0][row]); } - 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); - } - } - wasHandled = true; + case FocusLogic.CURRENT_PAGE_FIRST_ITEM: + child = container.getChildAt(0); 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; + case FocusLogic.CURRENT_PAGE_LAST_ITEM: + child = itemContainer.getChildAt(itemContainer.getChildCount() - 1); 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; + default: // Go to some item on the current page. + child = itemContainer.getChildAt(newIconIndex); break; - default: break; } - return wasHandled; + if (child != null) { + child.requestFocus(); + playSoundEffect(keyCode, v); + } else { + handleNoopKey(keyCode, v); + } + return consume; + } + + public void handleNoopKey(int keyCode, View v) { } } /** - * Handles key events in the workspace hotseat (bottom of the screen). + * 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. */ - static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) { - ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); - final CellLayout layout = (CellLayout) parent.getParent(); + static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP || !consume) { + return consume; + } + int orientation = v.getResources().getConfiguration().orientation; - // 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; + if (DEBUG) { + Log.v(TAG, String.format( + "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, orientation=%d", + KeyEvent.keyCodeToString(keyCode), orientation)); } - return wasHandled; - } - /** - * Private helper method to get the CellLayoutChildren given a CellLayout index. - */ - private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( - ViewGroup container, int i) { - CellLayout parent = (CellLayout) container.getChildAt(i); - return parent.getShortcutsAndWidgets(); - } + // Initialize the variables. + final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent(); + Hotseat hotseat = (Hotseat) hotseatLayout.getParent(); - /** - * 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)); + Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); + int pageIndex = workspace.getCurrentPage(); + int pageCount = workspace.getChildCount(); + int countX = -1; + int countY = -1; + int iconIndex = findIndexOfView(hotseatParent, v); + + final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex); + final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + + ViewGroup parent = null; + int[][] matrix = null; + + if (keyCode == KeyEvent.KEYCODE_DPAD_UP && + orientation == Configuration.ORIENTATION_PORTRAIT) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation, + hotseat.getAllAppsButtonRank(), true /* include all apps icon */); + iconIndex += iconParent.getChildCount(); + countX = iconLayout.getCountX(); + countY = iconLayout.getCountY() + hotseatLayout.getCountY(); + parent = iconParent; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && + orientation == Configuration.ORIENTATION_LANDSCAPE) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation, + hotseat.getAllAppsButtonRank(), true /* include all apps icon */); + iconIndex += iconParent.getChildCount(); + countX = iconLayout.getCountX() + hotseatLayout.getCountX(); + countY = iconLayout.getCountY(); + parent = iconParent; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && + orientation == Configuration.ORIENTATION_LANDSCAPE) { + 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; } - 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; + + // 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); } - return null; - } - private static View getIconInDirection(CellLayout layout, ViewGroup parent, int i, - int delta) { - final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - return findIndexOfIcon(views, i, delta); - } - private static View getIconInDirection(CellLayout layout, ViewGroup parent, View v, - int delta) { - final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - return findIndexOfIcon(views, views.indexOf(v), delta); - } - /** - * Private helper method to find the next closest BubbleTextView or FolderIcon in the direction - * delta on the next line. - * - * @param delta either -1 or 1 depending on the line and direction we want to search - */ - private static View getClosestIconOnLine(CellLayout layout, ViewGroup parent, View v, - int lineDelta) { - final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); - final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); - final int cellCountY = layout.getCountY(); - final int row = lp.cellY; - final int newRow = row + lineDelta; - if (0 <= newRow && newRow < cellCountY) { - float closestDistance = Float.MAX_VALUE; - int closestIndex = -1; - int index = views.indexOf(v); - int endIndex = (lineDelta < 0) ? -1 : views.size(); - while (index != endIndex) { - View newV = views.get(index); - CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams(); - boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row); - if (satisfiesRow && - (newV instanceof BubbleTextView || newV instanceof FolderIcon)) { - float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) + - Math.pow(tmpLp.cellY - lp.cellY, 2)); - if (tmpDistance < closestDistance) { - closestIndex = index; - closestDistance = tmpDistance; - } - } - if (index <= endIndex) { - ++index; - } else { - --index; - } + if (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; + } + int orientation = v.getResources().getConfiguration().orientation; + if (DEBUG) { + Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] orientation=%d", + KeyEvent.keyCodeToString(keyCode), orientation)); + } + + // 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); + final Hotseat hotseat = (Hotseat) launcher.findViewById(R.id.hotseat); + int pageIndex = workspace.indexOfChild(iconLayout); int pageCount = workspace.getChildCount(); + int countX = iconLayout.getCountX(); + int countY = iconLayout.getCountY(); + final int iconIndex = findIndexOfView(parent, v); - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous 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); - } - } + 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 && + orientation == Configuration.ORIENTATION_PORTRAIT) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation, + hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */); + countY = countY + 1; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && + orientation == Configuration.ORIENTATION_LANDSCAPE) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation, + 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: + int row = FocusLogic.findRow(matrix, iconIndex); + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + if (parent != null) { + iconLayout = (CellLayout) parent.getParent(); + matrix = FocusLogic.createSparseMatrix(iconLayout, + iconLayout.getCountX(), row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix, + FocusLogic.PIVOT, pageIndex - 1, pageCount); + newIcon = parent.getChildAt(newIconIndex); } - 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); - } + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(0); + workspace.snapToPage(pageIndex - 1); 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_LAST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(parent.getChildCount() - 1); + 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.NEXT_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = parent.getChildAt(0); + 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); - } - } + case FocusLogic.NEXT_PAGE_LEFT_COLUMN: + row = FocusLogic.findRow(matrix, iconIndex); + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + if (parent != null) { + iconLayout = (CellLayout) parent.getParent(); + matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix, + FocusLogic.PIVOT, pageIndex, pageCount); + newIcon = parent.getChildAt(newIconIndex); } - wasHandled = true; break; - case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; + case FocusLogic.CURRENT_PAGE_FIRST_ITEM: + newIcon = parent.getChildAt(0); 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_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; } /** * Handles key events for items in a Folder. */ static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP || !consume) { + return consume; + } + if (DEBUG) { + Log.v(TAG, String.format("Handle FOLDER keyevent=[%s].", + KeyEvent.keyCodeToString(keyCode))); + } + + // Initialize the variables. ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); final CellLayout layout = (CellLayout) parent.getParent(); - final ScrollView scrollView = (ScrollView) layout.getParent(); - final Folder folder = (Folder) scrollView.getParent(); + final Folder folder = (Folder) layout.getParent().getParent(); View title = folder.mFolderName; + Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); + final int countX = layout.getCountX(); + final int countY = layout.getCountY(); + final int iconIndex = findIndexOfView(parent, v); + int pageIndex = workspace.indexOfChild(layout); + int pageCount = workspace.getChildCount(); + int[][] map = FocusLogic.createFullMatrix(countX, countY, true /* incremental order */); - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon - View newIcon = getIconInDirection(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } + // Process the focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, map, iconIndex, + pageIndex, pageCount); + View newIcon = null; + switch (newIconIndex) { + case FocusLogic.NOOP: + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + newIcon = title; } - wasHandled = true; break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon - View newIcon = getIconInDirection(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - title.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: + case FocusLogic.NEXT_PAGE_FIRST_ITEM: + case FocusLogic.CURRENT_PAGE_FIRST_ITEM: + case FocusLogic.CURRENT_PAGE_LAST_ITEM: + if (DEBUG) { + Log.v(TAG, "Page advance handling not supported on folder icons."); } - 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; + default: // current page some item. + newIcon = parent.getChildAt(newIconIndex); + break; + } + if (newIcon != null) { + newIcon.requestFocus(); + playSoundEffect(keyCode, v); + } + return consume; + } + + // + // Helper methods. + // + + /** + * 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 helper method to get the CellLayoutChildren given a CellLayout index. + */ + private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( + ViewGroup container, int i) { + CellLayout parent = (CellLayout) container.getChildAt(i); + return parent.getShortcutsAndWidgets(); + } + + private static int findIndexOfView(ViewGroup parent, View v) { + for (int i = 0; i < parent.getChildCount(); i++) { + if (v != null && v.equals(parent.getChildAt(i))) { + return i; + } + } + return -1; + } + + /** + * Helper method to be used for playing sound effects. + */ + private static void playSoundEffect(int keyCode, View v) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + 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..af3b97634 100644 --- a/src/com/android/launcher3/FocusIndicatorView.java +++ b/src/com/android/launcher3/FocusIndicatorView.java @@ -23,7 +23,6 @@ import android.graphics.Canvas; import android.util.AttributeSet; import android.util.Pair; import android.view.View; -import android.view.ViewParent; public class FocusIndicatorView extends View implements View.OnFocusChangeListener { @@ -32,9 +31,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 +76,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 +90,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,27 +147,32 @@ 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 {@link #mCommonParent}, 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(); 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; + child = ((PagedView) parent).getPageAt(0); + } + + shift[0] += child.getLeft(); + shift[1] += child.getTop(); + + if (parent != commonParent) { + computeLocationRelativeToParentHelper(parent, commonParent, shift); } } 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..7ff60de4f 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,20 +40,20 @@ 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.FolderInfo.FolderListener; +import com.android.launcher3.Workspace.ItemOperator; 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. @@ -62,101 +63,112 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList View.OnFocusChangeListener { 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; + private static final boolean ALLOW_FOLDER_SCROLL = true; 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; + /** + * Fraction of the width to scroll when showing the next page hint. + */ + private static final float SCROLL_HINT_FRACTION = 0.07f; + + /** + * Time for which the scroll hint is shown before automatically changing page. + */ + public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY; + + /** + * 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 boolean mRearrangeOnClose = false; + 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 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; + private FolderIcon mFolderIcon; - private int mMaxCountX; - private int mMaxCountY; - private int mMaxNumItems; - private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); + + private FolderContent mContent; + private View mContentWrapper; + FolderEditText mFolderName; + + private View mFooter; + private int mFooterHeight; + + // Cell ranks used for drag and drop + private int mTargetRank, mPrevTargetRank, mEmptyCellRank; + + private int mState = STATE_NONE; + private boolean mRearrangeOnClose = false; 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; - 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; private boolean mDeferDropAfterUninstall; private boolean mUninstallSuccessful; + // Folder scrolling + private int mScrollAreaOffset; + private Alarm mOnScrollHintAlarm; + private Alarm mScrollPauseAlarm; + + // TODO: Use {@link #mContent} once {@link #ALLOW_FOLDER_SCROLL} is removed. + private FolderPagedView mPagedView; + + private int mScrollHintDir = DragController.SCROLL_NONE; + private 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,43 @@ 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); + + if (ALLOW_FOLDER_SCROLL) { + mOnScrollHintAlarm = new Alarm(); + mScrollPauseAlarm = new Alarm(); + } } @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 = (FolderContent) 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 = ALLOW_FOLDER_SCROLL ? findViewById(R.id.folder_footer) : mFolderName; + // 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(); + + if (ALLOW_FOLDER_SCROLL) { + mPagedView = (FolderPagedView) mContent; + } } private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @@ -246,14 +256,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 +316,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 +328,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 +348,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 +360,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); @@ -404,7 +396,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList * @return A new UserFolder. */ static Folder fromXml(Context context) { - return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null); + return (Folder) LayoutInflater.from(context).inflate( + ALLOW_FOLDER_SCROLL ? R.layout.user_folder_scroll : R.layout.user_folder, null); } /** @@ -429,6 +422,11 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void animateOpen() { if (!(getParent() instanceof DragLayer)) return; + if (ALLOW_FOLDER_SCROLL) { + // Always open on the first page. + mPagedView.snapToPageImmediately(0); + } + Animator openFolderAnim = null; final Runnable onCompleteRunnable; if (!Utilities.isLmpOrAbove()) { @@ -473,14 +471,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 +495,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 +507,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 +518,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList onCompleteRunnable.run(); } - setFocusOnFirstChild(); + mContent.setFocusOnFirstChild(); } }); openFolderAnim.start(); @@ -533,14 +530,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } public void beginExternalDrag(ShortcutInfo item) { - setupContentForNumItems(getItemCount() + 1); - findAndSetEmptyCells(item); - mCurrentDragInfo = item; - mEmptyCell[0] = item.cellX; - mEmptyCell[1] = item.cellY; + mEmptyCellRank = mContent.allocateNewLastItemRank(); mIsExternalDrag = true; - mDragInProgress = true; } @@ -555,13 +547,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 +582,102 @@ 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(); + if (ALLOW_FOLDER_SCROLL) { + // 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); + private void onDragOver(DragObject d, int reorderDelay) { + if (ALLOW_FOLDER_SCROLL && 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(); - } 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]; - } + mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); + mReorderAlarm.setAlarm(REORDER_DELAY); + mPrevTargetRank = mTargetRank; } - } - // 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; + if (!ALLOW_FOLDER_SCROLL) { + return; } - // 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; + float x = r[0]; + int currentPage = mPagedView.getNextPage(); + int cellWidth = mPagedView.getCurrentCellLayout().getCellWidth(); + if (currentPage > 0 && x < cellWidth * ICON_OVERSCROLL_WIDTH_FACTOR) { + // Show scroll hint on the left + if (mScrollHintDir != DragController.SCROLL_LEFT) { + mPagedView.showScrollHint(-SCROLL_HINT_FRACTION); + mScrollHintDir = DragController.SCROLL_LEFT; + } + + // Set alarm for when the hint is complete + if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != DragController.SCROLL_LEFT) { + mCurrentScrollDir = DragController.SCROLL_LEFT; + mOnScrollHintAlarm.cancelAlarm(); + mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d)); + mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION); + + mReorderAlarm.cancelAlarm(); + mTargetRank = mEmptyCellRank; + } + } else if (currentPage < (mPagedView.getPageCount() - 1) && + (x > (getWidth() - cellWidth * ICON_OVERSCROLL_WIDTH_FACTOR))) { + // Show scroll hint on the right + if (mScrollHintDir != DragController.SCROLL_RIGHT) { + mPagedView.showScrollHint(SCROLL_HINT_FRACTION); + mScrollHintDir = DragController.SCROLL_RIGHT; + } - // 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 != DragController.SCROLL_RIGHT) { + mCurrentScrollDir = DragController.SCROLL_RIGHT; + mOnScrollHintAlarm.cancelAlarm(); + mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d)); + mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION); - return res; + mReorderAlarm.cancelAlarm(); + mTargetRank = mEmptyCellRank; + } + } else { + mOnScrollHintAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mPagedView.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; + } + } } OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { @@ -783,8 +696,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } 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 +703,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); } mReorderAlarm.cancelAlarm(); + + if (ALLOW_FOLDER_SCROLL) { + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mPagedView.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; + } + } } public void onDropCompleted(final View target, final DragObject d, @@ -816,7 +736,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); } @@ -917,37 +837,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 +848,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 +907,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 +914,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,64 +925,55 @@ 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); - mScrollView.measure(contentAreaWidthSpec, contentAreaHeightSpec); - mFolderName.measure(contentAreaWidthSpec, - MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY)); - setMeasuredDimension(width, height); + mContent.setFixedSize(contentWidth, contentHeight); + mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec); + mFooter.measure(contentAreaWidthSpec, + MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY)); + + 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() { @@ -1116,7 +986,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mFolderIcon.requestFocus(); if (mRearrangeOnClose) { - setupContentForNumItems(getItemCount()); + rearrangeChildren(); mRearrangeOnClose = false; } if (getItemCount() <= 1) { @@ -1166,7 +1036,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 +1051,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 +1077,26 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList }; } + if (ALLOW_FOLDER_SCROLL) { + // 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 (!mPagedView.rankOnCurrentPage(mEmptyCellRank)) { + // Reorder again. + mTargetRank = getTargetRank(d, null); + + // Rearrange items immediately. + mReorderAlarmListener.onAlarm(mReorderAlarm); + + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + } + mPagedView.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 +1107,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 +1128,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; @@ -1277,12 +1155,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // 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.allocateNewLastItemRank()); LauncherModel.addOrMoveItemInDatabase( mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); } @@ -1293,27 +1166,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 +1197,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 +1223,121 @@ 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) { + mPagedView.scrollLeft(); + mScrollHintDir = DragController.SCROLL_NONE; + } else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) { + mPagedView.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); + } + } + + public static interface FolderContent { + void setFolder(Folder f); + + void removeItem(View v); + + boolean isFull(); + int getItemCount(); + + int getDesiredWidth(); + int getDesiredHeight(); + void setFixedSize(int width, int height); + + /** + * Iterates over all its items in a reading order. + * @return the view for which the operator returned true. + */ + View iterateOverItems(ItemOperator op); + View getLastItem(); + + String getAccessibilityDescription(); + + /** + * Binds items to the layout. + * @return list of items that could not be bound, probably because we hit the max size limit. + */ + ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> children); + + /** + * Create space for a new item at the end, and returns the rank for that item. + * Resizes the content if necessary. + */ + int allocateNewLastItemRank(); + + View createAndAddViewForRank(ShortcutInfo item, int rank); + + /** + * 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. + */ + void addViewForRank(View view, ShortcutInfo item, int rank); + + /** + * Reorders the items such that the {@param empty} spot moves to {@param target} + */ + void realTimeReorder(int empty, int target); + + /** + * @return the rank of the cell nearest to the provided pixel position. + */ + int findNearestArea(int pixelX, int pixelY); + + /** + * Updates position and rank of all the children in the view based. + * @param list the ordered list of children. + * @param itemCount if greater than the total children count, empty spaces are left + * at the end. + */ + void arrangeChildren(ArrayList<View> list, int itemCount); + + /** + * Sets the focus on the first visible child. + */ + void setFocusOnFirstChild(); } } 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/FolderCellLayout.java b/src/com/android/launcher3/FolderCellLayout.java new file mode 100644 index 000000000..1566912b4 --- /dev/null +++ b/src/com/android/launcher3/FolderCellLayout.java @@ -0,0 +1,330 @@ +/** + * 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.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.launcher3.Workspace.ItemOperator; + +import java.util.ArrayList; + +public class FolderCellLayout extends CellLayout implements Folder.FolderContent { + + 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[] sTempPosArray = new int[2]; + + private final FolderKeyEventListener mKeyListener = new FolderKeyEventListener(); + private final LayoutInflater mInflater; + private final IconCache mIconCache; + + private final int mMaxCountX; + private final int mMaxCountY; + private final int mMaxNumItems; + + // Indicates the last number of items used to set up the grid size + private int mAllocatedContentSize; + + private Folder mFolder; + private FocusIndicatorView mFocusIndicatorView; + + public FolderCellLayout(Context context) { + this(context, null); + } + + public FolderCellLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FolderCellLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + LauncherAppState app = LauncherAppState.getInstance(); + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + mMaxCountX = (int) grid.numColumns; + mMaxCountY = (int) grid.numRows; + mMaxNumItems = mMaxCountX * mMaxCountY; + + mInflater = LayoutInflater.from(context); + mIconCache = app.getIconCache(); + + setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); + getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); + setInvertIfRtl(true); + } + + @Override + public void setFolder(Folder folder) { + mFolder = folder; + mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator); + } + + /** + * 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; + int countX = getCountX(); + int countY = 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; + } + setGridSize(countX, countY); + } + + @Override + public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) { + ArrayList<ShortcutInfo> extra = new ArrayList<ShortcutInfo>(); + setupContentDimensions(Math.min(items.size(), mMaxNumItems)); + + int countX = getCountX(); + int rank = 0; + for (ShortcutInfo item : items) { + if (rank >= mMaxNumItems) { + extra.add(item); + continue; + } + + item.rank = rank; + item.cellX = rank % countX; + item.cellY = rank / countX; + addNewView(item); + rank++; + } + return extra; + } + + @Override + public int allocateNewLastItemRank() { + int rank = getItemCount(); + mFolder.rearrangeChildren(rank + 1); + return rank; + } + + @Override + public View createAndAddViewForRank(ShortcutInfo item, int rank) { + updateItemXY(item, rank); + return addNewView(item); + } + + @Override + public void addViewForRank(View view, ShortcutInfo item, int rank) { + updateItemXY(item, rank); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); + lp.cellX = item.cellX; + lp.cellY = item.cellY; + addViewToCellLayout(view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true); + } + + @Override + public void removeItem(View v) { + removeView(v); + } + + /** + * Updates the item cellX and cellY position + */ + private void updateItemXY(ShortcutInfo item, int rank) { + item.rank = rank; + int countX = getCountX(); + item.cellX = rank % countX; + item.cellY = rank / countX; + } + + private View addNewView(ShortcutInfo item) { + final BubbleTextView textView = (BubbleTextView) mInflater.inflate( + R.layout.folder_application, getShortcutsAndWidgets(), false); + textView.applyFromShortcutInfo(item, mIconCache, false); + textView.setOnClickListener(mFolder); + textView.setOnLongClickListener(mFolder); + textView.setOnFocusChangeListener(mFocusIndicatorView); + textView.setOnKeyListener(mKeyListener); + + CellLayout.LayoutParams lp = new CellLayout.LayoutParams( + item.cellX, item.cellY, item.spanX, item.spanY); + addViewToCellLayout(textView, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true); + return textView; + } + + /** + * Refer {@link #findNearestArea(int, int, int, int, View, boolean, int[])} + */ + @Override + public int findNearestArea(int pixelX, int pixelY) { + findNearestArea(pixelX, pixelY, 1, 1, null, false, sTempPosArray); + if (mFolder.isLayoutRtl()) { + sTempPosArray[0] = getCountX() - sTempPosArray[0] - 1; + } + + // Convert this position to rank. + return Math.min(mAllocatedContentSize - 1, + sTempPosArray[1] * getCountX() + sTempPosArray[0]); + } + + @Override + public boolean isFull() { + return getItemCount() >= mMaxNumItems; + } + + @Override + public int getItemCount() { + return getShortcutsAndWidgets().getChildCount(); + } + + @Override + public void arrangeChildren(ArrayList<View> list, int itemCount) { + setupContentDimensions(itemCount); + removeAllViews(); + + int newX, newY; + int rank = 0; + int countX = getCountX(); + for (View v : list) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); + newX = rank % countX; + newY = rank / countX; + ItemInfo info = (ItemInfo) v.getTag(); + if (info.cellX != newX || info.cellY != newY || info.rank != rank) { + info.cellX = newX; + info.cellY = newY; + info.rank = rank; + LauncherModel.addOrMoveItemInDatabase(getContext(), info, + mFolder.mInfo.id, 0, info.cellX, info.cellY); + } + lp.cellX = info.cellX; + lp.cellY = info.cellY; + rank ++; + addViewToCellLayout(v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true); + } + } + + @Override + public View iterateOverItems(ItemOperator op) { + for (int j = 0; j < getCountY(); j++) { + for (int i = 0; i < getCountX(); i++) { + View v = getChildAt(i, j); + if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v, this)) { + return v; + } + } + } + return null; + } + + @Override + public String getAccessibilityDescription() { + return String.format(getContext().getString(R.string.folder_opened), + getCountX(), getCountY()); + } + + @Override + public void setFocusOnFirstChild() { + View firstChild = getChildAt(0, 0); + if (firstChild != null) { + firstChild.requestFocus(); + } + } + + @Override + public View getLastItem() { + int lastRank = getShortcutsAndWidgets().getChildCount() - 1; + // count can be zero if the folder is not yet laid out. + int count = getCountX(); + if (count > 0) { + return getShortcutsAndWidgets().getChildAt(lastRank % count, lastRank / count); + } else { + return getShortcutsAndWidgets().getChildAt(lastRank); + } + } + + @Override + public void realTimeReorder(int empty, int target) { + boolean wrap; + int startX; + int endX; + int startY; + int delay = 0; + float delayAmount = START_VIEW_REORDER_DELAY; + + int countX = getCountX(); + int emptyX = empty % getCountX(); + int emptyY = empty / countX; + + int targetX = target % countX; + int targetY = target / countX; + + if (target > empty) { + wrap = emptyX == countX - 1; + startY = wrap ? emptyY + 1 : emptyY; + for (int y = startY; y <= targetY; y++) { + startX = y == emptyY ? emptyX + 1 : 0; + endX = y < targetY ? countX - 1 : targetX; + for (int x = startX; x <= endX; x++) { + View v = getChildAt(x,y); + if (animateChildToPosition(v, emptyX, emptyY, + REORDER_ANIMATION_DURATION, delay, true, true)) { + emptyX = x; + emptyY = y; + delay += delayAmount; + delayAmount *= VIEW_REORDER_DELAY_FACTOR; + } + } + } + } else { + wrap = emptyX == 0; + startY = wrap ? emptyY - 1 : emptyY; + for (int y = startY; y >= targetY; y--) { + startX = y == emptyY ? emptyX - 1 : countX - 1; + endX = y > targetY ? 0 : targetX; + for (int x = startX; x >= endX; x--) { + View v = getChildAt(x,y); + if (animateChildToPosition(v, emptyX, emptyY, + REORDER_ANIMATION_DURATION, delay, true, true)) { + emptyX = x; + emptyY = y; + delay += delayAmount; + delayAmount *= VIEW_REORDER_DELAY_FACTOR; + } + } + } + } + } +} diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java new file mode 100644 index 000000000..b4a7a7546 --- /dev/null +++ b/src/com/android/launcher3/FolderPagedView.java @@ -0,0 +1,572 @@ +/** + * 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.LayoutInflater; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener; +import com.android.launcher3.PageIndicator.PageMarkerResources; +import com.android.launcher3.Workspace.ItemOperator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class FolderPagedView extends PagedView implements Folder.FolderContent { + + private static final String TAG = "FolderPagedView"; + + 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[] sTempPosArray = new int[2]; + + // TODO: Remove this restriction + private static final int MAX_ITEMS_PER_PAGE = 3; + + private final LayoutInflater mInflater; + private final IconCache mIconCache; + private final HashMap<View, Runnable> mPageChangingViews = 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; + + public FolderPagedView(Context context, AttributeSet attrs) { + super(context, attrs); + LauncherAppState app = LauncherAppState.getInstance(); + setDataIsReady(); + + DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + mMaxCountX = Math.min((int) grid.numColumns, MAX_ITEMS_PER_PAGE); + mMaxCountY = Math.min((int) grid.numRows, MAX_ITEMS_PER_PAGE); + mMaxItemsPerPage = mMaxCountX * mMaxCountY; + + mInflater = LayoutInflater.from(context); + mIconCache = app.getIconCache(); + } + + @Override + public void setFolder(Folder folder) { + mFolder = folder; + mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator); + mKeyListener = new PagedFolderKeyEventListener(folder); + } + + /** + * 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); + } + } + + @Override + public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) { + ArrayList<View> icons = new ArrayList<View>(); + for (ShortcutInfo item : items) { + icons.add(createNewView(item)); + } + arrangeChildren(icons, icons.size(), false); + return new ArrayList<ShortcutInfo>(); + } + + /** + * 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. + */ + @Override + public int allocateNewLastItemRank() { + int rank = getItemCount(); + int total = rank + 1; + // Rearrange the items as the grid size might change. + mFolder.rearrangeChildren(total); + + setCurrentPage(getChildCount() - 1); + return rank; + } + + @Override + public View createAndAddViewForRank(ShortcutInfo item, int rank) { + View icon = createNewView(item); + addViewForRank(createNewView(item), item, rank); + return icon; + } + + @Override + 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; + } + + @Override + public void setFixedSize(int width, int height) { + for (int i = getChildCount() - 1; i >= 0; i --) { + ((CellLayout) getChildAt(i)).setFixedSize(width, height); + } + } + + @Override + 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. + * + */ + @Override + 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; + + 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) { + 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); + } + + rank ++; + position++; + } + + // Remove extra views. + boolean removed = false; + while (pageItr.hasNext()) { + removeView(pageItr.next()); + removed = true; + } + if (removed) { + setCurrentPage(0); + } + } + + @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; + } + + @Override + public int getItemCount() { + int lastPage = getChildCount() - 1; + if (lastPage < 0) { + // If there are no pages, there must be only one icon in the folder. + return 1; + } + return getPageAt(lastPage).getShortcutsAndWidgets().getChildCount() + + lastPage * mMaxItemsPerPage; + } + + @Override + 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_dark, R.drawable.ic_pageindicator_default_dark); + } + + @Override + public boolean isFull() { + return false; + } + + @Override + 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); + } + } + + @Override + 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; + } + + @Override + public String getAccessibilityDescription() { + return String.format(getContext().getString(R.string.folder_opened), + mGridCountX, mGridCountY); + } + + @Override + 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(); + } + } + + /** + * Scrolls the current view by a fraction + */ + public void showScrollHint(float 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 (!mPageChangingViews.isEmpty()) { + HashMap<View, Runnable> pendingViews = new HashMap<>(mPageChangingViews); + 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 + 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() { + mPageChangingViews.remove(v); + v.setTranslationX(oldTranslateX); + ((CellLayout) v.getParent().getParent()).removeView(v); + addViewForRank(v, (ShortcutInfo) v.getTag(), newRank); + } + }; + v.animate() + .translationXBy(direction > 0 ? -v.getWidth() : v.getWidth()) + .setDuration(REORDER_ANIMATION_DURATION) + .setStartDelay(0) + .withEndAction(endAction); + mPageChangingViews.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; + } + } + } +} 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..43f838e7f 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -18,15 +18,19 @@ 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.text.TextUtils; @@ -36,16 +40,12 @@ 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 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.Map.Entry; /** @@ -56,7 +56,6 @@ 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 = "."; @@ -69,36 +68,16 @@ public class IconCache { public CharSequence contentDescription; } - private static class CacheKey { - public ComponentName componentName; - public UserHandleCompat user; - - CacheKey(ComponentName componentName, UserHandleCompat user) { - this.componentName = componentName; - this.user = user; - } - - @Override - public int hashCode() { - return componentName.hashCode() + user.hashCode(); - } - - @Override - public boolean equals(Object o) { - CacheKey other = (CacheKey) o; - return other.componentName.equals(componentName) && other.user.equals(user); - } - } - private final 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; public IconCache(Context context) { ActivityManager activityManager = @@ -109,13 +88,10 @@ public class IconCache { mUserManager = UserManagerCompat.getInstance(mContext); mLauncherApps = LauncherAppsCompat.getInstance(mContext); mIconDpi = activityManager.getLauncherLargeIconDensity(); - - // need to set mIconDpi before getting default icon - UserHandleCompat myUser = UserHandleCompat.myUserHandle(); - mDefaultIcons.put(myUser, makeDefaultIcon(myUser)); + mIconDb = new IconDB(context); } - public Drawable getFullResDefaultActivityIcon() { + private Drawable getFullResDefaultActivityIcon() { return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon); } @@ -184,26 +160,160 @@ 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)) { + addIconToDB(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) { + 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_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); + + 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) { + continue; + } + if (app == null) { + itemsToRemove.add(c.getInt(rowIndex)); + continue; + } + ContentValues values = updateCacheAndGetContentValues(app); + mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values, + IconDB.COLUMN_COMPONENT + " = ?", + new String[] { cn }); + + 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; + } + addIconToDB(app, info, userSerial); + } + return updatedPackages; + } + + private void addIconToDB(LauncherActivityInfoCompat app, PackageInfo info, long userSerial) { + ContentValues values = updateCacheAndGetContentValues(app); + values.put(IconDB.COLUMN_COMPONENT, app.getComponentName().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); + + ContentValues values = new ContentValues(); + values.put(IconDB.COLUMN_ICON, ItemInfo.flattenBitmap(entry.icon)); + values.put(IconDB.COLUMN_LABEL, entry.title.toString()); + return values; + } + + + /** * Empty out the cache. */ public synchronized void flush() { @@ -214,7 +324,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 @@ -227,10 +337,8 @@ public class IconCache { /** * Fill in "application" with the icon and label for "info." */ - public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info, - HashMap<Object, CharSequence> labelCache) { - CacheEntry entry = cacheLocked(application.componentName, info, labelCache, - info.getUser(), false); + public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info) { + CacheEntry entry = cacheLocked(application.componentName, info, info.getUser(), false); application.title = entry.title; application.iconBitmap = entry.icon; @@ -246,15 +354,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); 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) { 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. @@ -263,16 +372,22 @@ public class IconCache { shortcutInfo.title = ""; shortcutInfo.usingFallbackIcon = true; } else { - LauncherActivityInfoCompat launcherActInfo = - mLauncherApps.resolveActivity(intent, user); - CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon); - - shortcutInfo.setIcon(entry.icon); - shortcutInfo.title = entry.title; - shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user); + getTitleAndIcon(shortcutInfo, component, info, user, true); } } + /** + * 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) { + CacheEntry entry = cacheLocked(component, info, user, usePkgIcon); + shortcutInfo.setIcon(entry.icon); + shortcutInfo.title = entry.title; + shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + } public synchronized Bitmap getDefaultIcon(UserHandleCompat user) { if (!mDefaultIcons.containsKey(user)) { @@ -281,16 +396,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,35 +405,17 @@ 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) { + ComponentKey cacheKey = new ComponentKey(componentName, user); CacheEntry entry = mCache.get(cacheKey); if (entry == null) { 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)) { + if (info != null) { + entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext); } else { if (usePackageIcon) { CacheEntry packageEntry = getEntryForPackage( @@ -338,6 +425,7 @@ public class IconCache { componentName.toShortString()); entry.icon = packageEntry.icon; entry.title = packageEntry.title; + entry.contentDescription = packageEntry.contentDescription; } } if (entry.icon == null) { @@ -347,6 +435,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,7 +450,7 @@ 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); if (!TextUtils.isEmpty(title)) { @@ -374,53 +467,41 @@ public class IconCache { */ private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) { ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);; - CacheKey cacheKey = new CacheKey(cn, user); + ComponentKey cacheKey = new ComponentKey(cn, user); CacheEntry entry = mCache.get(cacheKey); if (entry == null) { entry = new CacheEntry(); entry.title = ""; + entry.contentDescription = ""; mCache.put(cacheKey, entry); try { ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0); - entry.title = info.loadLabel(mPackageManager); entry.icon = Utilities.createIconBitmap(info.loadIcon(mPackageManager), mContext); + entry.title = info.loadLabel(mPackageManager); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); } catch (NameNotFoundException e) { if (DEBUG) Log.d(TAG, "Application not installed " + packageName); } - - if (entry.icon == null) { - entry.icon = getPreloadedIcon(cn, user); - } } 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 +509,86 @@ public class IconCache { // pass } - final String key = componentName.flattenToString(); - FileOutputStream resourceFile = null; + ContentValues values = new ContentValues(); + values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString()); + values.put(IconDB.COLUMN_USER, userSerial); + values.put(IconDB.COLUMN_ICON, ItemInfo.flattenBitmap(icon)); + values.put(IconDB.COLUMN_LABEL, label); + mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + + private boolean getEntryFromDB(ComponentName component, UserHandleCompat user, CacheEntry entry) { + Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, + new String[] {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 = Utilities.createIconBitmap(c, 0, mContext); + 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(); + private static final class IconDB extends SQLiteOpenHelper { + private final static int DB_VERSION = 1; + + 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_LABEL = "label"; - // We don't keep icons for other profiles in persistent cache. - if (!user.equals(UserHandleCompat.myUserHandle())) { - return null; + public IconDB(Context context) { + super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION); } - 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); - } - } 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 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_LABEL + " TEXT, " + + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + + ");"); } - return icon; - } + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(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; + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } } - 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 void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } } } diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index 1ab308558..201531ea8 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -186,11 +186,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)) { diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index a7e32d31a..bf03f745b 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; @@ -48,7 +46,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; @@ -82,19 +79,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; @@ -115,7 +110,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; @@ -132,7 +126,8 @@ 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; @@ -146,7 +141,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; @@ -166,7 +160,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 = @@ -215,9 +208,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 }; + enum State { NONE, WORKSPACE, APPS, APPS_SPRING_LOADED, WIDGETS, WIDGETS_SPRING_LOADED }; private State mState = State.WORKSPACE; private AnimatorSet mStateAnimation; + private LauncherStateTransitionAnimation mStateTransitionAnimation; private boolean mIsSafeModeEnabled; @@ -230,8 +224,6 @@ 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); @@ -239,7 +231,6 @@ public class Launcher extends Activity private static int NEW_APPS_PAGE_MOVE_DELAY = 500; private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5; private static int NEW_APPS_ANIMATION_DELAY = 500; - private static final int SINGLE_FRAME_DELAY = 16; private final BroadcastReceiver mCloseSystemDialogsReceiver = new CloseSystemDialogsIntentReceiver(); @@ -271,6 +262,7 @@ public class Launcher extends Activity private View mAllAppsButton; private SearchDropTargetBar mSearchDropTargetBar; + private AppsContainerView mAppsView; private AppsCustomizeTabHost mAppsCustomizeTabHost; private AppsCustomizePagedView mAppsCustomizeContent; private boolean mAutoAdvanceRunning = false; @@ -309,9 +301,6 @@ public class Launcher extends Activity 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; @@ -341,8 +330,6 @@ 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; @@ -362,6 +349,18 @@ public class Launcher extends Activity } } + // 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 + } + } + private Runnable mBuildLayersRunnable = new Runnable() { public void run() { if (mWorkspace != null) { @@ -425,6 +424,7 @@ public class Launcher extends Activity mIconCache.flushInvalidIcons(grid); mDragController = new DragController(this); mInflater = getLayoutInflater(); + mStateTransitionAnimation = new LauncherStateTransitionAnimation(this, this); mStats = new Stats(this); @@ -984,10 +984,12 @@ public class Launcher extends Activity super.onResume(); // Restore the previous launcher state - if (mOnResumeState == State.WORKSPACE) { + if (mOnResumeState == State.WORKSPACE || mOnResumeState == State.NONE) { 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; @@ -1296,8 +1298,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, @@ -1423,6 +1425,9 @@ public class Launcher extends Activity mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.search_drop_target_bar); + // Setup Apps + mAppsView = (AppsContainerView) findViewById(R.id.apps_view); + // Setup AppsCustomize mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane); mAppsCustomizeContent = (AppsCustomizePagedView) @@ -1436,7 +1441,10 @@ public class Launcher extends Activity dragController.addDropTarget(mWorkspace); if (mSearchDropTargetBar != null) { mSearchDropTargetBar.setup(this, dragController); - mSearchDropTargetBar.setQsbSearchBar(getQsbBar()); + if (getOrCreateQsbBar() == null) { + // Explicitly set it to null during initialization. + mSearchDropTargetBar.setQsbSearchBar(null); + } } if (getResources().getBoolean(R.bool.debug_memory_enabled)) { @@ -1637,16 +1645,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 && mAppsCustomizeTabHost != 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, @@ -1689,40 +1698,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); } } @@ -1735,12 +1723,12 @@ 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 @@ -1787,7 +1775,7 @@ public class Launcher extends Activity mAutoAdvanceSentTime = System.currentTimeMillis(); } - private void updateRunning() { + private void updateAutoAdvanceState() { boolean autoAdvanceRunning = mVisible && mUserPresent && !mWidgetsToAdvance.isEmpty(); if (autoAdvanceRunning != mAutoAdvanceRunning) { mAutoAdvanceRunning = autoAdvanceRunning; @@ -1833,14 +1821,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(); } } @@ -1854,14 +1842,18 @@ public class Launcher extends Activity 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 AppsCustomizeTabHost getWidgetsView() { + return mAppsCustomizeTabHost; + } + public Workspace getWorkspace() { return mWorkspace; } @@ -1947,6 +1939,11 @@ public class Launcher extends Activity imm.hideSoftInputFromWindow(v.getWindowToken(), 0); } + // Reset the apps view + if (!alreadyOnHome && mAppsView != null) { + mAppsView.scrollToTop(); + } + // Reset the apps customize page if (!alreadyOnHome && mAppsCustomizeTabHost != null) { mAppsCustomizeTabHost.reset(); @@ -2460,13 +2457,14 @@ public class Launcher extends Activity return; } - if (isAllAppsVisible()) { - if (mAppsCustomizeContent.getContentType() == - AppsCustomizePagedView.ContentType.Applications) { - showWorkspace(true); - } else { - showOverviewMode(true); - } + if (LauncherAppState.getInstance().getAccessibilityDelegate().onBackPressed()) { + return; + } + + if (isAppsViewVisible()) { + showWorkspace(true); + } else if (isWidgetsViewVisible()) { + showOverviewMode(true); } else if (mWorkspace.isInOverviewMode()) { mWorkspace.exitOverviewMode(true); } else if (mWorkspace.getOpenFolder() != null) { @@ -2597,10 +2595,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); + showAppsView(true /* animated */, false /* resetListToTop */); } if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickAllAppsButton(v); @@ -2771,7 +2769,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); } @@ -2921,9 +2919,40 @@ public class Launcher extends Activity Bundle optsBundle = null; if (useLaunchAnimation) { - ActivityOptions opts = Utilities.isLmpOrAbove() ? - ActivityOptions.makeCustomAnimation(this, R.anim.task_open_enter, R.anim.no_anim) : - ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getMeasuredWidth(), v.getMeasuredHeight()); + 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) { + opts = Utilities.isLmpOrAbove() ? + ActivityOptions.makeCustomAnimation(this, + R.anim.task_open_enter, R.anim.no_anim) : + ActivityOptions.makeScaleUpAnimation(v, 0, 0, + v.getMeasuredWidth(), v.getMeasuredHeight()); + } optsBundle = opts.toBundle(); } @@ -3146,7 +3175,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(); } @@ -3187,7 +3216,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(); @@ -3195,12 +3224,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) { @@ -3218,579 +3258,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); @@ -3806,19 +3273,24 @@ public class Launcher extends Activity } } - 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 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) @@ -3837,7 +3309,7 @@ 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() @@ -3848,7 +3320,8 @@ public class Launcher extends Activity 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); } @@ -3856,14 +3329,24 @@ 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) { if (resetPageToZero) { mAppsCustomizeTabHost.reset(); } - showAppsCustomizeHelper(animated, false, contentType); + showAppsOrWidgets(animated, State.WIDGETS); mAppsCustomizeTabHost.post(new Runnable() { @Override public void run() { @@ -3871,13 +3354,27 @@ public class Launcher extends Activity mAppsCustomizeTabHost.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); + } 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 @@ -3886,15 +3383,19 @@ public class Launcher extends Activity } void enterSpringLoadedDragMode() { - if (isAllAppsVisible()) { - hideAppsCustomizeHelper(Workspace.State.SPRING_LOADED, true, true, null); - mState = State.APPS_CUSTOMIZE_SPRING_LOADED; + 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, 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 @@ -3913,11 +3414,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. } @@ -3934,7 +3436,7 @@ public class Launcher extends Activity // NO-OP } - public View getQsbBar() { + public View getOrCreateQsbBar() { if (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch()) { return mLauncherCallbacks.getQsbBar(); } @@ -3983,6 +3485,7 @@ public class Launcher extends Activity mQsb.updateAppWidgetOptions(opts); mQsb.setPadding(0, 0, 0, 0); mSearchDropTargetBar.addView(mQsb); + mSearchDropTargetBar.setQsbSearchBar(mQsb); } } return mQsb; @@ -3994,8 +3497,10 @@ public class Launcher extends Activity 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)); } @@ -4218,9 +3723,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); } } @@ -4487,10 +3991,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)) { @@ -4525,14 +4029,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); } } @@ -4589,7 +4089,7 @@ public class Launcher extends Activity mSearchDropTargetBar.removeView(mQsb); mQsb = null; } - mSearchDropTargetBar.setQsbSearchBar(getQsbBar()); + getOrCreateQsbBar(); } /** @@ -4598,24 +4098,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 (mAppsCustomizeContent != null) { + mAppsCustomizeContent.onPackagesUpdated( + LauncherModel.getSortedWidgetsAndShortcuts(this, false /* refresh */)); } if (mLauncherCallbacks != null) { mLauncherCallbacks.bindAllApplications(apps); @@ -4637,9 +4125,8 @@ public class Launcher extends Activity return; } - if (!LauncherAppState.isDisableAllApps() && - mAppsCustomizeContent != null) { - mAppsCustomizeContent.updateApps(apps); + if (mAppsView != null) { + mAppsView.updateApps(apps); } } @@ -4753,9 +4240,8 @@ public class Launcher extends Activity } // Update AllApps - if (!LauncherAppState.isDisableAllApps() && - mAppsCustomizeContent != null) { - mAppsCustomizeContent.removeApps(appInfos); + if (mAppsView != null) { + mAppsView.removeApps(appInfos); } } @@ -5024,7 +4510,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, diff --git a/src/com/android/launcher3/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/LauncherAccessibilityDelegate.java index c9e277e4c..0ae1c0e90 100644 --- a/src/com/android/launcher3/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/LauncherAccessibilityDelegate.java @@ -1,11 +1,16 @@ 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; @@ -20,6 +25,21 @@ 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>(); @@ -36,6 +56,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 +72,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 +93,21 @@ 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); 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 +121,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, 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; } + + private void announceConfirmation(int resId) { + announceConfirmation(mLauncher.getResources().getString(resId)); + } + + private 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..d8896ccd2 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -43,9 +43,6 @@ 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; @@ -65,7 +62,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) { @@ -171,7 +168,7 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return mModel; } - AccessibilityDelegate getAccessibilityDelegate() { + LauncherAccessibilityDelegate getAccessibilityDelegate() { return mAccessibilityDelegate; } @@ -283,12 +280,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/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index 32bea5baa..97ff32790 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) { @@ -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); } /** @@ -686,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); } } @@ -753,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(); @@ -785,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) { @@ -803,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; } @@ -810,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); @@ -860,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) { @@ -1091,9 +1156,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(); @@ -1128,6 +1195,9 @@ public class LauncherBackupHelper implements BackupHelper { } private class InvalidBackupException extends IOException { + + private static final long serialVersionUID = 8931456637211665082L; + private InvalidBackupException(Throwable cause) { super(cause); } @@ -1136,4 +1206,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/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java index fa053650f..cedb3975d 100644 --- a/src/com/android/launcher3/LauncherFiles.java +++ b/src/com/android/launcher3/LauncherFiles.java @@ -25,6 +25,7 @@ public class LauncherFiles { WallpaperCropActivity.class.getName(); 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, @@ -36,5 +37,6 @@ public class LauncherFiles { STATS_LOG, WALLPAPER_CROP_PREFERENCES_KEY + XML, WALLPAPER_IMAGES_DB, - WIDGET_PREVIEWS_DB)); + WIDGET_PREVIEWS_DB, + APP_ICONS_DB)); } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index c39bcee09..2fd9db2c3 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; @@ -75,7 +74,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 @@ -91,9 +89,6 @@ public class LauncherModel extends BroadcastReceiver 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; @@ -167,9 +162,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>(); @@ -198,7 +190,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, @@ -490,13 +482,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, @@ -548,13 +534,7 @@ 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) { @@ -1146,7 +1126,6 @@ public class LauncherModel extends BroadcastReceiver break; } sBgItemsIdMap.remove(item.id); - sBgDbIconCache.remove(item); } } } @@ -1219,7 +1198,6 @@ public class LauncherModel extends BroadcastReceiver synchronized (sBgLock) { sBgItemsIdMap.remove(info.id); sBgFolders.remove(info.id); - sBgDbIconCache.remove(info); sBgWorkspaceItems.remove(info); } @@ -1229,7 +1207,6 @@ public class LauncherModel extends BroadcastReceiver synchronized (sBgLock) { for (ItemInfo childInfo : info.contents) { sBgItemsIdMap.remove(childInfo.id); - sBgDbIconCache.remove(childInfo); } } } @@ -1442,40 +1419,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. + */ + private 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() { @@ -1505,12 +1473,9 @@ public class LauncherModel extends BroadcastReceiver private 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; } @@ -1522,8 +1487,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 @@ -1531,20 +1495,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() { @@ -1613,15 +1575,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; } @@ -1638,7 +1598,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; @@ -1664,23 +1624,6 @@ public class LauncherModel extends BroadcastReceiver } } - // 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; @@ -1731,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(); @@ -1850,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); @@ -1890,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 @@ -1976,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); @@ -2007,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); } } @@ -2039,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); @@ -2088,7 +2017,16 @@ public class LauncherModel extends BroadcastReceiver continue; } - if (restored) { + if (itemReplaced) { + if (user.equals(UserHandleCompat.myUserHandle())) { + info = getAppShortcutInfo(manager, intent, user, context, null, + iconIndex, titleIndex, false); + } 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", @@ -2102,8 +2040,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); } else { info = getShortcutInfo(c, context, iconTypeIndex, iconPackageIndex, iconResourceIndex, iconIndex, @@ -2158,10 +2096,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"); } @@ -2322,9 +2256,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); @@ -2345,7 +2277,7 @@ public class LauncherModel extends BroadcastReceiver // Break early if we've stopped loading if (mStopped) { clearSBgDataStructures(); - return false; + return; } if (itemsToRemove.size() > 0) { @@ -2391,63 +2323,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); + sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext)); + // Log to disk + Launcher.addDumpLog(TAG, "11683562 - sBgWorkspaceScreens: " + + TextUtils.join(", ", sBgWorkspaceScreens), true); - LauncherAppState.getLauncherProvider().updateMaxScreenId(maxScreenId); - updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); - - // 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) { @@ -2476,7 +2374,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_NO_NOTIFICATION, + update, + BaseColumns._ID + "= ?", + new String[]{Long.toString(itemId)}); } /** Filters the set of items who are directly or indirectly (via another container) on the @@ -2673,7 +2581,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; @@ -2777,7 +2685,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. @@ -2881,20 +2789,48 @@ 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())) { @@ -3026,7 +2962,7 @@ public class LauncherModel extends BroadcastReceiver case OP_ADD: for (int i=0; i<N; i++) { if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]); - mIconCache.remove(packages[i], mUser); + mIconCache.updateIconsForPkg(packages[i], mUser); mBgAllAppsList.addPackage(context, packages[i], mUser); } @@ -3058,6 +2994,7 @@ public class LauncherModel extends BroadcastReceiver 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]); @@ -3077,12 +3014,15 @@ public class LauncherModel extends BroadcastReceiver shortcutSet.removeAll(Arrays.asList(mPackages)); prefs.edit().putStringSet(shortcutsSetKey, shortcutSet).commit(); } + 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); + mBgAllAppsList.removePackage(packages[i], mUser); WidgetPreviewLoader.removePackageFromDb( mApp.getWidgetPreviewCacheDb(), packages[i]); } @@ -3116,13 +3056,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); } @@ -3175,7 +3109,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(); @@ -3420,7 +3353,7 @@ public class LauncherModel extends BroadcastReceiver int promiseType) { final ShortcutInfo info = new ShortcutInfo(); info.user = UserHandleCompat.myUserHandle(); - mIconCache.getTitleAndIcon(info, intent, info.user, true); + mIconCache.getTitleAndIcon(info, intent, info.user); if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) { String title = (cursor != null) ? cursor.getString(titleIndex) : null; @@ -3463,22 +3396,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) { if (user == null) { Log.d(TAG, "Null user found in getShortcutInfo"); return null; @@ -3500,48 +3424,22 @@ 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); + 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( @@ -3620,7 +3518,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) { @@ -3629,7 +3527,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; @@ -3648,22 +3546,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); @@ -3712,45 +3594,6 @@ 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. @@ -3800,38 +3643,7 @@ public class LauncherModel extends BroadcastReceiver 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; diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 1040b1173..b7a271e4f 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -57,7 +57,6 @@ 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; static final String OLD_AUTHORITY = "com.android.launcher2.settings"; @@ -66,7 +65,6 @@ public class LauncherProvider extends ContentProvider { 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 EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd"; @@ -251,12 +249,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. */ @@ -478,142 +470,117 @@ 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: { + 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(); + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + // Old version remains, which means we wipe old data + break; + } finally { + db.endTransaction(); + } } - // 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: { + // 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 @@ -770,23 +737,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,32 +755,8 @@ 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) { @@ -1242,6 +1169,27 @@ public class LauncherProvider extends ContentProvider { } } + /** + * @return the max _id in the provided table. + */ + private 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/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java new file mode 100644 index 000000000..484ed5c30 --- /dev/null +++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java @@ -0,0 +1,832 @@ +/* + * 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.util.Log; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +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; + + private Launcher mLauncher; + private Callbacks mCb; + private 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(), null, animated, cb); + } + + /** + * Starts an animation to the widgets view. + */ + public void startAnimationToWidgets(final boolean animated) { + final AppsCustomizeTabHost toView = mLauncher.getWidgetsView(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + public void onRevealViewVisible(View revealView, View contentView, + View allAppsButtonView) { + // Hide the real page background, and swap in the fake one + ((AppsCustomizePagedView) contentView).setPageBackgroundsVisible(false); + revealView.setBackground(mLauncher.getDrawable(R.drawable.quantum_panel_dark)); + } + @Override + public void onAnimationComplete(View revealView, View contentView, View allAppsButtonView) { + // Show the real page background + ((AppsCustomizePagedView) contentView).setPageBackgroundsVisible(true); + } + @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(), toView.getPageIndicators(), animated, 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 View pageIndicatorsView, + final boolean animated, 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 page indicators + if (pageIndicatorsView != null) { + pageIndicatorsView.setAlpha(0.01f); + ObjectAnimator indicatorsAlpha = + ObjectAnimator.ofFloat(pageIndicatorsView, "alpha", 1f); + indicatorsAlpha.setDuration(revealDuration); + mStateAnimation.play(indicatorsAlpha); + } + + // 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); + } + } + + // Hide the search bar + 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); + + // Hide the search bar + 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(), null /* pageIndicatorsView */, 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) { + AppsCustomizeTabHost widgetsView = mLauncher.getWidgetsView(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + public void onRevealViewVisible(View revealView, View contentView, View allAppsButtonView) { + AppsCustomizePagedView pagedView = ((AppsCustomizePagedView) contentView); + + // Hide the real page background, and swap in the fake one + pagedView.stopScrolling(); + pagedView.setPageBackgroundsVisible(false); + revealView.setBackground(mLauncher.getDrawable(R.drawable.quantum_panel_dark)); + + // Hide the side pages of the Widget tray to avoid some ugly edge cases + final View currentPage = pagedView.getPageAt(pagedView.getNextPage()); + int count = pagedView.getChildCount(); + for (int i = 0; i < count; i++) { + View child = pagedView.getChildAt(i); + if (child != currentPage) { + child.setVisibility(View.INVISIBLE); + } + } + } + @Override + public void onAnimationComplete(View revealView, View contentView, View allAppsButtonView) { + AppsCustomizePagedView pagedView = ((AppsCustomizePagedView) contentView); + + // Show the real page background and force-update the page + pagedView.setPageBackgroundsVisible(true); + pagedView.setCurrentPage(pagedView.getNextPage()); + pagedView.updateCurrentPageScroll(); + + // Unhide the side pages + int count = pagedView.getChildCount(); + for (int i = 0; i < count; i++) { + View child = pagedView.getChildAt(i); + child.setVisibility(View.VISIBLE); + } + } + @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(), + widgetsView.getPageIndicators(), 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 View pageIndicatorsView, 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); + + // Setup the page indicators animation + if (pageIndicatorsView != null) { + pageIndicatorsView.setAlpha(1f); + ObjectAnimator indicatorsAlpha = + LauncherAnimUtils.ofFloat(pageIndicatorsView, "alpha", 0f); + indicatorsAlpha.setDuration(revealDuration); + indicatorsAlpha.setInterpolator(new DecelerateInterpolator(1.5f)); + mStateAnimation.play(indicatorsAlpha); + } + + 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/PagedViewWithDraggableItems.java b/src/com/android/launcher3/PagedViewWithDraggableItems.java index 0e593698d..f0743cf1c 100644 --- a/src/com/android/launcher3/PagedViewWithDraggableItems.java +++ b/src/com/android/launcher3/PagedViewWithDraggableItems.java @@ -109,7 +109,7 @@ public abstract class PagedViewWithDraggableItems extends PagedView // 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() || + if (!mLauncher.isWidgetsViewVisible() || mLauncher.getWorkspace().isSwitchingState()) return false; // Return if global dragging is not enabled if (!mLauncher.isDraggingEnabled()) return false; diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java index 99c2e0859..cc17820ff 100644 --- a/src/com/android/launcher3/SearchDropTargetBar.java +++ b/src/com/android/launcher3/SearchDropTargetBar.java @@ -197,6 +197,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 +210,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 +227,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/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 01f79314e..08ffaa299 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.graphics.Bitmap; import android.util.Log; +import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.compat.UserHandleCompat; import java.util.ArrayList; @@ -46,18 +47,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. @@ -184,8 +191,9 @@ 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); + } } @Override @@ -207,9 +215,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, diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 1a9b9a16c..497b43874 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; @@ -112,6 +114,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. diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index d963f2db9..312814039 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -131,8 +131,8 @@ public class WidgetPreviewLoader { 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, WeakReference<Bitmap>> mLoadedPreviews = new HashMap<>(); + private final ArrayList<SoftReference<Bitmap>> mUnusedBitmaps = new ArrayList<>(); private final Context mContext; private final int mAppIconSize; diff --git a/src/com/android/launcher3/WidgetsContainerView.java b/src/com/android/launcher3/WidgetsContainerView.java new file mode 100644 index 000000000..d0dd733a6 --- /dev/null +++ b/src/com/android/launcher3/WidgetsContainerView.java @@ -0,0 +1,88 @@ +package com.android.launcher3; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + + +class SectionedWidgetsRow { + String section; + List<List<Object>> widgets; + + public SectionedWidgetsRow(String sc) { + section = sc; + } +} + +class SectionedWidgetsAlgorithm { + public List<SectionedWidgetsRow> computeSectionedWidgetRows(List<Object> sortedWidgets, + int widgetsPerRow) { + List<SectionedWidgetsRow> rows = new ArrayList<>(); + LinkedHashMap<String, List<Object>> sections = computeSectionedApps(sortedWidgets); + for (Map.Entry<String, List<Object>> sectionEntry : sections.entrySet()) { + String section = sectionEntry.getKey(); + SectionedWidgetsRow row = new SectionedWidgetsRow(section); + List<Object> widgets = sectionEntry.getValue(); + int numRows = (int) Math.ceil((float) widgets.size() / widgetsPerRow); + for (int i = 0; i < numRows; i++) { + List<Object> widgetsInRow = new ArrayList<>(); + int offset = i * widgetsPerRow; + for (int j = 0; j < widgetsPerRow; j++) { + widgetsInRow.add(widgets.get(offset + j)); + } + row.widgets.add(widgetsInRow); + } + } + return rows; + } + + private LinkedHashMap<String, List<Object>> computeSectionedApps(List<Object> sortedWidgets) { + LinkedHashMap<String, List<Object>> sections = new LinkedHashMap<>(); + for (Object info : sortedWidgets) { + String section = getSection(info); + List<Object> sectionedWidgets = sections.get(section); + if (sectionedWidgets == null) { + sectionedWidgets = new ArrayList<>(); + sections.put(section, sectionedWidgets); + } + sectionedWidgets.add(info); + } + return sections; + } + + private String getSection(Object widgetOrShortcut) { + return "UNKNOWN"; + } +} + +/** + * The widgets list view container. + */ +public class WidgetsContainerView extends FrameLayout { + + + 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) { + this(context, attrs, defStyleAttr, 0); + } + + public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + } +} diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 6cfb6b29b..a59e25e08 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; @@ -492,7 +492,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 +561,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 +574,10 @@ public class Workspace extends SmoothPagedView mWorkspaceScreens.put(screenId, newScreen); mScreenOrder.add(insertIndex, screenId); addView(newScreen, insertIndex); + + if (LauncherAppState.getInstance().getAccessibilityDelegate().isInAccessibleDrag()) { + newScreen.enableAccessibleDrag(true); + } return screenId; } @@ -664,9 +664,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 +690,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 +698,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 +722,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 +731,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 +772,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 +816,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 +874,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 +901,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); @@ -1548,7 +1528,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 +1627,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 +1639,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); } @@ -1820,7 +1816,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 +1837,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); @@ -2050,8 +2046,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(); @@ -2210,8 +2205,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); } @@ -2331,7 +2326,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 +2347,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 +2399,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 +2429,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(); @@ -2588,6 +2583,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 +2610,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 +2647,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 +2708,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 +2725,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 +2758,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 +2799,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 +2849,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 +2874,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 +3076,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 +3192,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 +3583,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 +3607,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 +3682,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 +3720,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; } @@ -4115,7 +4103,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); } @@ -4236,19 +4223,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 +4240,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) { @@ -4306,88 +4291,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(); @@ -4879,7 +4782,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 +4817,7 @@ 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); } 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..47e1b7a98 --- /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(s.charAt(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/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/util/ComponentKey.java b/src/com/android/launcher3/util/ComponentKey.java new file mode 100644 index 000000000..0f17f009e --- /dev/null +++ b/src/com/android/launcher3/util/ComponentKey.java @@ -0,0 +1,51 @@ +package com.android.launcher3.util; + +/** + * 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. + */ + +import android.content.ComponentName; + +import com.android.launcher3.compat.UserHandleCompat; + +import java.util.Arrays; + +public class ComponentKey { + + public final ComponentName componentName; + public final UserHandleCompat user; + + private final int mHashCode; + + public ComponentKey(ComponentName componentName, UserHandleCompat user) { + assert (componentName != null); + assert (user != null); + this.componentName = componentName; + this.user = user; + mHashCode = Arrays.hashCode(new Object[] {componentName, user}); + + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(Object o) { + ComponentKey other = (ComponentKey) o; + return other.componentName.equals(componentName) && other.user.equals(user); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java new file mode 100644 index 000000000..0c6bfbf35 --- /dev/null +++ b/src/com/android/launcher3/util/FocusLogic.java @@ -0,0 +1,507 @@ +/* + * 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.content.res.Configuration; +import android.util.Log; +import android.view.KeyEvent; +import android.view.ViewGroup; + +import com.android.launcher3.CellLayout; + +/** + * 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 = "Focus"; + 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 CURRENT_PAGE_FIRST_ITEM = -5; + public static final int CURRENT_PAGE_LAST_ITEM = -6; + + public static final int NEXT_PAGE_FIRST_ITEM = -7; + public static final int NEXT_PAGE_LEFT_COLUMN = -8; + + // 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) { + if (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) { + return true; + } + return false; + } + + 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)); + } + + int newIndex = NOOP; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/); + if (newIndex == NOOP && pageIndex > 0) { + newIndex = PREVIOUS_PAGE_RIGHT_COLUMN; + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/); + if (newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = NEXT_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 incremental index starting + * with 0 or a matrix where all the values are initialized to {@link #EMPTY}. + * + * @param m number of columns in the matrix + * @param n number of rows in the matrix + * @param incrementOrder {@code true} if the matrix contents should increment in reading + * order with 0 indexing. {@code false} if each cell should be + * initialized to {@link #EMPTY}; + */ + // TODO: get rid of dynamic matrix creation. + public static int[][] createFullMatrix(int m, int n, boolean incrementOrder) { + int[][] matrix = new int [m][n]; + for (int i=0; i < m;i++) { + for (int j=0; j < n; j++) { + if (incrementOrder) { + matrix[i][j] = j * m + i; + } else { + matrix[i][j] = 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) { + ViewGroup parent = layout.getShortcutsAndWidgets(); + final int m = layout.getCountX(); + final int n = layout.getCountY(); + + int[][] matrix = createFullMatrix(m, n, false /* initialize to #EMPTY */); + + // 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[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, + int orientation, int allappsiconRank, boolean includeAllappsicon) { + + ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets(); + + int m, n; + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + m = iconLayout.getCountX(); + n = iconLayout.getCountY() + hotseatLayout.getCountY(); + } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + m = iconLayout.getCountX() + hotseatLayout.getCountX(); + n = iconLayout.getCountY(); + } else { + throw new IllegalStateException(String.format( + "orientation type=%d is not supported for key board events.", orientation)); + } + int[][] matrix = createFullMatrix(m, n, false /* set all cell to empty */); + + // 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 (orientation == Configuration.ORIENTATION_PORTRAIT) { + 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 if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + 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(), + false /* set all cell to empty */); + + // 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); + } + } + + /** + * Figure out the location of the icon. + * + */ + //TODO(hyunyoungs): this helper method should move to CellLayout class while removing the + // dynamic matrix creation all together. + public static int findRow(int[][] matrix, int iconIndex) { + int cntX = matrix.length; + int cntY = matrix[0].length; + + for (int i = 0; i < cntX; i++) { + for (int j = 0; j < cntY; j++) { + if (matrix[i][j] == iconIndex) { + return j; + } + } + } + return -1; + } +} 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/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); - } -} |