diff options
Diffstat (limited to 'src/com/android/camera/tinyplanet/TinyPlanetFragment.java')
-rw-r--r-- | src/com/android/camera/tinyplanet/TinyPlanetFragment.java | 475 |
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; + } + } +} |