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 TinyPlanetImage(byte[] jpegData, int size)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 onCreate(Bundle savedInstanceState)184 public void onCreate(Bundle savedInstanceState) { 185 super.onCreate(savedInstanceState); 186 setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Camera); 187 } 188 189 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 */ createPaddedSourceImage(Uri sourceImageUri, boolean previewSize)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 */ onCreateTinyPlanet()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 */ createFinalTinyPlanet()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 */ addExif(byte[] jpeg)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 getDisplaySize()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 onSizeChanged(int sizePx)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 onZoomChange(int zoom)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 onAngleChange(int angle)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 */ scheduleUpdate()427 private void scheduleUpdate() { 428 mHandler.removeCallbacks(mCreateTinyPlanetRunnable); 429 mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS); 430 } 431 getInputStream(Uri uri)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 */ createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth)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 getInt(XMPMeta xmp, String key)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