OSDN Git Service

Merge "Use audio focus instead of music command broadcast" into gb-ub-photos-denali
[android-x86/packages-apps-Camera2.git] / src / com / android / camera / tinyplanet / TinyPlanetFragment.java
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.util.Log;
32 import android.view.Display;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.View.OnClickListener;
36 import android.view.ViewGroup;
37 import android.view.Window;
38 import android.widget.Button;
39 import android.widget.SeekBar;
40 import android.widget.SeekBar.OnSeekBarChangeListener;
41
42 import com.adobe.xmp.XMPException;
43 import com.adobe.xmp.XMPMeta;
44 import com.android.camera.CameraActivity;
45 import com.android.camera.app.CameraApp;
46 import com.android.camera.app.MediaSaver.OnMediaSavedListener;
47 import com.android.camera.app.MediaSaver;
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 String 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 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 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
159                         int width = mSourceBitmap.getWidth();
160                         int height = mSourceBitmap.getHeight();
161                         TinyPlanetNative.process(mSourceBitmap, width, height, mResultBitmap,
162                                 mPreviewSizePx,
163                                 mCurrentZoom, mCurrentAngle);
164                     } finally {
165                         mResultLock.unlock();
166                     }
167                     return null;
168                 }
169
170                 protected void onPostExecute(Void result) {
171                     mPreview.setBitmap(mResultBitmap, mResultLock);
172                     synchronized (mRendering) {
173                         mRendering = false;
174                         if (mRenderOneMore) {
175                             mRenderOneMore = false;
176                             scheduleUpdate();
177                         }
178                     }
179                 }
180             }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
181         }
182     };
183
184     @Override
185     public void onCreate(Bundle savedInstanceState) {
186         super.onCreate(savedInstanceState);
187         setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Camera);
188     }
189
190     @Override
191     public View onCreateView(LayoutInflater inflater, ViewGroup container,
192             Bundle savedInstanceState) {
193         getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
194         getDialog().setCanceledOnTouchOutside(true);
195
196         View view = inflater.inflate(R.layout.tinyplanet_editor,
197                 container, false);
198         mPreview = (TinyPlanetPreview) view.findViewById(R.id.preview);
199         mPreview.setPreviewSizeChangeListener(this);
200
201         // Zoom slider setup.
202         SeekBar zoomSlider = (SeekBar) view.findViewById(R.id.zoomSlider);
203         zoomSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
204             @Override
205             public void onStopTrackingTouch(SeekBar seekBar) {
206                 // Do nothing.
207             }
208
209             @Override
210             public void onStartTrackingTouch(SeekBar seekBar) {
211                 // Do nothing.
212             }
213
214             @Override
215             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
216                 onZoomChange(progress);
217             }
218         });
219
220         // Rotation slider setup.
221         SeekBar angleSlider = (SeekBar) view.findViewById(R.id.angleSlider);
222         angleSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
223             @Override
224             public void onStopTrackingTouch(SeekBar seekBar) {
225                 // Do nothing.
226             }
227
228             @Override
229             public void onStartTrackingTouch(SeekBar seekBar) {
230                 // Do nothing.
231             }
232
233             @Override
234             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
235                 onAngleChange(progress);
236             }
237         });
238
239         Button createButton = (Button) view.findViewById(R.id.creatTinyPlanetButton);
240         createButton.setOnClickListener(new OnClickListener() {
241             @Override
242             public void onClick(View v) {
243                 onCreateTinyPlanet();
244             }
245         });
246
247         mOriginalTitle = getArguments().getString(ARGUMENT_TITLE);
248         mSourceImageUri = Uri.parse(getArguments().getString(ARGUMENT_URI));
249         mSourceBitmap = createPaddedSourceImage(mSourceImageUri, true);
250
251         if (mSourceBitmap == null) {
252             Log.e(TAG, "Could not decode source image.");
253             dismiss();
254         }
255         return view;
256     }
257
258     /**
259      * From the given URI this method creates a 360/180 padded image that is
260      * ready to be made a tiny planet.
261      */
262     private Bitmap createPaddedSourceImage(Uri sourceImageUri, boolean previewSize) {
263         InputStream is = getInputStream(sourceImageUri);
264         if (is == null) {
265             Log.e(TAG, "Could not create input stream for image.");
266             dismiss();
267         }
268         Bitmap sourceBitmap = BitmapFactory.decodeStream(is);
269
270         is = getInputStream(sourceImageUri);
271         XMPMeta xmp = XmpUtil.extractXMPMeta(is);
272
273         if (xmp != null) {
274             int size = previewSize ? getDisplaySize() : sourceBitmap.getWidth();
275             sourceBitmap = createPaddedBitmap(sourceBitmap, xmp, size);
276         }
277         return sourceBitmap;
278     }
279
280     /**
281      * Starts an asynchronous task to create a tiny planet. Once done, will add
282      * the new image to the filmstrip and dismisses the fragment.
283      */
284     private void onCreateTinyPlanet() {
285         // Make sure we stop rendering before we create the high-res tiny
286         // planet.
287         synchronized (mRendering) {
288             mRenderOneMore = false;
289         }
290
291         final String savingTinyPlanet = getActivity().getResources().getString(
292                 R.string.saving_tiny_planet);
293         (new AsyncTask<Void, Void, TinyPlanetImage>() {
294             @Override
295             protected void onPreExecute() {
296                 mDialog = ProgressDialog.show(getActivity(), null, savingTinyPlanet, true, false);
297             }
298
299             @Override
300             protected TinyPlanetImage doInBackground(Void... params) {
301                 return createTinyPlanet();
302             }
303
304             @Override
305             protected void onPostExecute(TinyPlanetImage image) {
306                 // Once created, store the new file and add it to the filmstrip.
307                 final CameraActivity activity = (CameraActivity) getActivity();
308                 MediaSaver mediaSaver = ((CameraApp) activity.getApplication()).getMediaSaver();
309                 OnMediaSavedListener doneListener =
310                         new OnMediaSavedListener() {
311                             @Override
312                             public void onMediaSaved(Uri uri) {
313                                 // Add the new photo to the filmstrip and exit
314                                 // the fragment.
315                                 activity.notifyNewMedia(uri);
316                                 mDialog.dismiss();
317                                 TinyPlanetFragment.this.dismiss();
318                             }
319                         };
320                 String tinyPlanetTitle = FILENAME_PREFIX + mOriginalTitle;
321                 mediaSaver.addImage(image.mJpegData, tinyPlanetTitle, (new Date()).getTime(),
322                         null,
323                         image.mSize, image.mSize, 0, null, doneListener, getActivity()
324                                 .getContentResolver());
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      */
333     private TinyPlanetImage createTinyPlanet() {
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      */
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
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
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
412         // Run directly and on this thread directly.
413         mCreateTinyPlanetRunnable.run();
414     }
415
416     private void onZoomChange(int zoom) {
417         // 1000 needs to be in sync with the max values declared in the layout
418         // xml file.
419         mCurrentZoom = zoom / 1000f;
420         scheduleUpdate();
421     }
422
423     private void onAngleChange(int angle) {
424         mCurrentAngle = (float) Math.toRadians(angle);
425         scheduleUpdate();
426     }
427
428     /**
429      * Delay-post a new preview rendering run.
430      */
431     private void scheduleUpdate() {
432         mHandler.removeCallbacks(mCreateTinyPlanetRunnable);
433         mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS);
434     }
435
436     private InputStream getInputStream(Uri uri) {
437         try {
438             return getActivity().getContentResolver().openInputStream(uri);
439         } catch (FileNotFoundException e) {
440             Log.e(TAG, "Could not load source image.", e);
441         }
442         return null;
443     }
444
445     /**
446      * To create a proper TinyPlanet, the input image must be 2:1 (360:180
447      * degrees). So if needed, we pad the source image with black.
448      */
449     private static Bitmap createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) {
450         try {
451             int croppedAreaWidth =
452                     getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS);
453             int croppedAreaHeight =
454                     getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS);
455             int fullPanoWidth =
456                     getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS);
457             int fullPanoHeight =
458                     getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS);
459             int left = getInt(xmp, CROPPED_AREA_LEFT);
460             int top = getInt(xmp, CROPPED_AREA_TOP);
461
462             if (fullPanoWidth == 0 || fullPanoHeight == 0) {
463                 return bitmapIn;
464             }
465             // Make sure the intermediate image has the similar size to the
466             // input.
467             Bitmap paddedBitmap = null;
468             float scale = intermediateWidth / (float) fullPanoWidth;
469             while (paddedBitmap == null) {
470                 try {
471                     paddedBitmap = Bitmap.createBitmap(
472                             (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale),
473                             Bitmap.Config.ARGB_8888);
474                 } catch (OutOfMemoryError e) {
475                     System.gc();
476                     scale /= 2;
477                 }
478             }
479             Canvas paddedCanvas = new Canvas(paddedBitmap);
480
481             int right = left + croppedAreaWidth;
482             int bottom = top + croppedAreaHeight;
483             RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale);
484             paddedCanvas.drawBitmap(bitmapIn, null, destRect, null);
485             return paddedBitmap;
486         } catch (XMPException ex) {
487             // Do nothing, just use mSourceBitmap as is.
488         }
489         return bitmapIn;
490     }
491
492     private static int getInt(XMPMeta xmp, String key) throws XMPException {
493         if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) {
494             return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key);
495         } else {
496             return 0;
497         }
498     }
499 }