TinyPlanetFragment.java revision fd4fc0e52ad69c2d486f5f46c2d465b4c4ba2849
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 17package com.android.camera.tinyplanet; 18 19import android.app.DialogFragment; 20import android.app.ProgressDialog; 21import android.graphics.Bitmap; 22import android.graphics.Bitmap.CompressFormat; 23import android.graphics.BitmapFactory; 24import android.graphics.Canvas; 25import android.graphics.Point; 26import android.graphics.RectF; 27import android.net.Uri; 28import android.os.AsyncTask; 29import android.os.Bundle; 30import android.os.Handler; 31import android.util.Log; 32import android.view.Display; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.View.OnClickListener; 36import android.view.ViewGroup; 37import android.view.Window; 38import android.widget.Button; 39import android.widget.SeekBar; 40import android.widget.SeekBar.OnSeekBarChangeListener; 41 42import com.adobe.xmp.XMPException; 43import com.adobe.xmp.XMPMeta; 44import com.android.camera.CameraActivity; 45import com.android.camera.app.MediaSaver.OnMediaSavedListener; 46import com.android.camera.app.MediaSaver; 47import com.android.camera.exif.ExifInterface; 48import com.android.camera.tinyplanet.TinyPlanetPreview.PreviewSizeListener; 49import com.android.camera.util.XmpUtil; 50import com.android.camera2.R; 51 52import java.io.ByteArrayOutputStream; 53import java.io.FileNotFoundException; 54import java.io.IOException; 55import java.io.InputStream; 56import java.util.Date; 57import java.util.TimeZone; 58import java.util.concurrent.locks.Lock; 59import java.util.concurrent.locks.ReentrantLock; 60 61/** 62 * An activity that provides an editor UI to create a TinyPlanet image from a 63 * 360 degree stereographically mapped panoramic image. 64 */ 65public class TinyPlanetFragment extends DialogFragment implements PreviewSizeListener { 66 /** Argument to tell the fragment the URI of the original panoramic image. */ 67 public static final String ARGUMENT_URI = "uri"; 68 /** Argument to tell the fragment the title of the original panoramic image. */ 69 public static final String ARGUMENT_TITLE = "title"; 70 71 public static final String CROPPED_AREA_IMAGE_WIDTH_PIXELS = 72 "CroppedAreaImageWidthPixels"; 73 public static final String CROPPED_AREA_IMAGE_HEIGHT_PIXELS = 74 "CroppedAreaImageHeightPixels"; 75 public static final String CROPPED_AREA_FULL_PANO_WIDTH_PIXELS = 76 "FullPanoWidthPixels"; 77 public static final String CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS = 78 "FullPanoHeightPixels"; 79 public static final String CROPPED_AREA_LEFT = 80 "CroppedAreaLeftPixels"; 81 public static final String CROPPED_AREA_TOP = 82 "CroppedAreaTopPixels"; 83 public static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/"; 84 85 private static final String TAG = "TinyPlanetActivity"; 86 /** Delay between a value update and the renderer running. */ 87 private static final int RENDER_DELAY_MILLIS = 50; 88 /** Filename prefix to prepend to the original name for the new file. */ 89 private static final String FILENAME_PREFIX = "TINYPLANET_"; 90 91 private Uri mSourceImageUri; 92 private TinyPlanetPreview mPreview; 93 private int mPreviewSizePx = 0; 94 private float mCurrentZoom = 0.5f; 95 private float mCurrentAngle = 0; 96 private ProgressDialog mDialog; 97 98 /** 99 * Lock for the result preview bitmap. We can't change it while we're trying 100 * to draw it. 101 */ 102 private Lock mResultLock = new ReentrantLock(); 103 104 /** The title of the original panoramic image. */ 105 private String mOriginalTitle = ""; 106 107 /** The padded source bitmap. */ 108 private Bitmap mSourceBitmap; 109 /** The resulting preview bitmap. */ 110 private Bitmap mResultBitmap; 111 112 /** Used to delay-post a tiny planet rendering task. */ 113 private Handler mHandler = new Handler(); 114 /** Whether rendering is in progress right now. */ 115 private Boolean mRendering = false; 116 /** 117 * Whether we should render one more time after the current rendering run is 118 * done. This is needed when there was an update to the values during the 119 * current rendering. 120 */ 121 private Boolean mRenderOneMore = false; 122 123 /** Tiny planet data plus size. */ 124 private static final class TinyPlanetImage { 125 public final byte[] mJpegData; 126 public final int mSize; 127 128 public TinyPlanetImage(byte[] jpegData, int size) { 129 mJpegData = jpegData; 130 mSize = size; 131 } 132 } 133 134 /** 135 * Creates and executes a task to create a tiny planet with the current 136 * values. 137 */ 138 private final Runnable mCreateTinyPlanetRunnable = new Runnable() { 139 @Override 140 public void run() { 141 synchronized (mRendering) { 142 if (mRendering) { 143 mRenderOneMore = true; 144 return; 145 } 146 mRendering = true; 147 } 148 149 (new AsyncTask<Void, Void, Void>() { 150 @Override 151 protected Void doInBackground(Void... params) { 152 mResultLock.lock(); 153 try { 154 if (mSourceBitmap == null || mResultBitmap == null) { 155 return null; 156 } 157 158 int width = mSourceBitmap.getWidth(); 159 int height = mSourceBitmap.getHeight(); 160 TinyPlanetNative.process(mSourceBitmap, width, height, mResultBitmap, 161 mPreviewSizePx, 162 mCurrentZoom, mCurrentAngle); 163 } finally { 164 mResultLock.unlock(); 165 } 166 return null; 167 } 168 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 createTinyPlanet(); 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 = activity.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 createTinyPlanet() { 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 411 // Run directly and on this thread directly. 412 mCreateTinyPlanetRunnable.run(); 413 } 414 415 private void onZoomChange(int zoom) { 416 // 1000 needs to be in sync with the max values declared in the layout 417 // xml file. 418 mCurrentZoom = zoom / 1000f; 419 scheduleUpdate(); 420 } 421 422 private void onAngleChange(int angle) { 423 mCurrentAngle = (float) Math.toRadians(angle); 424 scheduleUpdate(); 425 } 426 427 /** 428 * Delay-post a new preview rendering run. 429 */ 430 private void scheduleUpdate() { 431 mHandler.removeCallbacks(mCreateTinyPlanetRunnable); 432 mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS); 433 } 434 435 private InputStream getInputStream(Uri uri) { 436 try { 437 return getActivity().getContentResolver().openInputStream(uri); 438 } catch (FileNotFoundException e) { 439 Log.e(TAG, "Could not load source image.", e); 440 } 441 return null; 442 } 443 444 /** 445 * To create a proper TinyPlanet, the input image must be 2:1 (360:180 446 * degrees). So if needed, we pad the source image with black. 447 */ 448 private static Bitmap createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) { 449 try { 450 int croppedAreaWidth = 451 getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS); 452 int croppedAreaHeight = 453 getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS); 454 int fullPanoWidth = 455 getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS); 456 int fullPanoHeight = 457 getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS); 458 int left = getInt(xmp, CROPPED_AREA_LEFT); 459 int top = getInt(xmp, CROPPED_AREA_TOP); 460 461 if (fullPanoWidth == 0 || fullPanoHeight == 0) { 462 return bitmapIn; 463 } 464 // Make sure the intermediate image has the similar size to the 465 // input. 466 Bitmap paddedBitmap = null; 467 float scale = intermediateWidth / (float) fullPanoWidth; 468 while (paddedBitmap == null) { 469 try { 470 paddedBitmap = Bitmap.createBitmap( 471 (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale), 472 Bitmap.Config.ARGB_8888); 473 } catch (OutOfMemoryError e) { 474 System.gc(); 475 scale /= 2; 476 } 477 } 478 Canvas paddedCanvas = new Canvas(paddedBitmap); 479 480 int right = left + croppedAreaWidth; 481 int bottom = top + croppedAreaHeight; 482 RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale); 483 paddedCanvas.drawBitmap(bitmapIn, null, destRect, null); 484 return paddedBitmap; 485 } catch (XMPException ex) { 486 // Do nothing, just use mSourceBitmap as is. 487 } 488 return bitmapIn; 489 } 490 491 private static int getInt(XMPMeta xmp, String key) throws XMPException { 492 if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) { 493 return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key); 494 } else { 495 return 0; 496 } 497 } 498} 499