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