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