TinyPlanetFragment.java revision 21a2e9cb07caba660120b7f5eba6e053a6cb3ac3
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.MediaSaveService; 46import com.android.camera.MediaSaveService.OnMediaSavedListener; 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.io.OutputStream; 57import java.util.Date; 58import java.util.TimeZone; 59import java.util.concurrent.locks.Lock; 60import 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 */ 66public 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