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.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 
    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 = 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      */
    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      */
    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 
    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
    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 
    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 
    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      */
    427     private void scheduleUpdate() {
    428         mHandler.removeCallbacks(mCreateTinyPlanetRunnable);
    429         mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS);
    430     }
    431 
    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      */
    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 
    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