diff options
Diffstat (limited to 'src/org/cyanogenmod')
17 files changed, 4036 insertions, 0 deletions
diff --git a/src/org/cyanogenmod/wallpaperpicker/AlphaDisableableButton.java b/src/org/cyanogenmod/wallpaperpicker/AlphaDisableableButton.java new file mode 100644 index 0000000..9a3d6c3 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/AlphaDisableableButton.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.cyanogenmod.wallpaperpicker; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; + +/** + * A Button which becomes translucent when it is disabled + */ +public class AlphaDisableableButton extends Button { + public static float DISABLED_ALPHA_VALUE = 0.4f; + public AlphaDisableableButton(Context context) { + this(context, null); + } + + public AlphaDisableableButton(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AlphaDisableableButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setLayerType(LAYER_TYPE_HARDWARE, null); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if(enabled) { + setAlpha(1.0f); + } else { + setAlpha(DISABLED_ALPHA_VALUE); + } + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/CheckableFrameLayout.java b/src/org/cyanogenmod/wallpaperpicker/CheckableFrameLayout.java new file mode 100644 index 0000000..8f1bc3a --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/CheckableFrameLayout.java @@ -0,0 +1,63 @@ +/* + * 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 org.cyanogenmod.wallpaperpicker; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Checkable; +import android.widget.FrameLayout; + +public class CheckableFrameLayout extends FrameLayout implements Checkable { + private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked }; + boolean mChecked; + + public CheckableFrameLayout(Context context) { + super(context); + } + + public CheckableFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CheckableFrameLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public boolean isChecked() { + return mChecked; + } + + public void setChecked(boolean checked) { + if (checked != mChecked) { + mChecked = checked; + refreshDrawableState(); + } + } + + public void toggle() { + setChecked(!mChecked); + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/CropView.java b/src/org/cyanogenmod/wallpaperpicker/CropView.java new file mode 100644 index 0000000..118efd3 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/CropView.java @@ -0,0 +1,321 @@ +/* + * 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 org.cyanogenmod.wallpaperpicker; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.ScaleGestureDetector.OnScaleGestureListener; +import android.view.ViewConfiguration; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; + +import com.android.photos.views.TiledImageRenderer.TileSource; +import com.android.photos.views.TiledImageView; + +public class CropView extends TiledImageView implements OnScaleGestureListener { + + private ScaleGestureDetector mScaleGestureDetector; + private long mTouchDownTime; + private float mFirstX, mFirstY; + private float mLastX, mLastY; + private float mCenterX, mCenterY; + private float mMinScale; + private boolean mTouchEnabled = true; + private RectF mTempEdges = new RectF(); + private float[] mTempPoint = new float[] { 0, 0 }; + private float[] mTempCoef = new float[] { 0, 0 }; + private float[] mTempAdjustment = new float[] { 0, 0 }; + private float[] mTempImageDims = new float[] { 0, 0 }; + private float[] mTempRendererCenter = new float[] { 0, 0 }; + TouchCallback mTouchCallback; + Matrix mRotateMatrix; + Matrix mInverseRotateMatrix; + + public interface TouchCallback { + void onTouchDown(); + void onTap(); + void onTouchUp(); + } + + public CropView(Context context) { + this(context, null); + } + + public CropView(Context context, AttributeSet attrs) { + super(context, attrs); + mScaleGestureDetector = new ScaleGestureDetector(context, this); + mRotateMatrix = new Matrix(); + mInverseRotateMatrix = new Matrix(); + } + + private float[] getImageDims() { + final float imageWidth = mRenderer.source.getImageWidth(); + final float imageHeight = mRenderer.source.getImageHeight(); + float[] imageDims = mTempImageDims; + imageDims[0] = imageWidth; + imageDims[1] = imageHeight; + mRotateMatrix.mapPoints(imageDims); + imageDims[0] = Math.abs(imageDims[0]); + imageDims[1] = Math.abs(imageDims[1]); + return imageDims; + } + + private void getEdgesHelper(RectF edgesOut) { + final float width = getWidth(); + final float height = getHeight(); + final float[] imageDims = getImageDims(); + final float imageWidth = imageDims[0]; + final float imageHeight = imageDims[1]; + + float initialCenterX = mRenderer.source.getImageWidth() / 2f; + float initialCenterY = mRenderer.source.getImageHeight() / 2f; + + float[] rendererCenter = mTempRendererCenter; + rendererCenter[0] = mCenterX - initialCenterX; + rendererCenter[1] = mCenterY - initialCenterY; + mRotateMatrix.mapPoints(rendererCenter); + rendererCenter[0] += imageWidth / 2; + rendererCenter[1] += imageHeight / 2; + + final float scale = mRenderer.scale; + float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f) + * scale + width / 2f; + float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f) + * scale + height / 2f; + float leftEdge = centerX - imageWidth / 2f * scale; + float rightEdge = centerX + imageWidth / 2f * scale; + float topEdge = centerY - imageHeight / 2f * scale; + float bottomEdge = centerY + imageHeight / 2f * scale; + + edgesOut.left = leftEdge; + edgesOut.right = rightEdge; + edgesOut.top = topEdge; + edgesOut.bottom = bottomEdge; + } + + public int getImageRotation() { + return mRenderer.rotation; + } + + public RectF getCrop() { + final RectF edges = mTempEdges; + getEdgesHelper(edges); + final float scale = mRenderer.scale; + + float cropLeft = -edges.left / scale; + float cropTop = -edges.top / scale; + float cropRight = cropLeft + getWidth() / scale; + float cropBottom = cropTop + getHeight() / scale; + + return new RectF(cropLeft, cropTop, cropRight, cropBottom); + } + + public Point getSourceDimensions() { + return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight()); + } + + public void setTileSource(TileSource source, Runnable isReadyCallback) { + super.setTileSource(source, isReadyCallback); + mCenterX = mRenderer.centerX; + mCenterY = mRenderer.centerY; + mRotateMatrix.reset(); + mRotateMatrix.setRotate(mRenderer.rotation); + mInverseRotateMatrix.reset(); + mInverseRotateMatrix.setRotate(-mRenderer.rotation); + updateMinScale(getWidth(), getHeight(), source, true); + } + + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + updateMinScale(w, h, mRenderer.source, false); + } + + public void setScale(float scale) { + synchronized (mLock) { + mRenderer.scale = scale; + } + } + + private void updateMinScale(int w, int h, TileSource source, boolean resetScale) { + synchronized (mLock) { + if (resetScale) { + mRenderer.scale = 1; + } + if (source != null) { + final float[] imageDims = getImageDims(); + final float imageWidth = imageDims[0]; + final float imageHeight = imageDims[1]; + mMinScale = Math.max(w / imageWidth, h / imageHeight); + mRenderer.scale = + Math.max(mMinScale, resetScale ? Float.MIN_VALUE : mRenderer.scale); + } + } + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + // Don't need the lock because this will only fire inside of + // onTouchEvent + mRenderer.scale *= detector.getScaleFactor(); + mRenderer.scale = Math.max(mMinScale, mRenderer.scale); + invalidate(); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + } + + public void moveToLeft() { + if (getWidth() == 0 || getHeight() == 0) { + final ViewTreeObserver observer = getViewTreeObserver(); + observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + public void onGlobalLayout() { + moveToLeft(); + getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + } + final RectF edges = mTempEdges; + getEdgesHelper(edges); + final float scale = mRenderer.scale; + mCenterX += Math.ceil(edges.left / scale); + updateCenter(); + } + + private void updateCenter() { + mRenderer.centerX = Math.round(mCenterX); + mRenderer.centerY = Math.round(mCenterY); + } + + public void setTouchEnabled(boolean enabled) { + mTouchEnabled = enabled; + } + + public void setTouchCallback(TouchCallback cb) { + mTouchCallback = cb; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); + final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; + final int skipIndex = pointerUp ? event.getActionIndex() : -1; + + // Determine focal point + float sumX = 0, sumY = 0; + final int count = event.getPointerCount(); + for (int i = 0; i < count; i++) { + if (skipIndex == i) + continue; + sumX += event.getX(i); + sumY += event.getY(i); + } + final int div = pointerUp ? count - 1 : count; + float x = sumX / div; + float y = sumY / div; + + if (action == MotionEvent.ACTION_DOWN) { + mFirstX = x; + mFirstY = y; + mTouchDownTime = System.currentTimeMillis(); + if (mTouchCallback != null) { + mTouchCallback.onTouchDown(); + } + } else if (action == MotionEvent.ACTION_UP) { + ViewConfiguration config = ViewConfiguration.get(getContext()); + + float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y); + float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop(); + long now = System.currentTimeMillis(); + if (mTouchCallback != null) { + // only do this if it's a small movement + if (squaredDist < slop && + now < mTouchDownTime + ViewConfiguration.getTapTimeout()) { + mTouchCallback.onTap(); + } + mTouchCallback.onTouchUp(); + } + } + + if (!mTouchEnabled) { + return true; + } + + synchronized (mLock) { + mScaleGestureDetector.onTouchEvent(event); + switch (action) { + case MotionEvent.ACTION_MOVE: + float[] point = mTempPoint; + point[0] = (mLastX - x) / mRenderer.scale; + point[1] = (mLastY - y) / mRenderer.scale; + mInverseRotateMatrix.mapPoints(point); + mCenterX += point[0]; + mCenterY += point[1]; + updateCenter(); + invalidate(); + break; + } + if (mRenderer.source != null) { + // Adjust position so that the wallpaper covers the entire area + // of the screen + final RectF edges = mTempEdges; + getEdgesHelper(edges); + final float scale = mRenderer.scale; + + float[] coef = mTempCoef; + coef[0] = 1; + coef[1] = 1; + mRotateMatrix.mapPoints(coef); + float[] adjustment = mTempAdjustment; + mTempAdjustment[0] = 0; + mTempAdjustment[1] = 0; + if (edges.left > 0) { + adjustment[0] = edges.left / scale; + } else if (edges.right < getWidth()) { + adjustment[0] = (edges.right - getWidth()) / scale; + } + if (edges.top > 0) { + adjustment[1] = (float) Math.ceil(edges.top / scale); + } else if (edges.bottom < getHeight()) { + adjustment[1] = (edges.bottom - getHeight()) / scale; + } + for (int dim = 0; dim <= 1; dim++) { + if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]); + } + + mInverseRotateMatrix.mapPoints(adjustment); + mCenterX += adjustment[0]; + mCenterY += adjustment[1]; + updateCenter(); + } + } + + mLastX = x; + mLastY = y; + return true; + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/DrawableTileSource.java b/src/org/cyanogenmod/wallpaperpicker/DrawableTileSource.java new file mode 100644 index 0000000..5752047 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/DrawableTileSource.java @@ -0,0 +1,102 @@ +/* + * 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 org.cyanogenmod.wallpaperpicker; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import com.android.gallery3d.glrenderer.BasicTexture; +import com.android.gallery3d.glrenderer.BitmapTexture; +import com.android.photos.views.TiledImageRenderer; + +public class DrawableTileSource implements TiledImageRenderer.TileSource { + 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 + public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2; + + private int mTileSize; + private int mPreviewSize; + private Drawable mDrawable; + private BitmapTexture mPreview; + + public DrawableTileSource(Context context, Drawable d, int previewSize) { + mTileSize = TiledImageRenderer.suggestedTileSize(context); + mDrawable = d; + mPreviewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); + } + + @Override + public int getTileSize() { + return mTileSize; + } + + @Override + public int getImageWidth() { + return mDrawable.getIntrinsicWidth(); + } + + @Override + public int getImageHeight() { + return mDrawable.getIntrinsicHeight(); + } + + @Override + public int getRotation() { + return 0; + } + + @Override + public BasicTexture getPreview() { + if (mPreviewSize == 0) { + return null; + } + if (mPreview == null){ + float width = getImageWidth(); + float height = getImageHeight(); + while (width > MAX_PREVIEW_SIZE || height > MAX_PREVIEW_SIZE) { + width /= 2; + height /= 2; + } + Bitmap b = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + mDrawable.setBounds(new Rect(0, 0, (int) width, (int) height)); + mDrawable.draw(c); + c.setBitmap(null); + mPreview = new BitmapTexture(b); + } + return mPreview; + } + + @Override + public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { + int tileSize = getTileSize(); + if (bitmap == null) { + bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); + } + Canvas c = new Canvas(bitmap); + Rect bounds = new Rect(0, 0, getImageWidth(), getImageHeight()); + bounds.offset(-x, -y); + mDrawable.setBounds(bounds); + mDrawable.draw(c); + c.setBitmap(null); + return bitmap; + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/LiveWallpaperListAdapter.java b/src/org/cyanogenmod/wallpaperpicker/LiveWallpaperListAdapter.java new file mode 100644 index 0000000..f72564f --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/LiveWallpaperListAdapter.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.wallpaperpicker; + +import android.app.WallpaperInfo; +import android.app.WallpaperManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.service.wallpaper.WallpaperService; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import org.cyanogenmod.wallpaperpicker.util.Thunk; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter { + private static final String LOG_TAG = "LiveWallpaperListAdapter"; + + private final LayoutInflater mInflater; + private final PackageManager mPackageManager; + + @Thunk + List<LiveWallpaperTile> mWallpapers; + + @SuppressWarnings("unchecked") + public LiveWallpaperListAdapter(Context context) { + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mPackageManager = context.getPackageManager(); + + List<ResolveInfo> list = mPackageManager.queryIntentServices( + new Intent(WallpaperService.SERVICE_INTERFACE), + PackageManager.GET_META_DATA); + + mWallpapers = new ArrayList<LiveWallpaperTile>(); + + new LiveWallpaperEnumerator(context).execute(list); + } + + public int getCount() { + if (mWallpapers == null) { + return 0; + } + return mWallpapers.size(); + } + + public LiveWallpaperTile getItem(int position) { + return mWallpapers.get(position); + } + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + View view; + + if (convertView == null) { + view = mInflater.inflate(R.layout.wallpaper_picker_live_wallpaper_item, parent, false); + } else { + view = convertView; + } + + LiveWallpaperTile wallpaperInfo = mWallpapers.get(position); + wallpaperInfo.setView(view); + ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); + ImageView icon = (ImageView) view.findViewById(R.id.wallpaper_icon); + if (wallpaperInfo.mThumbnail != null) { + image.setImageDrawable(wallpaperInfo.mThumbnail); + icon.setVisibility(View.GONE); + } else { + icon.setImageDrawable(wallpaperInfo.mInfo.loadIcon(mPackageManager)); + icon.setVisibility(View.VISIBLE); + } + + TextView label = (TextView) view.findViewById(R.id.wallpaper_item_label); + label.setText(wallpaperInfo.mInfo.loadLabel(mPackageManager)); + + return view; + } + + public static class LiveWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo { + @Thunk Drawable mThumbnail; + @Thunk WallpaperInfo mInfo; + public LiveWallpaperTile(Drawable thumbnail, WallpaperInfo info, Intent intent) { + mThumbnail = thumbnail; + mInfo = info; + } + @Override + public void onClick(WallpaperPickerActivity a) { + Intent preview = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER); + preview.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, + mInfo.getComponent()); + a.startActivityForResultSafely(preview, + WallpaperPickerActivity.PICK_WALLPAPER_THIRD_PARTY_ACTIVITY); + } + } + + private class LiveWallpaperEnumerator extends + AsyncTask<List<ResolveInfo>, LiveWallpaperTile, Void> { + private Context mContext; + private int mWallpaperPosition; + + public LiveWallpaperEnumerator(Context context) { + super(); + mContext = context; + mWallpaperPosition = 0; + } + + @Override + protected Void doInBackground(List<ResolveInfo>... params) { + final PackageManager packageManager = mContext.getPackageManager(); + + List<ResolveInfo> list = params[0]; + + Collections.sort(list, new Comparator<ResolveInfo>() { + final Collator mCollator; + + { + mCollator = Collator.getInstance(); + } + + public int compare(ResolveInfo info1, ResolveInfo info2) { + return mCollator.compare(info1.loadLabel(packageManager), + info2.loadLabel(packageManager)); + } + }); + + for (ResolveInfo resolveInfo : list) { + WallpaperInfo info = null; + try { + info = new WallpaperInfo(mContext, resolveInfo); + } catch (XmlPullParserException e) { + Log.w(LOG_TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e); + continue; + } catch (IOException e) { + Log.w(LOG_TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e); + continue; + } + + + Drawable thumb = info.loadThumbnail(packageManager); + Intent launchIntent = new Intent(WallpaperService.SERVICE_INTERFACE); + launchIntent.setClassName(info.getPackageName(), info.getServiceName()); + LiveWallpaperTile wallpaper = new LiveWallpaperTile(thumb, info, launchIntent); + publishProgress(wallpaper); + } + // Send a null object to show loading is finished + publishProgress((LiveWallpaperTile) null); + + return null; + } + + @Override + protected void onProgressUpdate(LiveWallpaperTile...infos) { + for (LiveWallpaperTile info : infos) { + if (info == null) { + LiveWallpaperListAdapter.this.notifyDataSetChanged(); + break; + } + if (info.mThumbnail != null) { + info.mThumbnail.setDither(true); + } + if (mWallpaperPosition < mWallpapers.size()) { + mWallpapers.set(mWallpaperPosition, info); + } else { + mWallpapers.add(info); + } + mWallpaperPosition++; + } + } + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/Partner.java b/src/org/cyanogenmod/wallpaperpicker/Partner.java new file mode 100644 index 0000000..0e0d7d0 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/Partner.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.wallpaperpicker; + +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.util.Pair; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Utilities to discover and interact with partner customizations. There can + * only be one set of customizations on a device, and it must be bundled with + * the system. + */ +public class Partner { + + static final String TAG = "Launcher.Partner"; + + /** Marker action used to discover partner */ + private static final String + ACTION_PARTNER_CUSTOMIZATION = "com.android.launcher3.action.PARTNER_CUSTOMIZATION"; + + public static final String RES_FOLDER = "partner_folder"; + public static final String RES_WALLPAPERS = "partner_wallpapers"; + + public static final String RES_DEFAULT_WALLPAPER_HIDDEN = "default_wallpapper_hidden"; + public static final String RES_SYSTEM_WALLPAPER_DIR = "system_wallpaper_directory"; + + private static boolean sSearched = false; + private static List<Partner> sPartners; + + static { + sPartners = new ArrayList<Partner>(); + } + + /** + * Find and return first partner details, or {@code null} if none exists. + */ + public static synchronized Partner get(PackageManager pm) { + getAllPartners(pm); + return sPartners.size() > 0 ? sPartners.get(0) : null; + } + + /** + * Find and return all partner details, or {@code null} if none exists. + */ + public static synchronized List<Partner> getAllPartners(PackageManager pm) { + if (!sSearched) { + List<Pair<String, Resources>> apkInfos = + Utilities.findSystemApks(ACTION_PARTNER_CUSTOMIZATION, pm); + for (Pair<String, Resources> apkInfo : apkInfos) { + sPartners.add(new Partner(apkInfo.first, apkInfo.second)); + } + sSearched = true; + } + return sPartners; + } + + private final String mPackageName; + private final Resources mResources; + + private Partner(String packageName, Resources res) { + mPackageName = packageName; + mResources = res; + } + + public String getPackageName() { + return mPackageName; + } + + public Resources getResources() { + return mResources; + } + + public boolean hasFolder() { + int folder = getResources().getIdentifier(Partner.RES_FOLDER, + "xml", getPackageName()); + return folder != 0; + } + + public boolean hideDefaultWallpaper() { + int resId = getResources().getIdentifier(RES_DEFAULT_WALLPAPER_HIDDEN, "bool", + getPackageName()); + return resId != 0 && getResources().getBoolean(resId); + } + + public File getWallpaperDirectory() { + int resId = getResources().getIdentifier(RES_SYSTEM_WALLPAPER_DIR, "string", + getPackageName()); + return (resId != 0) ? new File(getResources().getString(resId)) : null; + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/PickerFiles.java b/src/org/cyanogenmod/wallpaperpicker/PickerFiles.java new file mode 100644 index 0000000..eb3485e --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/PickerFiles.java @@ -0,0 +1,30 @@ +package org.cyanogenmod.wallpaperpicker; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Central list of files the Launcher writes to the application data directory. + * + * To add a new Launcher file, create a String constant referring to the filename, and add it to + * ALL_FILES, as shown below. + */ +public class PickerFiles { + + private static final String XML = ".xml"; + + public static final String DEFAULT_WALLPAPER_THUMBNAIL = "default_thumb2.jpg"; + public static final String DEFAULT_WALLPAPER_THUMBNAIL_OLD = "default_thumb.jpg"; + public static final String WALLPAPER_CROP_PREFERENCES_KEY = + "org.cyanogenmod.wallpaperpicker.WallpaperCropActivity"; + + public static final String WALLPAPER_IMAGES_DB = "saved_wallpaper_images.db"; + + public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList( + DEFAULT_WALLPAPER_THUMBNAIL, + DEFAULT_WALLPAPER_THUMBNAIL_OLD, + WALLPAPER_CROP_PREFERENCES_KEY + XML, + WALLPAPER_IMAGES_DB)); + +} diff --git a/src/org/cyanogenmod/wallpaperpicker/SavedWallpaperImages.java b/src/org/cyanogenmod/wallpaperpicker/SavedWallpaperImages.java new file mode 100644 index 0000000..c3e4bb4 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/SavedWallpaperImages.java @@ -0,0 +1,227 @@ +/* + * 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 org.cyanogenmod.wallpaperpicker; + +import android.content.ContentValues; +import android.content.Context; +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.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListAdapter; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; + + +public class SavedWallpaperImages extends BaseAdapter implements ListAdapter { + private static String TAG = "SavedWallpaperImages"; + private ImageDb mDb; + private boolean mIsLockScreenWallpaper; + ArrayList<SavedWallpaperTile> mImages; + Context mContext; + LayoutInflater mLayoutInflater; + + public static class SavedWallpaperTile extends WallpaperPickerActivity.FileWallpaperInfo { + private int mDbId; + public SavedWallpaperTile(int dbId, File target, Drawable thumb, + boolean isLockScreenWallpaper) { + super(target, thumb, isLockScreenWallpaper); + mDbId = dbId; + } + + @Override + public void onDelete(WallpaperPickerActivity a) { + a.getSavedImages().deleteImage(mDbId); + } + } + + public SavedWallpaperImages(Context context, boolean isLockScreenWallpaper) { + // We used to store the saved images in the cache directory, but that meant they'd get + // deleted sometimes-- move them to the data directory + ImageDb.moveFromCacheDirectoryIfNecessary(context); + mDb = new ImageDb(context); + mContext = context; + mLayoutInflater = LayoutInflater.from(context); + mIsLockScreenWallpaper = isLockScreenWallpaper; + } + + public void loadThumbnailsAndImageIdList() { + mImages = new ArrayList<SavedWallpaperTile>(); + SQLiteDatabase db = mDb.getReadableDatabase(); + Cursor result = db.query(ImageDb.TABLE_NAME, + new String[] { ImageDb.COLUMN_ID, + ImageDb.COLUMN_IMAGE_THUMBNAIL_FILENAME, + ImageDb.COLUMN_IMAGE_FILENAME}, // cols to return + null, // select query + null, // args to select query + null, + null, + ImageDb.COLUMN_ID + " DESC", + null); + + while (result.moveToNext()) { + String filename = result.getString(1); + File file = new File(mContext.getFilesDir(), filename); + + Bitmap thumb = BitmapFactory.decodeFile(file.getAbsolutePath()); + if (thumb != null) { + mImages.add(new SavedWallpaperTile(result.getInt(0), + new File(mContext.getFilesDir(), result.getString(2)), + new BitmapDrawable(thumb), mIsLockScreenWallpaper)); + } + } + result.close(); + } + + public int getCount() { + return mImages.size(); + } + + public SavedWallpaperTile getItem(int position) { + return mImages.get(position); + } + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + Drawable thumbDrawable = mImages.get(position).mThumb; + if (thumbDrawable == null) { + Log.e(TAG, "Error decoding thumbnail for wallpaper #" + position); + } + return WallpaperPickerActivity.createImageTileView( + mLayoutInflater, convertView, parent, thumbDrawable); + } + + private Pair<String, String> getImageFilenames(int id) { + SQLiteDatabase db = mDb.getReadableDatabase(); + Cursor result = db.query(ImageDb.TABLE_NAME, + new String[] { ImageDb.COLUMN_IMAGE_THUMBNAIL_FILENAME, + ImageDb.COLUMN_IMAGE_FILENAME }, // cols to return + ImageDb.COLUMN_ID + " = ?", // select query + new String[] { Integer.toString(id) }, // args to select query + null, + null, + null, + null); + if (result.getCount() > 0) { + result.moveToFirst(); + String thumbFilename = result.getString(0); + String imageFilename = result.getString(1); + result.close(); + return new Pair<String, String>(thumbFilename, imageFilename); + } else { + return null; + } + } + + public void deleteImage(int id) { + Pair<String, String> filenames = getImageFilenames(id); + File imageFile = new File(mContext.getFilesDir(), filenames.first); + imageFile.delete(); + File thumbFile = new File(mContext.getFilesDir(), filenames.second); + thumbFile.delete(); + SQLiteDatabase db = mDb.getWritableDatabase(); + db.delete(ImageDb.TABLE_NAME, + ImageDb.COLUMN_ID + " = ?", // SELECT query + new String[] { + Integer.toString(id) // args to SELECT query + }); + } + + public void writeImage(Bitmap thumbnail, byte[] imageBytes) { + try { + File imageFile = File.createTempFile("wallpaper", "", mContext.getFilesDir()); + FileOutputStream imageFileStream = + mContext.openFileOutput(imageFile.getName(), Context.MODE_PRIVATE); + imageFileStream.write(imageBytes); + imageFileStream.close(); + + File thumbFile = File.createTempFile("wallpaperthumb", "", mContext.getFilesDir()); + FileOutputStream thumbFileStream = + mContext.openFileOutput(thumbFile.getName(), Context.MODE_PRIVATE); + thumbnail.compress(Bitmap.CompressFormat.JPEG, 95, thumbFileStream); + thumbFileStream.close(); + + SQLiteDatabase db = mDb.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(ImageDb.COLUMN_IMAGE_THUMBNAIL_FILENAME, thumbFile.getName()); + values.put(ImageDb.COLUMN_IMAGE_FILENAME, imageFile.getName()); + db.insert(ImageDb.TABLE_NAME, null, values); + } catch (IOException e) { + Log.e(TAG, "Failed writing images to storage " + e); + } + } + + static class ImageDb extends SQLiteOpenHelper { + final static int DB_VERSION = 1; + final static String TABLE_NAME = "saved_wallpaper_images"; + final static String COLUMN_ID = "id"; + final static String COLUMN_IMAGE_THUMBNAIL_FILENAME = "image_thumbnail"; + final static String COLUMN_IMAGE_FILENAME = "image"; + + Context mContext; + + public ImageDb(Context context) { + super(context, context.getDatabasePath(PickerFiles.WALLPAPER_IMAGES_DB).getPath(), + null, DB_VERSION); + // Store the context for later use + mContext = context; + } + + public static void moveFromCacheDirectoryIfNecessary(Context context) { + // We used to store the saved images in the cache directory, but that meant they'd get + // deleted sometimes-- move them to the data directory + File oldSavedImagesFile = new File(context.getCacheDir(), + PickerFiles.WALLPAPER_IMAGES_DB); + File savedImagesFile = context.getDatabasePath(PickerFiles.WALLPAPER_IMAGES_DB); + if (oldSavedImagesFile.exists()) { + oldSavedImagesFile.renameTo(savedImagesFile); + } + } + @Override + public void onCreate(SQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + + COLUMN_ID + " INTEGER NOT NULL, " + + COLUMN_IMAGE_THUMBNAIL_FILENAME + " TEXT NOT NULL, " + + COLUMN_IMAGE_FILENAME + " TEXT NOT NULL, " + + "PRIMARY KEY (" + COLUMN_ID + " ASC) " + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + // Delete all the records; they'll be repopulated as this is a cache + db.execSQL("DELETE FROM " + TABLE_NAME); + } + } + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/StylusEventHelper.java b/src/org/cyanogenmod/wallpaperpicker/StylusEventHelper.java new file mode 100644 index 0000000..d6d9649 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/StylusEventHelper.java @@ -0,0 +1,82 @@ +package org.cyanogenmod.wallpaperpicker; + +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * Helper for identifying when a stylus touches a view while the primary stylus button is pressed. + * This can occur in {@value MotionEvent#ACTION_DOWN} or {@value MotionEvent#ACTION_MOVE}. On a + * stylus button press this performs the view's {@link View#performLongClick()} method, if the view + * is long clickable. + */ +public class StylusEventHelper { + private boolean mIsButtonPressed; + private View mView; + + public StylusEventHelper(View view) { + mView = view; + } + + /** + * Call this in onTouchEvent method of a view to identify a stylus button press and perform a + * long click (if the view is long clickable). + * + * @param event The event to check for a stylus button press. + * @return Whether a stylus event occurred and was handled. + */ + public boolean checkAndPerformStylusEvent(MotionEvent event) { + final float slop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop(); + + if (!mView.isLongClickable()) { + // We don't do anything unless the view is long clickable. + return false; + } + + final boolean stylusButtonPressed = isStylusButtonPressed(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mIsButtonPressed = false; + if (stylusButtonPressed && mView.performLongClick()) { + mIsButtonPressed = true; + return true; + } + break; + case MotionEvent.ACTION_MOVE: + if (Utilities.pointInView(mView, event.getX(), event.getY(), slop)) { + if (!mIsButtonPressed && stylusButtonPressed && mView.performLongClick()) { + mIsButtonPressed = true; + return true; + } else if (mIsButtonPressed && !stylusButtonPressed) { + mIsButtonPressed = false; + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsButtonPressed = false; + break; + } + return false; + } + + /** + * Whether a stylus button press is occurring. + */ + public boolean inStylusButtonPressed() { + return mIsButtonPressed; + } + + /** + * Identifies if the provided {@link MotionEvent} is a stylus with the primary stylus button + * pressed. + * + * @param event The event to check. + * @return Whether a stylus button press occurred. + */ + private static boolean isStylusButtonPressed(MotionEvent event) { + return event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS + && ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) + == MotionEvent.BUTTON_SECONDARY); + } +}
\ No newline at end of file diff --git a/src/org/cyanogenmod/wallpaperpicker/ThirdPartyWallpaperPickerListAdapter.java b/src/org/cyanogenmod/wallpaperpicker/ThirdPartyWallpaperPickerListAdapter.java new file mode 100644 index 0000000..25465aa --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/ThirdPartyWallpaperPickerListAdapter.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.wallpaperpicker; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListAdapter; +import android.widget.TextView; + +import org.cyanogenmod.wallpaperpicker.util.Thunk; + +import java.util.ArrayList; +import java.util.List; + +public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements ListAdapter { + private final LayoutInflater mInflater; + private final PackageManager mPackageManager; + private final int mIconSize; + + private List<ThirdPartyWallpaperTile> mThirdPartyWallpaperPickers = + new ArrayList<ThirdPartyWallpaperTile>(); + + public static class ThirdPartyWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo { + @Thunk + ResolveInfo mResolveInfo; + public ThirdPartyWallpaperTile(ResolveInfo resolveInfo) { + mResolveInfo = resolveInfo; + } + @Override + public void onClick(WallpaperPickerActivity a) { + final ComponentName itemComponentName = new ComponentName( + mResolveInfo.activityInfo.packageName, mResolveInfo.activityInfo.name); + Intent launchIntent = new Intent(Intent.ACTION_SET_WALLPAPER); + launchIntent.setComponent(itemComponentName); + a.startActivityForResultSafely( + launchIntent, WallpaperPickerActivity.PICK_WALLPAPER_THIRD_PARTY_ACTIVITY); + } + } + + public ThirdPartyWallpaperPickerListAdapter(Context context) { + mInflater = LayoutInflater.from(context); + mPackageManager = context.getPackageManager(); + mIconSize = context.getResources().getDimensionPixelSize(R.dimen.wallpaperItemIconSize); + final PackageManager pm = mPackageManager; + + final Intent pickWallpaperIntent = new Intent(Intent.ACTION_SET_WALLPAPER); + final List<ResolveInfo> apps = + pm.queryIntentActivities(pickWallpaperIntent, 0); + + // Get list of image picker intents + Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT); + pickImageIntent.setType("image/*"); + final List<ResolveInfo> imagePickerActivities = + pm.queryIntentActivities(pickImageIntent, 0); + final ComponentName[] imageActivities = new ComponentName[imagePickerActivities.size()]; + for (int i = 0; i < imagePickerActivities.size(); i++) { + ActivityInfo activityInfo = imagePickerActivities.get(i).activityInfo; + imageActivities[i] = new ComponentName(activityInfo.packageName, activityInfo.name); + } + + outerLoop: + for (ResolveInfo info : apps) { + final ComponentName itemComponentName = + new ComponentName(info.activityInfo.packageName, info.activityInfo.name); + final String itemPackageName = itemComponentName.getPackageName(); + // Exclude anything from our own package, and the old Launcher, + // and live wallpaper picker + if (itemPackageName.equals(context.getPackageName()) || + itemPackageName.equals("com.android.launcher") || + itemPackageName.equals("com.android.wallpaper.livepicker")) { + continue; + } + // Exclude any package that already responds to the image picker intent + for (ResolveInfo imagePickerActivityInfo : imagePickerActivities) { + if (itemPackageName.equals( + imagePickerActivityInfo.activityInfo.packageName)) { + continue outerLoop; + } + } + mThirdPartyWallpaperPickers.add(new ThirdPartyWallpaperTile(info)); + } + } + + public int getCount() { + return mThirdPartyWallpaperPickers.size(); + } + + public ThirdPartyWallpaperTile getItem(int position) { + return mThirdPartyWallpaperPickers.get(position); + } + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + View view; + + if (convertView == null) { + view = mInflater.inflate(R.layout.wallpaper_picker_third_party_item, parent, false); + } else { + view = convertView; + } + + ResolveInfo info = mThirdPartyWallpaperPickers.get(position).mResolveInfo; + TextView label = (TextView) view.findViewById(R.id.wallpaper_item_label); + label.setText(info.loadLabel(mPackageManager)); + Drawable icon = info.loadIcon(mPackageManager); + icon.setBounds(new Rect(0, 0, mIconSize, mIconSize)); + label.setCompoundDrawables(null, icon, null, null); + return view; + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/Utilities.java b/src/org/cyanogenmod/wallpaperpicker/Utilities.java new file mode 100644 index 0000000..21a22be --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/Utilities.java @@ -0,0 +1,549 @@ +/* + * 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 org.cyanogenmod.wallpaperpicker; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.View; +import android.widget.Toast; +import org.cyanogenmod.wallpaperpicker.util.WallpaperUtils; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Various utilities shared amongst the Launcher's classes. + */ +public final class Utilities { + + private static final String TAG = "WallpaperPicker.Utilities"; + + private static final float WALLPAPER_SCREENS_SPAN = 2f; + + private static final Rect sOldBounds = new Rect(); + private static final Canvas sCanvas = new Canvas(); + + private static final Pattern sTrimPattern = + Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); + + static { + sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, + Paint.FILTER_BITMAP_FLAG)); + } + static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff }; + static int sColorIndex = 0; + + private static final int[] sLoc0 = new int[2]; + private static final int[] sLoc1 = new int[2]; + + // TODO: use Build.VERSION_CODES when available + public static final boolean ATLEAST_MARSHMALLOW = Build.VERSION.SDK_INT >= 23; + + public static final boolean ATLEAST_LOLLIPOP_MR1 = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1; + + public static final boolean ATLEAST_LOLLIPOP = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + + public static final boolean ATLEAST_KITKAT = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + public static final boolean ATLEAST_JB_MR1 = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; + + public static final boolean ATLEAST_JB_MR2 = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; + + // To turn on these properties, type + // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] + private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; + private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); + + public static boolean isPropertyEnabled(String propertyName) { + return Log.isLoggable(propertyName, Log.VERBOSE); + } + + /** + * Utility method to determine whether the given point, in local coordinates, + * is inside the view, where the area of the view is expanded by the slop factor. + * This method is called while processing touch-move events to determine if the event + * is still within the view. + */ + public static boolean pointInView(View v, float localX, float localY, float slop) { + return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) && + localY < (v.getHeight() + slop); + } + + public static void scaleRect(Rect r, float scale) { + if (scale != 1.0f) { + r.left = (int) (r.left * scale + 0.5f); + r.top = (int) (r.top * scale + 0.5f); + r.right = (int) (r.right * scale + 0.5f); + r.bottom = (int) (r.bottom * scale + 0.5f); + } + } + + public static int[] getCenterDeltaInScreenSpace(View v0, View v1, int[] delta) { + v0.getLocationInWindow(sLoc0); + v1.getLocationInWindow(sLoc1); + + sLoc0[0] += (v0.getMeasuredWidth() * v0.getScaleX()) / 2; + sLoc0[1] += (v0.getMeasuredHeight() * v0.getScaleY()) / 2; + sLoc1[0] += (v1.getMeasuredWidth() * v1.getScaleX()) / 2; + sLoc1[1] += (v1.getMeasuredHeight() * v1.getScaleY()) / 2; + + if (delta == null) { + delta = new int[2]; + } + + delta[0] = sLoc1[0] - sLoc0[0]; + delta[1] = sLoc1[1] - sLoc0[1]; + + return delta; + } + + public static void scaleRectAboutCenter(Rect r, float scale) { + int cx = r.centerX(); + int cy = r.centerY(); + r.offset(-cx, -cy); + Utilities.scaleRect(r, scale); + r.offset(cx, cy); + } + + static boolean isSystemApp(Context context, Intent intent) { + PackageManager pm = context.getPackageManager(); + ComponentName cn = intent.getComponent(); + String packageName = null; + if (cn == null) { + ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + if ((info != null) && (info.activityInfo != null)) { + packageName = info.activityInfo.packageName; + } + } else { + packageName = cn.getPackageName(); + } + if (packageName != null) { + try { + PackageInfo info = pm.getPackageInfo(packageName, 0); + return (info != null) && (info.applicationInfo != null) && + ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); + } catch (NameNotFoundException e) { + return false; + } + } else { + return false; + } + } + + /** + * This picks a dominant color, looking for high-saturation, high-value, repeated hues. + * @param bitmap The bitmap to scan + * @param samples The approximate max number of samples to use. + */ + static int findDominantColorByHue(Bitmap bitmap, int samples) { + final int height = bitmap.getHeight(); + final int width = bitmap.getWidth(); + int sampleStride = (int) Math.sqrt((height * width) / samples); + if (sampleStride < 1) { + sampleStride = 1; + } + + // This is an out-param, for getting the hsv values for an rgb + float[] hsv = new float[3]; + + // First get the best hue, by creating a histogram over 360 hue buckets, + // where each pixel contributes a score weighted by saturation, value, and alpha. + float[] hueScoreHistogram = new float[360]; + float highScore = -1; + int bestHue = -1; + + for (int y = 0; y < height; y += sampleStride) { + for (int x = 0; x < width; x += sampleStride) { + int argb = bitmap.getPixel(x, y); + int alpha = 0xFF & (argb >> 24); + if (alpha < 0x80) { + // Drop mostly-transparent pixels. + continue; + } + // Remove the alpha channel. + int rgb = argb | 0xFF000000; + Color.colorToHSV(rgb, hsv); + // Bucket colors by the 360 integer hues. + int hue = (int) hsv[0]; + if (hue < 0 || hue >= hueScoreHistogram.length) { + // Defensively avoid array bounds violations. + continue; + } + float score = hsv[1] * hsv[2]; + hueScoreHistogram[hue] += score; + if (hueScoreHistogram[hue] > highScore) { + highScore = hueScoreHistogram[hue]; + bestHue = hue; + } + } + } + + SparseArray<Float> rgbScores = new SparseArray<Float>(); + int bestColor = 0xff000000; + highScore = -1; + // Go back over the RGB colors that match the winning hue, + // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. + // The highest-scoring RGB color wins. + for (int y = 0; y < height; y += sampleStride) { + for (int x = 0; x < width; x += sampleStride) { + int rgb = bitmap.getPixel(x, y) | 0xff000000; + Color.colorToHSV(rgb, hsv); + int hue = (int) hsv[0]; + if (hue == bestHue) { + float s = hsv[1]; + float v = hsv[2]; + int bucket = (int) (s * 100) + (int) (v * 10000); + // Score by cumulative saturation * value. + float score = s * v; + Float oldTotal = rgbScores.get(bucket); + float newTotal = oldTotal == null ? score : oldTotal + score; + rgbScores.put(bucket, newTotal); + if (newTotal > highScore) { + highScore = newTotal; + // All the colors in the winning bucket are very similar. Last in wins. + bestColor = rgb; + } + } + } + } + return bestColor; + } + + /* + * Finds a system apk which had a broadcast receiver listening to a particular action. + * @param action intent action used to find the apk + * @return a pair of apk package name and the resources. + */ + static Pair<String, Resources> findSystemApk(String action, PackageManager pm) { + final Intent intent = new Intent(action); + for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { + if (info.activityInfo != null && + (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + final String packageName = info.activityInfo.packageName; + try { + final Resources res = pm.getResourcesForApplication(packageName); + return Pair.create(packageName, res); + } catch (NameNotFoundException e) { + Log.w(TAG, "Failed to find resources for " + packageName); + } + } + } + return null; + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static boolean isViewAttachedToWindow(View v) { + if (ATLEAST_KITKAT) { + return v.isAttachedToWindow(); + } else { + // A proxy call which returns null, if the view is not attached to the window. + return v.getKeyDispatcherState() != null; + } + } + + /* + * Finds all system apks which had a broadcast receiver listening to a particular action. + * @param action intent action used to find the apk + * @return a list of pairs of apk package name and the resources. + */ + static List<Pair<String, Resources>> findSystemApks(String action, PackageManager pm) { + final Intent intent = new Intent(action); + List<Pair<String, Resources>> systemApks = new ArrayList<Pair<String, Resources>>(); + for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { + if (info.activityInfo != null && + (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + final String packageName = info.activityInfo.packageName; + try { + final Resources res = pm.getResourcesForApplication(packageName); + systemApks.add(Pair.create(packageName, res)); + } catch (NameNotFoundException e) { + Log.w(TAG, "Failed to find resources for " + packageName); + } + } + } + return systemApks; + } + + /** + * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX} + * provided by the same package which is set to be global search activity. + * If widgetCategory is not supported, or no such widget is found, returns the first widget + * provided by the package. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static AppWidgetProviderInfo getSearchWidgetProvider(Context context) { + SearchManager searchManager = + (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); + ComponentName searchComponent = searchManager.getGlobalSearchActivity(); + if (searchComponent == null) return null; + String providerPkg = searchComponent.getPackageName(); + + AppWidgetProviderInfo defaultWidgetForSearchPackage = null; + + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) { + if (info.provider.getPackageName().equals(providerPkg)) { + if (ATLEAST_JB_MR1) { + if ((info.widgetCategory & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) { + return info; + } else if (defaultWidgetForSearchPackage == null) { + defaultWidgetForSearchPackage = info; + } + } else { + return info; + } + } + } + return defaultWidgetForSearchPackage; + } + + /** + * Compresses the bitmap to a byte array for serialization. + */ + public static byte[] flattenBitmap(Bitmap bitmap) { + // Try go guesstimate how much space the icon will take when serialized + // to avoid unnecessary allocations/copies during the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w(TAG, "Could not write bitmap"); + return null; + } + } + + /** + * Find the first vacant cell, if there is one. + * + * @param vacant Holds the x and y coordinate of the vacant cell + * @param spanX Horizontal cell span. + * @param spanY Vertical cell span. + * + * @return true if a vacant cell was found + */ + public static boolean findVacantCell(int[] vacant, int spanX, int spanY, + int xCount, int yCount, boolean[][] occupied) { + + for (int y = 0; (y + spanY) <= yCount; y++) { + for (int x = 0; (x + spanX) <= xCount; x++) { + boolean available = !occupied[x][y]; + out: for (int i = x; i < x + spanX; i++) { + for (int j = y; j < y + spanY; j++) { + available = available && !occupied[i][j]; + if (!available) break out; + } + } + + if (available) { + vacant[0] = x; + vacant[1] = y; + return true; + } + } + } + + return false; + } + + /** + * Trims the string, removing all whitespace at the beginning and end of the string. + * Non-breaking whitespaces are also removed. + */ + public static String trim(CharSequence s) { + if (s == null) { + return null; + } + + // Just strip any sequence of whitespace or java space characters from the beginning and end + Matcher m = sTrimPattern.matcher(s); + return m.replaceAll("$1"); + } + + /** + * Calculates the height of a given string at a specific text size. + */ + public static float calculateTextHeight(float textSizePx) { + Paint p = new Paint(); + p.setTextSize(textSizePx); + Paint.FontMetrics fm = p.getFontMetrics(); + return -fm.top + fm.bottom; + } + + /** + * Convenience println with multiple args. + */ + public static void println(String key, Object... args) { + StringBuilder b = new StringBuilder(); + b.append(key); + b.append(": "); + boolean isFirstArgument = true; + for (Object arg : args) { + if (isFirstArgument) { + isFirstArgument = false; + } else { + b.append(", "); + } + b.append(arg); + } + System.out.println(b.toString()); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isRtl(Resources res) { + return ATLEAST_JB_MR1 && + (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + } + + public static float dpiFromPx(int size, DisplayMetrics metrics){ + return (size / metrics.density); + } + public static int pxFromDp(float size, DisplayMetrics metrics) { + return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + size, metrics)); + } + public static int pxFromSp(float size, DisplayMetrics metrics) { + return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + size, metrics)); + } + + public static boolean isPackageInstalled(Context context, String pkg) { + PackageManager packageManager = context.getPackageManager(); + try { + PackageInfo pi = packageManager.getPackageInfo(pkg, 0); + return pi.applicationInfo.enabled; + } catch (NameNotFoundException e) { + return false; + } + } + + public static void startActivityForResultSafely( + Activity activity, Intent intent, int requestCode) { + try { + activity.startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + } catch (SecurityException e) { + Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Wallpaper picker does not have the permission to launch " + intent + + ". Make sure to create a MAIN intent-filter for the corresponding activity " + + "or use the exported attribute for this activity.", e); + } + } + + public static Bitmap getThemeWallpaper(Context context, String path, String pkgName, + boolean thumb) { + InputStream is = null; + try { + Resources res = context.getPackageManager().getResourcesForApplication(pkgName); + if (res == null) { + return null; + } + + AssetManager am = res.getAssets(); + String[] wallpapers = am.list(path); + if (wallpapers == null || wallpapers.length == 0) { + return null; + } + is = am.open(path + File.separator + wallpapers[0]); + + BitmapFactory.Options bounds = new BitmapFactory.Options(); + bounds.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, bounds); + if ((bounds.outWidth == -1) || (bounds.outHeight == -1)) + return null; + + int originalSize = (bounds.outHeight > bounds.outWidth) ? bounds.outHeight + : bounds.outWidth; + Point outSize; + + if (thumb) { + outSize = getDefaultThumbnailSize(context.getResources()); + } else { + outSize = WallpaperUtils.getDefaultWallpaperSize(res, + ((Activity) context).getWindowManager()); + } + int thumbSampleSize = (outSize.y > outSize.x) ? outSize.y : outSize.x; + + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inSampleSize = originalSize / thumbSampleSize; + return BitmapFactory.decodeStream(is, null, opts); + } catch (IOException e) { + return null; + } catch (PackageManager.NameNotFoundException e) { + return null; + } catch (OutOfMemoryError e) { + return null; + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + } + + public static Point getDefaultThumbnailSize(Resources res) { + return new Point(res.getDimensionPixelSize(R.dimen.wallpaperThumbnailWidth), + res.getDimensionPixelSize(R.dimen.wallpaperThumbnailHeight)); + + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/WallpaperCropActivity.java b/src/org/cyanogenmod/wallpaperpicker/WallpaperCropActivity.java new file mode 100644 index 0000000..945ccac --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/WallpaperCropActivity.java @@ -0,0 +1,540 @@ +/* + * 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 org.cyanogenmod.wallpaperpicker; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.Activity; +import android.app.WallpaperManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.RectF; +import android.net.Uri; +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.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.BitmapRegionTileSource.BitmapSource.InBitmapProvider; +import com.android.photos.views.TiledImageRenderer.TileSource; + +import org.cyanogenmod.wallpaperpicker.base.BaseActivity; +import org.cyanogenmod.wallpaperpicker.util.Thunk; +import org.cyanogenmod.wallpaperpicker.util.WallpaperUtils; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +public class WallpaperCropActivity extends BaseActivity implements Handler.Callback { + private static final String LOGTAG = "CropActivity"; + + private static final int REQUEST_CODE_STORAGE_PERMISSION_CHECK = 100; + + protected static final String WALLPAPER_WIDTH_KEY = WallpaperUtils.WALLPAPER_WIDTH_KEY; + protected static final String WALLPAPER_HEIGHT_KEY = WallpaperUtils.WALLPAPER_HEIGHT_KEY; + + /** + * The maximum bitmap size we allow to be returned through the intent. + * Intents have a maximum of 1MB in total size. However, the Bitmap seems to + * have some overhead to hit so that we go way below the limit here to make + * sure the intent stays below 1MB.We should consider just returning a byte + * array instead of a Bitmap instance to avoid overhead. + */ + public static final int MAX_BMAP_IN_INTENT = 750000; + public static final float WALLPAPER_SCREENS_SPAN = WallpaperUtils.WALLPAPER_SCREENS_SPAN; + + private static final int MSG_LOAD_IMAGE = 1; + + protected CropView mCropView; + protected View mProgressView; + protected Uri mUri; + protected View mSetWallpaperButton; + + private HandlerThread mLoaderThread; + private Handler mLoaderHandler; + @Thunk + LoadRequest mCurrentLoadRequest; + private byte[] mTempStorageForDecoding = new byte[16 * 1024]; + // A weak-set of reusable bitmaps + @Thunk Set<Bitmap> mReusableBitmaps = + Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>()); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (!hasStoragePermissions()) { + requestStoragePermissions(); + } else { + load(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + if (requestCode == REQUEST_CODE_STORAGE_PERMISSION_CHECK) { + for (int i = 0; i < permissions.length; i++ ) { + final String permission = permissions[i]; + final int grantResult = grantResults[i]; + if (permission.equals(Manifest.permission.READ_EXTERNAL_STORAGE)) { + if (grantResult == PackageManager.PERMISSION_GRANTED) { + load(); + } else { + Toast.makeText(this, getString(R.string.storage_permission_denied), + Toast.LENGTH_SHORT).show(); + finish(); + } + } + } + } + } + + private void load() { + mLoaderThread = new HandlerThread("wallpaper_loader"); + mLoaderThread.start(); + mLoaderHandler = new Handler(mLoaderThread.getLooper(), this); + + init(); + } + + protected void init() { + setContentView(R.layout.wallpaper_cropper); + + mCropView = (CropView) findViewById(R.id.cropView); + mProgressView = findViewById(R.id.loading); + + Intent cropIntent = getIntent(); + final Uri imageUri = cropIntent.getData(); + + if (imageUri == null) { + Log.e(LOGTAG, "No URI passed in intent, exiting WallpaperCropActivity"); + finish(); + return; + } + + // Action bar + // Show the custom action bar view + final ActionBar actionBar = getActionBar(); + actionBar.setCustomView(R.layout.actionbar_set_wallpaper); + actionBar.getCustomView().setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + boolean finishActivityWhenDone = true; + cropImageAndSetWallpaper(imageUri, null, false, finishActivityWhenDone); + } + }); + mSetWallpaperButton = findViewById(R.id.set_wallpaper_button); + + // Load image in background + final BitmapRegionTileSource.UriBitmapSource bitmapSource = + new BitmapRegionTileSource.UriBitmapSource(getContext(), imageUri); + mSetWallpaperButton.setEnabled(false); + Runnable onLoad = new Runnable() { + public void run() { + if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) { + Toast.makeText(getContext(), R.string.wallpaper_load_fail, + Toast.LENGTH_LONG).show(); + finish(); + } else { + mSetWallpaperButton.setEnabled(true); + } + } + }; + setCropViewTileSource(bitmapSource, true, false, null, onLoad); + } + + @Override + public void onDestroy() { + if (mCropView != null) { + mCropView.destroy(); + } + if (mLoaderThread != null) { + mLoaderThread.quit(); + } + super.onDestroy(); + } + + /** + * 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 (isActivityDestroyed()) { + // 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; + } + } + + req.result = new BitmapRegionTileSource(getContext(), 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); + } + } + }); + return true; + } + return false; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + protected boolean isActivityDestroyed() { + return Utilities.ATLEAST_JB_MR1 && isDestroyed(); + } + + @Thunk void addReusableBitmap(TileSource src) { + synchronized (mReusableBitmaps) { + if (Utilities.ATLEAST_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(); + } + mProgressView.setVisibility(View.GONE); + } + + 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) + mProgressView.postDelayed(new Runnable() { + public void run() { + if (mCurrentLoadRequest == req) { + mProgressView.setVisibility(View.VISIBLE); + } + } + }, 1000); + } + + + protected void setWallpaper(Uri uri, boolean isLockScreenPicker, + final boolean finishActivityWhenDone) { + int rotation = BitmapUtils.getRotationFromExif(getContext(), uri); + BitmapCropTask cropTask = new BitmapCropTask( + getContext(), uri, null, rotation, 0, 0, true, isLockScreenPicker, false, null); + final Point bounds = cropTask.getImageBounds(); + Runnable onEndCrop = new Runnable() { + public void run() { + updateWallpaperDimensions(bounds.x, bounds.y); + if (finishActivityWhenDone) { + setResult(Activity.RESULT_OK); + finish(); + } + } + }; + cropTask.setOnEndRunnable(onEndCrop); + cropTask.setNoCrop(true); + cropTask.execute(); + } + + protected void cropImageAndSetWallpaper(Resources res, int resId, boolean isLockScreenPicker, + final boolean finishActivityWhenDone) { + // crop this image and scale it down to the default wallpaper size for + // this device + int rotation = BitmapUtils.getRotationFromExif(res, resId); + Point inSize = mCropView.getSourceDimensions(); + Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), + getWindowManager()); + RectF crop = Utils.getMaxCropRect( + inSize.x, inSize.y, outSize.x, outSize.y, false); + Runnable onEndCrop = new Runnable() { + public void run() { + // Passing 0, 0 will cause launcher to revert to using the + // default wallpaper size + updateWallpaperDimensions(0, 0); + if (finishActivityWhenDone) { + setResult(Activity.RESULT_OK); + finish(); + } + } + }; + BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, resId, + crop, rotation, outSize.x, outSize.y, true, isLockScreenPicker, false, onEndCrop); + cropTask.execute(); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + protected void cropImageAndSetWallpaper(Uri uri, + BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler, + boolean isLockScreenPicker, final boolean finishActivityWhenDone) { + boolean centerCrop = getResources().getBoolean(R.bool.center_crop); + // Get the crop + boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; + + Display d = getWindowManager().getDefaultDisplay(); + + Point displaySize = new Point(); + d.getSize(displaySize); + boolean isPortrait = displaySize.x < displaySize.y; + + Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), + getWindowManager()); + // Get the crop + RectF cropRect = mCropView.getCrop(); + + Point inSize = mCropView.getSourceDimensions(); + + int cropRotation = mCropView.getImageRotation(); + float cropScale = mCropView.getWidth() / (float) cropRect.width(); + + + Matrix rotateMatrix = new Matrix(); + rotateMatrix.setRotate(cropRotation); + float[] rotatedInSize = new float[] { inSize.x, inSize.y }; + rotateMatrix.mapPoints(rotatedInSize); + rotatedInSize[0] = Math.abs(rotatedInSize[0]); + rotatedInSize[1] = Math.abs(rotatedInSize[1]); + + + // due to rounding errors in the cropview renderer the edges can be slightly offset + // therefore we ensure that the boundaries are sanely defined + cropRect.left = Math.max(0, cropRect.left); + cropRect.right = Math.min(rotatedInSize[0], cropRect.right); + cropRect.top = Math.max(0, cropRect.top); + cropRect.bottom = Math.min(rotatedInSize[1], cropRect.bottom); + + // ADJUST CROP WIDTH + // Extend the crop all the way to the right, for parallax + // (or all the way to the left, in RTL) + float extraSpace; + if (centerCrop) { + extraSpace = 2f * Math.min(rotatedInSize[0] - cropRect.right, cropRect.left); + } else { + extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left; + } + // Cap the amount of extra width + float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width(); + extraSpace = Math.min(extraSpace, maxExtraSpace); + + if (centerCrop) { + cropRect.left -= extraSpace / 2f; + cropRect.right += extraSpace / 2f; + } else { + if (ltr) { + cropRect.right += extraSpace; + } else { + cropRect.left -= extraSpace; + } + } + + // ADJUST CROP HEIGHT + if (isPortrait) { + cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale; + } else { // LANDSCAPE + float extraPortraitHeight = + defaultWallpaperSize.y / cropScale - cropRect.height(); + float expandHeight = + Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top), + extraPortraitHeight / 2); + cropRect.top -= expandHeight; + cropRect.bottom += expandHeight; + } + final int outWidth = (int) Math.round(cropRect.width() * cropScale); + final int outHeight = (int) Math.round(cropRect.height() * cropScale); + + Runnable onEndCrop = new Runnable() { + public void run() { + updateWallpaperDimensions(outWidth, outHeight); + if (finishActivityWhenDone) { + setResult(Activity.RESULT_OK); + finish(); + } + } + }; + BitmapCropTask cropTask = new BitmapCropTask(getContext(), uri, cropRect, cropRotation, + outWidth, outHeight, true, isLockScreenPicker, false, onEndCrop); + if (onBitmapCroppedHandler != null) { + cropTask.setOnBitmapCropped(onBitmapCroppedHandler); + } + cropTask.execute(); + } + + protected void cropImageAndSetWallpaper(String path, String packageName, + boolean isLockScreenPicker, final boolean finishActivityWhenDone) { + // crop this image and scale it down to the default wallpaper size for + // this device + Point inSize = mCropView.getSourceDimensions(); + Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), + getWindowManager()); + RectF cropRect = Utils.getMaxCropRect( + inSize.x, inSize.y, outSize.x, outSize.y, false); + + final Resources res; + try { + res = getPackageManager().getResourcesForApplication(packageName); + if (res == null) { + return; + } + } catch (PackageManager.NameNotFoundException e) { + return; + } + + Runnable onEndCrop = new Runnable() { + public void run() { + if (finishActivityWhenDone) { + setResult(Activity.RESULT_OK); + finish(); + } + } + }; + BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, path, cropRect, + 0, outSize.x, outSize.y, true, isLockScreenPicker, false, onEndCrop); + if (cropTask != null) { + cropTask.execute(); + } + } + + protected void updateWallpaperDimensions(int width, int height) { + String spKey = PickerFiles.WALLPAPER_CROP_PREFERENCES_KEY; + SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); + SharedPreferences.Editor editor = sp.edit(); + if (width != 0 && height != 0) { + editor.putInt(WALLPAPER_WIDTH_KEY, width); + editor.putInt(WALLPAPER_HEIGHT_KEY, height); + } else { + editor.remove(WALLPAPER_WIDTH_KEY); + editor.remove(WALLPAPER_HEIGHT_KEY); + } + editor.commit(); + WallpaperUtils.suggestWallpaperDimension(getResources(), + sp, getWindowManager(), WallpaperManager.getInstance(getContext()), true); + } + + private boolean hasStoragePermissions() { + return checkCallingOrSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) == + PackageManager.PERMISSION_GRANTED; + } + + private void requestStoragePermissions() { + requestPermissions(new String[] {android.Manifest.permission.READ_EXTERNAL_STORAGE}, + REQUEST_CODE_STORAGE_PERMISSION_CHECK); + } + + static class LoadRequest { + BitmapSource src; + boolean touchEnabled; + boolean moveToLeft; + Runnable postExecute; + CropViewScaleProvider scaleProvider; + + TileSource result; + } + + interface CropViewScaleProvider { + float getScale(TileSource src); + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/WallpaperPickerActivity.java b/src/org/cyanogenmod/wallpaperpicker/WallpaperPickerActivity.java new file mode 100644 index 0000000..a5c1d03 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/WallpaperPickerActivity.java @@ -0,0 +1,1410 @@ +/* + * 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 org.cyanogenmod.wallpaperpicker; + +import android.animation.LayoutTransition; +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.Activity; +import android.app.WallpaperManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.Manifest; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Process; +import android.provider.MediaStore; +import android.util.Log; +import android.util.Pair; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewGroup; +import android.view.ViewPropertyAnimator; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.FrameLayout; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +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 cyanogenmod.providers.ThemesContract; + +import org.cyanogenmod.wallpaperpicker.util.Thunk; +import org.cyanogenmod.wallpaperpicker.util.WallpaperUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.cyanogenmod.wallpaperpicker.util.Constants.THEME_LOCKSCREEN_PATH; +import static org.cyanogenmod.wallpaperpicker.util.Constants.THEME_WALLPAPER_PATH; + +public class WallpaperPickerActivity extends WallpaperCropActivity { + static final String TAG = "WallpaperPickerActivity"; + + public static final int IMAGE_PICK = 5; + public static final int PICK_WALLPAPER_THIRD_PARTY_ACTIVITY = 6; + private static final String TEMP_WALLPAPER_TILES = "TEMP_WALLPAPER_TILES"; + private static final String SELECTED_INDEX = "SELECTED_INDEX"; + private static final int FLAG_POST_DELAY_MILLIS = 200; + private static final String ACTION_SET_KEYGUARD_WALLPAPER = + "android.intent.action.SET_KEYGUARD_WALLPAPER"; + + @Thunk + View mSelectedTile; + @Thunk boolean mIgnoreNextTap; + @Thunk OnClickListener mThumbnailOnClickListener; + + @Thunk LinearLayout mWallpapersView; + @Thunk HorizontalScrollView mWallpaperScrollContainer; + @Thunk View mWallpaperStrip; + + @Thunk ActionMode.Callback mActionModeCallback; + @Thunk ActionMode mActionMode; + + @Thunk View.OnLongClickListener mLongClickListener; + + ArrayList<Uri> mTempWallpaperTiles = new ArrayList<Uri>(); + private SavedWallpaperImages mSavedImages; + @Thunk int mSelectedIndex = -1; + + private boolean mIsLockScreenPicker; + + public static abstract class WallpaperTileInfo { + protected View mView; + protected boolean mIsLockScreenWallpaper; + public Drawable mThumb; + + public void setView(View v) { + mView = v; + } + public void onClick(WallpaperPickerActivity a) {} + public void onSave(WallpaperPickerActivity a) {} + public void onDelete(WallpaperPickerActivity a) {} + public boolean isSelectable() { return false; } + public boolean isNamelessWallpaper() { return false; } + public void onIndexUpdated(CharSequence label) { + if (isNamelessWallpaper()) { + mView.setContentDescription(label); + } + } + } + + public static class PickImageInfo extends WallpaperTileInfo { + @Override + public void onClick(WallpaperPickerActivity a) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + a.startActivityForResultSafely(intent, IMAGE_PICK); + } + } + + public static class UriWallpaperInfo extends WallpaperTileInfo { + private Uri mUri; + public UriWallpaperInfo(Uri uri, boolean isLockScreenWallpaper) { + mUri = uri; + mIsLockScreenWallpaper = isLockScreenWallpaper; + } + @Override + public void onClick(final WallpaperPickerActivity a) { + a.setWallpaperButtonEnabled(false); + final BitmapRegionTileSource.UriBitmapSource bitmapSource = + new BitmapRegionTileSource.UriBitmapSource(a.getContext(), mUri); + a.setCropViewTileSource(bitmapSource, true, false, null, new Runnable() { + + @Override + public void run() { + if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + a.selectTile(mView); + a.setWallpaperButtonEnabled(true); + } else { + ViewGroup parent = (ViewGroup) mView.getParent(); + if (parent != null) { + parent.removeView(mView); + Toast.makeText(a.getContext(), R.string.image_load_fail, + Toast.LENGTH_SHORT).show(); + } + } + } + }); + } + @Override + public void onSave(final WallpaperPickerActivity a) { + boolean finishActivityWhenDone = true; + 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 + Bitmap thumb = createThumbnail( + thumbSize, null, null, imageBytes, null, 0, 0, true); + a.getSavedImages().writeImage(thumb, imageBytes); + } + }; + a.cropImageAndSetWallpaper(mUri, h, mIsLockScreenWallpaper, finishActivityWhenDone); + } + @Override + public boolean isSelectable() { + return true; + } + @Override + public boolean isNamelessWallpaper() { + return true; + } + } + + public static class FileWallpaperInfo extends WallpaperTileInfo { + private File mFile; + + public FileWallpaperInfo(File target, Drawable thumb, boolean isLockScreenWallpaper) { + mFile = target; + mThumb = thumb; + mIsLockScreenWallpaper = isLockScreenWallpaper; + } + @Override + public void onClick(final WallpaperPickerActivity a) { + a.setWallpaperButtonEnabled(false); + final BitmapRegionTileSource.UriBitmapSource bitmapSource = + new BitmapRegionTileSource.UriBitmapSource(a.getContext(), Uri.fromFile(mFile)); + a.setCropViewTileSource(bitmapSource, false, true, null, new Runnable() { + + @Override + public void run() { + if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + a.setWallpaperButtonEnabled(true); + } + } + }); + } + @Override + public void onSave(WallpaperPickerActivity a) { + a.setWallpaper(Uri.fromFile(mFile), mIsLockScreenWallpaper, true); + } + @Override + public boolean isSelectable() { + return true; + } + @Override + public boolean isNamelessWallpaper() { + return true; + } + } + + public static class ResourceWallpaperInfo extends WallpaperTileInfo { + private Resources mResources; + private int mResId; + + public ResourceWallpaperInfo(Resources res, int resId, Drawable thumb, + boolean isLockScreenWallpaper) { + mResources = res; + mResId = resId; + mThumb = thumb; + mIsLockScreenWallpaper = isLockScreenWallpaper; + } + @Override + public void onClick(final WallpaperPickerActivity a) { + a.setWallpaperButtonEnabled(false); + final BitmapRegionTileSource.ResourceBitmapSource bitmapSource = + new BitmapRegionTileSource.ResourceBitmapSource(mResources, mResId); + a.setCropViewTileSource(bitmapSource, false, false, new CropViewScaleProvider() { + + @Override + public float getScale(TileSource src) { + Point wallpaperSize = WallpaperUtils.getDefaultWallpaperSize( + a.getResources(), a.getWindowManager()); + RectF crop = Utils.getMaxCropRect( + src.getImageWidth(), src.getImageHeight(), + wallpaperSize.x, wallpaperSize.y, false); + return wallpaperSize.x / crop.width(); + } + }, new Runnable() { + + @Override + public void run() { + if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + a.setWallpaperButtonEnabled(true); + } + } + }); + } + @Override + public void onSave(WallpaperPickerActivity a) { + boolean finishActivityWhenDone = true; + a.cropImageAndSetWallpaper(mResources, mResId, mIsLockScreenWallpaper, + finishActivityWhenDone); + } + @Override + public boolean isSelectable() { + return true; + } + @Override + public boolean isNamelessWallpaper() { + return true; + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static class DefaultWallpaperInfo extends WallpaperTileInfo { + public DefaultWallpaperInfo(Drawable thumb) { + mThumb = thumb; + mIsLockScreenWallpaper = false; + } + @Override + public void onClick(WallpaperPickerActivity a) { + CropView c = a.getCropView(); + Drawable defaultWallpaper = WallpaperManager.getInstance(a.getContext()) + .getBuiltInDrawable(c.getWidth(), c.getHeight(), false, 0.5f, 0.5f); + if (defaultWallpaper == null) { + Log.w(TAG, "Null default wallpaper encountered."); + c.setTileSource(null, null); + return; + } + + LoadRequest req = new LoadRequest(); + req.moveToLeft = false; + req.touchEnabled = false; + req.scaleProvider = new CropViewScaleProvider() { + + @Override + public float getScale(TileSource src) { + return 1f; + } + }; + req.result = new DrawableTileSource(a.getContext(), + defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE); + a.onLoadRequestComplete(req, true); + } + @Override + public void onSave(WallpaperPickerActivity a) { + try { + WallpaperManager.getInstance(a.getContext()).clear(); + a.setResult(Activity.RESULT_OK); + } catch (IOException e) { + Log.w("Setting wallpaper to default threw exception", e); + } + a.finish(); + } + @Override + public boolean isSelectable() { + return true; + } + @Override + public boolean isNamelessWallpaper() { + return false; + } + } + + @TargetApi(Build.VERSION_CODES.M) + public static class NoWallpaperInfo extends WallpaperTileInfo { + public NoWallpaperInfo(Drawable thumb, boolean isLockScreenWallpaper) { + mThumb = thumb; + mIsLockScreenWallpaper = isLockScreenWallpaper; + } + @Override + public void onClick(WallpaperPickerActivity a) { + LoadRequest req = new LoadRequest(); + req.moveToLeft = false; + req.touchEnabled = false; + req.scaleProvider = new CropViewScaleProvider() { + + @Override + public float getScale(TileSource src) { + return 1f; + } + }; + req.result = new DrawableTileSource(a.getContext(), + mThumb, DrawableTileSource.MAX_PREVIEW_SIZE); + a.onLoadRequestComplete(req, true); + } + @Override + public void onSave(WallpaperPickerActivity a) { + try { + if (!mIsLockScreenWallpaper) { + BitmapDrawable bd = (BitmapDrawable) mThumb; + WallpaperManager.getInstance(a.getContext()).setBitmap(bd.getBitmap()); + } else { + WallpaperManager.getInstance(a.getContext()).clearKeyguardWallpaper(); + } + a.setResult(Activity.RESULT_OK); + } catch (IOException e) { + Log.w("Setting wallpaper to default threw exception", e); + } + a.finish(); + } + @Override + public boolean isSelectable() { + return true; + } + @Override + public boolean isNamelessWallpaper() { + return true; + } + } + + /** + * For themes which have regular wallpapers + */ + @TargetApi(Build.VERSION_CODES.M) + public static class ThemeWallpaperInfo extends WallpaperTileInfo { + String mPackageName; + Drawable mThumb; + Context mContext; + + public ThemeWallpaperInfo(Context context, String packageName, Drawable thumb, + boolean isLockScreenWallpaper) { + this.mContext = context; + this.mPackageName = packageName; + this.mThumb = thumb; + mIsLockScreenWallpaper = isLockScreenWallpaper; + } + + @Override + public void onClick(final WallpaperPickerActivity a) { + try { + final BitmapRegionTileSource.ThemeBitmapSource source; + Resources res = a.getPackageManager().getResourcesForApplication(mPackageName); + if (res == null) { + return; + } + + int rotation = 0; + source = new BitmapRegionTileSource.ThemeBitmapSource(res.getAssets(), + mIsLockScreenWallpaper ? THEME_LOCKSCREEN_PATH : THEME_WALLPAPER_PATH); + a.setCropViewTileSource(source, false, false, new CropViewScaleProvider() { + + @Override + public float getScale(TileSource src) { + Point wallpaperSize = WallpaperUtils.getDefaultWallpaperSize( + a.getResources(), a.getWindowManager()); + RectF crop = Utils.getMaxCropRect( + src.getImageWidth(), src.getImageHeight(), + wallpaperSize.x, wallpaperSize.y, false); + return wallpaperSize.x / crop.width(); + } + }, new Runnable() { + + @Override + public void run() { + if (source.getLoadingState() == BitmapSource.State.LOADED) { + a.setWallpaperButtonEnabled(true); + } + } + }); + } catch (PackageManager.NameNotFoundException e) { + } + } + + @Override + public void onSave(WallpaperPickerActivity a) { + a.cropImageAndSetWallpaper( + mIsLockScreenWallpaper ? THEME_LOCKSCREEN_PATH : THEME_WALLPAPER_PATH, + mPackageName, + mIsLockScreenWallpaper, + true); + } + + @Override + public boolean isNamelessWallpaper() { + return true; + } + + @Override + public boolean isSelectable() { + return true; + } + } + + /** + * shows the system wallpaper behind the window and hides the {@link + * #mCropView} if visible + * @param visible should the system wallpaper be shown + */ + protected void setSystemWallpaperVisiblity(final boolean visible) { + // hide our own wallpaper preview if necessary + if(!visible) { + mCropView.setVisibility(View.VISIBLE); + } else { + changeWallpaperFlags(visible); + } + // the change of the flag must be delayed in order to avoid flickering, + // a simple post / double post does not suffice here + mCropView.postDelayed(new Runnable() { + @Override + public void run() { + if(!visible) { + changeWallpaperFlags(visible); + } else { + mCropView.setVisibility(View.INVISIBLE); + } + } + }, FLAG_POST_DELAY_MILLIS); + } + + @Thunk void changeWallpaperFlags(boolean visible) { + int desiredWallpaperFlag = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0; + int currentWallpaperFlag = getWindow().getAttributes().flags + & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + if (desiredWallpaperFlag != currentWallpaperFlag) { + getWindow().setFlags(desiredWallpaperFlag, + WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER); + } + } + + @Override + protected void onLoadRequestComplete(LoadRequest req, boolean success) { + super.onLoadRequestComplete(req, success); + if (success) { + setSystemWallpaperVisiblity(false); + } + } + + // called by onCreate; this is subclassed to overwrite WallpaperCropActivity + protected void init() { + setContentView(R.layout.wallpaper_picker); + + mIsLockScreenPicker = ACTION_SET_KEYGUARD_WALLPAPER.equals(getIntent().getAction()); + + mCropView = (CropView) findViewById(R.id.cropView); + mProgressView = findViewById(R.id.loading); + mWallpaperScrollContainer = (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container); + mWallpaperStrip = findViewById(R.id.wallpaper_strip); + mCropView.setTouchCallback(new CropView.TouchCallback() { + ViewPropertyAnimator mAnim; + @Override + public void onTouchDown() { + if (mAnim != null) { + mAnim.cancel(); + } + if (mWallpaperStrip.getAlpha() == 1f) { + mIgnoreNextTap = true; + } + mAnim = mWallpaperStrip.animate(); + mAnim.alpha(0f) + .setDuration(150) + .withEndAction(new Runnable() { + public void run() { + mWallpaperStrip.setVisibility(View.INVISIBLE); + } + }); + mAnim.setInterpolator(new AccelerateInterpolator(0.75f)); + mAnim.start(); + } + @Override + public void onTouchUp() { + mIgnoreNextTap = false; + } + @Override + public void onTap() { + boolean ignoreTap = mIgnoreNextTap; + mIgnoreNextTap = false; + if (!ignoreTap) { + if (mAnim != null) { + mAnim.cancel(); + } + mWallpaperStrip.setVisibility(View.VISIBLE); + mAnim = mWallpaperStrip.animate(); + mAnim.alpha(1f) + .setDuration(150) + .setInterpolator(new DecelerateInterpolator(0.75f)); + mAnim.start(); + } + } + }); + + if (mIsLockScreenPicker) { + final Bitmap keyguardBmp = WallpaperManager.getInstance(this).getKeyguardBitmap(); + setCropViewTileSource(new BitmapRegionTileSource.DumbBitmapSource(keyguardBmp), false, + true, null, null); + } else { + mCropView.setVisibility(View.INVISIBLE); + } + + mThumbnailOnClickListener = new OnClickListener() { + public void onClick(View v) { + if (mActionMode != null) { + // When CAB is up, clicking toggles the item instead + if (v.isLongClickable()) { + mLongClickListener.onLongClick(v); + } + return; + } + setWallpaperButtonEnabled(true); + WallpaperTileInfo info = (WallpaperTileInfo) v.getTag(); + if (info.isSelectable() && v.getVisibility() == View.VISIBLE) { + selectTile(v); + } + info.onClick(WallpaperPickerActivity.this); + } + }; + mLongClickListener = new View.OnLongClickListener() { + // Called when the user long-clicks on someView + public boolean onLongClick(View view) { + CheckableFrameLayout c = (CheckableFrameLayout) view; + c.toggle(); + + if (mActionMode != null) { + mActionMode.invalidate(); + } else { + // Start the CAB using the ActionMode.Callback defined below + mActionMode = startActionMode(mActionModeCallback); + int childCount = mWallpapersView.getChildCount(); + for (int i = 0; i < childCount; i++) { + mWallpapersView.getChildAt(i).setSelected(false); + } + } + return true; + } + }; + + mWallpapersView = (LinearLayout) findViewById(R.id.wallpaper_list); + + // Populate the built-in wallpapers + ArrayList<WallpaperTileInfo> wallpapers = findBundledWallpapers(); + mWallpapersView = (LinearLayout) findViewById(R.id.wallpaper_list); + SimpleWallpapersAdapter ia = new SimpleWallpapersAdapter(getContext(), wallpapers); + populateWallpapersFromAdapter(mWallpapersView, ia, false); + + // Populate the saved wallpapers + mSavedImages = new SavedWallpaperImages(getContext(), mIsLockScreenPicker); + mSavedImages.loadThumbnailsAndImageIdList(); + populateWallpapersFromAdapter(mWallpapersView, mSavedImages, false); + + // theme wallpapers + new AsyncTask<Void, Void, ArrayList<ThemeWallpaperInfo>>() { + @Override + protected ArrayList<ThemeWallpaperInfo> doInBackground(Void... params) { + return findThemeWallpapers(); + } + + @Override + protected void onPostExecute(ArrayList<ThemeWallpaperInfo> themeWallpaperInfos) { + LinearLayout themeList = (LinearLayout) findViewById(R.id.theme_wallpaper_list); + ThemeWallpapersAdapter ta = new ThemeWallpapersAdapter( + WallpaperPickerActivity.this, themeWallpaperInfos); + populateWallpapersFromAdapter(themeList, ta, false); + } + }.execute((Void) null); + + // Add a tile for the no wallpaper option + LinearLayout themeList = (LinearLayout) findViewById(R.id.theme_wallpaper_list); + FrameLayout noWallpaperTile = (FrameLayout) getLayoutInflater(). + inflate(R.layout.wallpaper_picker_no_wallpaper_item, themeList, false); + themeList.addView(noWallpaperTile); + NoWallpaperInfo noWallpaperInfo = new NoWallpaperInfo(getDrawable(R.drawable.black), + mIsLockScreenPicker); + noWallpaperTile.setTag(noWallpaperInfo); + noWallpaperInfo.setView(noWallpaperTile); + noWallpaperTile.setOnClickListener(mThumbnailOnClickListener); + + if (!mIsLockScreenPicker) { + // Populate the live wallpapers + final LinearLayout liveWallpapersView = + (LinearLayout) findViewById(R.id.live_wallpaper_list); + final LiveWallpaperListAdapter a = new LiveWallpaperListAdapter(getContext()); + a.registerDataSetObserver(new DataSetObserver() { + public void onChanged() { + liveWallpapersView.removeAllViews(); + populateWallpapersFromAdapter(liveWallpapersView, a, false); + initializeScrollForRtl(); + updateTileIndices(); + } + }); + + // Populate the third-party wallpaper pickers + final LinearLayout thirdPartyWallpapersView = + (LinearLayout) findViewById(R.id.third_party_wallpaper_list); + final ThirdPartyWallpaperPickerListAdapter ta = + new ThirdPartyWallpaperPickerListAdapter(getContext()); + populateWallpapersFromAdapter(thirdPartyWallpapersView, ta, false); + } + + // Add a tile for the Gallery + LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list); + FrameLayout pickImageTile = (FrameLayout) getLayoutInflater(). + inflate(R.layout.wallpaper_picker_image_picker_item, masterWallpaperList, false); + masterWallpaperList.addView(pickImageTile, 0); + + // Make its background the last photo taken on external storage + Bitmap lastPhoto = getThumbnailOfLastPhoto(); + if (lastPhoto != null) { + ImageView galleryThumbnailBg = + (ImageView) pickImageTile.findViewById(R.id.wallpaper_image); + galleryThumbnailBg.setImageBitmap(lastPhoto); + int colorOverlay = getResources().getColor(R.color.wallpaper_picker_translucent_gray); + galleryThumbnailBg.setColorFilter(colorOverlay, PorterDuff.Mode.SRC_ATOP); + } + + PickImageInfo pickImageInfo = new PickImageInfo(); + pickImageTile.setTag(pickImageInfo); + pickImageInfo.setView(pickImageTile); + pickImageTile.setOnClickListener(mThumbnailOnClickListener); + + // Select the first item; wait for a layout pass so that we initialize the dimensions of + // cropView or the defaultWallpaperView first + mCropView.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if ((right - left) > 0 && (bottom - top) > 0) { + if (mSelectedIndex >= 0 && mSelectedIndex < mWallpapersView.getChildCount()) { + mThumbnailOnClickListener.onClick( + mWallpapersView.getChildAt(mSelectedIndex)); + setSystemWallpaperVisiblity(false); + } + v.removeOnLayoutChangeListener(this); + } + } + }); + + updateTileIndices(); + + // Update the scroll for RTL + initializeScrollForRtl(); + + // Create smooth layout transitions for when items are deleted + final LayoutTransition transitioner = new LayoutTransition(); + transitioner.setDuration(200); + transitioner.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0); + transitioner.setAnimator(LayoutTransition.DISAPPEARING, null); + mWallpapersView.setLayoutTransition(transitioner); + + // Action bar + // Show the custom action bar view + final ActionBar actionBar = getActionBar(); + actionBar.setCustomView(R.layout.actionbar_set_wallpaper); + actionBar.getCustomView().setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Ensure that a tile is slelected and loaded. + if (mSelectedTile != null && mCropView.getTileSource() != null) { + // Prevent user from selecting any new tile. + mWallpaperStrip.setVisibility(View.GONE); + actionBar.hide(); + + WallpaperTileInfo info = (WallpaperTileInfo) mSelectedTile.getTag(); + info.onSave(WallpaperPickerActivity.this); + } else { + // no tile was selected, so we just finish the activity and go back + setResult(Activity.RESULT_OK); + finish(); + } + } + }); + mSetWallpaperButton = findViewById(R.id.set_wallpaper_button); + + // CAB for deleting items + mActionModeCallback = new ActionMode.Callback() { + // Called when the action mode is created; startActionMode() was called + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // Inflate a menu resource providing context menu items + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.cab_delete_wallpapers, menu); + return true; + } + + private int numCheckedItems() { + int childCount = mWallpapersView.getChildCount(); + int numCheckedItems = 0; + for (int i = 0; i < childCount; i++) { + CheckableFrameLayout c = (CheckableFrameLayout) mWallpapersView.getChildAt(i); + if (c.isChecked()) { + numCheckedItems++; + } + } + return numCheckedItems; + } + + // Called each time the action mode is shown. Always called after onCreateActionMode, + // but may be called multiple times if the mode is invalidated. + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + int numCheckedItems = numCheckedItems(); + if (numCheckedItems == 0) { + mode.finish(); + return true; + } else { + mode.setTitle(getResources().getQuantityString( + R.plurals.number_of_items_selected, numCheckedItems, numCheckedItems)); + return true; + } + } + + // Called when the user selects a contextual menu item + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + int itemId = item.getItemId(); + if (itemId == R.id.menu_delete) { + int childCount = mWallpapersView.getChildCount(); + ArrayList<View> viewsToRemove = new ArrayList<View>(); + boolean selectedTileRemoved = false; + for (int i = 0; i < childCount; i++) { + CheckableFrameLayout c = + (CheckableFrameLayout) mWallpapersView.getChildAt(i); + if (c.isChecked()) { + WallpaperTileInfo info = (WallpaperTileInfo) c.getTag(); + info.onDelete(WallpaperPickerActivity.this); + viewsToRemove.add(c); + if (i == mSelectedIndex) { + selectedTileRemoved = true; + } + } + } + for (View v : viewsToRemove) { + mWallpapersView.removeView(v); + } + if (selectedTileRemoved) { + mSelectedIndex = -1; + mSelectedTile = null; + setSystemWallpaperVisiblity(true); + } + updateTileIndices(); + mode.finish(); // Action picked, so close the CAB + return true; + } else { + return false; + } + } + + // Called when the user exits the action mode + @Override + public void onDestroyActionMode(ActionMode mode) { + int childCount = mWallpapersView.getChildCount(); + for (int i = 0; i < childCount; i++) { + CheckableFrameLayout c = (CheckableFrameLayout) mWallpapersView.getChildAt(i); + c.setChecked(false); + } + if (mSelectedTile != null) { + mSelectedTile.setSelected(true); + } + mActionMode = null; + } + }; + } + + public void setWallpaperButtonEnabled(boolean enabled) { + mSetWallpaperButton.setEnabled(enabled); + } + + @Thunk void selectTile(View v) { + if (mSelectedTile != null) { + mSelectedTile.setSelected(false); + mSelectedTile = null; + } + mSelectedTile = v; + v.setSelected(true); + mSelectedIndex = mWallpapersView.indexOfChild(v); + // TODO: Remove this once the accessibility framework and + // services have better support for selection state. + v.announceForAccessibility( + getContext().getString(R.string.announce_selection, v.getContentDescription())); + } + + @Thunk void initializeScrollForRtl() { + if (Utilities.isRtl(getResources())) { + final ViewTreeObserver observer = mWallpaperScrollContainer.getViewTreeObserver(); + observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + public void onGlobalLayout() { + LinearLayout masterWallpaperList = + (LinearLayout) findViewById(R.id.master_wallpaper_list); + mWallpaperScrollContainer.scrollTo(masterWallpaperList.getWidth(), 0); + mWallpaperScrollContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + } + } + + protected Bitmap getThumbnailOfLastPhoto() { + boolean canReadExternalStorage = getActivity().checkPermission( + Manifest.permission.READ_EXTERNAL_STORAGE, Process.myPid(), Process.myUid()) == + PackageManager.PERMISSION_GRANTED; + + if (!canReadExternalStorage) { + // MediaStore.Images.Media.EXTERNAL_CONTENT_URI requires + // the READ_EXTERNAL_STORAGE permission + return null; + } + + Cursor cursor = MediaStore.Images.Media.query(getContext().getContentResolver(), + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + new String[] { MediaStore.Images.ImageColumns._ID, + MediaStore.Images.ImageColumns.DATE_TAKEN}, + null, null, MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC LIMIT 1"); + + Bitmap thumb = null; + if (cursor != null) { + if (cursor.moveToNext()) { + int id = cursor.getInt(0); + thumb = MediaStore.Images.Thumbnails.getThumbnail(getContext().getContentResolver(), + id, MediaStore.Images.Thumbnails.MINI_KIND, null); + } + cursor.close(); + } + return thumb; + } + + public void onStop() { + super.onStop(); + mWallpaperStrip = findViewById(R.id.wallpaper_strip); + if (mWallpaperStrip != null && mWallpaperStrip.getAlpha() < 1f) { + mWallpaperStrip.setAlpha(1f); + mWallpaperStrip.setVisibility(View.VISIBLE); + } + } + + public void onSaveInstanceState(Bundle outState) { + outState.putParcelableArrayList(TEMP_WALLPAPER_TILES, mTempWallpaperTiles); + outState.putInt(SELECTED_INDEX, mSelectedIndex); + } + + protected void onRestoreInstanceState(Bundle savedInstanceState) { + ArrayList<Uri> uris = savedInstanceState.getParcelableArrayList(TEMP_WALLPAPER_TILES); + for (Uri uri : uris) { + addTemporaryWallpaperTile(uri, true); + } + mSelectedIndex = savedInstanceState.getInt(SELECTED_INDEX, -1); + } + + @Thunk void populateWallpapersFromAdapter(ViewGroup parent, BaseAdapter adapter, + boolean addLongPressHandler) { + for (int i = 0; i < adapter.getCount(); i++) { + FrameLayout thumbnail = (FrameLayout) adapter.getView(i, null, parent); + parent.addView(thumbnail, i); + WallpaperTileInfo info = (WallpaperTileInfo) adapter.getItem(i); + thumbnail.setTag(info); + info.setView(thumbnail); + if (addLongPressHandler) { + addLongPressHandler(thumbnail); + } + thumbnail.setOnClickListener(mThumbnailOnClickListener); + } + } + + @Thunk void updateTileIndices() { + LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list); + final int childCount = masterWallpaperList.getChildCount(); + final Resources res = getResources(); + + // Do two passes; the first pass gets the total number of tiles + int numTiles = 0; + for (int passNum = 0; passNum < 2; passNum++) { + int tileIndex = 0; + for (int i = 0; i < childCount; i++) { + View child = masterWallpaperList.getChildAt(i); + LinearLayout subList; + + int subListStart; + int subListEnd; + if (child.getTag() instanceof WallpaperTileInfo) { + subList = masterWallpaperList; + subListStart = i; + subListEnd = i + 1; + } else { // if (child instanceof LinearLayout) { + subList = (LinearLayout) child; + subListStart = 0; + subListEnd = subList.getChildCount(); + } + + for (int j = subListStart; j < subListEnd; j++) { + WallpaperTileInfo info = (WallpaperTileInfo) subList.getChildAt(j).getTag(); + if (info.isNamelessWallpaper()) { + if (passNum == 0) { + numTiles++; + } else { + CharSequence label = res.getString( + R.string.wallpaper_accessibility_name, ++tileIndex, numTiles); + info.onIndexUpdated(label); + } + } + } + } + } + } + + @Thunk static Point getDefaultThumbnailSize(Resources res) { + return new Point(res.getDimensionPixelSize(R.dimen.wallpaperThumbnailWidth), + res.getDimensionPixelSize(R.dimen.wallpaperThumbnailHeight)); + + } + + @Thunk static Bitmap createThumbnail(Point size, Context context, Uri uri, byte[] imageBytes, + Resources res, int resId, int rotation, boolean leftAligned) { + int width = size.x; + int height = size.y; + + BitmapCropTask cropTask; + if (uri != null) { + cropTask = new BitmapCropTask( + context, uri, null, rotation, width, height, false, false, true, null); + } else if (imageBytes != null) { + cropTask = new BitmapCropTask( + imageBytes, null, rotation, width, height, false, false, true, null); + } else { + cropTask = new BitmapCropTask( + context, res, resId, null, rotation, width, height, false, false, true, null); + } + Point bounds = cropTask.getImageBounds(); + if (bounds == null || bounds.x == 0 || bounds.y == 0) { + return null; + } + + Matrix rotateMatrix = new Matrix(); + rotateMatrix.setRotate(rotation); + float[] rotatedBounds = new float[] { bounds.x, bounds.y }; + rotateMatrix.mapPoints(rotatedBounds); + rotatedBounds[0] = Math.abs(rotatedBounds[0]); + rotatedBounds[1] = Math.abs(rotatedBounds[1]); + + RectF cropRect = Utils.getMaxCropRect( + (int) rotatedBounds[0], (int) rotatedBounds[1], width, height, leftAligned); + cropTask.setCropBounds(cropRect); + + if (cropTask.cropBitmap()) { + return cropTask.getCroppedBitmap(); + } else { + return null; + } + } + + private void addTemporaryWallpaperTile(final Uri uri, boolean fromRestore) { + mTempWallpaperTiles.add(uri); + // Add a tile for the image picked from Gallery + final FrameLayout pickedImageThumbnail = (FrameLayout) getLayoutInflater(). + inflate(R.layout.wallpaper_picker_item, mWallpapersView, false); + pickedImageThumbnail.setVisibility(View.GONE); + mWallpapersView.addView(pickedImageThumbnail, 0); + + // Load the thumbnail + final ImageView image = (ImageView) pickedImageThumbnail.findViewById(R.id.wallpaper_image); + final Point defaultSize = getDefaultThumbnailSize(this.getResources()); + final Context context = getContext(); + new AsyncTask<Void, Bitmap, Bitmap>() { + protected Bitmap doInBackground(Void...args) { + try { + int rotation = BitmapUtils.getRotationFromExif(context, uri); + return createThumbnail(defaultSize, context, uri, null, null, 0, rotation, false); + } catch (SecurityException securityException) { + if (isActivityDestroyed()) { + // Temporarily granted permissions are revoked when the activity + // finishes, potentially resulting in a SecurityException here. + // Even though {@link #isDestroyed} might also return true in different + // situations where the configuration changes, we are fine with + // catching these cases here as well. + cancel(false); + } else { + // otherwise it had a different cause and we throw it further + throw securityException; + } + return null; + } + } + protected void onPostExecute(Bitmap thumb) { + if (!isCancelled() && thumb != null) { + image.setImageBitmap(thumb); + Drawable thumbDrawable = image.getDrawable(); + thumbDrawable.setDither(true); + pickedImageThumbnail.setVisibility(View.VISIBLE); + } else { + Log.e(TAG, "Error loading thumbnail for uri=" + uri); + } + } + }.execute(); + + UriWallpaperInfo info = new UriWallpaperInfo(uri, mIsLockScreenPicker); + pickedImageThumbnail.setTag(info); + info.setView(pickedImageThumbnail); + addLongPressHandler(pickedImageThumbnail); + updateTileIndices(); + pickedImageThumbnail.setOnClickListener(mThumbnailOnClickListener); + if (!fromRestore) { + mThumbnailOnClickListener.onClick(pickedImageThumbnail); + } + } + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == IMAGE_PICK && resultCode == Activity.RESULT_OK) { + if (data != null && data.getData() != null) { + Uri uri = data.getData(); + addTemporaryWallpaperTile(uri, false); + } + } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY + && resultCode == Activity.RESULT_OK) { + // Something was set on the third-party activity. + setResult(Activity.RESULT_OK); + finish(); + } + } + + private void addLongPressHandler(View v) { + v.setOnLongClickListener(mLongClickListener); + + // Enable stylus button to also trigger long click. + final StylusEventHelper stylusEventHelper = new StylusEventHelper(v); + v.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent event) { + return stylusEventHelper.checkAndPerformStylusEvent(event); + } + }); + } + + private ArrayList<WallpaperTileInfo> findBundledWallpapers() { + final PackageManager pm = getContext().getPackageManager(); + final ArrayList<WallpaperTileInfo> bundled = new ArrayList<WallpaperTileInfo>(24); + + List<Partner> partners = Partner.getAllPartners(pm); + boolean hideDefault = false; + if (partners != null) { + for (Partner partner : partners) { + final Resources partnerRes = partner.getResources(); + final int resId = partnerRes.getIdentifier(Partner.RES_WALLPAPERS, "array", + partner.getPackageName()); + if (resId != 0) { + addWallpapers(bundled, partnerRes, partner.getPackageName(), resId); + } + + // Add system wallpapers + File systemDir = partner.getWallpaperDirectory(); + if (systemDir != null && systemDir.isDirectory()) { + for (File file : systemDir.listFiles()) { + if (!file.isFile()) { + continue; + } + String name = file.getName(); + int dotPos = name.lastIndexOf('.'); + String extension = ""; + if (dotPos >= -1) { + extension = name.substring(dotPos); + name = name.substring(0, dotPos); + } + + if (name.endsWith("_small")) { + // it is a thumbnail + continue; + } + + File thumbnail = new File(systemDir, name + "_small" + extension); + Bitmap thumb = BitmapFactory.decodeFile(thumbnail.getAbsolutePath()); + if (thumb != null) { + bundled.add(new FileWallpaperInfo(file, new BitmapDrawable(thumb), + mIsLockScreenPicker)); + } + } + } + if (partner.hideDefaultWallpaper()) { + hideDefault = true; + } + } + } + + Pair<ApplicationInfo, Integer> r = getWallpaperArrayResourceId(); + if (r != null) { + try { + Resources wallpaperRes = getContext().getPackageManager() + .getResourcesForApplication(r.first); + addWallpapers(bundled, wallpaperRes, r.first.packageName, r.second); + } catch (PackageManager.NameNotFoundException e) { + } + } + + if (!hideDefault && !mIsLockScreenPicker) { + // Add an entry for the default wallpaper (stored in system resources) + WallpaperTileInfo defaultWallpaperInfo = Utilities.ATLEAST_KITKAT + ? getDefaultWallpaper() : getPreKKDefaultWallpaperInfo(); + if (defaultWallpaperInfo != null) { + bundled.add(0, defaultWallpaperInfo); + } + } + + return bundled; + } + + private ArrayList<ThemeWallpaperInfo> findThemeWallpapers() { + ArrayList<ThemeWallpaperInfo> themeWallpapers = + new ArrayList<ThemeWallpaperInfo>(); + ContentResolver cr = getContentResolver(); + String[] projection = {ThemesContract.ThemesColumns.PKG_NAME}; + String selection = (mIsLockScreenPicker + ? ThemesContract.ThemesColumns.MODIFIES_LOCKSCREEN + : ThemesContract.ThemesColumns.MODIFIES_LAUNCHER) + "=?"; + String[] selectionArgs = {"1"}; + String sortOrder = null; + Cursor c = cr.query(ThemesContract.ThemesColumns.CONTENT_URI, projection, selection, + selectionArgs, sortOrder); + if (c != null) { + Bitmap bmp; + while (c.moveToNext()) { + String pkgName = c.getString( + c.getColumnIndexOrThrow(ThemesContract.ThemesColumns.PKG_NAME)); + bmp = Utilities.getThemeWallpaper(this, + mIsLockScreenPicker ? THEME_LOCKSCREEN_PATH: THEME_WALLPAPER_PATH, + pkgName, true /* thumb*/); + if (bmp != null) { + themeWallpapers.add( + new ThemeWallpaperInfo(this, pkgName, + new BitmapDrawable(getResources(), bmp), mIsLockScreenPicker)); + + Log.d("", String.format("Loaded bitmap of size %dx%d for %s", + bmp.getWidth(), bmp.getHeight(), pkgName)); + } + } + c.close(); + } + return themeWallpapers; + } + + private boolean writeImageToFileAsJpeg(File f, Bitmap b) { + try { + f.createNewFile(); + FileOutputStream thumbFileStream = + getContext().openFileOutput(f.getName(), Context.MODE_PRIVATE); + b.compress(Bitmap.CompressFormat.JPEG, 95, thumbFileStream); + thumbFileStream.close(); + return true; + } catch (IOException e) { + Log.e(TAG, "Error while writing bitmap to file " + e); + f.delete(); + } + return false; + } + + private File getDefaultThumbFile() { + return new File(getContext().getFilesDir(), Build.VERSION.SDK_INT + + "_" + PickerFiles.DEFAULT_WALLPAPER_THUMBNAIL); + } + + private boolean saveDefaultWallpaperThumb(Bitmap b) { + // Delete old thumbnails. + new File(getContext().getFilesDir(), PickerFiles.DEFAULT_WALLPAPER_THUMBNAIL_OLD).delete(); + new File(getContext().getFilesDir(), PickerFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); + + for (int i = Build.VERSION_CODES.JELLY_BEAN; i < Build.VERSION.SDK_INT; i++) { + new File(getContext().getFilesDir(), i + "_" + + PickerFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); + } + return writeImageToFileAsJpeg(getDefaultThumbFile(), b); + } + + private ResourceWallpaperInfo getPreKKDefaultWallpaperInfo() { + Resources sysRes = Resources.getSystem(); + int resId = sysRes.getIdentifier("default_wallpaper", "drawable", "android"); + + File defaultThumbFile = getDefaultThumbFile(); + Bitmap thumb = null; + boolean defaultWallpaperExists = false; + if (defaultThumbFile.exists()) { + thumb = BitmapFactory.decodeFile(defaultThumbFile.getAbsolutePath()); + defaultWallpaperExists = true; + } else { + Resources res = getResources(); + Point defaultThumbSize = getDefaultThumbnailSize(res); + int rotation = BitmapUtils.getRotationFromExif(res, resId); + thumb = createThumbnail( + defaultThumbSize, getContext(), null, null, sysRes, resId, rotation, false); + if (thumb != null) { + defaultWallpaperExists = saveDefaultWallpaperThumb(thumb); + } + } + if (defaultWallpaperExists) { + return new ResourceWallpaperInfo(sysRes, resId, new BitmapDrawable(thumb), + mIsLockScreenPicker); + } + return null; + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private DefaultWallpaperInfo getDefaultWallpaper() { + File defaultThumbFile = getDefaultThumbFile(); + Bitmap thumb = null; + boolean defaultWallpaperExists = false; + if (defaultThumbFile.exists()) { + thumb = BitmapFactory.decodeFile(defaultThumbFile.getAbsolutePath()); + defaultWallpaperExists = true; + } else { + Resources res = getResources(); + Point defaultThumbSize = getDefaultThumbnailSize(res); + Drawable wallpaperDrawable = WallpaperManager.getInstance(getContext()).getBuiltInDrawable( + defaultThumbSize.x, defaultThumbSize.y, true, 0.5f, 0.5f); + if (wallpaperDrawable != null) { + thumb = Bitmap.createBitmap( + defaultThumbSize.x, defaultThumbSize.y, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(thumb); + wallpaperDrawable.setBounds(0, 0, defaultThumbSize.x, defaultThumbSize.y); + wallpaperDrawable.draw(c); + c.setBitmap(null); + } + if (thumb != null) { + defaultWallpaperExists = saveDefaultWallpaperThumb(thumb); + } + } + if (defaultWallpaperExists) { + return new DefaultWallpaperInfo(new BitmapDrawable(thumb)); + } + return null; + } + + public Pair<ApplicationInfo, Integer> getWallpaperArrayResourceId() { + final String packageName = getResources().getResourcePackageName(R.array.wallpapers); + try { + ApplicationInfo info = getContext().getPackageManager() + .getApplicationInfo(packageName, 0); + return new Pair<>(info, R.array.wallpapers); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + private void addWallpapers(ArrayList<WallpaperTileInfo> known, Resources res, + String packageName, int listResId) { + final String[] extras = res.getStringArray(listResId); + for (String extra : extras) { + int resId = res.getIdentifier(extra, "drawable", packageName); + if (resId != 0) { + final int thumbRes = res.getIdentifier(extra + "_small", "drawable", packageName); + + if (thumbRes != 0) { + ResourceWallpaperInfo wallpaperInfo = + new ResourceWallpaperInfo(res, resId, res.getDrawable(thumbRes), + mIsLockScreenPicker); + known.add(wallpaperInfo); + // Log.d(TAG, "add: [" + packageName + "]: " + extra + " (" + res + ")"); + } + } else { + Log.e(TAG, "Couldn't find wallpaper " + extra); + } + } + } + + public CropView getCropView() { + return mCropView; + } + + public SavedWallpaperImages getSavedImages() { + return mSavedImages; + } + + private static class SimpleWallpapersAdapter extends ArrayAdapter<WallpaperTileInfo> { + private final LayoutInflater mLayoutInflater; + + SimpleWallpapersAdapter(Context context, ArrayList<WallpaperTileInfo> wallpapers) { + super(context, R.layout.wallpaper_picker_item, wallpapers); + mLayoutInflater = LayoutInflater.from(context); + } + + public View getView(int position, View convertView, ViewGroup parent) { + Drawable thumb = getItem(position).mThumb; + if (thumb == null) { + Log.e(TAG, "Error decoding thumbnail for wallpaper #" + position); + } + return createImageTileView(mLayoutInflater, convertView, parent, thumb); + } + } + + public static View createImageTileView(LayoutInflater layoutInflater, + View convertView, ViewGroup parent, Drawable thumb) { + View view; + + if (convertView == null) { + view = layoutInflater.inflate(R.layout.wallpaper_picker_item, parent, false); + } else { + view = convertView; + } + + ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); + + if (thumb != null) { + image.setImageDrawable(thumb); + thumb.setDither(true); + } + + return view; + } + + public void startActivityForResultSafely(Intent intent, int requestCode) { + Utilities.startActivityForResultSafely(getActivity(), intent, requestCode); + } + + private static class ThemeWallpapersAdapter extends BaseAdapter implements ListAdapter { + private LayoutInflater mLayoutInflater; + private ArrayList<ThemeWallpaperInfo> mWallpapers; + + ThemeWallpapersAdapter(Activity activity, ArrayList<ThemeWallpaperInfo> wallpapers) { + mLayoutInflater = activity.getLayoutInflater(); + mWallpapers = wallpapers; + } + + public int getCount() { + return mWallpapers.size(); + } + + public ThemeWallpaperInfo getItem(int position) { + return mWallpapers.get(position); + } + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + Drawable thumb = mWallpapers.get(position).mThumb; + if (thumb == null) { + Log.e(TAG, "Error decoding thumbnail for wallpaper #" + position); + } + return createImageTileView(mLayoutInflater, convertView, parent, thumb); + } + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/base/BaseActivity.java b/src/org/cyanogenmod/wallpaperpicker/base/BaseActivity.java new file mode 100644 index 0000000..670a61a --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/base/BaseActivity.java @@ -0,0 +1,21 @@ +package org.cyanogenmod.wallpaperpicker.base; + +import android.app.Activity; +import android.content.Context; + +/** + * A wrapper over {@link Activity} which allows to override some methods. + * The base implementation can change from an Activity to a Fragment (or any other custom + * implementation), Callers should not assume that the base class extends Context, instead use + * either {@link #getContext} or {@link #getActivity} + */ +public class BaseActivity extends Activity { + + public Context getContext() { + return this; + } + + public Activity getActivity() { + return this; + } +} diff --git a/src/org/cyanogenmod/wallpaperpicker/util/Constants.java b/src/org/cyanogenmod/wallpaperpicker/util/Constants.java new file mode 100644 index 0000000..111fb46 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/util/Constants.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 org.cyanogenmod.wallpaperpicker.util; + +public class Constants { + + public static final String THEME_WALLPAPER_PATH = "wallpapers"; + public static final String THEME_LOCKSCREEN_PATH = "lockscreen"; + +} diff --git a/src/org/cyanogenmod/wallpaperpicker/util/Thunk.java b/src/org/cyanogenmod/wallpaperpicker/util/Thunk.java new file mode 100644 index 0000000..6f35514 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/util/Thunk.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.wallpaperpicker.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the given field or method has package visibility solely to prevent the creation + * of a synthetic method. In practice, you should treat this field/method as if it were private. + * <p> + * + * When a private method is called from an inner class, the Java compiler generates a simple + * package private shim method that the class generated from the inner class can call. This results + * in unnecessary bloat and runtime method call overhead. It also gets us closer to the dex method + * count limit. + * <p> + * + * If you'd like to see warnings for these synthetic methods in eclipse, turn on: + * Window > Preferences > Java > Compiler > Errors/Warnings > "Access to a non-accessible member + * of an enclosing type". + * <p> + * + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface Thunk { }
\ No newline at end of file diff --git a/src/org/cyanogenmod/wallpaperpicker/util/WallpaperUtils.java b/src/org/cyanogenmod/wallpaperpicker/util/WallpaperUtils.java new file mode 100644 index 0000000..ced3844 --- /dev/null +++ b/src/org/cyanogenmod/wallpaperpicker/util/WallpaperUtils.java @@ -0,0 +1,125 @@ +/* + * 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 org.cyanogenmod.wallpaperpicker.util; + +import android.annotation.TargetApi; +import android.app.WallpaperManager; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Point; +import android.os.Build; +import android.view.WindowManager; + +import org.cyanogenmod.wallpaperpicker.Utilities; + +/** + * Utility methods for wallpaper management. + */ +public final class WallpaperUtils { + + public static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; + public static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; + public static final float WALLPAPER_SCREENS_SPAN = 2f; + + public static void suggestWallpaperDimension(Resources res, + final SharedPreferences sharedPrefs, + WindowManager windowManager, + final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { + final Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(res, windowManager); + // If we have saved a wallpaper width/height, use that instead + + int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); + int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1); + + if (savedWidth == -1 || savedHeight == -1) { + if (!fallBackToDefaults) { + return; + } else { + savedWidth = defaultWallpaperSize.x; + savedHeight = defaultWallpaperSize.y; + } + } + + if (savedWidth != wallpaperManager.getDesiredMinimumWidth() || + savedHeight != wallpaperManager.getDesiredMinimumHeight()) { + wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight); + } + } + + /** + * As a ratio of screen height, the total distance we want the parallax effect to span + * horizontally + */ + public static float wallpaperTravelToScreenWidthRatio(int width, int height) { + float aspectRatio = width / (float) height; + + // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width + // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width + // We will use these two data points to extrapolate how much the wallpaper parallax effect + // to span (ie travel) at any aspect ratio: + + final float ASPECT_RATIO_LANDSCAPE = 16/10f; + final float ASPECT_RATIO_PORTRAIT = 10/16f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; + + // To find out the desired width at different aspect ratios, we use the following two + // formulas, where the coefficient on x is the aspect ratio (width/height): + // (16/10)x + y = 1.5 + // (10/16)x + y = 1.2 + // We solve for x and y and end up with a final formula: + final float x = + (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / + (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); + final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; + return x * aspectRatio + y; + } + + private static Point sDefaultWallpaperSize; + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) { + if (sDefaultWallpaperSize == null) { + Point minDims = new Point(); + Point maxDims = new Point(); + windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); + + int maxDim = Math.max(maxDims.x, maxDims.y); + int minDim = Math.max(minDims.x, minDims.y); + + if (Utilities.ATLEAST_JB_MR1) { + Point realSize = new Point(); + windowManager.getDefaultDisplay().getRealSize(realSize); + maxDim = Math.max(realSize.x, realSize.y); + minDim = Math.min(realSize.x, realSize.y); + } + + // We need to ensure that there is enough extra space in the wallpaper + // for the intended parallax effects + final int defaultWidth, defaultHeight; + if (res.getConfiguration().smallestScreenWidthDp >= 720) { + defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); + defaultHeight = maxDim; + } else { + defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); + defaultHeight = maxDim; + } + sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); + } + return sDefaultWallpaperSize; + } +} |