| /* |
| * 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.wallpaperpicker; |
| |
| import android.annotation.TargetApi; |
| import android.app.ActionBar; |
| import android.app.Activity; |
| import android.app.WallpaperManager; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.pm.ActivityInfo; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.Matrix; |
| import android.graphics.Point; |
| import android.graphics.RectF; |
| import android.graphics.drawable.Drawable; |
| 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.wallpaperpicker.common.CropAndSetWallpaperTask; |
| 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 com.android.wallpaperpicker.common.DialogUtils; |
| import com.android.wallpaperpicker.common.InputStreamProvider; |
| |
| 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 = "WallpaperCropActivity"; |
| |
| private static final int MSG_LOAD_IMAGE = 1; |
| |
| protected CropView mCropView; |
| protected View mProgressView; |
| protected View mSetWallpaperButton; |
| |
| private HandlerThread mLoaderThread; |
| private Handler mLoaderHandler; |
| private LoadRequest mCurrentLoadRequest; |
| private byte[] mTempStorageForDecoding = new byte[16 * 1024]; |
| // A weak-set of reusable bitmaps |
| private Set<Bitmap> mReusableBitmaps = |
| Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>()); |
| |
| private final DialogInterface.OnCancelListener mOnDialogCancelListener = |
| new DialogInterface.OnCancelListener() { |
| @Override |
| public void onCancel(DialogInterface dialog) { |
| showActionBarAndTiles(); |
| } |
| }; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| mLoaderThread = new HandlerThread("wallpaper_loader"); |
| mLoaderThread.start(); |
| mLoaderHandler = new Handler(mLoaderThread.getLooper(), this); |
| |
| init(); |
| if (!enableRotation()) { |
| setRequestedOrientation(ActivityInfo.SCREEN_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) { |
| actionBar.hide(); |
| // Never fade on finish because we return to the app that started us (e.g. |
| // Photos), not the home screen. |
| cropImageAndSetWallpaper(imageUri, null, false /* shouldFadeOutOnFinish */); |
| } |
| }); |
| mSetWallpaperButton = findViewById(R.id.set_wallpaper_button); |
| |
| // Load image in background |
| final BitmapRegionTileSource.InputStreamSource bitmapSource = |
| new BitmapRegionTileSource.InputStreamSource(this, imageUri); |
| mSetWallpaperButton.setEnabled(false); |
| Runnable onLoad = new Runnable() { |
| public void run() { |
| if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) { |
| Toast.makeText(WallpaperCropActivity.this, 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} |
| */ |
| @TargetApi(Build.VERSION_CODES.KITKAT) |
| @Override |
| public boolean handleMessage(Message msg) { |
| if (msg.what == MSG_LOAD_IMAGE) { |
| final LoadRequest req = (LoadRequest) msg.obj; |
| final boolean loadSuccess; |
| |
| if (req.src == null) { |
| Drawable defaultWallpaper = WallpaperManager.getInstance(this) |
| .getBuiltInDrawable(mCropView.getWidth(), mCropView.getHeight(), |
| false, 0.5f, 0.5f); |
| |
| if (defaultWallpaper == null) { |
| loadSuccess = false; |
| Log.w(LOGTAG, "Null default wallpaper encountered."); |
| } else { |
| loadSuccess = true; |
| req.result = new DrawableTileSource(this, |
| defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE); |
| } |
| } else { |
| 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(WallpaperCropActivity.this, req.src, |
| mTempStorageForDecoding); |
| loadSuccess = req.src.getLoadingState() == BitmapSource.State.LOADED; |
| } |
| |
| runOnUiThread(new Runnable() { |
| |
| @Override |
| public void run() { |
| if (req == mCurrentLoadRequest) { |
| onLoadRequestComplete(req, loadSuccess); |
| } else { |
| addReusableBitmap(req.result); |
| } |
| } |
| }); |
| return true; |
| } |
| return false; |
| } |
| |
| @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) |
| public boolean isActivityDestroyed() { |
| return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed(); |
| } |
| |
| private void addReusableBitmap(TileSource src) { |
| synchronized (mReusableBitmaps) { |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT |
| && src instanceof BitmapRegionTileSource) { |
| Bitmap preview = ((BitmapRegionTileSource) src).getBitmap(); |
| if (preview != null && preview.isMutable()) { |
| mReusableBitmaps.add(preview); |
| } |
| } |
| } |
| } |
| |
| public DialogInterface.OnCancelListener getOnDialogCancelListener() { |
| return mOnDialogCancelListener; |
| } |
| |
| private void showActionBarAndTiles() { |
| getActionBar().show(); |
| View wallpaperStrip = findViewById(R.id.wallpaper_strip); |
| if (wallpaperStrip != null) { |
| wallpaperStrip.setVisibility(View.VISIBLE); |
| } |
| } |
| |
| 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.scaleAndOffsetProvider != null) { |
| TileSource src = req.result; |
| Point wallpaperSize = WallpaperUtils.getDefaultWallpaperSize( |
| getResources(), getWindowManager()); |
| RectF crop = Utils.getMaxCropRect(src.getImageWidth(), src.getImageHeight(), |
| wallpaperSize.x, wallpaperSize.y, false /* leftAligned */); |
| mCropView.setScale(req.scaleAndOffsetProvider.getScale(wallpaperSize, crop)); |
| mCropView.setParallaxOffset(req.scaleAndOffsetProvider.getParallaxOffset(), crop); |
| } |
| |
| // 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); |
| } |
| |
| @TargetApi(Build.VERSION_CODES.KITKAT) |
| public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled, |
| boolean moveToLeft, CropViewScaleAndOffsetProvider scaleAndOffsetProvider, |
| Runnable postExecute) { |
| final LoadRequest req = new LoadRequest(); |
| req.moveToLeft = moveToLeft; |
| req.src = bitmapSource; |
| req.touchEnabled = touchEnabled; |
| req.postExecute = postExecute; |
| req.scaleAndOffsetProvider = scaleAndOffsetProvider; |
| 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 true; |
| } |
| |
| public void cropImageAndSetWallpaper(Resources res, int resId, boolean shouldFadeOutOnFinish) { |
| // crop this image and scale it down to the default wallpaper size for |
| // this device |
| InputStreamProvider streamProvider = InputStreamProvider.fromResource(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); |
| // Passing 0, 0 will cause launcher to revert to using the |
| // default wallpaper size |
| CropAndFinishHandler onEndCrop = new CropAndFinishHandler(new Point(0, 0), |
| shouldFadeOutOnFinish); |
| CropAndSetWallpaperTask cropTask = new CropAndSetWallpaperTask( |
| streamProvider, this, crop, streamProvider.getRotationFromExif(this), |
| outSize.x, outSize.y, onEndCrop); |
| DialogUtils.executeCropTaskAfterPrompt(this, cropTask, getOnDialogCancelListener()); |
| } |
| |
| @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) |
| public void cropImageAndSetWallpaper(Uri uri, |
| CropAndSetWallpaperTask.OnBitmapCroppedHandler onBitmapCroppedHandler, |
| boolean shouldFadeOutOnFinish) { |
| // 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 = 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 (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); |
| CropAndFinishHandler onEndCrop = new CropAndFinishHandler(new Point(outWidth, outHeight), |
| shouldFadeOutOnFinish); |
| |
| CropAndSetWallpaperTask cropTask = new CropAndSetWallpaperTask( |
| InputStreamProvider.fromUri(this, uri), this, |
| cropRect, cropRotation, outWidth, outHeight, onEndCrop) { |
| @Override |
| protected void onPreExecute() { |
| // Give some feedback so user knows something is happening. |
| mProgressView.setVisibility(View.VISIBLE); |
| } |
| }; |
| if (onBitmapCroppedHandler != null) { |
| cropTask.setOnBitmapCropped(onBitmapCroppedHandler); |
| } |
| DialogUtils.executeCropTaskAfterPrompt(this, cropTask, getOnDialogCancelListener()); |
| } |
| |
| public void setBoundsAndFinish(Point bounds, boolean overrideTransition) { |
| WallpaperUtils.saveWallpaperDimensions(bounds.x, bounds.y, this); |
| setResult(Activity.RESULT_OK); |
| finish(); |
| if (overrideTransition) { |
| overridePendingTransition(0, R.anim.fade_out); |
| } |
| } |
| |
| public class CropAndFinishHandler implements CropAndSetWallpaperTask.OnEndCropHandler { |
| private final Point mBounds; |
| private boolean mShouldFadeOutOnFinish; |
| |
| /** |
| * @param shouldFadeOutOnFinish Whether the wallpaper picker should override the default |
| * exit animation to fade out instead. This should only be set to true if the wallpaper |
| * preview will exactly match the actual wallpaper on the page we are returning to. |
| */ |
| public CropAndFinishHandler(Point bounds, boolean shouldFadeOutOnFinish) { |
| mBounds = bounds; |
| mShouldFadeOutOnFinish = shouldFadeOutOnFinish; |
| } |
| |
| @Override |
| public void run(boolean cropSucceeded) { |
| setBoundsAndFinish(mBounds, cropSucceeded && mShouldFadeOutOnFinish); |
| } |
| } |
| |
| static class LoadRequest { |
| BitmapSource src; |
| boolean touchEnabled; |
| boolean moveToLeft; |
| Runnable postExecute; |
| CropViewScaleAndOffsetProvider scaleAndOffsetProvider; |
| |
| TileSource result; |
| } |
| |
| public interface CropViewScaleAndOffsetProvider { |
| float getScale(Point wallpaperSize, RectF crop); |
| float getParallaxOffset(); |
| } |
| } |