• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.camera.tinyplanet;
18 
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.view.Display;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.View.OnClickListener;
35 import android.view.ViewGroup;
36 import android.view.Window;
37 import android.widget.Button;
38 import android.widget.SeekBar;
39 import android.widget.SeekBar.OnSeekBarChangeListener;
40 
41 import com.adobe.xmp.XMPException;
42 import com.adobe.xmp.XMPMeta;
43 import com.android.camera.CameraActivity;
44 import com.android.camera.app.CameraServicesImpl;
45 import com.android.camera.app.MediaSaver;
46 import com.android.camera.app.MediaSaver.OnMediaSavedListener;
47 import com.android.camera.debug.Log;
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;
52 
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;
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  */
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";
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 
TinyPlanetImage(byte[] jpegData, int size)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
onCreate(Bundle savedInstanceState)184     public void onCreate(Bundle savedInstanceState) {
185         super.onCreate(savedInstanceState);
186         setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Camera);
187     }
188 
189     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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      */
createPaddedSourceImage(Uri sourceImageUri, boolean previewSize)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      */
onCreateTinyPlanet()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 = CameraServicesImpl.instance().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);
323             }
324         }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
325     }
326 
327     /**
328      * Creates the high quality tiny planet file and adds it to the media
329      * service. Don't call this on the UI thread.
330      */
createFinalTinyPlanet()331     private TinyPlanetImage createFinalTinyPlanet() {
332         // Free some memory we don't need anymore as we're going to dimiss the
333         // fragment after the tiny planet creation.
334         mResultLock.lock();
335         try {
336             mResultBitmap.recycle();
337             mResultBitmap = null;
338             mSourceBitmap.recycle();
339             mSourceBitmap = null;
340         } finally {
341             mResultLock.unlock();
342         }
343 
344         // Create a high-resolution padded image.
345         Bitmap sourceBitmap = createPaddedSourceImage(mSourceImageUri, false);
346         int width = sourceBitmap.getWidth();
347         int height = sourceBitmap.getHeight();
348 
349         int outputSize = width / 2;
350         Bitmap resultBitmap = Bitmap.createBitmap(outputSize, outputSize,
351                 Bitmap.Config.ARGB_8888);
352 
353         TinyPlanetNative.process(sourceBitmap, width, height, resultBitmap,
354                 outputSize, mCurrentZoom, mCurrentAngle);
355 
356         // Free the sourceImage memory as we don't need it and we need memory
357         // for the JPEG bytes.
358         sourceBitmap.recycle();
359         sourceBitmap = null;
360 
361         ByteArrayOutputStream jpeg = new ByteArrayOutputStream();
362         resultBitmap.compress(CompressFormat.JPEG, 100, jpeg);
363         return new TinyPlanetImage(addExif(jpeg.toByteArray()), outputSize);
364     }
365 
366     /**
367      * Adds basic EXIF data to the tiny planet image so it an be rewritten
368      * later.
369      *
370      * @param jpeg the JPEG data of the tiny planet.
371      * @return The JPEG data containing basic EXIF.
372      */
addExif(byte[] jpeg)373     private byte[] addExif(byte[] jpeg) {
374         ExifInterface exif = new ExifInterface();
375         exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, System.currentTimeMillis(),
376                 TimeZone.getDefault());
377         ByteArrayOutputStream jpegOut = new ByteArrayOutputStream();
378         try {
379             exif.writeExif(jpeg, jpegOut);
380         } catch (IOException e) {
381             Log.e(TAG, "Could not write EXIF", e);
382         }
383         return jpegOut.toByteArray();
384     }
385 
getDisplaySize()386     private int getDisplaySize() {
387         Display display = getActivity().getWindowManager().getDefaultDisplay();
388         Point size = new Point();
389         display.getSize(size);
390         return Math.min(size.x, size.y);
391     }
392 
393     @Override
onSizeChanged(int sizePx)394     public void onSizeChanged(int sizePx) {
395         mPreviewSizePx = sizePx;
396         mResultLock.lock();
397         try {
398             if (mResultBitmap == null || mResultBitmap.getWidth() != sizePx
399                     || mResultBitmap.getHeight() != sizePx) {
400                 if (mResultBitmap != null) {
401                     mResultBitmap.recycle();
402                 }
403                 mResultBitmap = Bitmap.createBitmap(mPreviewSizePx, mPreviewSizePx,
404                         Bitmap.Config.ARGB_8888);
405             }
406         } finally {
407             mResultLock.unlock();
408         }
409         scheduleUpdate();
410     }
411 
onZoomChange(int zoom)412     private void onZoomChange(int zoom) {
413         // 1000 needs to be in sync with the max values declared in the layout
414         // xml file.
415         mCurrentZoom = zoom / 1000f;
416         scheduleUpdate();
417     }
418 
onAngleChange(int angle)419     private void onAngleChange(int angle) {
420         mCurrentAngle = (float) Math.toRadians(angle);
421         scheduleUpdate();
422     }
423 
424     /**
425      * Delay-post a new preview rendering run.
426      */
scheduleUpdate()427     private void scheduleUpdate() {
428         mHandler.removeCallbacks(mCreateTinyPlanetRunnable);
429         mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS);
430     }
431 
getInputStream(Uri uri)432     private InputStream getInputStream(Uri uri) {
433         try {
434             return getActivity().getContentResolver().openInputStream(uri);
435         } catch (FileNotFoundException e) {
436             Log.e(TAG, "Could not load source image.", e);
437         }
438         return null;
439     }
440 
441     /**
442      * To create a proper TinyPlanet, the input image must be 2:1 (360:180
443      * degrees). So if needed, we pad the source image with black.
444      */
createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth)445     private static Bitmap createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) {
446         try {
447             int croppedAreaWidth =
448                     getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS);
449             int croppedAreaHeight =
450                     getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS);
451             int fullPanoWidth =
452                     getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS);
453             int fullPanoHeight =
454                     getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS);
455             int left = getInt(xmp, CROPPED_AREA_LEFT);
456             int top = getInt(xmp, CROPPED_AREA_TOP);
457 
458             if (fullPanoWidth == 0 || fullPanoHeight == 0) {
459                 return bitmapIn;
460             }
461             // Make sure the intermediate image has the similar size to the
462             // input.
463             Bitmap paddedBitmap = null;
464             float scale = intermediateWidth / (float) fullPanoWidth;
465             while (paddedBitmap == null) {
466                 try {
467                     paddedBitmap = Bitmap.createBitmap(
468                             (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale),
469                             Bitmap.Config.ARGB_8888);
470                 } catch (OutOfMemoryError e) {
471                     System.gc();
472                     scale /= 2;
473                 }
474             }
475             Canvas paddedCanvas = new Canvas(paddedBitmap);
476 
477             int right = left + croppedAreaWidth;
478             int bottom = top + croppedAreaHeight;
479             RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale);
480             paddedCanvas.drawBitmap(bitmapIn, null, destRect, null);
481             return paddedBitmap;
482         } catch (XMPException ex) {
483             // Do nothing, just use mSourceBitmap as is.
484         }
485         return bitmapIn;
486     }
487 
getInt(XMPMeta xmp, String key)488     private static int getInt(XMPMeta xmp, String key) throws XMPException {
489         if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) {
490             return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key);
491         } else {
492             return 0;
493         }
494     }
495 }
496