summaryrefslogtreecommitdiffstats
path: root/src/com/android/camera/tinyplanet/TinyPlanetFragment.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/camera/tinyplanet/TinyPlanetFragment.java')
-rw-r--r--src/com/android/camera/tinyplanet/TinyPlanetFragment.java475
1 files changed, 475 insertions, 0 deletions
diff --git a/src/com/android/camera/tinyplanet/TinyPlanetFragment.java b/src/com/android/camera/tinyplanet/TinyPlanetFragment.java
new file mode 100644
index 000000000..2c9ad41a2
--- /dev/null
+++ b/src/com/android/camera/tinyplanet/TinyPlanetFragment.java
@@ -0,0 +1,475 @@
+/*
+ * 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.camera.tinyplanet;
+
+import android.app.DialogFragment;
+import android.app.ProgressDialog;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.android.camera.CameraActivity;
+import com.android.camera.MediaSaveService;
+import com.android.camera.MediaSaveService.OnMediaSavedListener;
+import com.android.camera.tinyplanet.TinyPlanetPreview.PreviewSizeListener;
+import com.android.camera.util.XmpUtil;
+import com.android.camera2.R;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.Date;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * An activity that provides an editor UI to create a TinyPlanet image from a
+ * 360 degree stereographically mapped panoramic image.
+ */
+public class TinyPlanetFragment extends DialogFragment implements PreviewSizeListener {
+ /** Argument to tell the fragment the URI of the original panoramic image. */
+ public static final String ARGUMENT_URI = "uri";
+ /** Argument to tell the fragment the title of the original panoramic image. */
+ public static final String ARGUMENT_TITLE = "title";
+
+ public static final String CROPPED_AREA_IMAGE_WIDTH_PIXELS =
+ "CroppedAreaImageWidthPixels";
+ public static final String CROPPED_AREA_IMAGE_HEIGHT_PIXELS =
+ "CroppedAreaImageHeightPixels";
+ public static final String CROPPED_AREA_FULL_PANO_WIDTH_PIXELS =
+ "FullPanoWidthPixels";
+ public static final String CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS =
+ "FullPanoHeightPixels";
+ public static final String CROPPED_AREA_LEFT =
+ "CroppedAreaLeftPixels";
+ public static final String CROPPED_AREA_TOP =
+ "CroppedAreaTopPixels";
+ public static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/";
+
+ private static final String TAG = "TinyPlanetActivity";
+ /** Delay between a value update and the renderer running. */
+ private static final int RENDER_DELAY_MILLIS = 50;
+ /** Filename prefix to prepend to the original name for the new file. */
+ private static final String FILENAME_PREFIX = "TINYPLANET_";
+
+ private Uri mSourceImageUri;
+ private TinyPlanetPreview mPreview;
+ private int mPreviewSizePx = 0;
+ private float mCurrentZoom = 0.5f;
+ private float mCurrentAngle = 0;
+ private ProgressDialog mDialog;
+
+ /**
+ * Lock for the result preview bitmap. We can't change it while we're trying
+ * to draw it.
+ */
+ private Lock mResultLock = new ReentrantLock();
+
+ /** The title of the original panoramic image. */
+ private String mOriginalTitle = "";
+
+ /** The padded source bitmap. */
+ private Bitmap mSourceBitmap;
+ /** The resulting preview bitmap. */
+ private Bitmap mResultBitmap;
+
+ /** Used to delay-post a tiny planet rendering task. */
+ private Handler mHandler = new Handler();
+ /** Whether rendering is in progress right now. */
+ private Boolean mRendering = false;
+ /**
+ * Whether we should render one more time after the current rendering run is
+ * done. This is needed when there was an update to the values during the
+ * current rendering.
+ */
+ private Boolean mRenderOneMore = false;
+
+ /** Tiny planet data plus size. */
+ private static final class TinyPlanetImage {
+ public final byte[] mJpegData;
+ public final int mSize;
+
+ public TinyPlanetImage(byte[] jpegData, int size) {
+ mJpegData = jpegData;
+ mSize = size;
+ }
+ }
+
+ /**
+ * Creates and executes a task to create a tiny planet with the current
+ * values.
+ */
+ private final Runnable mCreateTinyPlanetRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mRendering) {
+ if (mRendering) {
+ mRenderOneMore = true;
+ return;
+ }
+ mRendering = true;
+ }
+
+ (new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ mResultLock.lock();
+ try {
+ if (mSourceBitmap == null || mResultBitmap == null) {
+ return null;
+ }
+
+ int width = mSourceBitmap.getWidth();
+ int height = mSourceBitmap.getHeight();
+ TinyPlanetNative.process(mSourceBitmap, width, height, mResultBitmap,
+ mPreviewSizePx,
+ mCurrentZoom, mCurrentAngle);
+ } finally {
+ mResultLock.unlock();
+ }
+ return null;
+ }
+
+ protected void onPostExecute(Void result) {
+ mPreview.setBitmap(mResultBitmap, mResultLock);
+ synchronized (mRendering) {
+ mRendering = false;
+ if (mRenderOneMore) {
+ mRenderOneMore = false;
+ scheduleUpdate();
+ }
+ }
+ }
+ }).execute();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Camera);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ getDialog().setCanceledOnTouchOutside(true);
+
+ View view = inflater.inflate(R.layout.tinyplanet_editor,
+ container, false);
+ mPreview = (TinyPlanetPreview) view.findViewById(R.id.preview);
+ mPreview.setPreviewSizeChangeListener(this);
+
+ // Zoom slider setup.
+ SeekBar zoomSlider = (SeekBar) view.findViewById(R.id.zoomSlider);
+ zoomSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ onZoomChange(progress);
+ }
+ });
+
+ // Rotation slider setup.
+ SeekBar angleSlider = (SeekBar) view.findViewById(R.id.angleSlider);
+ angleSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ onAngleChange(progress);
+ }
+ });
+
+ Button createButton = (Button) view.findViewById(R.id.creatTinyPlanetButton);
+ createButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onCreateTinyPlanet();
+ }
+ });
+
+ mOriginalTitle = getArguments().getString(ARGUMENT_TITLE);
+ mSourceImageUri = Uri.parse(getArguments().getString(ARGUMENT_URI));
+ mSourceBitmap = createPaddedSourceImage(mSourceImageUri, true);
+
+ if (mSourceBitmap == null) {
+ Log.e(TAG, "Could not decode source image.");
+ dismiss();
+ }
+ return view;
+ }
+
+ /**
+ * From the given URI this method creates a 360/180 padded image that is
+ * ready to be made a tiny planet.
+ */
+ private Bitmap createPaddedSourceImage(Uri sourceImageUri, boolean previewSize) {
+ InputStream is = getInputStream(sourceImageUri);
+ if (is == null) {
+ Log.e(TAG, "Could not create input stream for image.");
+ dismiss();
+ }
+ Bitmap sourceBitmap = BitmapFactory.decodeStream(is);
+
+ is = getInputStream(sourceImageUri);
+ XMPMeta xmp = XmpUtil.extractXMPMeta(is);
+
+ if (xmp != null) {
+ int size = previewSize ? getDisplaySize() : sourceBitmap.getWidth();
+ sourceBitmap = createPaddedBitmap(sourceBitmap, xmp, size);
+ }
+ return sourceBitmap;
+ }
+
+ /**
+ * Starts an asynchronous task to create a tiny planet. Once done, will add
+ * the new image to the filmstrip and dismisses the fragment.
+ */
+ private void onCreateTinyPlanet() {
+ // Make sure we stop rendering before we create the high-res tiny
+ // planet.
+ synchronized (mRendering) {
+ mRenderOneMore = false;
+ }
+
+ final String savingTinyPlanet = getActivity().getResources().getString(
+ R.string.saving_tiny_planet);
+ (new AsyncTask<Void, Void, TinyPlanetImage>() {
+ @Override
+ protected void onPreExecute() {
+ mDialog = ProgressDialog.show(getActivity(), null, savingTinyPlanet, true, false);
+ }
+
+ @Override
+ protected TinyPlanetImage doInBackground(Void... params) {
+ return createTinyPlanet();
+ }
+
+ @Override
+ protected void onPostExecute(TinyPlanetImage image) {
+ // Once created, store the new file and add it to the filmstrip.
+ final CameraActivity activity = (CameraActivity) getActivity();
+ MediaSaveService mediaSaveService = activity.getMediaSaveService();
+ OnMediaSavedListener doneListener =
+ new OnMediaSavedListener() {
+ @Override
+ public void onMediaSaved(Uri uri) {
+ // Add the new photo to the filmstrip and exit
+ // the fragment.
+ activity.notifyNewMedia(uri);
+ mDialog.dismiss();
+ TinyPlanetFragment.this.dismiss();
+ }
+ };
+ String tinyPlanetTitle = FILENAME_PREFIX + mOriginalTitle;
+ mediaSaveService.addImage(image.mJpegData, tinyPlanetTitle, (new Date()).getTime(),
+ null,
+ image.mSize, image.mSize, 0, null, doneListener, getActivity()
+ .getContentResolver());
+ }
+ }).execute();
+ }
+
+ /**
+ * Creates the high quality tiny planet file and adds it to the media
+ * service. Don't call this on the UI thread.
+ */
+ private TinyPlanetImage createTinyPlanet() {
+ // Free some memory we don't need anymore as we're going to dimiss the
+ // fragment after the tiny planet creation.
+ mResultLock.lock();
+ try {
+ mResultBitmap.recycle();
+ mResultBitmap = null;
+ mSourceBitmap.recycle();
+ mSourceBitmap = null;
+ } finally {
+ mResultLock.unlock();
+ }
+
+ // Create a high-resolution padded image.
+ Bitmap sourceBitmap = createPaddedSourceImage(mSourceImageUri, false);
+ int width = sourceBitmap.getWidth();
+ int height = sourceBitmap.getHeight();
+
+ int outputSize = width / 2;
+ Bitmap resultBitmap = Bitmap.createBitmap(outputSize, outputSize,
+ Bitmap.Config.ARGB_8888);
+
+ TinyPlanetNative.process(sourceBitmap, width, height, resultBitmap,
+ outputSize, mCurrentZoom, mCurrentAngle);
+
+ // Free the sourceImage memory as we don't need it and we need memory
+ // for the JPEG bytes.
+ sourceBitmap.recycle();
+ sourceBitmap = null;
+
+ ByteArrayOutputStream jpeg = new ByteArrayOutputStream();
+ resultBitmap.compress(CompressFormat.JPEG, 100, jpeg);
+ return new TinyPlanetImage(jpeg.toByteArray(), outputSize);
+ }
+
+ private int getDisplaySize() {
+ Display display = getActivity().getWindowManager().getDefaultDisplay();
+ Point size = new Point();
+ display.getSize(size);
+ return Math.min(size.x, size.y);
+ }
+
+ @Override
+ public void onSizeChanged(int sizePx) {
+ mPreviewSizePx = sizePx;
+ mResultLock.lock();
+ try {
+ if (mResultBitmap == null || mResultBitmap.getWidth() != sizePx
+ || mResultBitmap.getHeight() != sizePx) {
+ if (mResultBitmap != null) {
+ mResultBitmap.recycle();
+ }
+ mResultBitmap = Bitmap.createBitmap(mPreviewSizePx, mPreviewSizePx,
+ Bitmap.Config.ARGB_8888);
+ }
+ } finally {
+ mResultLock.unlock();
+ }
+
+ // Run directly and on this thread directly.
+ mCreateTinyPlanetRunnable.run();
+ }
+
+ private void onZoomChange(int zoom) {
+ // 1000 needs to be in sync with the max values declared in the layout
+ // xml file.
+ mCurrentZoom = zoom / 1000f;
+ scheduleUpdate();
+ }
+
+ private void onAngleChange(int angle) {
+ mCurrentAngle = (float) Math.toRadians(angle);
+ scheduleUpdate();
+ }
+
+ /**
+ * Delay-post a new preview rendering run.
+ */
+ private void scheduleUpdate() {
+ mHandler.removeCallbacks(mCreateTinyPlanetRunnable);
+ mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS);
+ }
+
+ private InputStream getInputStream(Uri uri) {
+ try {
+ return getActivity().getContentResolver().openInputStream(uri);
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Could not load source image.", e);
+ }
+ return null;
+ }
+
+ /**
+ * To create a proper TinyPlanet, the input image must be 2:1 (360:180
+ * degrees). So if needed, we pad the source image with black.
+ */
+ private static Bitmap createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) {
+ try {
+ int croppedAreaWidth =
+ getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS);
+ int croppedAreaHeight =
+ getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS);
+ int fullPanoWidth =
+ getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS);
+ int fullPanoHeight =
+ getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS);
+ int left = getInt(xmp, CROPPED_AREA_LEFT);
+ int top = getInt(xmp, CROPPED_AREA_TOP);
+
+ if (fullPanoWidth == 0 || fullPanoHeight == 0) {
+ return bitmapIn;
+ }
+ // Make sure the intermediate image has the similar size to the
+ // input.
+ Bitmap paddedBitmap = null;
+ float scale = intermediateWidth / (float) fullPanoWidth;
+ while (paddedBitmap == null) {
+ try {
+ paddedBitmap = Bitmap.createBitmap(
+ (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale),
+ Bitmap.Config.ARGB_8888);
+ } catch (OutOfMemoryError e) {
+ System.gc();
+ scale /= 2;
+ }
+ }
+ Canvas paddedCanvas = new Canvas(paddedBitmap);
+
+ int right = left + croppedAreaWidth;
+ int bottom = top + croppedAreaHeight;
+ RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale);
+ paddedCanvas.drawBitmap(bitmapIn, null, destRect, null);
+ return paddedBitmap;
+ } catch (XMPException ex) {
+ // Do nothing, just use mSourceBitmap as is.
+ }
+ return bitmapIn;
+ }
+
+ private static int getInt(XMPMeta xmp, String key) throws XMPException {
+ if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) {
+ return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key);
+ } else {
+ return 0;
+ }
+ }
+}