2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.camera.tinyplanet;
19 import android.app.DialogFragment;
20 import android.app.ProgressDialog;
21 import android.graphics.Bitmap;
22 import android.graphics.Bitmap.CompressFormat;
23 import android.graphics.BitmapFactory;
24 import android.graphics.Canvas;
25 import android.graphics.Point;
26 import android.graphics.RectF;
27 import android.net.Uri;
28 import android.os.AsyncTask;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.util.Log;
32 import android.view.Display;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.View.OnClickListener;
36 import android.view.ViewGroup;
37 import android.view.Window;
38 import android.widget.Button;
39 import android.widget.SeekBar;
40 import android.widget.SeekBar.OnSeekBarChangeListener;
42 import com.adobe.xmp.XMPException;
43 import com.adobe.xmp.XMPMeta;
44 import com.android.camera.CameraActivity;
45 import com.android.camera.app.CameraApp;
46 import com.android.camera.app.MediaSaver.OnMediaSavedListener;
47 import com.android.camera.app.MediaSaver;
48 import com.android.camera.exif.ExifInterface;
49 import com.android.camera.tinyplanet.TinyPlanetPreview.PreviewSizeListener;
50 import com.android.camera.util.XmpUtil;
51 import com.android.camera2.R;
53 import java.io.ByteArrayOutputStream;
54 import java.io.FileNotFoundException;
55 import java.io.IOException;
56 import java.io.InputStream;
57 import java.util.Date;
58 import java.util.TimeZone;
59 import java.util.concurrent.locks.Lock;
60 import java.util.concurrent.locks.ReentrantLock;
63 * An activity that provides an editor UI to create a TinyPlanet image from a
64 * 360 degree stereographically mapped panoramic image.
66 public class TinyPlanetFragment extends DialogFragment implements PreviewSizeListener {
67 /** Argument to tell the fragment the URI of the original panoramic image. */
68 public static final String ARGUMENT_URI = "uri";
69 /** Argument to tell the fragment the title of the original panoramic image. */
70 public static final String ARGUMENT_TITLE = "title";
72 public static final String CROPPED_AREA_IMAGE_WIDTH_PIXELS =
73 "CroppedAreaImageWidthPixels";
74 public static final String CROPPED_AREA_IMAGE_HEIGHT_PIXELS =
75 "CroppedAreaImageHeightPixels";
76 public static final String CROPPED_AREA_FULL_PANO_WIDTH_PIXELS =
77 "FullPanoWidthPixels";
78 public static final String CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS =
79 "FullPanoHeightPixels";
80 public static final String CROPPED_AREA_LEFT =
81 "CroppedAreaLeftPixels";
82 public static final String CROPPED_AREA_TOP =
83 "CroppedAreaTopPixels";
84 public static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/";
86 private static final String TAG = "TinyPlanetActivity";
87 /** Delay between a value update and the renderer running. */
88 private static final int RENDER_DELAY_MILLIS = 50;
89 /** Filename prefix to prepend to the original name for the new file. */
90 private static final String FILENAME_PREFIX = "TINYPLANET_";
92 private Uri mSourceImageUri;
93 private TinyPlanetPreview mPreview;
94 private int mPreviewSizePx = 0;
95 private float mCurrentZoom = 0.5f;
96 private float mCurrentAngle = 0;
97 private ProgressDialog mDialog;
100 * Lock for the result preview bitmap. We can't change it while we're trying
103 private Lock mResultLock = new ReentrantLock();
105 /** The title of the original panoramic image. */
106 private String mOriginalTitle = "";
108 /** The padded source bitmap. */
109 private Bitmap mSourceBitmap;
110 /** The resulting preview bitmap. */
111 private Bitmap mResultBitmap;
113 /** Used to delay-post a tiny planet rendering task. */
114 private Handler mHandler = new Handler();
115 /** Whether rendering is in progress right now. */
116 private Boolean mRendering = false;
118 * Whether we should render one more time after the current rendering run is
119 * done. This is needed when there was an update to the values during the
122 private Boolean mRenderOneMore = false;
124 /** Tiny planet data plus size. */
125 private static final class TinyPlanetImage {
126 public final byte[] mJpegData;
127 public final int mSize;
129 public TinyPlanetImage(byte[] jpegData, int size) {
130 mJpegData = jpegData;
136 * Creates and executes a task to create a tiny planet with the current
139 private final Runnable mCreateTinyPlanetRunnable = new Runnable() {
142 synchronized (mRendering) {
144 mRenderOneMore = true;
150 (new AsyncTask<Void, Void, Void>() {
152 protected Void doInBackground(Void... params) {
155 if (mSourceBitmap == null || mResultBitmap == null) {
159 int width = mSourceBitmap.getWidth();
160 int height = mSourceBitmap.getHeight();
161 TinyPlanetNative.process(mSourceBitmap, width, height, mResultBitmap,
163 mCurrentZoom, mCurrentAngle);
165 mResultLock.unlock();
170 protected void onPostExecute(Void result) {
171 mPreview.setBitmap(mResultBitmap, mResultLock);
172 synchronized (mRendering) {
174 if (mRenderOneMore) {
175 mRenderOneMore = false;
180 }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
185 public void onCreate(Bundle savedInstanceState) {
186 super.onCreate(savedInstanceState);
187 setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Camera);
191 public View onCreateView(LayoutInflater inflater, ViewGroup container,
192 Bundle savedInstanceState) {
193 getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
194 getDialog().setCanceledOnTouchOutside(true);
196 View view = inflater.inflate(R.layout.tinyplanet_editor,
198 mPreview = (TinyPlanetPreview) view.findViewById(R.id.preview);
199 mPreview.setPreviewSizeChangeListener(this);
201 // Zoom slider setup.
202 SeekBar zoomSlider = (SeekBar) view.findViewById(R.id.zoomSlider);
203 zoomSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
205 public void onStopTrackingTouch(SeekBar seekBar) {
210 public void onStartTrackingTouch(SeekBar seekBar) {
215 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
216 onZoomChange(progress);
220 // Rotation slider setup.
221 SeekBar angleSlider = (SeekBar) view.findViewById(R.id.angleSlider);
222 angleSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
224 public void onStopTrackingTouch(SeekBar seekBar) {
229 public void onStartTrackingTouch(SeekBar seekBar) {
234 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
235 onAngleChange(progress);
239 Button createButton = (Button) view.findViewById(R.id.creatTinyPlanetButton);
240 createButton.setOnClickListener(new OnClickListener() {
242 public void onClick(View v) {
243 onCreateTinyPlanet();
247 mOriginalTitle = getArguments().getString(ARGUMENT_TITLE);
248 mSourceImageUri = Uri.parse(getArguments().getString(ARGUMENT_URI));
249 mSourceBitmap = createPaddedSourceImage(mSourceImageUri, true);
251 if (mSourceBitmap == null) {
252 Log.e(TAG, "Could not decode source image.");
259 * From the given URI this method creates a 360/180 padded image that is
260 * ready to be made a tiny planet.
262 private Bitmap createPaddedSourceImage(Uri sourceImageUri, boolean previewSize) {
263 InputStream is = getInputStream(sourceImageUri);
265 Log.e(TAG, "Could not create input stream for image.");
268 Bitmap sourceBitmap = BitmapFactory.decodeStream(is);
270 is = getInputStream(sourceImageUri);
271 XMPMeta xmp = XmpUtil.extractXMPMeta(is);
274 int size = previewSize ? getDisplaySize() : sourceBitmap.getWidth();
275 sourceBitmap = createPaddedBitmap(sourceBitmap, xmp, size);
281 * Starts an asynchronous task to create a tiny planet. Once done, will add
282 * the new image to the filmstrip and dismisses the fragment.
284 private void onCreateTinyPlanet() {
285 // Make sure we stop rendering before we create the high-res tiny
287 synchronized (mRendering) {
288 mRenderOneMore = false;
291 final String savingTinyPlanet = getActivity().getResources().getString(
292 R.string.saving_tiny_planet);
293 (new AsyncTask<Void, Void, TinyPlanetImage>() {
295 protected void onPreExecute() {
296 mDialog = ProgressDialog.show(getActivity(), null, savingTinyPlanet, true, false);
300 protected TinyPlanetImage doInBackground(Void... params) {
301 return createTinyPlanet();
305 protected void onPostExecute(TinyPlanetImage image) {
306 // Once created, store the new file and add it to the filmstrip.
307 final CameraActivity activity = (CameraActivity) getActivity();
308 MediaSaver mediaSaver = ((CameraApp) activity.getApplication()).getMediaSaver();
309 OnMediaSavedListener doneListener =
310 new OnMediaSavedListener() {
312 public void onMediaSaved(Uri uri) {
313 // Add the new photo to the filmstrip and exit
315 activity.notifyNewMedia(uri);
317 TinyPlanetFragment.this.dismiss();
320 String tinyPlanetTitle = FILENAME_PREFIX + mOriginalTitle;
321 mediaSaver.addImage(image.mJpegData, tinyPlanetTitle, (new Date()).getTime(),
323 image.mSize, image.mSize, 0, null, doneListener, getActivity()
324 .getContentResolver());
326 }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
330 * Creates the high quality tiny planet file and adds it to the media
331 * service. Don't call this on the UI thread.
333 private TinyPlanetImage createTinyPlanet() {
334 // Free some memory we don't need anymore as we're going to dimiss the
335 // fragment after the tiny planet creation.
338 mResultBitmap.recycle();
339 mResultBitmap = null;
340 mSourceBitmap.recycle();
341 mSourceBitmap = null;
343 mResultLock.unlock();
346 // Create a high-resolution padded image.
347 Bitmap sourceBitmap = createPaddedSourceImage(mSourceImageUri, false);
348 int width = sourceBitmap.getWidth();
349 int height = sourceBitmap.getHeight();
351 int outputSize = width / 2;
352 Bitmap resultBitmap = Bitmap.createBitmap(outputSize, outputSize,
353 Bitmap.Config.ARGB_8888);
355 TinyPlanetNative.process(sourceBitmap, width, height, resultBitmap,
356 outputSize, mCurrentZoom, mCurrentAngle);
358 // Free the sourceImage memory as we don't need it and we need memory
359 // for the JPEG bytes.
360 sourceBitmap.recycle();
363 ByteArrayOutputStream jpeg = new ByteArrayOutputStream();
364 resultBitmap.compress(CompressFormat.JPEG, 100, jpeg);
365 return new TinyPlanetImage(addExif(jpeg.toByteArray()), outputSize);
369 * Adds basic EXIF data to the tiny planet image so it an be rewritten
372 * @param jpeg the JPEG data of the tiny planet.
373 * @return The JPEG data containing basic EXIF.
375 private byte[] addExif(byte[] jpeg) {
376 ExifInterface exif = new ExifInterface();
377 exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, System.currentTimeMillis(),
378 TimeZone.getDefault());
379 ByteArrayOutputStream jpegOut = new ByteArrayOutputStream();
381 exif.writeExif(jpeg, jpegOut);
382 } catch (IOException e) {
383 Log.e(TAG, "Could not write EXIF", e);
385 return jpegOut.toByteArray();
388 private int getDisplaySize() {
389 Display display = getActivity().getWindowManager().getDefaultDisplay();
390 Point size = new Point();
391 display.getSize(size);
392 return Math.min(size.x, size.y);
396 public void onSizeChanged(int sizePx) {
397 mPreviewSizePx = sizePx;
400 if (mResultBitmap == null || mResultBitmap.getWidth() != sizePx
401 || mResultBitmap.getHeight() != sizePx) {
402 if (mResultBitmap != null) {
403 mResultBitmap.recycle();
405 mResultBitmap = Bitmap.createBitmap(mPreviewSizePx, mPreviewSizePx,
406 Bitmap.Config.ARGB_8888);
409 mResultLock.unlock();
412 // Run directly and on this thread directly.
413 mCreateTinyPlanetRunnable.run();
416 private void onZoomChange(int zoom) {
417 // 1000 needs to be in sync with the max values declared in the layout
419 mCurrentZoom = zoom / 1000f;
423 private void onAngleChange(int angle) {
424 mCurrentAngle = (float) Math.toRadians(angle);
429 * Delay-post a new preview rendering run.
431 private void scheduleUpdate() {
432 mHandler.removeCallbacks(mCreateTinyPlanetRunnable);
433 mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS);
436 private InputStream getInputStream(Uri uri) {
438 return getActivity().getContentResolver().openInputStream(uri);
439 } catch (FileNotFoundException e) {
440 Log.e(TAG, "Could not load source image.", e);
446 * To create a proper TinyPlanet, the input image must be 2:1 (360:180
447 * degrees). So if needed, we pad the source image with black.
449 private static Bitmap createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) {
451 int croppedAreaWidth =
452 getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS);
453 int croppedAreaHeight =
454 getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS);
456 getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS);
458 getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS);
459 int left = getInt(xmp, CROPPED_AREA_LEFT);
460 int top = getInt(xmp, CROPPED_AREA_TOP);
462 if (fullPanoWidth == 0 || fullPanoHeight == 0) {
465 // Make sure the intermediate image has the similar size to the
467 Bitmap paddedBitmap = null;
468 float scale = intermediateWidth / (float) fullPanoWidth;
469 while (paddedBitmap == null) {
471 paddedBitmap = Bitmap.createBitmap(
472 (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale),
473 Bitmap.Config.ARGB_8888);
474 } catch (OutOfMemoryError e) {
479 Canvas paddedCanvas = new Canvas(paddedBitmap);
481 int right = left + croppedAreaWidth;
482 int bottom = top + croppedAreaHeight;
483 RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale);
484 paddedCanvas.drawBitmap(bitmapIn, null, destRect, null);
486 } catch (XMPException ex) {
487 // Do nothing, just use mSourceBitmap as is.
492 private static int getInt(XMPMeta xmp, String key) throws XMPException {
493 if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) {
494 return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key);