| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.launcher3; |
| |
| import android.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.res.Configuration; |
| 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.view.WindowManager; |
| import android.widget.Toast; |
| |
| import com.android.gallery3d.common.BitmapCropTask; |
| import com.android.gallery3d.common.BitmapUtils; |
| import com.android.gallery3d.common.Utils; |
| import com.android.launcher3.util.Thunk; |
| import com.android.photos.BitmapRegionTileSource; |
| import com.android.photos.BitmapRegionTileSource.BitmapSource; |
| import com.android.photos.BitmapRegionTileSource.BitmapSource.InBitmapProvider; |
| import com.android.photos.views.TiledImageRenderer.TileSource; |
| |
| import java.util.Collections; |
| import java.util.Set; |
| import java.util.WeakHashMap; |
| |
| public class WallpaperCropActivity extends Activity implements Handler.Callback { |
| private static final String LOGTAG = "Launcher3.CropActivity"; |
| |
| protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; |
| protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; |
| |
| /** |
| * 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 = 2f; |
| |
| private static final int MSG_LOAD_IMAGE = 1; |
| |
| protected CropView mCropView; |
| protected View mProgressView; |
| protected Uri mUri; |
| protected View mSetWallpaperButton; |
| |
| private HandlerThread mLoaderThread; |
| private Handler mLoaderHandler; |
| @Thunk LoadRequest mCurrentLoadRequest; |
| private byte[] mTempStorageForDecoding = new byte[16 * 1024]; |
| // A weak-set of reusable bitmaps |
| @Thunk Set<Bitmap> mReusableBitmaps = |
| Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>()); |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| mLoaderThread = new HandlerThread("wallpaper_loader"); |
| mLoaderThread.start(); |
| mLoaderHandler = new Handler(mLoaderThread.getLooper(), this); |
| |
| init(); |
| if (!enableRotation()) { |
| setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT); |
| } |
| } |
| |
| 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, finishActivityWhenDone); |
| } |
| }); |
| mSetWallpaperButton = findViewById(R.id.set_wallpaper_button); |
| |
| // Load image in background |
| final BitmapRegionTileSource.UriBitmapSource bitmapSource = |
| new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024); |
| mSetWallpaperButton.setEnabled(false); |
| Runnable onLoad = new Runnable() { |
| public void run() { |
| if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) { |
| Toast.makeText(WallpaperCropActivity.this, |
| getString(R.string.wallpaper_load_fail), |
| Toast.LENGTH_LONG).show(); |
| finish(); |
| } else { |
| mSetWallpaperButton.setEnabled(true); |
| } |
| } |
| }; |
| setCropViewTileSource(bitmapSource, true, false, null, onLoad); |
| } |
| |
| @Override |
| protected 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 (isDestroyed()) { |
| // Temporarily granted permissions are revoked when the activity |
| // finishes, potentially resulting in a SecurityException here. |
| // Even though {@link #isDestroyed} might also return true in different |
| // situations where the configuration changes, we are fine with |
| // catching these cases here as well. |
| return true; |
| } else { |
| // otherwise it had a different cause and we throw it further |
| throw securityException; |
| } |
| } |
| |
| req.result = new BitmapRegionTileSource(this, req.src, mTempStorageForDecoding); |
| runOnUiThread(new Runnable() { |
| |
| @Override |
| public void run() { |
| if (req == mCurrentLoadRequest) { |
| onLoadRequestComplete(req, |
| req.src.getLoadingState() == BitmapSource.State.LOADED); |
| } else { |
| addReusableBitmap(req.result); |
| } |
| } |
| }); |
| return true; |
| } |
| return false; |
| } |
| |
| @Thunk void addReusableBitmap(TileSource src) { |
| synchronized (mReusableBitmaps) { |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT |
| && src instanceof BitmapRegionTileSource) { |
| Bitmap preview = ((BitmapRegionTileSource) src).getBitmap(); |
| if (preview != null && preview.isMutable()) { |
| mReusableBitmaps.add(preview); |
| } |
| } |
| } |
| } |
| |
| protected void onLoadRequestComplete(LoadRequest req, boolean success) { |
| mCurrentLoadRequest = null; |
| if (success) { |
| TileSource oldSrc = mCropView.getTileSource(); |
| mCropView.setTileSource(req.result, null); |
| mCropView.setTouchEnabled(req.touchEnabled); |
| if (req.moveToLeft) { |
| mCropView.moveToLeft(); |
| } |
| if (req.scaleProvider != null) { |
| mCropView.setScale(req.scaleProvider.getScale(req.result)); |
| } |
| |
| // Free last image |
| if (oldSrc != null) { |
| // Call yield instead of recycle, as we only want to free GL resource. |
| // We can still reuse the bitmap for decoding any other image. |
| oldSrc.getPreview().yield(); |
| } |
| addReusableBitmap(oldSrc); |
| } |
| if (req.postExecute != null) { |
| req.postExecute.run(); |
| } |
| } |
| |
| public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled, |
| boolean moveToLeft, CropViewScaleProvider scaleProvider, Runnable postExecute) { |
| final LoadRequest req = new LoadRequest(); |
| req.moveToLeft = moveToLeft; |
| req.src = bitmapSource; |
| req.touchEnabled = touchEnabled; |
| req.postExecute = postExecute; |
| req.scaleProvider = scaleProvider; |
| mCurrentLoadRequest = req; |
| |
| // Remove any pending requests |
| mLoaderHandler.removeMessages(MSG_LOAD_IMAGE); |
| Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget(); |
| |
| // We don't want to show the spinner every time we load an image, because that would be |
| // annoying; instead, only start showing the spinner if loading the image has taken |
| // longer than 1 sec (ie 1000 ms) |
| mProgressView.postDelayed(new Runnable() { |
| public void run() { |
| if (mCurrentLoadRequest == req) { |
| mProgressView.setVisibility(View.VISIBLE); |
| } |
| } |
| }, 1000); |
| } |
| |
| |
| public boolean enableRotation() { |
| return getResources().getBoolean(R.bool.allow_rotation); |
| } |
| |
| public static String getSharedPreferencesKey() { |
| return LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; |
| } |
| |
| protected void setWallpaper(Uri uri, final boolean finishActivityWhenDone) { |
| int rotation = BitmapUtils.getRotationFromExif(this, uri); |
| BitmapCropTask cropTask = new BitmapCropTask( |
| this, uri, null, rotation, 0, 0, true, 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, 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 = BitmapUtils.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(this, res, resId, |
| crop, rotation, outSize.x, outSize.y, true, false, onEndCrop); |
| cropTask.execute(); |
| } |
| |
| @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) |
| protected void cropImageAndSetWallpaper(Uri uri, |
| BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) { |
| boolean centerCrop = getResources().getBoolean(R.bool.center_crop); |
| // Get the crop |
| boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; |
| |
| Display d = getWindowManager().getDefaultDisplay(); |
| |
| Point displaySize = new Point(); |
| d.getSize(displaySize); |
| boolean isPortrait = displaySize.x < displaySize.y; |
| |
| Point defaultWallpaperSize = BitmapUtils.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(this, uri, |
| cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop); |
| if (onBitmapCroppedHandler != null) { |
| cropTask.setOnBitmapCropped(onBitmapCroppedHandler); |
| } |
| cropTask.execute(); |
| } |
| |
| protected void updateWallpaperDimensions(int width, int height) { |
| String spKey = getSharedPreferencesKey(); |
| SharedPreferences sp = 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(); |
| |
| suggestWallpaperDimension(getResources(), |
| sp, getWindowManager(), WallpaperManager.getInstance(this), true); |
| } |
| |
| public static void suggestWallpaperDimension(Resources res, |
| final SharedPreferences sharedPrefs, |
| WindowManager windowManager, |
| final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { |
| final Point defaultWallpaperSize = BitmapUtils.getDefaultWallpaperSize(res, windowManager); |
| // If we have saved a wallpaper width/height, use that instead |
| |
| int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); |
| 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); |
| } |
| } |
| |
| static class LoadRequest { |
| BitmapSource src; |
| boolean touchEnabled; |
| boolean moveToLeft; |
| Runnable postExecute; |
| CropViewScaleProvider scaleProvider; |
| |
| TileSource result; |
| } |
| |
| interface CropViewScaleProvider { |
| float getScale(TileSource src); |
| } |
| } |