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.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.MediaSaveService;
     46 import com.android.camera.MediaSaveService.OnMediaSavedListener;
     47 import com.android.camera.exif.ExifInterface;
     48 import com.android.camera.tinyplanet.TinyPlanetPreview.PreviewSizeListener;
     49 import com.android.camera.util.XmpUtil;
     50 import com.android.camera2.R;
     51 
     52 import java.io.ByteArrayOutputStream;
     53 import java.io.FileNotFoundException;
     54 import java.io.IOException;
     55 import java.io.InputStream;
     56 import java.io.OutputStream;
     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                 MediaSaveService mediaSaveService = activity.getMediaSaveService();
    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                 mediaSaveService.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 }
    500