blob: f1a1082ae1cc958fcb48b97b8976528596e81cba [file] [log] [blame]
/*
* 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.app.MediaSaver.OnMediaSavedListener;
import com.android.camera.app.MediaSaver;
import com.android.camera.exif.ExifInterface;
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.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.TimeZone;
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();
}
}
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
};
@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();
MediaSaver mediaSaver = activity.getMediaSaver();
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;
mediaSaver.addImage(image.mJpegData, tinyPlanetTitle, (new Date()).getTime(),
null,
image.mSize, image.mSize, 0, null, doneListener, getActivity()
.getContentResolver());
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* 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(addExif(jpeg.toByteArray()), outputSize);
}
/**
* Adds basic EXIF data to the tiny planet image so it an be rewritten
* later.
*
* @param jpeg the JPEG data of the tiny planet.
* @return The JPEG data containing basic EXIF.
*/
private byte[] addExif(byte[] jpeg) {
ExifInterface exif = new ExifInterface();
exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, System.currentTimeMillis(),
TimeZone.getDefault());
ByteArrayOutputStream jpegOut = new ByteArrayOutputStream();
try {
exif.writeExif(jpeg, jpegOut);
} catch (IOException e) {
Log.e(TAG, "Could not write EXIF", e);
}
return jpegOut.toByteArray();
}
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;
}
}
}