blob: 57c0581b8f87df55acb1e50b8be1ce81739e67c6 [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
74 protected CropView mCropView;
75 protected Uri mUri;
76
77 @Override
78 protected void onCreate(Bundle savedInstanceState) {
79 super.onCreate(savedInstanceState);
80 init();
Michael Jurkae72aa7f2013-10-07 17:03:30 -070081 if (!enableRotation()) {
82 setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT);
83 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +020084 }
85
86 protected void init() {
87 setContentView(R.layout.wallpaper_cropper);
88
89 mCropView = (CropView) findViewById(R.id.cropView);
90
Michael Jurka69784062013-10-14 14:42:50 -070091 Intent cropIntent = getIntent();
Michael Jurkae8d1bf72013-09-09 15:58:54 +020092 final Uri imageUri = cropIntent.getData();
93
Michael Jurka69784062013-10-14 14:42:50 -070094 if (imageUri == null) {
95 Log.e(LOGTAG, "No URI passed in intent, exiting WallpaperCropActivity");
96 finish();
97 return;
98 }
99
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200100 // Action bar
101 // Show the custom action bar view
102 final ActionBar actionBar = getActionBar();
103 actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
104 actionBar.getCustomView().setOnClickListener(
105 new View.OnClickListener() {
106 @Override
107 public void onClick(View v) {
108 boolean finishActivityWhenDone = true;
109 cropImageAndSetWallpaper(imageUri, null, finishActivityWhenDone);
110 }
111 });
Michael Jurka5271ea12013-10-28 14:37:37 +0100112
113 // Load image in background
Michael Jurka7b215cb2013-10-30 14:40:39 +0100114 final BitmapRegionTileSource.UriBitmapSource bitmapSource =
115 new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024);
116 Runnable onLoad = new Runnable() {
117 public void run() {
118 if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) {
119 Toast.makeText(WallpaperCropActivity.this,
120 getString(R.string.wallpaper_load_fail),
121 Toast.LENGTH_LONG).show();
122 finish();
123 }
124 }
125 };
126 setCropViewTileSource(bitmapSource, true, false, onLoad);
Michael Jurka5271ea12013-10-28 14:37:37 +0100127 }
128
Michael Jurka7b215cb2013-10-30 14:40:39 +0100129 public void setCropViewTileSource(
130 final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled,
131 final boolean moveToLeft, final Runnable postExecute) {
Michael Jurka5271ea12013-10-28 14:37:37 +0100132 final Context context = WallpaperCropActivity.this;
133 final View progressView = findViewById(R.id.loading);
134 final AsyncTask<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() {
135 protected Void doInBackground(Void...args) {
136 if (!isCancelled()) {
137 bitmapSource.loadInBackground();
138 }
139 return null;
140 }
141 protected void onPostExecute(Void arg) {
142 if (!isCancelled()) {
143 progressView.setVisibility(View.INVISIBLE);
Michael Jurka7b215cb2013-10-30 14:40:39 +0100144 if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
145 mCropView.setTileSource(
146 new BitmapRegionTileSource(context, bitmapSource), null);
147 mCropView.setTouchEnabled(touchEnabled);
148 if (moveToLeft) {
149 mCropView.moveToLeft();
150 }
Michael Jurka5271ea12013-10-28 14:37:37 +0100151 }
152 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100153 if (postExecute != null) {
154 postExecute.run();
155 }
Michael Jurka5271ea12013-10-28 14:37:37 +0100156 }
157 };
158 // We don't want to show the spinner every time we load an image, because that would be
159 // annoying; instead, only start showing the spinner if loading the image has taken
160 // longer than 1 sec (ie 1000 ms)
161 progressView.postDelayed(new Runnable() {
162 public void run() {
163 if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) {
164 progressView.setVisibility(View.VISIBLE);
165 }
166 }
167 }, 1000);
168 loadBitmapTask.execute();
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700169 }
170
171 public boolean enableRotation() {
172 return getResources().getBoolean(R.bool.allow_rotation);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200173 }
174
175 public static String getSharedPreferencesKey() {
176 return WallpaperCropActivity.class.getName();
177 }
178
179 // As a ratio of screen height, the total distance we want the parallax effect to span
180 // horizontally
181 private static float wallpaperTravelToScreenWidthRatio(int width, int height) {
182 float aspectRatio = width / (float) height;
183
184 // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width
185 // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width
186 // We will use these two data points to extrapolate how much the wallpaper parallax effect
187 // to span (ie travel) at any aspect ratio:
188
189 final float ASPECT_RATIO_LANDSCAPE = 16/10f;
190 final float ASPECT_RATIO_PORTRAIT = 10/16f;
191 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f;
192 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f;
193
194 // To find out the desired width at different aspect ratios, we use the following two
195 // formulas, where the coefficient on x is the aspect ratio (width/height):
196 // (16/10)x + y = 1.5
197 // (10/16)x + y = 1.2
198 // We solve for x and y and end up with a final formula:
199 final float x =
200 (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) /
201 (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
202 final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
203 return x * aspectRatio + y;
204 }
205
206 static protected Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) {
207 Point minDims = new Point();
208 Point maxDims = new Point();
209 windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
210
211 int maxDim = Math.max(maxDims.x, maxDims.y);
Michael Jurka8b483602013-09-20 04:32:20 +0200212 int minDim = Math.max(minDims.x, minDims.y);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200213
214 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
215 Point realSize = new Point();
216 windowManager.getDefaultDisplay().getRealSize(realSize);
217 maxDim = Math.max(realSize.x, realSize.y);
Michael Jurka8b483602013-09-20 04:32:20 +0200218 minDim = Math.min(realSize.x, realSize.y);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200219 }
220
221 // We need to ensure that there is enough extra space in the wallpaper
222 // for the intended
223 // parallax effects
224 final int defaultWidth, defaultHeight;
225 if (isScreenLarge(res)) {
226 defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
227 defaultHeight = maxDim;
228 } else {
229 defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
230 defaultHeight = maxDim;
231 }
232 return new Point(defaultWidth, defaultHeight);
233 }
234
Michael Jurka69784062013-10-14 14:42:50 -0700235 public static int getRotationFromExif(String path) {
236 return getRotationFromExifHelper(path, null, 0, null, null);
237 }
238
239 public static int getRotationFromExif(Context context, Uri uri) {
240 return getRotationFromExifHelper(null, null, 0, context, uri);
241 }
242
243 public static int getRotationFromExif(Resources res, int resId) {
244 return getRotationFromExifHelper(null, res, resId, null, null);
245 }
246
247 private static int getRotationFromExifHelper(
248 String path, Resources res, int resId, Context context, Uri uri) {
249 ExifInterface ei = new ExifInterface();
Michael Jurkab2552642013-10-31 11:07:24 +0100250 InputStream is = null;
251 BufferedInputStream bis = null;
Michael Jurka69784062013-10-14 14:42:50 -0700252 try {
253 if (path != null) {
254 ei.readExif(path);
255 } else if (uri != null) {
Michael Jurkab2552642013-10-31 11:07:24 +0100256 is = context.getContentResolver().openInputStream(uri);
257 bis = new BufferedInputStream(is);
Michael Jurka69784062013-10-14 14:42:50 -0700258 ei.readExif(bis);
259 } else {
Michael Jurkab2552642013-10-31 11:07:24 +0100260 is = res.openRawResource(resId);
261 bis = new BufferedInputStream(is);
Michael Jurka69784062013-10-14 14:42:50 -0700262 ei.readExif(bis);
263 }
264 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
265 if (ori != null) {
266 return ExifInterface.getRotationForOrientationValue(ori.shortValue());
267 }
268 } catch (IOException e) {
269 Log.w(LOGTAG, "Getting exif data failed", e);
Michael Jurkab2552642013-10-31 11:07:24 +0100270 } finally {
271 Utils.closeSilently(bis);
272 Utils.closeSilently(is);
Michael Jurka69784062013-10-14 14:42:50 -0700273 }
274 return 0;
275 }
276
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200277 protected void setWallpaper(String filePath, final boolean finishActivityWhenDone) {
Michael Jurka69784062013-10-14 14:42:50 -0700278 int rotation = getRotationFromExif(filePath);
279 BitmapCropTask cropTask = new BitmapCropTask(
280 this, filePath, null, rotation, 0, 0, true, false, null);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200281 final Point bounds = cropTask.getImageBounds();
282 Runnable onEndCrop = new Runnable() {
283 public void run() {
284 updateWallpaperDimensions(bounds.x, bounds.y);
285 if (finishActivityWhenDone) {
286 setResult(Activity.RESULT_OK);
287 finish();
288 }
289 }
290 };
291 cropTask.setOnEndRunnable(onEndCrop);
292 cropTask.setNoCrop(true);
293 cropTask.execute();
294 }
295
296 protected void cropImageAndSetWallpaper(
297 Resources res, int resId, final boolean finishActivityWhenDone) {
298 // crop this image and scale it down to the default wallpaper size for
299 // this device
Michael Jurka69784062013-10-14 14:42:50 -0700300 int rotation = getRotationFromExif(res, resId);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200301 Point inSize = mCropView.getSourceDimensions();
302 Point outSize = getDefaultWallpaperSize(getResources(),
303 getWindowManager());
304 RectF crop = getMaxCropRect(
305 inSize.x, inSize.y, outSize.x, outSize.y, false);
306 Runnable onEndCrop = new Runnable() {
307 public void run() {
308 // Passing 0, 0 will cause launcher to revert to using the
309 // default wallpaper size
310 updateWallpaperDimensions(0, 0);
311 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 Jurkae72aa7f2013-10-07 17:03:30 -0700329 // Get the crop
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700330 boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
331
Michael Jurkaeed96a02013-11-06 13:22:51 +0100332
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 Jurka69784062013-10-14 14:42:50 -0700357 float extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200358 // Cap the amount of extra width
Michael Jurkaeed96a02013-11-06 13:22:51 +0100359 float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width();
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700360 extraSpace = Math.min(extraSpace, maxExtraSpace);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200361
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700362 if (ltr) {
363 cropRect.right += extraSpace;
364 } else {
365 cropRect.left -= extraSpace;
366 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200367
368 // ADJUST CROP HEIGHT
369 if (isPortrait) {
Michael Jurkaeed96a02013-11-06 13:22:51 +0100370 cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200371 } else { // LANDSCAPE
372 float extraPortraitHeight =
Michael Jurkaeed96a02013-11-06 13:22:51 +0100373 defaultWallpaperSize.y / cropScale - cropRect.height();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200374 float expandHeight =
Michael Jurka69784062013-10-14 14:42:50 -0700375 Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top),
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200376 extraPortraitHeight / 2);
377 cropRect.top -= expandHeight;
378 cropRect.bottom += expandHeight;
379 }
380 final int outWidth = (int) Math.round(cropRect.width() * cropScale);
381 final int outHeight = (int) Math.round(cropRect.height() * cropScale);
382
383 Runnable onEndCrop = new Runnable() {
384 public void run() {
385 updateWallpaperDimensions(outWidth, outHeight);
386 if (finishActivityWhenDone) {
387 setResult(Activity.RESULT_OK);
388 finish();
389 }
390 }
391 };
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700392 BitmapCropTask cropTask = new BitmapCropTask(this, uri,
Michael Jurka69784062013-10-14 14:42:50 -0700393 cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200394 if (onBitmapCroppedHandler != null) {
395 cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
396 }
397 cropTask.execute();
398 }
399
400 public interface OnBitmapCroppedHandler {
401 public void onBitmapCropped(byte[] imageBytes);
402 }
403
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700404 protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200405 Uri mInUri = null;
406 Context mContext;
407 String mInFilePath;
408 byte[] mInImageBytes;
409 int mInResId = 0;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200410 RectF mCropBounds = null;
411 int mOutWidth, mOutHeight;
Michael Jurka69784062013-10-14 14:42:50 -0700412 int mRotation;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200413 String mOutputFormat = "jpg"; // for now
414 boolean mSetWallpaper;
415 boolean mSaveCroppedBitmap;
416 Bitmap mCroppedBitmap;
417 Runnable mOnEndRunnable;
418 Resources mResources;
419 OnBitmapCroppedHandler mOnBitmapCroppedHandler;
420 boolean mNoCrop;
421
422 public BitmapCropTask(Context c, String filePath,
Michael Jurka69784062013-10-14 14:42:50 -0700423 RectF cropBounds, int rotation, int outWidth, int outHeight,
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200424 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
425 mContext = c;
426 mInFilePath = filePath;
Michael Jurka69784062013-10-14 14:42:50 -0700427 init(cropBounds, rotation,
428 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200429 }
430
431 public BitmapCropTask(byte[] imageBytes,
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 mInImageBytes = imageBytes;
Michael Jurka69784062013-10-14 14:42:50 -0700435 init(cropBounds, rotation,
436 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200437 }
438
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700439 public BitmapCropTask(Context c, Uri inUri,
Michael Jurka69784062013-10-14 14:42:50 -0700440 RectF cropBounds, int rotation, int outWidth, int outHeight,
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200441 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700442 mContext = c;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200443 mInUri = inUri;
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, Resources res, int inResId,
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 mInResId = inResId;
453 mResources = res;
Michael Jurka69784062013-10-14 14:42:50 -0700454 init(cropBounds, rotation,
455 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200456 }
457
Michael Jurka69784062013-10-14 14:42:50 -0700458 private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200459 boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
460 mCropBounds = cropBounds;
Michael Jurka69784062013-10-14 14:42:50 -0700461 mRotation = rotation;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200462 mOutWidth = outWidth;
463 mOutHeight = outHeight;
464 mSetWallpaper = setWallpaper;
465 mSaveCroppedBitmap = saveCroppedBitmap;
466 mOnEndRunnable = onEndRunnable;
467 }
468
469 public void setOnBitmapCropped(OnBitmapCroppedHandler handler) {
470 mOnBitmapCroppedHandler = handler;
471 }
472
473 public void setNoCrop(boolean value) {
474 mNoCrop = value;
475 }
476
477 public void setOnEndRunnable(Runnable onEndRunnable) {
478 mOnEndRunnable = onEndRunnable;
479 }
480
481 // Helper to setup input stream
Michael Jurka7b215cb2013-10-30 14:40:39 +0100482 private InputStream regenerateInputStream() {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200483 if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) {
484 Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " +
485 "image byte array given");
486 } else {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200487 try {
488 if (mInUri != null) {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100489 return new BufferedInputStream(
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700490 mContext.getContentResolver().openInputStream(mInUri));
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200491 } else if (mInFilePath != null) {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100492 return mContext.openFileInput(mInFilePath);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200493 } else if (mInImageBytes != null) {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100494 return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200495 } else {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100496 return new BufferedInputStream(mResources.openRawResource(mInResId));
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200497 }
498 } catch (FileNotFoundException e) {
499 Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
500 }
501 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100502 return null;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200503 }
504
505 public Point getImageBounds() {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100506 InputStream is = regenerateInputStream();
507 if (is != null) {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200508 BitmapFactory.Options options = new BitmapFactory.Options();
509 options.inJustDecodeBounds = true;
Michael Jurka7b215cb2013-10-30 14:40:39 +0100510 BitmapFactory.decodeStream(is, null, options);
511 Utils.closeSilently(is);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200512 if (options.outWidth != 0 && options.outHeight != 0) {
513 return new Point(options.outWidth, options.outHeight);
514 }
515 }
516 return null;
517 }
518
519 public void setCropBounds(RectF cropBounds) {
520 mCropBounds = cropBounds;
521 }
522
523 public Bitmap getCroppedBitmap() {
524 return mCroppedBitmap;
525 }
526 public boolean cropBitmap() {
527 boolean failure = false;
528
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200529
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700530 WallpaperManager wallpaperManager = null;
531 if (mSetWallpaper) {
532 wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
533 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100534
535
536 if (mSetWallpaper && mNoCrop) {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200537 try {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100538 InputStream is = regenerateInputStream();
539 if (is != null) {
540 wallpaperManager.setStream(is);
541 Utils.closeSilently(is);
542 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200543 } catch (IOException e) {
544 Log.w(LOGTAG, "cannot write stream to wallpaper", e);
545 failure = true;
546 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200547 return !failure;
Michael Jurka7b215cb2013-10-30 14:40:39 +0100548 } else {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200549 // Find crop bounds (scaled to original image size)
550 Rect roundedTrueCrop = new Rect();
Michael Jurka69784062013-10-14 14:42:50 -0700551 Matrix rotateMatrix = new Matrix();
552 Matrix inverseRotateMatrix = new Matrix();
553 if (mRotation > 0) {
554 rotateMatrix.setRotate(mRotation);
555 inverseRotateMatrix.setRotate(-mRotation);
556
557 mCropBounds.roundOut(roundedTrueCrop);
558 mCropBounds = new RectF(roundedTrueCrop);
559
560 Point bounds = getImageBounds();
Michael Jurka7b215cb2013-10-30 14:40:39 +0100561 if (bounds == null) {
562 Log.w(LOGTAG, "cannot get bounds for image");
563 failure = true;
564 return false;
565 }
Michael Jurka69784062013-10-14 14:42:50 -0700566
567 float[] rotatedBounds = new float[] { bounds.x, bounds.y };
568 rotateMatrix.mapPoints(rotatedBounds);
569 rotatedBounds[0] = Math.abs(rotatedBounds[0]);
570 rotatedBounds[1] = Math.abs(rotatedBounds[1]);
571
572 mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
573 inverseRotateMatrix.mapRect(mCropBounds);
574 mCropBounds.offset(bounds.x/2, bounds.y/2);
575
Michael Jurka69784062013-10-14 14:42:50 -0700576 }
577
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200578 mCropBounds.roundOut(roundedTrueCrop);
579
580 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
581 Log.w(LOGTAG, "crop has bad values for full size image");
582 failure = true;
583 return false;
584 }
585
586 // See how much we're reducing the size of the image
Michael Jurkab2552642013-10-31 11:07:24 +0100587 int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
588 roundedTrueCrop.height() / mOutHeight));
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200589 // Attempt to open a region decoder
590 BitmapRegionDecoder decoder = null;
Michael Jurkab2552642013-10-31 11:07:24 +0100591 InputStream is = null;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200592 try {
Michael Jurkab2552642013-10-31 11:07:24 +0100593 is = regenerateInputStream();
Michael Jurka7b215cb2013-10-30 14:40:39 +0100594 if (is == null) {
595 Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString());
596 failure = true;
597 return false;
598 }
599 decoder = BitmapRegionDecoder.newInstance(is, false);
600 Utils.closeSilently(is);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200601 } catch (IOException e) {
602 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
Michael Jurkab2552642013-10-31 11:07:24 +0100603 } finally {
604 Utils.closeSilently(is);
605 is = null;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200606 }
607
608 Bitmap crop = null;
609 if (decoder != null) {
610 // Do region decoding to get crop bitmap
611 BitmapFactory.Options options = new BitmapFactory.Options();
612 if (scaleDownSampleSize > 1) {
613 options.inSampleSize = scaleDownSampleSize;
614 }
615 crop = decoder.decodeRegion(roundedTrueCrop, options);
616 decoder.recycle();
617 }
618
619 if (crop == null) {
620 // BitmapRegionDecoder has failed, try to crop in-memory
Michael Jurkab2552642013-10-31 11:07:24 +0100621 is = regenerateInputStream();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200622 Bitmap fullSize = null;
Michael Jurka7b215cb2013-10-30 14:40:39 +0100623 if (is != null) {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200624 BitmapFactory.Options options = new BitmapFactory.Options();
625 if (scaleDownSampleSize > 1) {
626 options.inSampleSize = scaleDownSampleSize;
627 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100628 fullSize = BitmapFactory.decodeStream(is, null, options);
629 Utils.closeSilently(is);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200630 }
631 if (fullSize != null) {
Michael Jurka69784062013-10-14 14:42:50 -0700632 mCropBounds.left /= scaleDownSampleSize;
633 mCropBounds.top /= scaleDownSampleSize;
634 mCropBounds.bottom /= scaleDownSampleSize;
635 mCropBounds.right /= scaleDownSampleSize;
636 mCropBounds.roundOut(roundedTrueCrop);
637
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200638 crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
639 roundedTrueCrop.top, roundedTrueCrop.width(),
640 roundedTrueCrop.height());
641 }
642 }
643
644 if (crop == null) {
645 Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
646 failure = true;
647 return false;
648 }
Michael Jurka69784062013-10-14 14:42:50 -0700649 if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
650 float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
651 rotateMatrix.mapPoints(dimsAfter);
652 dimsAfter[0] = Math.abs(dimsAfter[0]);
653 dimsAfter[1] = Math.abs(dimsAfter[1]);
654
655 if (!(mOutWidth > 0 && mOutHeight > 0)) {
656 mOutWidth = Math.round(dimsAfter[0]);
657 mOutHeight = Math.round(dimsAfter[1]);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200658 }
Michael Jurka69784062013-10-14 14:42:50 -0700659
660 RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200661 RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
Michael Jurka69784062013-10-14 14:42:50 -0700662
663 Matrix m = new Matrix();
664 if (mRotation == 0) {
665 m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
666 } else {
667 Matrix m1 = new Matrix();
668 m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
669 Matrix m2 = new Matrix();
670 m2.setRotate(mRotation);
671 Matrix m3 = new Matrix();
672 m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
673 Matrix m4 = new Matrix();
674 m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
675
676 Matrix c1 = new Matrix();
677 c1.setConcat(m2, m1);
678 Matrix c2 = new Matrix();
679 c2.setConcat(m4, m3);
680 m.setConcat(c2, c1);
681 }
682
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200683 Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
684 (int) returnRect.height(), Bitmap.Config.ARGB_8888);
685 if (tmp != null) {
686 Canvas c = new Canvas(tmp);
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700687 Paint p = new Paint();
688 p.setFilterBitmap(true);
689 c.drawBitmap(crop, m, p);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200690 crop = tmp;
691 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200692 }
693
694 if (mSaveCroppedBitmap) {
695 mCroppedBitmap = crop;
696 }
697
698 // Get output compression format
699 CompressFormat cf =
700 convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
701
702 // Compress to byte array
703 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
704 if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
705 // If we need to set to the wallpaper, set it
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700706 if (mSetWallpaper && wallpaperManager != null) {
707 try {
708 byte[] outByteArray = tmpOut.toByteArray();
709 wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
710 if (mOnBitmapCroppedHandler != null) {
711 mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200712 }
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700713 } catch (IOException e) {
714 Log.w(LOGTAG, "cannot write stream to wallpaper", e);
715 failure = true;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200716 }
717 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200718 } else {
719 Log.w(LOGTAG, "cannot compress bitmap");
720 failure = true;
721 }
722 }
723 return !failure; // True if any of the operations failed
724 }
725
726 @Override
727 protected Boolean doInBackground(Void... params) {
728 return cropBitmap();
729 }
730
731 @Override
732 protected void onPostExecute(Boolean result) {
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700733 if (mOnEndRunnable != null) {
734 mOnEndRunnable.run();
735 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200736 }
737 }
738
739 protected void updateWallpaperDimensions(int width, int height) {
740 String spKey = getSharedPreferencesKey();
Michael Jurkaeed96a02013-11-06 13:22:51 +0100741 SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200742 SharedPreferences.Editor editor = sp.edit();
743 if (width != 0 && height != 0) {
744 editor.putInt(WALLPAPER_WIDTH_KEY, width);
745 editor.putInt(WALLPAPER_HEIGHT_KEY, height);
746 } else {
747 editor.remove(WALLPAPER_WIDTH_KEY);
748 editor.remove(WALLPAPER_HEIGHT_KEY);
749 }
750 editor.commit();
751
752 suggestWallpaperDimension(getResources(),
753 sp, getWindowManager(), WallpaperManager.getInstance(this));
754 }
755
756 static public void suggestWallpaperDimension(Resources res,
757 final SharedPreferences sharedPrefs,
758 WindowManager windowManager,
759 final WallpaperManager wallpaperManager) {
Michael Jurka69784062013-10-14 14:42:50 -0700760 final Point defaultWallpaperSize = getDefaultWallpaperSize(res, windowManager);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200761
762 new Thread("suggestWallpaperDimension") {
763 public void run() {
764 // If we have saved a wallpaper width/height, use that instead
765 int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, defaultWallpaperSize.x);
766 int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, defaultWallpaperSize.y);
767 wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
768 }
769 }.start();
770 }
771
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200772 protected static RectF getMaxCropRect(
773 int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
774 RectF cropRect = new RectF();
775 // Get a crop rect that will fit this
776 if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
777 cropRect.top = 0;
778 cropRect.bottom = inHeight;
779 cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
780 cropRect.right = inWidth - cropRect.left;
781 if (leftAligned) {
782 cropRect.right -= cropRect.left;
783 cropRect.left = 0;
784 }
785 } else {
786 cropRect.left = 0;
787 cropRect.right = inWidth;
788 cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
789 cropRect.bottom = inHeight - cropRect.top;
790 }
791 return cropRect;
792 }
793
794 protected static CompressFormat convertExtensionToCompressFormat(String extension) {
795 return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
796 }
797
798 protected static String getFileExtension(String requestFormat) {
799 String outputFormat = (requestFormat == null)
800 ? "jpg"
801 : requestFormat;
802 outputFormat = outputFormat.toLowerCase();
803 return (outputFormat.equals("png") || outputFormat.equals("gif"))
804 ? "png" // We don't support gif compression.
805 : "jpg";
806 }
807}