Home | History | Annotate | Download | only in tinyplanet
      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.CameraApp;
     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 
    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