TinyPlanetFragment.java revision 2bca210e5fc8a77685775ffb403096167b017dce
1/*
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
17package com.android.camera.tinyplanet;
18
19import android.app.DialogFragment;
20import android.app.ProgressDialog;
21import android.graphics.Bitmap;
22import android.graphics.Bitmap.CompressFormat;
23import android.graphics.BitmapFactory;
24import android.graphics.Canvas;
25import android.graphics.Point;
26import android.graphics.RectF;
27import android.net.Uri;
28import android.os.AsyncTask;
29import android.os.Bundle;
30import android.os.Handler;
31import android.view.Display;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.View.OnClickListener;
35import android.view.ViewGroup;
36import android.view.Window;
37import android.widget.Button;
38import android.widget.SeekBar;
39import android.widget.SeekBar.OnSeekBarChangeListener;
40
41import com.adobe.xmp.XMPException;
42import com.adobe.xmp.XMPMeta;
43import com.android.camera.CameraActivity;
44import com.android.camera.app.CameraApp;
45import com.android.camera.app.MediaSaver;
46import com.android.camera.app.MediaSaver.OnMediaSavedListener;
47import com.android.camera.debug.Log;
48import com.android.camera.exif.ExifInterface;
49import com.android.camera.tinyplanet.TinyPlanetPreview.PreviewSizeListener;
50import com.android.camera.util.XmpUtil;
51import com.android.camera2.R;
52
53import java.io.ByteArrayOutputStream;
54import java.io.FileNotFoundException;
55import java.io.IOException;
56import java.io.InputStream;
57import java.util.Date;
58import java.util.TimeZone;
59import java.util.concurrent.locks.Lock;
60import java.util.concurrent.locks.ReentrantLock;
61
62/**
63 * An activity that provides an editor UI to create a TinyPlanet image from a
64 * 360 degree stereographically mapped panoramic image.
65 */
66public 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";
71
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/";
85
86    private static final Log.Tag TAG = new Log.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_";
91
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;
98
99    /**
100     * Lock for the result preview bitmap. We can't change it while we're trying
101     * to draw it.
102     */
103    private final Lock mResultLock = new ReentrantLock();
104
105    /** The title of the original panoramic image. */
106    private String mOriginalTitle = "";
107
108    /** The padded source bitmap. */
109    private Bitmap mSourceBitmap;
110    /** The resulting preview bitmap. */
111    private Bitmap mResultBitmap;
112
113    /** Used to delay-post a tiny planet rendering task. */
114    private final Handler mHandler = new Handler();
115    /** Whether rendering is in progress right now. */
116    private Boolean mRendering = false;
117    /**
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
120     * current rendering.
121     */
122    private Boolean mRenderOneMore = false;
123
124    /** Tiny planet data plus size. */
125    private static final class TinyPlanetImage {
126        public final byte[] mJpegData;
127        public final int mSize;
128
129        public TinyPlanetImage(byte[] jpegData, int size) {
130            mJpegData = jpegData;
131            mSize = size;
132        }
133    }
134
135    /**
136     * Creates and executes a task to create a tiny planet with the current
137     * values.
138     */
139    private final Runnable mCreateTinyPlanetRunnable = new Runnable() {
140        @Override
141        public void run() {
142            synchronized (mRendering) {
143                if (mRendering) {
144                    mRenderOneMore = true;
145                    return;
146                }
147                mRendering = true;
148            }
149
150            (new AsyncTask<Void, Void, Void>() {
151                @Override
152                protected Void doInBackground(Void... params) {
153                    mResultLock.lock();
154                    try {
155                        if (mSourceBitmap == null || mResultBitmap == null) {
156                            return null;
157                        }
158                        int width = mSourceBitmap.getWidth();
159                        int height = mSourceBitmap.getHeight();
160                        TinyPlanetNative.process(mSourceBitmap, width, height, mResultBitmap,
161                                mPreviewSizePx, mCurrentZoom, mCurrentAngle);
162                    } finally {
163                        mResultLock.unlock();
164                    }
165                    return null;
166                }
167
168                @Override
169                protected void onPostExecute(Void result) {
170                    mPreview.setBitmap(mResultBitmap, mResultLock);
171                    synchronized (mRendering) {
172                        mRendering = false;
173                        if (mRenderOneMore) {
174                            mRenderOneMore = false;
175                            scheduleUpdate();
176                        }
177                    }
178                }
179            }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
180        }
181    };
182
183    @Override
184    public void onCreate(Bundle savedInstanceState) {
185        super.onCreate(savedInstanceState);
186        setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Camera);
187    }
188
189    @Override
190    public View onCreateView(LayoutInflater inflater, ViewGroup container,
191            Bundle savedInstanceState) {
192        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
193        getDialog().setCanceledOnTouchOutside(true);
194
195        View view = inflater.inflate(R.layout.tinyplanet_editor,
196                container, false);
197        mPreview = (TinyPlanetPreview) view.findViewById(R.id.preview);
198        mPreview.setPreviewSizeChangeListener(this);
199
200        // Zoom slider setup.
201        SeekBar zoomSlider = (SeekBar) view.findViewById(R.id.zoomSlider);
202        zoomSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
203            @Override
204            public void onStopTrackingTouch(SeekBar seekBar) {
205                // Do nothing.
206            }
207
208            @Override
209            public void onStartTrackingTouch(SeekBar seekBar) {
210                // Do nothing.
211            }
212
213            @Override
214            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
215                onZoomChange(progress);
216            }
217        });
218
219        // Rotation slider setup.
220        SeekBar angleSlider = (SeekBar) view.findViewById(R.id.angleSlider);
221        angleSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
222            @Override
223            public void onStopTrackingTouch(SeekBar seekBar) {
224                // Do nothing.
225            }
226
227            @Override
228            public void onStartTrackingTouch(SeekBar seekBar) {
229                // Do nothing.
230            }
231
232            @Override
233            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
234                onAngleChange(progress);
235            }
236        });
237
238        Button createButton = (Button) view.findViewById(R.id.creatTinyPlanetButton);
239        createButton.setOnClickListener(new OnClickListener() {
240            @Override
241            public void onClick(View v) {
242                onCreateTinyPlanet();
243            }
244        });
245
246        mOriginalTitle = getArguments().getString(ARGUMENT_TITLE);
247        mSourceImageUri = Uri.parse(getArguments().getString(ARGUMENT_URI));
248        mSourceBitmap = createPaddedSourceImage(mSourceImageUri, true);
249
250        if (mSourceBitmap == null) {
251            Log.e(TAG, "Could not decode source image.");
252            dismiss();
253        }
254        return view;
255    }
256
257    /**
258     * From the given URI this method creates a 360/180 padded image that is
259     * ready to be made a tiny planet.
260     */
261    private Bitmap createPaddedSourceImage(Uri sourceImageUri, boolean previewSize) {
262        InputStream is = getInputStream(sourceImageUri);
263        if (is == null) {
264            Log.e(TAG, "Could not create input stream for image.");
265            dismiss();
266        }
267        Bitmap sourceBitmap = BitmapFactory.decodeStream(is);
268
269        is = getInputStream(sourceImageUri);
270        XMPMeta xmp = XmpUtil.extractXMPMeta(is);
271
272        if (xmp != null) {
273            int size = previewSize ? getDisplaySize() : sourceBitmap.getWidth();
274            sourceBitmap = createPaddedBitmap(sourceBitmap, xmp, size);
275        }
276        return sourceBitmap;
277    }
278
279    /**
280     * Starts an asynchronous task to create a tiny planet. Once done, will add
281     * the new image to the filmstrip and dismisses the fragment.
282     */
283    private void onCreateTinyPlanet() {
284        // Make sure we stop rendering before we create the high-res tiny
285        // planet.
286        synchronized (mRendering) {
287            mRenderOneMore = false;
288        }
289
290        final String savingTinyPlanet = getActivity().getResources().getString(
291                R.string.saving_tiny_planet);
292        (new AsyncTask<Void, Void, TinyPlanetImage>() {
293            @Override
294            protected void onPreExecute() {
295                mDialog = ProgressDialog.show(getActivity(), null, savingTinyPlanet, true, false);
296            }
297
298            @Override
299            protected TinyPlanetImage doInBackground(Void... params) {
300                return createFinalTinyPlanet();
301            }
302
303            @Override
304            protected void onPostExecute(TinyPlanetImage image) {
305                // Once created, store the new file and add it to the filmstrip.
306                final CameraActivity activity = (CameraActivity) getActivity();
307                MediaSaver mediaSaver = ((CameraApp) activity.getApplication()).getMediaSaver();
308                OnMediaSavedListener doneListener =
309                        new OnMediaSavedListener() {
310                            @Override
311                            public void onMediaSaved(Uri uri) {
312                                // Add the new photo to the filmstrip and exit
313                                // the fragment.
314                                activity.notifyNewMedia(uri);
315                                mDialog.dismiss();
316                                TinyPlanetFragment.this.dismiss();
317                            }
318                        };
319                String tinyPlanetTitle = FILENAME_PREFIX + mOriginalTitle;
320                mediaSaver.addImage(image.mJpegData, tinyPlanetTitle, (new Date()).getTime(),
321                        null,
322                        image.mSize, image.mSize, 0, null, doneListener, getActivity()
323                                .getContentResolver());
324            }
325        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
326    }
327
328    /**
329     * Creates the high quality tiny planet file and adds it to the media
330     * service. Don't call this on the UI thread.
331     */
332    private TinyPlanetImage createFinalTinyPlanet() {
333        // Free some memory we don't need anymore as we're going to dimiss the
334        // fragment after the tiny planet creation.
335        mResultLock.lock();
336        try {
337            mResultBitmap.recycle();
338            mResultBitmap = null;
339            mSourceBitmap.recycle();
340            mSourceBitmap = null;
341        } finally {
342            mResultLock.unlock();
343        }
344
345        // Create a high-resolution padded image.
346        Bitmap sourceBitmap = createPaddedSourceImage(mSourceImageUri, false);
347        int width = sourceBitmap.getWidth();
348        int height = sourceBitmap.getHeight();
349
350        int outputSize = width / 2;
351        Bitmap resultBitmap = Bitmap.createBitmap(outputSize, outputSize,
352                Bitmap.Config.ARGB_8888);
353
354        TinyPlanetNative.process(sourceBitmap, width, height, resultBitmap,
355                outputSize, mCurrentZoom, mCurrentAngle);
356
357        // Free the sourceImage memory as we don't need it and we need memory
358        // for the JPEG bytes.
359        sourceBitmap.recycle();
360        sourceBitmap = null;
361
362        ByteArrayOutputStream jpeg = new ByteArrayOutputStream();
363        resultBitmap.compress(CompressFormat.JPEG, 100, jpeg);
364        return new TinyPlanetImage(addExif(jpeg.toByteArray()), outputSize);
365    }
366
367    /**
368     * Adds basic EXIF data to the tiny planet image so it an be rewritten
369     * later.
370     *
371     * @param jpeg the JPEG data of the tiny planet.
372     * @return The JPEG data containing basic EXIF.
373     */
374    private byte[] addExif(byte[] jpeg) {
375        ExifInterface exif = new ExifInterface();
376        exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, System.currentTimeMillis(),
377                TimeZone.getDefault());
378        ByteArrayOutputStream jpegOut = new ByteArrayOutputStream();
379        try {
380            exif.writeExif(jpeg, jpegOut);
381        } catch (IOException e) {
382            Log.e(TAG, "Could not write EXIF", e);
383        }
384        return jpegOut.toByteArray();
385    }
386
387    private int getDisplaySize() {
388        Display display = getActivity().getWindowManager().getDefaultDisplay();
389        Point size = new Point();
390        display.getSize(size);
391        return Math.min(size.x, size.y);
392    }
393
394    @Override
395    public void onSizeChanged(int sizePx) {
396        mPreviewSizePx = sizePx;
397        mResultLock.lock();
398        try {
399            if (mResultBitmap == null || mResultBitmap.getWidth() != sizePx
400                    || mResultBitmap.getHeight() != sizePx) {
401                if (mResultBitmap != null) {
402                    mResultBitmap.recycle();
403                }
404                mResultBitmap = Bitmap.createBitmap(mPreviewSizePx, mPreviewSizePx,
405                        Bitmap.Config.ARGB_8888);
406            }
407        } finally {
408            mResultLock.unlock();
409        }
410        scheduleUpdate();
411    }
412
413    private void onZoomChange(int zoom) {
414        // 1000 needs to be in sync with the max values declared in the layout
415        // xml file.
416        mCurrentZoom = zoom / 1000f;
417        scheduleUpdate();
418    }
419
420    private void onAngleChange(int angle) {
421        mCurrentAngle = (float) Math.toRadians(angle);
422        scheduleUpdate();
423    }
424
425    /**
426     * Delay-post a new preview rendering run.
427     */
428    private void scheduleUpdate() {
429        mHandler.removeCallbacks(mCreateTinyPlanetRunnable);
430        mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS);
431    }
432
433    private InputStream getInputStream(Uri uri) {
434        try {
435            return getActivity().getContentResolver().openInputStream(uri);
436        } catch (FileNotFoundException e) {
437            Log.e(TAG, "Could not load source image.", e);
438        }
439        return null;
440    }
441
442    /**
443     * To create a proper TinyPlanet, the input image must be 2:1 (360:180
444     * degrees). So if needed, we pad the source image with black.
445     */
446    private static Bitmap createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) {
447        try {
448            int croppedAreaWidth =
449                    getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS);
450            int croppedAreaHeight =
451                    getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS);
452            int fullPanoWidth =
453                    getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS);
454            int fullPanoHeight =
455                    getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS);
456            int left = getInt(xmp, CROPPED_AREA_LEFT);
457            int top = getInt(xmp, CROPPED_AREA_TOP);
458
459            if (fullPanoWidth == 0 || fullPanoHeight == 0) {
460                return bitmapIn;
461            }
462            // Make sure the intermediate image has the similar size to the
463            // input.
464            Bitmap paddedBitmap = null;
465            float scale = intermediateWidth / (float) fullPanoWidth;
466            while (paddedBitmap == null) {
467                try {
468                    paddedBitmap = Bitmap.createBitmap(
469                            (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale),
470                            Bitmap.Config.ARGB_8888);
471                } catch (OutOfMemoryError e) {
472                    System.gc();
473                    scale /= 2;
474                }
475            }
476            Canvas paddedCanvas = new Canvas(paddedBitmap);
477
478            int right = left + croppedAreaWidth;
479            int bottom = top + croppedAreaHeight;
480            RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale);
481            paddedCanvas.drawBitmap(bitmapIn, null, destRect, null);
482            return paddedBitmap;
483        } catch (XMPException ex) {
484            // Do nothing, just use mSourceBitmap as is.
485        }
486        return bitmapIn;
487    }
488
489    private static int getInt(XMPMeta xmp, String key) throws XMPException {
490        if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) {
491            return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key);
492        } else {
493            return 0;
494        }
495    }
496}
497