blob: b9b87b17348ad40dba3cc9a8c95cdf0e91c2978f [file] [log] [blame]
Michael Jurkae8d1bf72013-09-09 15:58:54 +02001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16/* Copied from Launcher3 */
17package com.android.wallpapercropper;
18
19import android.app.ActionBar;
20import android.app.Activity;
21import android.app.WallpaperManager;
22import android.content.Context;
23import android.content.Intent;
24import android.content.SharedPreferences;
25import android.content.res.Configuration;
26import android.content.res.Resources;
27import android.graphics.Bitmap;
28import android.graphics.Bitmap.CompressFormat;
29import android.graphics.BitmapFactory;
30import android.graphics.BitmapRegionDecoder;
31import android.graphics.Canvas;
32import android.graphics.Matrix;
33import android.graphics.Paint;
34import android.graphics.Point;
35import android.graphics.Rect;
36import android.graphics.RectF;
37import android.net.Uri;
38import android.os.AsyncTask;
39import android.os.Bundle;
40import android.util.Log;
41import android.view.Display;
42import android.view.View;
43import android.view.WindowManager;
Michael Jurka7b215cb2013-10-30 14:40:39 +010044import android.widget.Toast;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020045
46import com.android.gallery3d.common.Utils;
Michael Jurka69784062013-10-14 14:42:50 -070047import com.android.gallery3d.exif.ExifInterface;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020048import com.android.photos.BitmapRegionTileSource;
Michael Jurka7b215cb2013-10-30 14:40:39 +010049import com.android.photos.BitmapRegionTileSource.BitmapSource;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020050
51import java.io.BufferedInputStream;
52import java.io.ByteArrayInputStream;
53import java.io.ByteArrayOutputStream;
54import java.io.FileNotFoundException;
55import java.io.IOException;
56import java.io.InputStream;
57
58public class WallpaperCropActivity extends Activity {
59 private static final String LOGTAG = "Launcher3.CropActivity";
60
61 protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width";
62 protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height";
63 private static final int DEFAULT_COMPRESS_QUALITY = 90;
64 /**
65 * The maximum bitmap size we allow to be returned through the intent.
66 * Intents have a maximum of 1MB in total size. However, the Bitmap seems to
67 * have some overhead to hit so that we go way below the limit here to make
68 * sure the intent stays below 1MB.We should consider just returning a byte
69 * array instead of a Bitmap instance to avoid overhead.
70 */
71 public static final int MAX_BMAP_IN_INTENT = 750000;
72 private static final float WALLPAPER_SCREENS_SPAN = 2f;
73
Michael Jurka35f922d2014-01-14 14:08:07 +010074 protected static Point sDefaultWallpaperSize;
75
Michael Jurkae8d1bf72013-09-09 15:58:54 +020076 protected CropView mCropView;
77 protected Uri mUri;
78
79 @Override
80 protected void onCreate(Bundle savedInstanceState) {
81 super.onCreate(savedInstanceState);
82 init();
Michael Jurkae72aa7f2013-10-07 17:03:30 -070083 if (!enableRotation()) {
84 setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT);
85 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +020086 }
87
88 protected void init() {
89 setContentView(R.layout.wallpaper_cropper);
90
91 mCropView = (CropView) findViewById(R.id.cropView);
92
Michael Jurka69784062013-10-14 14:42:50 -070093 Intent cropIntent = getIntent();
Michael Jurkae8d1bf72013-09-09 15:58:54 +020094 final Uri imageUri = cropIntent.getData();
95
Michael Jurka69784062013-10-14 14:42:50 -070096 if (imageUri == null) {
97 Log.e(LOGTAG, "No URI passed in intent, exiting WallpaperCropActivity");
98 finish();
99 return;
100 }
101
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200102 // Action bar
103 // Show the custom action bar view
104 final ActionBar actionBar = getActionBar();
105 actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
106 actionBar.getCustomView().setOnClickListener(
107 new View.OnClickListener() {
108 @Override
109 public void onClick(View v) {
110 boolean finishActivityWhenDone = true;
111 cropImageAndSetWallpaper(imageUri, null, finishActivityWhenDone);
112 }
113 });
Michael Jurka5271ea12013-10-28 14:37:37 +0100114
115 // Load image in background
Michael Jurka7b215cb2013-10-30 14:40:39 +0100116 final BitmapRegionTileSource.UriBitmapSource bitmapSource =
117 new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024);
118 Runnable onLoad = new Runnable() {
119 public void run() {
120 if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) {
121 Toast.makeText(WallpaperCropActivity.this,
122 getString(R.string.wallpaper_load_fail),
123 Toast.LENGTH_LONG).show();
124 finish();
125 }
126 }
127 };
128 setCropViewTileSource(bitmapSource, true, false, onLoad);
Michael Jurka5271ea12013-10-28 14:37:37 +0100129 }
130
Michael Jurka7b215cb2013-10-30 14:40:39 +0100131 public void setCropViewTileSource(
132 final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled,
133 final boolean moveToLeft, final Runnable postExecute) {
Michael Jurka5271ea12013-10-28 14:37:37 +0100134 final Context context = WallpaperCropActivity.this;
135 final View progressView = findViewById(R.id.loading);
136 final AsyncTask<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() {
137 protected Void doInBackground(Void...args) {
138 if (!isCancelled()) {
139 bitmapSource.loadInBackground();
140 }
141 return null;
142 }
143 protected void onPostExecute(Void arg) {
144 if (!isCancelled()) {
145 progressView.setVisibility(View.INVISIBLE);
Michael Jurka7b215cb2013-10-30 14:40:39 +0100146 if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
147 mCropView.setTileSource(
148 new BitmapRegionTileSource(context, bitmapSource), null);
149 mCropView.setTouchEnabled(touchEnabled);
150 if (moveToLeft) {
151 mCropView.moveToLeft();
152 }
Michael Jurka5271ea12013-10-28 14:37:37 +0100153 }
154 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100155 if (postExecute != null) {
156 postExecute.run();
157 }
Michael Jurka5271ea12013-10-28 14:37:37 +0100158 }
159 };
160 // We don't want to show the spinner every time we load an image, because that would be
161 // annoying; instead, only start showing the spinner if loading the image has taken
162 // longer than 1 sec (ie 1000 ms)
163 progressView.postDelayed(new Runnable() {
164 public void run() {
165 if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) {
166 progressView.setVisibility(View.VISIBLE);
167 }
168 }
169 }, 1000);
170 loadBitmapTask.execute();
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700171 }
172
173 public boolean enableRotation() {
174 return getResources().getBoolean(R.bool.allow_rotation);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200175 }
176
177 public static String getSharedPreferencesKey() {
178 return WallpaperCropActivity.class.getName();
179 }
180
181 // As a ratio of screen height, the total distance we want the parallax effect to span
182 // horizontally
183 private static float wallpaperTravelToScreenWidthRatio(int width, int height) {
184 float aspectRatio = width / (float) height;
185
186 // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width
187 // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width
188 // We will use these two data points to extrapolate how much the wallpaper parallax effect
189 // to span (ie travel) at any aspect ratio:
190
191 final float ASPECT_RATIO_LANDSCAPE = 16/10f;
192 final float ASPECT_RATIO_PORTRAIT = 10/16f;
193 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f;
194 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f;
195
196 // To find out the desired width at different aspect ratios, we use the following two
197 // formulas, where the coefficient on x is the aspect ratio (width/height):
198 // (16/10)x + y = 1.5
199 // (10/16)x + y = 1.2
200 // We solve for x and y and end up with a final formula:
201 final float x =
202 (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) /
203 (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
204 final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
205 return x * aspectRatio + y;
206 }
207
208 static protected Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) {
Michael Jurka35f922d2014-01-14 14:08:07 +0100209 if (sDefaultWallpaperSize == null) {
210 Point minDims = new Point();
211 Point maxDims = new Point();
212 windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200213
Michael Jurka35f922d2014-01-14 14:08:07 +0100214 int maxDim = Math.max(maxDims.x, maxDims.y);
215 int minDim = Math.max(minDims.x, minDims.y);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200216
Michael Jurka35f922d2014-01-14 14:08:07 +0100217 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
218 Point realSize = new Point();
219 windowManager.getDefaultDisplay().getRealSize(realSize);
220 maxDim = Math.max(realSize.x, realSize.y);
221 minDim = Math.min(realSize.x, realSize.y);
222 }
223
224 // We need to ensure that there is enough extra space in the wallpaper
225 // for the intended parallax effects
226 final int defaultWidth, defaultHeight;
227 if (isScreenLarge(res)) {
228 defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
229 defaultHeight = maxDim;
230 } else {
231 defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
232 defaultHeight = maxDim;
233 }
234 sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200235 }
Michael Jurka35f922d2014-01-14 14:08:07 +0100236 return sDefaultWallpaperSize;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200237 }
238
Michael Jurka69784062013-10-14 14:42:50 -0700239 public static int getRotationFromExif(String path) {
240 return getRotationFromExifHelper(path, null, 0, null, null);
241 }
242
243 public static int getRotationFromExif(Context context, Uri uri) {
244 return getRotationFromExifHelper(null, null, 0, context, uri);
245 }
246
247 public static int getRotationFromExif(Resources res, int resId) {
248 return getRotationFromExifHelper(null, res, resId, null, null);
249 }
250
251 private static int getRotationFromExifHelper(
252 String path, Resources res, int resId, Context context, Uri uri) {
253 ExifInterface ei = new ExifInterface();
Michael Jurkab2552642013-10-31 11:07:24 +0100254 InputStream is = null;
255 BufferedInputStream bis = null;
Michael Jurka69784062013-10-14 14:42:50 -0700256 try {
257 if (path != null) {
258 ei.readExif(path);
259 } else if (uri != null) {
Michael Jurkab2552642013-10-31 11:07:24 +0100260 is = context.getContentResolver().openInputStream(uri);
261 bis = new BufferedInputStream(is);
Michael Jurka69784062013-10-14 14:42:50 -0700262 ei.readExif(bis);
263 } else {
Michael Jurkab2552642013-10-31 11:07:24 +0100264 is = res.openRawResource(resId);
265 bis = new BufferedInputStream(is);
Michael Jurka69784062013-10-14 14:42:50 -0700266 ei.readExif(bis);
267 }
268 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
269 if (ori != null) {
270 return ExifInterface.getRotationForOrientationValue(ori.shortValue());
271 }
272 } catch (IOException e) {
273 Log.w(LOGTAG, "Getting exif data failed", e);
Michael Jurkab2552642013-10-31 11:07:24 +0100274 } finally {
275 Utils.closeSilently(bis);
276 Utils.closeSilently(is);
Michael Jurka69784062013-10-14 14:42:50 -0700277 }
278 return 0;
279 }
280
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200281 protected void setWallpaper(String filePath, final boolean finishActivityWhenDone) {
Michael Jurka69784062013-10-14 14:42:50 -0700282 int rotation = getRotationFromExif(filePath);
283 BitmapCropTask cropTask = new BitmapCropTask(
284 this, filePath, null, rotation, 0, 0, true, false, null);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200285 final Point bounds = cropTask.getImageBounds();
286 Runnable onEndCrop = new Runnable() {
287 public void run() {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200288 if (finishActivityWhenDone) {
289 setResult(Activity.RESULT_OK);
290 finish();
291 }
292 }
293 };
294 cropTask.setOnEndRunnable(onEndCrop);
295 cropTask.setNoCrop(true);
296 cropTask.execute();
297 }
298
299 protected void cropImageAndSetWallpaper(
300 Resources res, int resId, final boolean finishActivityWhenDone) {
301 // crop this image and scale it down to the default wallpaper size for
302 // this device
Michael Jurka69784062013-10-14 14:42:50 -0700303 int rotation = getRotationFromExif(res, resId);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200304 Point inSize = mCropView.getSourceDimensions();
305 Point outSize = getDefaultWallpaperSize(getResources(),
306 getWindowManager());
307 RectF crop = getMaxCropRect(
308 inSize.x, inSize.y, outSize.x, outSize.y, false);
309 Runnable onEndCrop = new Runnable() {
310 public void run() {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200311 if (finishActivityWhenDone) {
312 setResult(Activity.RESULT_OK);
313 finish();
314 }
315 }
316 };
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700317 BitmapCropTask cropTask = new BitmapCropTask(this, res, resId,
Michael Jurka69784062013-10-14 14:42:50 -0700318 crop, rotation, outSize.x, outSize.y, true, false, onEndCrop);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200319 cropTask.execute();
320 }
321
322 private static boolean isScreenLarge(Resources res) {
323 Configuration config = res.getConfiguration();
324 return config.smallestScreenWidthDp >= 720;
325 }
326
327 protected void cropImageAndSetWallpaper(Uri uri,
328 OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) {
Michael Jurka2ab14da2014-01-14 14:48:20 +0100329 boolean centerCrop = getResources().getBoolean(R.bool.center_crop);
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700330 // Get the crop
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700331 boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
332
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200333 Display d = getWindowManager().getDefaultDisplay();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200334
335 Point displaySize = new Point();
336 d.getSize(displaySize);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200337 boolean isPortrait = displaySize.x < displaySize.y;
Michael Jurkaeed96a02013-11-06 13:22:51 +0100338
339 Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(),
340 getWindowManager());
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200341 // Get the crop
342 RectF cropRect = mCropView.getCrop();
Michael Jurka69784062013-10-14 14:42:50 -0700343 int cropRotation = mCropView.getImageRotation();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200344 float cropScale = mCropView.getWidth() / (float) cropRect.width();
345
Michael Jurka69784062013-10-14 14:42:50 -0700346 Point inSize = mCropView.getSourceDimensions();
347 Matrix rotateMatrix = new Matrix();
348 rotateMatrix.setRotate(cropRotation);
349 float[] rotatedInSize = new float[] { inSize.x, inSize.y };
350 rotateMatrix.mapPoints(rotatedInSize);
351 rotatedInSize[0] = Math.abs(rotatedInSize[0]);
352 rotatedInSize[1] = Math.abs(rotatedInSize[1]);
353
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200354 // ADJUST CROP WIDTH
355 // Extend the crop all the way to the right, for parallax
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700356 // (or all the way to the left, in RTL)
Michael Jurka2ab14da2014-01-14 14:48:20 +0100357 float extraSpace;
358 if (centerCrop) {
359 extraSpace = 2f * Math.min(rotatedInSize[0] - cropRect.right, cropRect.left);
360 } else {
361 extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left;
362 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200363 // Cap the amount of extra width
Michael Jurkaeed96a02013-11-06 13:22:51 +0100364 float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width();
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700365 extraSpace = Math.min(extraSpace, maxExtraSpace);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200366
Michael Jurka2ab14da2014-01-14 14:48:20 +0100367 if (centerCrop) {
368 cropRect.left -= extraSpace / 2f;
369 cropRect.right += extraSpace / 2f;
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700370 } else {
Michael Jurka2ab14da2014-01-14 14:48:20 +0100371 if (ltr) {
372 cropRect.right += extraSpace;
373 } else {
374 cropRect.left -= extraSpace;
375 }
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700376 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200377
378 // ADJUST CROP HEIGHT
379 if (isPortrait) {
Michael Jurkaeed96a02013-11-06 13:22:51 +0100380 cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200381 } else { // LANDSCAPE
382 float extraPortraitHeight =
Michael Jurkaeed96a02013-11-06 13:22:51 +0100383 defaultWallpaperSize.y / cropScale - cropRect.height();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200384 float expandHeight =
Michael Jurka69784062013-10-14 14:42:50 -0700385 Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top),
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200386 extraPortraitHeight / 2);
387 cropRect.top -= expandHeight;
388 cropRect.bottom += expandHeight;
389 }
390 final int outWidth = (int) Math.round(cropRect.width() * cropScale);
391 final int outHeight = (int) Math.round(cropRect.height() * cropScale);
392
393 Runnable onEndCrop = new Runnable() {
394 public void run() {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200395 if (finishActivityWhenDone) {
396 setResult(Activity.RESULT_OK);
397 finish();
398 }
399 }
400 };
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700401 BitmapCropTask cropTask = new BitmapCropTask(this, uri,
Michael Jurka69784062013-10-14 14:42:50 -0700402 cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200403 if (onBitmapCroppedHandler != null) {
404 cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
405 }
406 cropTask.execute();
407 }
408
409 public interface OnBitmapCroppedHandler {
410 public void onBitmapCropped(byte[] imageBytes);
411 }
412
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700413 protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200414 Uri mInUri = null;
415 Context mContext;
416 String mInFilePath;
417 byte[] mInImageBytes;
418 int mInResId = 0;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200419 RectF mCropBounds = null;
420 int mOutWidth, mOutHeight;
Michael Jurka69784062013-10-14 14:42:50 -0700421 int mRotation;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200422 String mOutputFormat = "jpg"; // for now
423 boolean mSetWallpaper;
424 boolean mSaveCroppedBitmap;
425 Bitmap mCroppedBitmap;
426 Runnable mOnEndRunnable;
427 Resources mResources;
428 OnBitmapCroppedHandler mOnBitmapCroppedHandler;
429 boolean mNoCrop;
430
431 public BitmapCropTask(Context c, String filePath,
Michael Jurka69784062013-10-14 14:42:50 -0700432 RectF cropBounds, int rotation, int outWidth, int outHeight,
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200433 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
434 mContext = c;
435 mInFilePath = filePath;
Michael Jurka69784062013-10-14 14:42:50 -0700436 init(cropBounds, rotation,
437 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200438 }
439
440 public BitmapCropTask(byte[] imageBytes,
Michael Jurka69784062013-10-14 14:42:50 -0700441 RectF cropBounds, int rotation, int outWidth, int outHeight,
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200442 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
443 mInImageBytes = imageBytes;
Michael Jurka69784062013-10-14 14:42:50 -0700444 init(cropBounds, rotation,
445 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200446 }
447
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700448 public BitmapCropTask(Context c, Uri inUri,
Michael Jurka69784062013-10-14 14:42:50 -0700449 RectF cropBounds, int rotation, int outWidth, int outHeight,
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200450 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700451 mContext = c;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200452 mInUri = inUri;
Michael Jurka69784062013-10-14 14:42:50 -0700453 init(cropBounds, rotation,
454 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200455 }
456
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700457 public BitmapCropTask(Context c, Resources res, int inResId,
Michael Jurka69784062013-10-14 14:42:50 -0700458 RectF cropBounds, int rotation, int outWidth, int outHeight,
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200459 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700460 mContext = c;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200461 mInResId = inResId;
462 mResources = res;
Michael Jurka69784062013-10-14 14:42:50 -0700463 init(cropBounds, rotation,
464 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200465 }
466
Michael Jurka69784062013-10-14 14:42:50 -0700467 private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200468 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
469 mCropBounds = cropBounds;
Michael Jurka69784062013-10-14 14:42:50 -0700470 mRotation = rotation;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200471 mOutWidth = outWidth;
472 mOutHeight = outHeight;
473 mSetWallpaper = setWallpaper;
474 mSaveCroppedBitmap = saveCroppedBitmap;
475 mOnEndRunnable = onEndRunnable;
476 }
477
478 public void setOnBitmapCropped(OnBitmapCroppedHandler handler) {
479 mOnBitmapCroppedHandler = handler;
480 }
481
482 public void setNoCrop(boolean value) {
483 mNoCrop = value;
484 }
485
486 public void setOnEndRunnable(Runnable onEndRunnable) {
487 mOnEndRunnable = onEndRunnable;
488 }
489
490 // Helper to setup input stream
Michael Jurka7b215cb2013-10-30 14:40:39 +0100491 private InputStream regenerateInputStream() {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200492 if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) {
493 Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " +
494 "image byte array given");
495 } else {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200496 try {
497 if (mInUri != null) {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100498 return new BufferedInputStream(
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700499 mContext.getContentResolver().openInputStream(mInUri));
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200500 } else if (mInFilePath != null) {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100501 return mContext.openFileInput(mInFilePath);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200502 } else if (mInImageBytes != null) {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100503 return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200504 } else {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100505 return new BufferedInputStream(mResources.openRawResource(mInResId));
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200506 }
507 } catch (FileNotFoundException e) {
508 Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
509 }
510 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100511 return null;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200512 }
513
514 public Point getImageBounds() {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100515 InputStream is = regenerateInputStream();
516 if (is != null) {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200517 BitmapFactory.Options options = new BitmapFactory.Options();
518 options.inJustDecodeBounds = true;
Michael Jurka7b215cb2013-10-30 14:40:39 +0100519 BitmapFactory.decodeStream(is, null, options);
520 Utils.closeSilently(is);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200521 if (options.outWidth != 0 && options.outHeight != 0) {
522 return new Point(options.outWidth, options.outHeight);
523 }
524 }
525 return null;
526 }
527
528 public void setCropBounds(RectF cropBounds) {
529 mCropBounds = cropBounds;
530 }
531
532 public Bitmap getCroppedBitmap() {
533 return mCroppedBitmap;
534 }
535 public boolean cropBitmap() {
536 boolean failure = false;
537
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200538
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700539 WallpaperManager wallpaperManager = null;
540 if (mSetWallpaper) {
541 wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
542 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100543
544
545 if (mSetWallpaper && mNoCrop) {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200546 try {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100547 InputStream is = regenerateInputStream();
548 if (is != null) {
549 wallpaperManager.setStream(is);
550 Utils.closeSilently(is);
551 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200552 } catch (IOException e) {
553 Log.w(LOGTAG, "cannot write stream to wallpaper", e);
554 failure = true;
555 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200556 return !failure;
Michael Jurka7b215cb2013-10-30 14:40:39 +0100557 } else {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200558 // Find crop bounds (scaled to original image size)
559 Rect roundedTrueCrop = new Rect();
Michael Jurka69784062013-10-14 14:42:50 -0700560 Matrix rotateMatrix = new Matrix();
561 Matrix inverseRotateMatrix = new Matrix();
Michael Jurkae39c9a92013-12-02 15:05:44 -0800562
563 Point bounds = getImageBounds();
Michael Jurka69784062013-10-14 14:42:50 -0700564 if (mRotation > 0) {
565 rotateMatrix.setRotate(mRotation);
566 inverseRotateMatrix.setRotate(-mRotation);
567
568 mCropBounds.roundOut(roundedTrueCrop);
569 mCropBounds = new RectF(roundedTrueCrop);
570
Michael Jurka7b215cb2013-10-30 14:40:39 +0100571 if (bounds == null) {
572 Log.w(LOGTAG, "cannot get bounds for image");
573 failure = true;
574 return false;
575 }
Michael Jurka69784062013-10-14 14:42:50 -0700576
577 float[] rotatedBounds = new float[] { bounds.x, bounds.y };
578 rotateMatrix.mapPoints(rotatedBounds);
579 rotatedBounds[0] = Math.abs(rotatedBounds[0]);
580 rotatedBounds[1] = Math.abs(rotatedBounds[1]);
581
582 mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
583 inverseRotateMatrix.mapRect(mCropBounds);
584 mCropBounds.offset(bounds.x/2, bounds.y/2);
585
Michael Jurka69784062013-10-14 14:42:50 -0700586 }
587
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200588 mCropBounds.roundOut(roundedTrueCrop);
589
590 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
591 Log.w(LOGTAG, "crop has bad values for full size image");
592 failure = true;
593 return false;
594 }
595
596 // See how much we're reducing the size of the image
Michael Jurkab2552642013-10-31 11:07:24 +0100597 int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
598 roundedTrueCrop.height() / mOutHeight));
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200599 // Attempt to open a region decoder
600 BitmapRegionDecoder decoder = null;
Michael Jurkab2552642013-10-31 11:07:24 +0100601 InputStream is = null;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200602 try {
Michael Jurkab2552642013-10-31 11:07:24 +0100603 is = regenerateInputStream();
Michael Jurka7b215cb2013-10-30 14:40:39 +0100604 if (is == null) {
605 Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString());
606 failure = true;
607 return false;
608 }
609 decoder = BitmapRegionDecoder.newInstance(is, false);
610 Utils.closeSilently(is);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200611 } catch (IOException e) {
612 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
Michael Jurkab2552642013-10-31 11:07:24 +0100613 } finally {
614 Utils.closeSilently(is);
615 is = null;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200616 }
617
618 Bitmap crop = null;
619 if (decoder != null) {
620 // Do region decoding to get crop bitmap
621 BitmapFactory.Options options = new BitmapFactory.Options();
622 if (scaleDownSampleSize > 1) {
623 options.inSampleSize = scaleDownSampleSize;
624 }
625 crop = decoder.decodeRegion(roundedTrueCrop, options);
626 decoder.recycle();
627 }
628
629 if (crop == null) {
630 // BitmapRegionDecoder has failed, try to crop in-memory
Michael Jurkab2552642013-10-31 11:07:24 +0100631 is = regenerateInputStream();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200632 Bitmap fullSize = null;
Michael Jurka7b215cb2013-10-30 14:40:39 +0100633 if (is != null) {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200634 BitmapFactory.Options options = new BitmapFactory.Options();
635 if (scaleDownSampleSize > 1) {
636 options.inSampleSize = scaleDownSampleSize;
637 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100638 fullSize = BitmapFactory.decodeStream(is, null, options);
639 Utils.closeSilently(is);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200640 }
641 if (fullSize != null) {
Michael Jurkae39c9a92013-12-02 15:05:44 -0800642 // Find out the true sample size that was used by the decoder
643 scaleDownSampleSize = bounds.x / fullSize.getWidth();
Michael Jurka69784062013-10-14 14:42:50 -0700644 mCropBounds.left /= scaleDownSampleSize;
645 mCropBounds.top /= scaleDownSampleSize;
646 mCropBounds.bottom /= scaleDownSampleSize;
647 mCropBounds.right /= scaleDownSampleSize;
648 mCropBounds.roundOut(roundedTrueCrop);
649
Michael Jurkae39c9a92013-12-02 15:05:44 -0800650 // Adjust values to account for issues related to rounding
651 if (roundedTrueCrop.width() > fullSize.getWidth()) {
652 // Adjust the width
653 roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth();
654 }
655 if (roundedTrueCrop.right > fullSize.getWidth()) {
656 // Adjust the left value
657 int adjustment = roundedTrueCrop.left -
658 Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width());
659 roundedTrueCrop.left -= adjustment;
660 roundedTrueCrop.right -= adjustment;
661 }
662 if (roundedTrueCrop.height() > fullSize.getHeight()) {
663 // Adjust the height
664 roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight();
665 }
666 if (roundedTrueCrop.bottom > fullSize.getHeight()) {
667 // Adjust the top value
668 int adjustment = roundedTrueCrop.top -
669 Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height());
670 roundedTrueCrop.top -= adjustment;
671 roundedTrueCrop.bottom -= adjustment;
672 }
673
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200674 crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
675 roundedTrueCrop.top, roundedTrueCrop.width(),
676 roundedTrueCrop.height());
677 }
678 }
679
680 if (crop == null) {
681 Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
682 failure = true;
683 return false;
684 }
Michael Jurka69784062013-10-14 14:42:50 -0700685 if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
686 float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
687 rotateMatrix.mapPoints(dimsAfter);
688 dimsAfter[0] = Math.abs(dimsAfter[0]);
689 dimsAfter[1] = Math.abs(dimsAfter[1]);
690
691 if (!(mOutWidth > 0 && mOutHeight > 0)) {
692 mOutWidth = Math.round(dimsAfter[0]);
693 mOutHeight = Math.round(dimsAfter[1]);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200694 }
Michael Jurka69784062013-10-14 14:42:50 -0700695
696 RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200697 RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
Michael Jurka69784062013-10-14 14:42:50 -0700698
699 Matrix m = new Matrix();
700 if (mRotation == 0) {
701 m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
702 } else {
703 Matrix m1 = new Matrix();
704 m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
705 Matrix m2 = new Matrix();
706 m2.setRotate(mRotation);
707 Matrix m3 = new Matrix();
708 m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
709 Matrix m4 = new Matrix();
710 m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
711
712 Matrix c1 = new Matrix();
713 c1.setConcat(m2, m1);
714 Matrix c2 = new Matrix();
715 c2.setConcat(m4, m3);
716 m.setConcat(c2, c1);
717 }
718
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200719 Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
720 (int) returnRect.height(), Bitmap.Config.ARGB_8888);
721 if (tmp != null) {
722 Canvas c = new Canvas(tmp);
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700723 Paint p = new Paint();
724 p.setFilterBitmap(true);
725 c.drawBitmap(crop, m, p);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200726 crop = tmp;
727 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200728 }
729
730 if (mSaveCroppedBitmap) {
731 mCroppedBitmap = crop;
732 }
733
734 // Get output compression format
735 CompressFormat cf =
736 convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
737
738 // Compress to byte array
739 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
740 if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
741 // If we need to set to the wallpaper, set it
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700742 if (mSetWallpaper && wallpaperManager != null) {
743 try {
744 byte[] outByteArray = tmpOut.toByteArray();
745 wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
746 if (mOnBitmapCroppedHandler != null) {
747 mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200748 }
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700749 } catch (IOException e) {
750 Log.w(LOGTAG, "cannot write stream to wallpaper", e);
751 failure = true;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200752 }
753 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200754 } else {
755 Log.w(LOGTAG, "cannot compress bitmap");
756 failure = true;
757 }
758 }
759 return !failure; // True if any of the operations failed
760 }
761
762 @Override
763 protected Boolean doInBackground(Void... params) {
764 return cropBitmap();
765 }
766
767 @Override
768 protected void onPostExecute(Boolean result) {
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700769 if (mOnEndRunnable != null) {
770 mOnEndRunnable.run();
771 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200772 }
773 }
774
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200775 protected static RectF getMaxCropRect(
776 int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
777 RectF cropRect = new RectF();
778 // Get a crop rect that will fit this
779 if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
780 cropRect.top = 0;
781 cropRect.bottom = inHeight;
782 cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
783 cropRect.right = inWidth - cropRect.left;
784 if (leftAligned) {
785 cropRect.right -= cropRect.left;
786 cropRect.left = 0;
787 }
788 } else {
789 cropRect.left = 0;
790 cropRect.right = inWidth;
791 cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
792 cropRect.bottom = inHeight - cropRect.top;
793 }
794 return cropRect;
795 }
796
797 protected static CompressFormat convertExtensionToCompressFormat(String extension) {
798 return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
799 }
800
801 protected static String getFileExtension(String requestFormat) {
802 String outputFormat = (requestFormat == null)
803 ? "jpg"
804 : requestFormat;
805 outputFormat = outputFormat.toLowerCase();
806 return (outputFormat.equals("png") || outputFormat.equals("gif"))
807 ? "png" // We don't support gif compression.
808 : "jpg";
809 }
810}