blob: bdb601b460fdd654456d162cd3f853f9be83ea57 [file] [log] [blame]
/*
* 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();
}
}