diff options
-rw-r--r-- | Android.mk | 4 | ||||
-rwxr-xr-x | jni/Android.mk | 15 | ||||
-rw-r--r-- | jni/tinyplanet.cc | 151 | ||||
-rw-r--r-- | res/layout/tinyplanet_editor.xml | 73 | ||||
-rw-r--r-- | res/values/strings.xml | 10 | ||||
-rw-r--r-- | src/com/android/camera/CameraActivity.java | 211 | ||||
-rw-r--r-- | src/com/android/camera/MediaSaveService.java | 2 | ||||
-rw-r--r-- | src/com/android/camera/data/LocalMediaData.java | 4 | ||||
-rw-r--r-- | src/com/android/camera/tinyplanet/TinyPlanetFragment.java | 475 | ||||
-rw-r--r-- | src/com/android/camera/tinyplanet/TinyPlanetNative.java | 42 | ||||
-rw-r--r-- | src/com/android/camera/tinyplanet/TinyPlanetPreview.java | 117 | ||||
-rw-r--r-- | src/com/android/camera/ui/FilmStripView.java | 80 | ||||
-rw-r--r-- | src/com/android/camera/util/XmpUtil.java | 405 |
13 files changed, 1518 insertions, 71 deletions
diff --git a/Android.mk b/Android.mk index ef4772844..285e59543 100644 --- a/Android.mk +++ b/Android.mk @@ -30,9 +30,9 @@ LOCAL_PROGUARD_FLAG_FILES := proguard.flags # the libraries in the APK, otherwise just put them in /system/lib and # leave them out of the APK ifneq (,$(TARGET_BUILD_APPS)) - LOCAL_JNI_SHARED_LIBRARIES := libjni_mosaic + LOCAL_JNI_SHARED_LIBRARIES := libjni_mosaic libjni_tinyplanet else - LOCAL_REQUIRED_MODULES := libjni_mosaic + LOCAL_REQUIRED_MODULES := libjni_mosaic libjni_tinyplanet endif include $(BUILD_PACKAGE) diff --git a/jni/Android.mk b/jni/Android.mk index 9f6f73925..b9bafcf6c 100755 --- a/jni/Android.mk +++ b/jni/Android.mk @@ -58,3 +58,18 @@ LOCAL_MODULE_TAGS := optional LOCAL_MODULE := libjni_mosaic include $(BUILD_SHARED_LIBRARY) + +# TinyPlanet +include $(CLEAR_VARS) + +LOCAL_CPP_EXTENSION := .cc +LOCAL_LDFLAGS := -llog -ljnigraphics +LOCAL_SDK_VERSION := 9 +LOCAL_MODULE := libjni_tinyplanet +LOCAL_SRC_FILES := tinyplanet.cc + +LOCAL_CFLAGS += -ffast-math -O3 -funroll-loops +LOCAL_ARM_MODE := arm + +include $(BUILD_SHARED_LIBRARY) + diff --git a/jni/tinyplanet.cc b/jni/tinyplanet.cc new file mode 100644 index 000000000..dfb31d768 --- /dev/null +++ b/jni/tinyplanet.cc @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <jni.h> +#include <math.h> +#include <android/bitmap.h> + +#ifdef __cplusplus +extern "C" { +#endif + + +#define PI_F 3.141592653589f + +class ImageRGBA { + public: + ImageRGBA(unsigned char* image, int width, int height) + : image_(image), width_(width), height_(height) { + width_step_ = width * 4; + } + + int Width() const { + return width_; + } + + int Height() const { + return height_; + } + + // Pixel accessor. + unsigned char* operator()(int x, int y) { + return image_ + y * width_step_ + x * 4; + } + const unsigned char* operator()(int x, int y) const { + return image_ + y * width_step_ + x * 4; + } + + private: + unsigned char* image_; + int width_; + int height_; + int width_step_; +}; + +// Interpolate a pixel in a 3 channel image. +inline void InterpolatePixel(const ImageRGBA &image, float x, float y, + unsigned char* dest) { + // Get pointers and scale factors for the source pixels. + float ax = x - floor(x); + float ay = y - floor(y); + float axn = 1.0f - ax; + float ayn = 1.0f - ay; + const unsigned char *p = image(x, y); + const unsigned char *p2 = image(x, y + 1); + + // Interpolate each image color plane. + dest[0] = static_cast<unsigned char>(axn * ayn * p[0] + ax * ayn * p[4] + + ax * ay * p2[4] + axn * ay * p2[0] + 0.5f); + p++; + p2++; + + dest[1] = static_cast<unsigned char>(axn * ayn * p[0] + ax * ayn * p[4] + + ax * ay * p2[4] + axn * ay * p2[0] + 0.5f); + p++; + p2++; + + dest[2] = static_cast<unsigned char>(axn * ayn * p[0] + ax * ayn * p[4] + + ax * ay * p2[4] + axn * ay * p2[0] + 0.5f); + p++; + p2++; + dest[3] = 0xFF; +} + +// Wrap circular coordinates around the globe +inline float wrap(float value, float dimension) { + return value - (dimension * floor(value/dimension)); +} + +void StereographicProjection(float scale, float angle, unsigned char* input_image, + int input_width, int input_height, + unsigned char* output_image, int output_width, + int output_height) { + ImageRGBA input(input_image, input_width, input_height); + ImageRGBA output(output_image, output_width, output_height); + + const float image_scale = output_width * scale; + + for (int x = 0; x < output_width; x++) { + // Center and scale x + float xf = (x - output_width / 2.0f) / image_scale; + + for (int y = 0; y < output_height; y++) { + // Center and scale y + float yf = (y - output_height / 2.0f) / image_scale; + + // Convert to polar + float r = hypotf(xf, yf); + float theta = angle+atan2(yf, xf); + if (theta>PI_F) theta-=2*PI_F; + + // Project onto plane + float phi = 2 * atan(1 / r); + // (theta stays the same) + + // Map to panorama image + float px = (theta / (2 * PI_F)) * input_width; + float py = (phi / PI_F) * input_height; + + // Wrap around the globe + px = wrap(px, input_width); + py = wrap(py, input_height); + + // Write the interpolated pixel + InterpolatePixel(input, px, py, output(x, y)); + } + } +} + + +JNIEXPORT void JNICALL Java_com_android_camera_tinyplanet_TinyPlanetNative_process(JNIEnv* env, jobject obj, jobject bitmap_in, jint width, jint height, jobject bitmap_out, jint output_size, jfloat scale, jfloat angle) +{ + char* source = 0; + char* destination = 0; + AndroidBitmap_lockPixels(env, bitmap_in, (void**) &source); + AndroidBitmap_lockPixels(env, bitmap_out, (void**) &destination); + unsigned char * rgb_in = (unsigned char * )source; + unsigned char * rgb_out = (unsigned char * )destination; + + StereographicProjection(scale, angle, rgb_in, width, height, rgb_out, output_size, output_size); + AndroidBitmap_unlockPixels(env, bitmap_in); + AndroidBitmap_unlockPixels(env, bitmap_out); +} + +#ifdef __cplusplus +} +#endif + + diff --git a/res/layout/tinyplanet_editor.xml b/res/layout/tinyplanet_editor.xml new file mode 100644 index 000000000..8880c2cfb --- /dev/null +++ b/res/layout/tinyplanet_editor.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <com.android.camera.tinyplanet.TinyPlanetPreview + android:id="@+id/preview" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_gravity="center_horizontal" + android:layout_weight="1" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical" > + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/tiny_planet_zoom" + android:textColor="#FFF" /> + + <SeekBar + android:id="@+id/zoomSlider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:contentDescription="@string/tiny_planet_zoom" + android:max="1000" + android:progress="500" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="5dp" + android:text="@string/tiny_planet_rotate" + android:textColor="#FFF" /> + + <SeekBar + android:id="@+id/angleSlider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:contentDescription="@string/tiny_planet_rotate" + android:max="360" + android:progress="0" /> + + <Button + android:id="@+id/creatTinyPlanetButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="5dp" + android:text="@string/create_tiny_planet" /> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 16a3bedd1..fe4bbfc8c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -625,4 +625,14 @@ CHAR LIMIT = NONE] --> <string name="camera_menu_more_label">MORE OPTIONS</string> <!-- settings label [CHAR LIMIT=50] --> <string name="camera_menu_settings_label">SETTINGS</string> + + <!-- Tiny Planet --> + <!-- Button to press for creating a tiny planet image. [CHAR LIMIT = 30] --> + <string name="create_tiny_planet">Create Tiny Planet</string> + <!-- Message shown while a tiny planet image is being saved. [CHAR LIMIT = 30] --> + <string name="saving_tiny_planet">Saving Tiny Planet …</string> + <!-- Label above a slider that let's the user set the zoom of a tiny planet image. [CHAR LIMIT = 15] --> + <string name="tiny_planet_zoom">Zoom</string> + <!-- Label above a slider that let's the user set the rotation of a tiny planet image. [CHAR LIMIT = 15] --> + <string name="tiny_planet_rotate">Rotate</string> </resources> diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java index b27c44827..fe65c0290 100644 --- a/src/com/android/camera/CameraActivity.java +++ b/src/com/android/camera/CameraActivity.java @@ -16,6 +16,7 @@ package com.android.camera; +import android.animation.Animator; import android.app.ActionBar; import android.app.Activity; import android.content.BroadcastReceiver; @@ -43,6 +44,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.OrientationEventListener; import android.view.View; import android.view.ViewGroup; @@ -63,6 +65,7 @@ import com.android.camera.data.LocalData; import com.android.camera.data.LocalDataAdapter; import com.android.camera.data.MediaDetails; import com.android.camera.data.SimpleViewData; +import com.android.camera.tinyplanet.TinyPlanetFragment; import com.android.camera.ui.ModuleSwitcher; import com.android.camera.ui.DetailsDialog; import com.android.camera.ui.FilmStripView; @@ -73,7 +76,7 @@ import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper; import com.android.camera2.R; public class CameraActivity extends Activity - implements ModuleSwitcher.ModuleSwitchListener { + implements ModuleSwitcher.ModuleSwitchListener { private static final String TAG = "CAM_Activity"; @@ -149,6 +152,7 @@ public class CameraActivity extends Activity private ActionBar mActionBar; private Menu mActionBarMenu; private ViewGroup mUndoDeletionBar; + private boolean mIsUndoingDeletion = false; private ShareActionProvider mStandardShareActionProvider; private Intent mStandardShareIntent; @@ -157,13 +161,14 @@ public class CameraActivity extends Activity private final int DEFAULT_SYSTEM_UI_VISIBILITY = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + private boolean mPendingDeletion = false; public void gotoGallery() { mFilmStripView.getController().goToNextItem(); } private class MyOrientationEventListener - extends OrientationEventListener { + extends OrientationEventListener { public MyOrientationEventListener(Context context) { super(context); } @@ -173,7 +178,9 @@ public class CameraActivity extends Activity // We keep the last known orientation. So if the user first orient // the camera then point the camera to floor or sky, we still have // the correct orientation. - if (orientation == ORIENTATION_UNKNOWN) return; + if (orientation == ORIENTATION_UNKNOWN) { + return; + } mLastRawOrientation = orientation; mCurrentModule.onOrientationChanged(orientation); } @@ -181,18 +188,20 @@ public class CameraActivity extends Activity private MediaSaveService mMediaSaveService; private ServiceConnection mConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName className, IBinder b) { - mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService(); - mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService); + @Override + public void onServiceConnected(ComponentName className, IBinder b) { + mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService(); + mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService); + } + + @Override + public void onServiceDisconnected(ComponentName className) { + if (mMediaSaveService != null) { + mMediaSaveService.setListener(null); + mMediaSaveService = null; } - @Override - public void onServiceDisconnected(ComponentName className) { - if (mMediaSaveService != null) { - mMediaSaveService.setListener(null); - mMediaSaveService = null; - } - }}; + } + }; // close activity when screen turns off private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { @@ -203,6 +212,7 @@ public class CameraActivity extends Activity }; private static BroadcastReceiver sScreenOffReceiver; + private static class ScreenOffReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -277,8 +287,12 @@ public class CameraActivity extends Activity } else { if (isCameraID) { mCurrentModule.onPreviewFocusChanged(true); - // Don't show the action bar in Camera preview. + // Don't show the action bar in Camera + // preview. setActionBarVisibilityAndLightsOut(true); + if (mPendingDeletion) { + performDeletion(); + } } else { updateActionBarMenu(dataID); } @@ -289,7 +303,7 @@ public class CameraActivity extends Activity return; } int panoStitchingProgress = mPanoramaManager.getTaskProgress( - contentUri); + contentUri); if (panoStitchingProgress < 0) { hidePanoStitchingProgress(); return; @@ -369,6 +383,7 @@ public class CameraActivity extends Activity /** * According to the data type, make the menu items for supported operations * visible. + * * @param dataID the data ID of the current item. */ private void updateActionBarMenu(int dataID) { @@ -463,14 +478,6 @@ public class CameraActivity extends Activity item.setVisible(visible); } - private Runnable mDeletionRunnable = new Runnable() { - @Override - public void run() { - hideUndoDeletionBar(); - mDataAdapter.executeDeletion(CameraActivity.this); - } - }; - private ImageTaskManager.TaskListener mStitchingListener = new ImageTaskManager.TaskListener() { @Override @@ -544,9 +551,13 @@ public class CameraActivity extends Activity private void removeData(int dataID) { mDataAdapter.removeData(CameraActivity.this, dataID); - showUndoDeletionBar(); - mMainHandler.removeCallbacks(mDeletionRunnable); - mMainHandler.postDelayed(mDeletionRunnable, 3000); + if (mDataAdapter.getTotalNumber() > 1) { + showUndoDeletionBar(); + } else { + // If camera preview is the only view left in filmstrip, + // no need to show undo bar. + performDeletion(); + } } private void bindMediaSaveService() { @@ -652,7 +663,7 @@ public class CameraActivity extends Activity case R.id.action_show_on_map: double[] latLong = localData.getLatLong(); if (latLong != null) { - CameraUtil.showOnMap(this, latLong); + CameraUtil.showOnMap(this, latLong); } return true; default: @@ -772,7 +783,8 @@ public class CameraActivity extends Activity mDataAdapter.requestLoad(getContentResolver()); } } else { - // Put a lock placeholder as the last image by setting its date to 0. + // Put a lock placeholder as the last image by setting its date to + // 0. ImageView v = (ImageView) getLayoutInflater().inflate( R.layout.secure_album_placeholder, null); mDataAdapter = new FixedLastDataAdapter( @@ -804,6 +816,21 @@ public class CameraActivity extends Activity } @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + boolean result = super.dispatchTouchEvent(ev); + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + // Real deletion is postponed until the next user interaction after + // the gesture that triggers deletion. Until real deletion is performed, + // users can click the undo button to bring back the image that they + // chose to delete. + if (mPendingDeletion && !mIsUndoingDeletion) { + performDeletion(); + } + } + return result; + } + + @Override public void onPause() { mOrientationListener.disable(); mCurrentModule.onPauseBeforeSuper(); @@ -838,8 +865,9 @@ public class CameraActivity extends Activity @Override public void onResume() { // TODO: Handle this in OrientationManager. + // Auto-rotate off if (Settings.System.getInt(getContentResolver(), - Settings.System.ACCELEROMETER_ROTATION, 0) == 0) {// auto-rotate off + Settings.System.ACCELEROMETER_ROTATION, 0) == 0) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); mAutoRotateScreen = false; } else { @@ -879,7 +907,9 @@ public class CameraActivity extends Activity @Override public void onDestroy() { - if (mSecureCamera) unregisterReceiver(mScreenOffReceiver); + if (mSecureCamera) { + unregisterReceiver(mScreenOffReceiver); + } super.onDestroy(); } @@ -891,11 +921,15 @@ public class CameraActivity extends Activity @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (mCurrentModule.onKeyDown(keyCode, event)) return true; + if (mCurrentModule.onKeyDown(keyCode, event)) { + return true; + } // Prevent software keyboard or voice search from showing up. if (keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_MENU) { - if (event.isLongPress()) return true; + if (event.isLongPress()) { + return true; + } } return super.onKeyDown(keyCode, event); @@ -903,7 +937,9 @@ public class CameraActivity extends Activity @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (mCurrentModule.onKeyUp(keyCode, event)) return true; + if (mCurrentModule.onKeyUp(keyCode, event)) { + return true; + } return super.onKeyUp(keyCode, event); } @@ -991,7 +1027,9 @@ public class CameraActivity extends Activity @Override public void onModuleSelected(int moduleIndex) { - if (mCurrentModuleIndex == moduleIndex) return; + if (mCurrentModuleIndex == moduleIndex) { + return; + } CameraHolder.instance().keep(); closeModule(mCurrentModule); @@ -1010,8 +1048,8 @@ public class CameraActivity extends Activity } /** - * Sets the mCurrentModuleIndex, creates a new module instance for the - * given index an sets it as mCurrentModule. + * Sets the mCurrentModuleIndex, creates a new module instance for the given + * index an sets it as mCurrentModule. */ private void setModuleFromIndex(int moduleIndex) { mCurrentModuleIndex = moduleIndex; @@ -1051,6 +1089,22 @@ public class CameraActivity extends Activity startActivityForResult(Intent.createChooser(intent, null), REQ_CODE_EDIT); } + /** + * Launch the tiny planet editor. + * + * @param data the data must be a 360 degree stereographically mapped + * panoramic image. It will not be modified, instead a new item + * with the result will be added to the filmstrip. + */ + public void launchTinyPlanetEditor(LocalData data) { + TinyPlanetFragment fragment = new TinyPlanetFragment(); + Bundle bundle = new Bundle(); + bundle.putString(TinyPlanetFragment.ARGUMENT_URI, data.getContentUri().toString()); + bundle.putString(TinyPlanetFragment.ARGUMENT_TITLE, data.getTitle()); + fragment.setArguments(bundle); + fragment.show(getFragmentManager(), "tiny_planet"); + } + private void openModule(CameraModule module) { module.init(this, mCameraModuleRootView); module.onResumeBeforeSuper(); @@ -1063,9 +1117,21 @@ public class CameraActivity extends Activity ((ViewGroup) mCameraModuleRootView).removeAllViews(); } - private void showUndoDeletionBar() { + private void performDeletion() { + if (!mPendingDeletion) { + return; + } + hideUndoDeletionBar(false); + mDataAdapter.executeDeletion(CameraActivity.this); + } + + public void showUndoDeletionBar() { + if (mPendingDeletion) { + performDeletion(); + } + Log.v(TAG, "showing undo bar"); + mPendingDeletion = true; if (mUndoDeletionBar == null) { - Log.v(TAG, "showing undo bar"); ViewGroup v = (ViewGroup) getLayoutInflater().inflate( R.layout.undo_bar, mAboveFilmstripControlLayout, true); mUndoDeletionBar = (ViewGroup) v.findViewById(R.id.camera_undo_deletion_bar); @@ -1074,29 +1140,64 @@ public class CameraActivity extends Activity @Override public void onClick(View view) { mDataAdapter.undoDataRemoval(); - mMainHandler.removeCallbacks(mDeletionRunnable); - hideUndoDeletionBar(); + hideUndoDeletionBar(true); + } + }); + // Setting undo bar clickable to avoid touch events going through + // the bar to the buttons (eg. edit button, etc) underneath the bar. + mUndoDeletionBar.setClickable(true); + // When there is user interaction going on with the undo button, we + // do not want to hide the undo bar. + button.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + mIsUndoingDeletion = true; + } else if (event.getActionMasked() == MotionEvent.ACTION_UP) { + mIsUndoingDeletion =false; + } + return false; } }); } mUndoDeletionBar.setAlpha(0f); mUndoDeletionBar.setVisibility(View.VISIBLE); - mUndoDeletionBar.animate().setDuration(200).alpha(1f).start(); + mUndoDeletionBar.animate().setDuration(200).alpha(1f).setListener(null).start(); } - private void hideUndoDeletionBar() { + private void hideUndoDeletionBar(boolean withAnimation) { Log.v(TAG, "Hiding undo deletion bar"); + mPendingDeletion = false; if (mUndoDeletionBar != null) { - mUndoDeletionBar.animate() - .setDuration(200) - .alpha(0f) - .withEndAction(new Runnable() { - @Override - public void run() { - mUndoDeletionBar.setVisibility(View.GONE); - } - }) - .start(); + if (withAnimation) { + mUndoDeletionBar.animate() + .setDuration(200) + .alpha(0f) + .setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + // Do nothing. + } + + @Override + public void onAnimationEnd(Animator animation) { + mUndoDeletionBar.setVisibility(View.GONE); + } + + @Override + public void onAnimationCancel(Animator animation) { + // Do nothing. + } + + @Override + public void onAnimationRepeat(Animator animation) { + // Do nothing. + } + }) + .start(); + } else { + mUndoDeletionBar.setVisibility(View.GONE); + } } } @@ -1105,8 +1206,8 @@ public class CameraActivity extends Activity } /** - * Enable/disable swipe-to-filmstrip. - * Will always disable swipe if in capture intent. + * Enable/disable swipe-to-filmstrip. Will always disable swipe if in + * capture intent. * * @param enable {@code true} to enable swipe. */ diff --git a/src/com/android/camera/MediaSaveService.java b/src/com/android/camera/MediaSaveService.java index 988f17f94..9c42a5c07 100644 --- a/src/com/android/camera/MediaSaveService.java +++ b/src/com/android/camera/MediaSaveService.java @@ -52,7 +52,7 @@ public class MediaSaveService extends Service { public void onQueueStatus(boolean full); } - interface OnMediaSavedListener { + public interface OnMediaSavedListener { public void onMediaSaved(Uri uri); } diff --git a/src/com/android/camera/data/LocalMediaData.java b/src/com/android/camera/data/LocalMediaData.java index 8e5216d17..3679b08e4 100644 --- a/src/com/android/camera/data/LocalMediaData.java +++ b/src/com/android/camera/data/LocalMediaData.java @@ -581,7 +581,9 @@ public abstract class LocalMediaData implements LocalData { String rotation = null; try { retriever.setDataSource(path); - } catch (IllegalArgumentException ex) { + } catch (RuntimeException ex) { + // setDataSource() can cause RuntimeException beyond + // IllegalArgumentException. e.g: data contain *.avi file. retriever.release(); Log.e(TAG, "MediaMetadataRetriever.setDataSource() fail:" + ex.getMessage()); diff --git a/src/com/android/camera/tinyplanet/TinyPlanetFragment.java b/src/com/android/camera/tinyplanet/TinyPlanetFragment.java new file mode 100644 index 000000000..c49f77ef0 --- /dev/null +++ b/src/com/android/camera/tinyplanet/TinyPlanetFragment.java @@ -0,0 +1,475 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.tinyplanet; + +import android.app.DialogFragment; +import android.app.ProgressDialog; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.RectF; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.android.camera.CameraActivity; +import com.android.camera.MediaSaveService; +import com.android.camera.MediaSaveService.OnMediaSavedListener; +import com.android.camera.tinyplanet.TinyPlanetPreview.PreviewSizeListener; +import com.android.camera.util.XmpUtil; +import com.android.camera2.R; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Date; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * An activity that provides an editor UI to create a TinyPlanet image from a + * 360 degree stereographically mapped panoramic image. + */ +public class TinyPlanetFragment extends DialogFragment implements PreviewSizeListener { + /** Argument to tell the fragment the URI of the original panoramic image. */ + public static final String ARGUMENT_URI = "uri"; + /** Argument to tell the fragment the title of the original panoramic image. */ + public static final String ARGUMENT_TITLE = "title"; + + public static final String CROPPED_AREA_IMAGE_WIDTH_PIXELS = + "CroppedAreaImageWidthPixels"; + public static final String CROPPED_AREA_IMAGE_HEIGHT_PIXELS = + "CroppedAreaImageHeightPixels"; + public static final String CROPPED_AREA_FULL_PANO_WIDTH_PIXELS = + "FullPanoWidthPixels"; + public static final String CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS = + "FullPanoHeightPixels"; + public static final String CROPPED_AREA_LEFT = + "CroppedAreaLeftPixels"; + public static final String CROPPED_AREA_TOP = + "CroppedAreaTopPixels"; + public static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/"; + + private static final String TAG = "TinyPlanetActivity"; + /** Delay between a value update and the renderer running. */ + private static final int RENDER_DELAY_MILLIS = 50; + /** Filename prefix to prepend to the original name for the new file. */ + private static final String FILENAME_PREFIX = "TINYPLANET_"; + + private Uri mSourceImageUri; + private TinyPlanetPreview mPreview; + private int mPreviewSizePx = 0; + private float mCurrentZoom = 0.5f; + private float mCurrentAngle = 0; + private ProgressDialog mDialog; + + /** + * Lock for the result preview bitmap. We can't change it while we're trying + * to draw it. + */ + private Lock mResultLock = new ReentrantLock(); + + /** The title of the original panoramic image. */ + private String mOriginalTitle = ""; + + /** The padded source bitmap. */ + private Bitmap mSourceBitmap; + /** The resulting preview bitmap. */ + private Bitmap mResultBitmap; + + /** Used to delay-post a tiny planet rendering task. */ + private Handler mHandler = new Handler(); + /** Whether rendering is in progress right now. */ + private Boolean mRendering = false; + /** + * Whether we should render one more time after the current rendering run is + * done. This is needed when there was an update to the values during the + * current rendering. + */ + private Boolean mRenderOneMore = false; + + /** Tiny planet data plus size. */ + private static final class TinyPlanetImage { + public final byte[] mJpegData; + public final int mSize; + + public TinyPlanetImage(byte[] jpegData, int size) { + mJpegData = jpegData; + mSize = size; + } + } + + /** + * Creates and executes a task to create a tiny planet with the current + * values. + */ + private final Runnable mCreateTinyPlanetRunnable = new Runnable() { + @Override + public void run() { + synchronized (mRendering) { + if (mRendering) { + mRenderOneMore = true; + return; + } + mRendering = true; + } + + (new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + mResultLock.lock(); + try { + if (mSourceBitmap == null || mResultBitmap == null) { + return null; + } + + int width = mSourceBitmap.getWidth(); + int height = mSourceBitmap.getHeight(); + TinyPlanetNative.process(mSourceBitmap, width, height, mResultBitmap, + mPreviewSizePx, + mCurrentZoom, mCurrentAngle); + } finally { + mResultLock.unlock(); + } + return null; + } + + protected void onPostExecute(Void result) { + mPreview.setBitmap(mResultBitmap, mResultLock); + synchronized (mRendering) { + mRendering = false; + if (mRenderOneMore) { + mRenderOneMore = false; + scheduleUpdate(); + } + } + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Camera); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); + getDialog().setCanceledOnTouchOutside(true); + + View view = inflater.inflate(R.layout.tinyplanet_editor, + container, false); + mPreview = (TinyPlanetPreview) view.findViewById(R.id.preview); + mPreview.setPreviewSizeChangeListener(this); + + // Zoom slider setup. + SeekBar zoomSlider = (SeekBar) view.findViewById(R.id.zoomSlider); + zoomSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do nothing. + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do nothing. + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + onZoomChange(progress); + } + }); + + // Rotation slider setup. + SeekBar angleSlider = (SeekBar) view.findViewById(R.id.angleSlider); + angleSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do nothing. + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do nothing. + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + onAngleChange(progress); + } + }); + + Button createButton = (Button) view.findViewById(R.id.creatTinyPlanetButton); + createButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onCreateTinyPlanet(); + } + }); + + mOriginalTitle = getArguments().getString(ARGUMENT_TITLE); + mSourceImageUri = Uri.parse(getArguments().getString(ARGUMENT_URI)); + mSourceBitmap = createPaddedSourceImage(mSourceImageUri, true); + + if (mSourceBitmap == null) { + Log.e(TAG, "Could not decode source image."); + dismiss(); + } + return view; + } + + /** + * From the given URI this method creates a 360/180 padded image that is + * ready to be made a tiny planet. + */ + private Bitmap createPaddedSourceImage(Uri sourceImageUri, boolean previewSize) { + InputStream is = getInputStream(sourceImageUri); + if (is == null) { + Log.e(TAG, "Could not create input stream for image."); + dismiss(); + } + Bitmap sourceBitmap = BitmapFactory.decodeStream(is); + + is = getInputStream(sourceImageUri); + XMPMeta xmp = XmpUtil.extractXMPMeta(is); + + if (xmp != null) { + int size = previewSize ? getDisplaySize() : sourceBitmap.getWidth(); + sourceBitmap = createPaddedBitmap(sourceBitmap, xmp, size); + } + return sourceBitmap; + } + + /** + * Starts an asynchronous task to create a tiny planet. Once done, will add + * the new image to the filmstrip and dismisses the fragment. + */ + private void onCreateTinyPlanet() { + // Make sure we stop rendering before we create the high-res tiny + // planet. + synchronized (mRendering) { + mRenderOneMore = false; + } + + final String savingTinyPlanet = getActivity().getResources().getString( + R.string.saving_tiny_planet); + (new AsyncTask<Void, Void, TinyPlanetImage>() { + @Override + protected void onPreExecute() { + mDialog = ProgressDialog.show(getActivity(), null, savingTinyPlanet, true, false); + } + + @Override + protected TinyPlanetImage doInBackground(Void... params) { + return createTinyPlanet(); + } + + @Override + protected void onPostExecute(TinyPlanetImage image) { + // Once created, store the new file and add it to the filmstrip. + final CameraActivity activity = (CameraActivity) getActivity(); + MediaSaveService mediaSaveService = activity.getMediaSaveService(); + OnMediaSavedListener doneListener = + new OnMediaSavedListener() { + @Override + public void onMediaSaved(Uri uri) { + // Add the new photo to the filmstrip and exit + // the fragment. + activity.notifyNewMedia(uri); + mDialog.dismiss(); + TinyPlanetFragment.this.dismiss(); + } + }; + String tinyPlanetTitle = FILENAME_PREFIX + mOriginalTitle; + mediaSaveService.addImage(image.mJpegData, tinyPlanetTitle, (new Date()).getTime(), + null, + image.mSize, image.mSize, 0, null, doneListener, getActivity() + .getContentResolver()); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + /** + * Creates the high quality tiny planet file and adds it to the media + * service. Don't call this on the UI thread. + */ + private TinyPlanetImage createTinyPlanet() { + // Free some memory we don't need anymore as we're going to dimiss the + // fragment after the tiny planet creation. + mResultLock.lock(); + try { + mResultBitmap.recycle(); + mResultBitmap = null; + mSourceBitmap.recycle(); + mSourceBitmap = null; + } finally { + mResultLock.unlock(); + } + + // Create a high-resolution padded image. + Bitmap sourceBitmap = createPaddedSourceImage(mSourceImageUri, false); + int width = sourceBitmap.getWidth(); + int height = sourceBitmap.getHeight(); + + int outputSize = width / 2; + Bitmap resultBitmap = Bitmap.createBitmap(outputSize, outputSize, + Bitmap.Config.ARGB_8888); + + TinyPlanetNative.process(sourceBitmap, width, height, resultBitmap, + outputSize, mCurrentZoom, mCurrentAngle); + + // Free the sourceImage memory as we don't need it and we need memory + // for the JPEG bytes. + sourceBitmap.recycle(); + sourceBitmap = null; + + ByteArrayOutputStream jpeg = new ByteArrayOutputStream(); + resultBitmap.compress(CompressFormat.JPEG, 100, jpeg); + return new TinyPlanetImage(jpeg.toByteArray(), outputSize); + } + + private int getDisplaySize() { + Display display = getActivity().getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + return Math.min(size.x, size.y); + } + + @Override + public void onSizeChanged(int sizePx) { + mPreviewSizePx = sizePx; + mResultLock.lock(); + try { + if (mResultBitmap == null || mResultBitmap.getWidth() != sizePx + || mResultBitmap.getHeight() != sizePx) { + if (mResultBitmap != null) { + mResultBitmap.recycle(); + } + mResultBitmap = Bitmap.createBitmap(mPreviewSizePx, mPreviewSizePx, + Bitmap.Config.ARGB_8888); + } + } finally { + mResultLock.unlock(); + } + + // Run directly and on this thread directly. + mCreateTinyPlanetRunnable.run(); + } + + private void onZoomChange(int zoom) { + // 1000 needs to be in sync with the max values declared in the layout + // xml file. + mCurrentZoom = zoom / 1000f; + scheduleUpdate(); + } + + private void onAngleChange(int angle) { + mCurrentAngle = (float) Math.toRadians(angle); + scheduleUpdate(); + } + + /** + * Delay-post a new preview rendering run. + */ + private void scheduleUpdate() { + mHandler.removeCallbacks(mCreateTinyPlanetRunnable); + mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS); + } + + private InputStream getInputStream(Uri uri) { + try { + return getActivity().getContentResolver().openInputStream(uri); + } catch (FileNotFoundException e) { + Log.e(TAG, "Could not load source image.", e); + } + return null; + } + + /** + * To create a proper TinyPlanet, the input image must be 2:1 (360:180 + * degrees). So if needed, we pad the source image with black. + */ + private static Bitmap createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) { + try { + int croppedAreaWidth = + getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS); + int croppedAreaHeight = + getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS); + int fullPanoWidth = + getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS); + int fullPanoHeight = + getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS); + int left = getInt(xmp, CROPPED_AREA_LEFT); + int top = getInt(xmp, CROPPED_AREA_TOP); + + if (fullPanoWidth == 0 || fullPanoHeight == 0) { + return bitmapIn; + } + // Make sure the intermediate image has the similar size to the + // input. + Bitmap paddedBitmap = null; + float scale = intermediateWidth / (float) fullPanoWidth; + while (paddedBitmap == null) { + try { + paddedBitmap = Bitmap.createBitmap( + (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale), + Bitmap.Config.ARGB_8888); + } catch (OutOfMemoryError e) { + System.gc(); + scale /= 2; + } + } + Canvas paddedCanvas = new Canvas(paddedBitmap); + + int right = left + croppedAreaWidth; + int bottom = top + croppedAreaHeight; + RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale); + paddedCanvas.drawBitmap(bitmapIn, null, destRect, null); + return paddedBitmap; + } catch (XMPException ex) { + // Do nothing, just use mSourceBitmap as is. + } + return bitmapIn; + } + + private static int getInt(XMPMeta xmp, String key) throws XMPException { + if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) { + return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key); + } else { + return 0; + } + } +} diff --git a/src/com/android/camera/tinyplanet/TinyPlanetNative.java b/src/com/android/camera/tinyplanet/TinyPlanetNative.java new file mode 100644 index 000000000..301db59ce --- /dev/null +++ b/src/com/android/camera/tinyplanet/TinyPlanetNative.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.tinyplanet; + +import android.graphics.Bitmap; + +/** + * TinyPlanet native interface. + */ +public class TinyPlanetNative { + static { + System.loadLibrary("jni_tinyplanet"); + } + + /** + * Create a tiny planet. + * + * @param in the 360 degree stereographically mapped panoramic input image. + * @param width the width of the input image. + * @param height the height of the input image. + * @param out the resulting tiny planet. + * @param outputSize the width and height of the square output image. + * @param scale the scale factor (used for fast previews). + * @param angleRadians the angle of the tiny planet in radians. + */ + public static native void process(Bitmap in, int width, int height, Bitmap out, int outputSize, + float scale, float angleRadians); +} diff --git a/src/com/android/camera/tinyplanet/TinyPlanetPreview.java b/src/com/android/camera/tinyplanet/TinyPlanetPreview.java new file mode 100644 index 000000000..7e7aff5fa --- /dev/null +++ b/src/com/android/camera/tinyplanet/TinyPlanetPreview.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.tinyplanet; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import java.util.concurrent.locks.Lock; + +/** + * Shows a preview of the TinyPlanet on the screen while editing. + */ +public class TinyPlanetPreview extends View { + /** + * Classes implementing this interface get informed about changes to the + * preview size. + */ + public static interface PreviewSizeListener { + /** + * Called when the preview size has changed. + * + * @param sizePx the size in pixels of the square preview area + */ + public void onSizeChanged(int sizePx); + } + + private Paint mPaint = new Paint(); + private Bitmap mPreview; + private Lock mLock; + private PreviewSizeListener mPreviewSizeListener; + private int mSize = 0; + + public TinyPlanetPreview(Context context) { + super(context); + } + + public TinyPlanetPreview(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TinyPlanetPreview(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Sets the bitmap and waits for a draw to happen before returning. + */ + public void setBitmap(Bitmap preview, Lock lock) { + mPreview = preview; + mLock = lock; + invalidate(); + } + + public void setPreviewSizeChangeListener(PreviewSizeListener listener) { + mPreviewSizeListener = listener; + if (mSize > 0) { + mPreviewSizeListener.onSizeChanged(mSize); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mLock != null && mLock.tryLock() && mPreview != null && !mPreview.isRecycled()) { + try { + canvas.drawBitmap(mPreview, 0, 0, mPaint); + } finally { + mLock.unlock(); + } + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Make sure the view is square + int size = Math.min(getMeasuredWidth(), getMeasuredHeight()); + setMeasuredDimension(size, size); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed && mPreviewSizeListener != null) { + int width = right - left; + int height = bottom - top; + + // These should be the same as we enforce a square layout, but let's + // be safe. + int mSize = Math.min(width, height); + + // Tell the listener about our new size so the renderer can adapt. + if (mSize > 0 && mPreviewSizeListener != null) { + mPreviewSizeListener.onSizeChanged(mSize); + } + } + } +} diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java index 5e0e35bbe..64f8a69d4 100644 --- a/src/com/android/camera/ui/FilmStripView.java +++ b/src/com/android/camera/ui/FilmStripView.java @@ -92,6 +92,7 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { // This is true if and only if the user is scrolling, private boolean mIsUserScrolling; + private ValueAnimator.AnimatorUpdateListener mViewItemUpdateListener; /** * Common interface for all images in the filmstrip. @@ -381,19 +382,24 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { private View mView; private RectF mViewArea; + private ValueAnimator mTranslationXAnimator; + /** * Constructor. * * @param id The id of the data from {@link DataAdapter}. * @param v The {@code View} representing the data. */ - public ViewItem(int id, View v) { + public ViewItem( + int id, View v, ValueAnimator.AnimatorUpdateListener listener) { v.setPivotX(0f); v.setPivotY(0f); mDataId = id; mView = v; mLeftPosition = -1; mViewArea = new RectF(); + mTranslationXAnimator = new ValueAnimator(); + mTranslationXAnimator.addUpdateListener(listener); } /** Returns the data id from {@link DataAdapter}. */ @@ -459,6 +465,14 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { mView.setTranslationX(transX * scale); } + public void animateTranslationX( + float targetX, long duration_ms, TimeInterpolator interpolator) { + mTranslationXAnimator.setInterpolator(interpolator); + mTranslationXAnimator.setDuration(duration_ms); + mTranslationXAnimator.setFloatValues(mView.getTranslationX(), targetX); + mTranslationXAnimator.start(); + } + /** Adjusts the translation of X regarding the view scale. */ public void translateXBy(float transX, float scale) { mView.setTranslationX(mView.getTranslationX() + transX * scale); @@ -514,7 +528,9 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { * @param scale The current scale of the filmstrip. */ public void layoutIn(Rect drawArea, int refCenter, float scale) { - int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale); + final float translationX = (mTranslationXAnimator.isRunning() ? + (Float) mTranslationXAnimator.getAnimatedValue() : 0f); + int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter + translationX) * scale); int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale); layoutAt(left, top); mView.setScaleX(scale); @@ -625,6 +641,14 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { mGestureRecognizer = new FilmStripGestureRecognizer(cameraActivity, new MyGestureReceiver()); mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop); + mViewItemUpdateListener = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + // Force the geometry update to happen to layout the view items. + mController.forceGeometryUpdate(); + invalidate(); + } + }; } /** @@ -801,7 +825,7 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { if (v == null) { return null; } - ViewItem item = new ViewItem(dataID, v); + ViewItem item = new ViewItem(dataID, v, mViewItemUpdateListener); v = item.getView(); if (v != mCameraView) { addView(item.getView()); @@ -938,7 +962,11 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { @Override public void onTinyPlanet() { - // TODO: Bring tiny planet to Camera2. + ImageData data = mDataAdapter.getImageData(getCurrentId()); + if (data == null || !(data instanceof LocalData)) { + return; + } + mActivity.launchTinyPlanetEditor((LocalData) data); } /** @@ -1011,6 +1039,7 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { if (getCurrentViewType() == ImageData.TYPE_STICKY_VIEW && !mController.isScaling() && mScale != FULL_SCREEN_SCALE) { + // Now going to full screen camera mController.goToFullScreen(); } } @@ -1263,8 +1292,10 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { } } - private void slideViewBack(View v) { - v.animate().translationX(0) + private void slideViewBack(ViewItem item) { + item.animateTranslationX( + 0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); + item.getView().animate() .alpha(1f) .setDuration(GEOMETRY_ADJUST_TIME_MS) .setInterpolator(mViewAnimInterpolator) @@ -1366,7 +1397,7 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { for (int i = 0; i < BUFFER_SIZE; i++) { if (mViewItem[i] != null && mViewItem[i].getScaledTranslationX(mScale) != 0f) { - slideViewBack(mViewItem[i].getView()); + slideViewBack(mViewItem[i]); } } @@ -1379,11 +1410,26 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { .translationYBy(transY) .setInterpolator(mViewAnimInterpolator) .setDuration(GEOMETRY_ADJUST_TIME_MS) - .withEndAction(new Runnable() { + .setListener(new Animator.AnimatorListener() { @Override - public void run() { + public void onAnimationStart(Animator animation) { + // Do nothing. + } + + @Override + public void onAnimationEnd(Animator animation) { checkForRemoval(data, removedView); } + + @Override + public void onAnimationCancel(Animator animation) { + // Do nothing. + } + + @Override + public void onAnimationRepeat(Animator animation) { + // Do nothing. + } }) .start(); layoutViewItems(); @@ -1443,7 +1489,7 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { mViewItem[i] = mViewItem[i - 1]; if (mViewItem[i] != null) { mViewItem[i].setTranslationX(-offsetX, mScale); - slideViewBack(mViewItem[i].getView()); + slideViewBack(mViewItem[i]); } } } else { @@ -1457,7 +1503,7 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { for (int i = 1; i <= insertedItem; i++) { if (mViewItem[i] != null) { mViewItem[i].setTranslationX(offsetX, mScale); - slideViewBack(mViewItem[i].getView()); + slideViewBack(mViewItem[i]); mViewItem[i - 1] = mViewItem[i]; } } @@ -1687,6 +1733,8 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { mCenterX = -1; layoutViewItems(); + + mListener.onCurrentDataChanged(mViewItem[mCurrentItem].getId(), true); } private void promoteData(int itemID, int dataID) { @@ -1714,6 +1762,7 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { private ValueAnimator mZoomAnimator; private boolean mHasNewScale; private float mNewScale; + private boolean mGeometryUpdateForced; private final Scroller mScroller; private boolean mHasNewPosition; @@ -1739,6 +1788,7 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { mHasNewPosition = false; mCanStopScroll = true; mHasNewScale = false; + mGeometryUpdateForced = false; mScaleAnimator = new ValueAnimator(); mScaleAnimator.addUpdateListener(MyController.this); @@ -1756,14 +1806,20 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { return mScaleAnimator.isRunning(); } + void forceGeometryUpdate() { + mGeometryUpdateForced = true; + } + boolean hasNewGeometry() { + boolean forceNewGeometry = mGeometryUpdateForced; + mGeometryUpdateForced = false; mHasNewPosition = mScroller.computeScrollOffset(); if (!mHasNewPosition) { mCanStopScroll = true; } // If the position is locked, then we always return true to force // the position value to use the locked value. - return (mHasNewPosition || mHasNewScale); + return (mHasNewPosition || mHasNewScale || forceNewGeometry); } /** diff --git a/src/com/android/camera/util/XmpUtil.java b/src/com/android/camera/util/XmpUtil.java new file mode 100644 index 000000000..c985a6bd8 --- /dev/null +++ b/src/com/android/camera/util/XmpUtil.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.util; + +import android.util.Log; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.options.SerializeOptions; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +/** + * Util class to read/write xmp from a jpeg image file. It only supports jpeg + * image format, and doesn't support extended xmp now. + * To use it: + * XMPMeta xmpMeta = XmpUtil.extractOrCreateXMPMeta(filename); + * xmpMeta.setProperty(PanoConstants.GOOGLE_PANO_NAMESPACE, "property_name", "value"); + * XmpUtil.writeXMPMeta(filename, xmpMeta); + * + * Or if you don't care the existing XMP meta data in image file: + * XMPMeta xmpMeta = XmpUtil.createXMPMeta(); + * xmpMeta.setPropertyBoolean(PanoConstants.GOOGLE_PANO_NAMESPACE, "bool_property_name", "true"); + * XmpUtil.writeXMPMeta(filename, xmpMeta); + */ +public class XmpUtil { + private static final String TAG = "XmpUtil"; + private static final int XMP_HEADER_SIZE = 29; + private static final String XMP_HEADER = "http://ns.adobe.com/xap/1.0/\0"; + private static final int MAX_XMP_BUFFER_SIZE = 65502; + + private static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/"; + private static final String PANO_PREFIX = "GPano"; + + private static final int M_SOI = 0xd8; // File start marker. + private static final int M_APP1 = 0xe1; // Marker for Exif or XMP. + private static final int M_SOS = 0xda; // Image data marker. + + // Jpeg file is composed of many sections and image data. This class is used + // to hold the section data from image file. + private static class Section { + public int marker; + public int length; + public byte[] data; + } + + static { + try { + XMPMetaFactory.getSchemaRegistry().registerNamespace( + GOOGLE_PANO_NAMESPACE, PANO_PREFIX); + } catch (XMPException e) { + e.printStackTrace(); + } + } + + /** + * Extracts XMPMeta from JPEG image file. + * + * @param filename JPEG image file name. + * @return Extracted XMPMeta or null. + */ + public static XMPMeta extractXMPMeta(String filename) { + if (!filename.toLowerCase().endsWith(".jpg") + && !filename.toLowerCase().endsWith(".jpeg")) { + Log.d(TAG, "XMP parse: only jpeg file is supported"); + return null; + } + + try { + return extractXMPMeta(new FileInputStream(filename)); + } catch (FileNotFoundException e) { + Log.e(TAG, "Could not read file: " + filename, e); + return null; + } + } + + /** + * Extracts XMPMeta from a JPEG image file stream. + * + * @param is the input stream containing the JPEG image file. + * @return Extracted XMPMeta or null. + */ + public static XMPMeta extractXMPMeta(InputStream is) { + List<Section> sections = parse(is, true); + if (sections == null) { + return null; + } + // Now we don't support extended xmp. + for (Section section : sections) { + if (hasXMPHeader(section.data)) { + int end = getXMPContentEnd(section.data); + byte[] buffer = new byte[end - XMP_HEADER_SIZE]; + System.arraycopy( + section.data, XMP_HEADER_SIZE, buffer, 0, buffer.length); + try { + XMPMeta result = XMPMetaFactory.parseFromBuffer(buffer); + return result; + } catch (XMPException e) { + Log.d(TAG, "XMP parse error", e); + return null; + } + } + } + return null; + } + + /** + * Creates a new XMPMeta. + */ + public static XMPMeta createXMPMeta() { + return XMPMetaFactory.create(); + } + + /** + * Tries to extract XMP meta from image file first, if failed, create one. + */ + public static XMPMeta extractOrCreateXMPMeta(String filename) { + XMPMeta meta = extractXMPMeta(filename); + return meta == null ? createXMPMeta() : meta; + } + + /** + * Writes the XMPMeta to the jpeg image file. + */ + public static boolean writeXMPMeta(String filename, XMPMeta meta) { + if (!filename.toLowerCase().endsWith(".jpg") + && !filename.toLowerCase().endsWith(".jpeg")) { + Log.d(TAG, "XMP parse: only jpeg file is supported"); + return false; + } + List<Section> sections = null; + try { + sections = parse(new FileInputStream(filename), false); + sections = insertXMPSection(sections, meta); + if (sections == null) { + return false; + } + } catch (FileNotFoundException e) { + Log.e(TAG, "Could not read file: " + filename, e); + return false; + } + FileOutputStream os = null; + try { + // Overwrite the image file with the new meta data. + os = new FileOutputStream(filename); + writeJpegFile(os, sections); + } catch (IOException e) { + Log.d(TAG, "Write file failed:" + filename, e); + return false; + } finally { + if (os != null) { + try { + os.close(); + } catch (IOException e) { + // Ignore. + } + } + } + return true; + } + + /** + * Updates a jpeg file from inputStream with XMPMeta to outputStream. + */ + public static boolean writeXMPMeta(InputStream inputStream, OutputStream outputStream, + XMPMeta meta) { + List<Section> sections = parse(inputStream, false); + sections = insertXMPSection(sections, meta); + if (sections == null) { + return false; + } + try { + // Overwrite the image file with the new meta data. + writeJpegFile(outputStream, sections); + } catch (IOException e) { + Log.d(TAG, "Write to stream failed", e); + return false; + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + // Ignore. + } + } + } + return true; + } + + /** + * Write a list of sections to a Jpeg file. + */ + private static void writeJpegFile(OutputStream os, List<Section> sections) + throws IOException { + // Writes the jpeg file header. + os.write(0xff); + os.write(M_SOI); + for (Section section : sections) { + os.write(0xff); + os.write(section.marker); + if (section.length > 0) { + // It's not the image data. + int lh = section.length >> 8; + int ll = section.length & 0xff; + os.write(lh); + os.write(ll); + } + os.write(section.data); + } + } + + private static List<Section> insertXMPSection( + List<Section> sections, XMPMeta meta) { + if (sections == null || sections.size() <= 1) { + return null; + } + byte[] buffer; + try { + SerializeOptions options = new SerializeOptions(); + options.setUseCompactFormat(true); + // We have to omit packet wrapper here because + // javax.xml.parsers.DocumentBuilder + // fails to parse the packet end <?xpacket end="w"?> in android. + options.setOmitPacketWrapper(true); + buffer = XMPMetaFactory.serializeToBuffer(meta, options); + } catch (XMPException e) { + Log.d(TAG, "Serialize xmp failed", e); + return null; + } + if (buffer.length > MAX_XMP_BUFFER_SIZE) { + // Do not support extended xmp now. + return null; + } + // The XMP section starts with XMP_HEADER and then the real xmp data. + byte[] xmpdata = new byte[buffer.length + XMP_HEADER_SIZE]; + System.arraycopy(XMP_HEADER.getBytes(), 0, xmpdata, 0, XMP_HEADER_SIZE); + System.arraycopy(buffer, 0, xmpdata, XMP_HEADER_SIZE, buffer.length); + Section xmpSection = new Section(); + xmpSection.marker = M_APP1; + // Adds the length place (2 bytes) to the section length. + xmpSection.length = xmpdata.length + 2; + xmpSection.data = xmpdata; + + for (int i = 0; i < sections.size(); ++i) { + // If we can find the old xmp section, replace it with the new one. + if (sections.get(i).marker == M_APP1 + && hasXMPHeader(sections.get(i).data)) { + // Replace with the new xmp data. + sections.set(i, xmpSection); + return sections; + } + } + // If the first section is Exif, insert XMP data before the second section, + // otherwise, make xmp data the first section. + List<Section> newSections = new ArrayList<Section>(); + int position = (sections.get(0).marker == M_APP1) ? 1 : 0; + newSections.addAll(sections.subList(0, position)); + newSections.add(xmpSection); + newSections.addAll(sections.subList(position, sections.size())); + return newSections; + } + + /** + * Checks whether the byte array has XMP header. The XMP section contains + * a fixed length header XMP_HEADER. + * + * @param data Xmp metadata. + */ + private static boolean hasXMPHeader(byte[] data) { + if (data.length < XMP_HEADER_SIZE) { + return false; + } + try { + byte[] header = new byte[XMP_HEADER_SIZE]; + System.arraycopy(data, 0, header, 0, XMP_HEADER_SIZE); + if (new String(header, "UTF-8").equals(XMP_HEADER)) { + return true; + } + } catch (UnsupportedEncodingException e) { + return false; + } + return false; + } + + /** + * Gets the end of the xmp meta content. If there is no packet wrapper, + * return data.length, otherwise return 1 + the position of last '>' + * without '?' before it. + * Usually the packet wrapper end is "<?xpacket end="w"?> but + * javax.xml.parsers.DocumentBuilder fails to parse it in android. + * + * @param data xmp metadata bytes. + * @return The end of the xmp metadata content. + */ + private static int getXMPContentEnd(byte[] data) { + for (int i = data.length - 1; i >= 1; --i) { + if (data[i] == '>') { + if (data[i - 1] != '?') { + return i + 1; + } + } + } + // It should not reach here for a valid xmp meta. + return data.length; + } + + /** + * Parses the jpeg image file. If readMetaOnly is true, only keeps the Exif + * and XMP sections (with marker M_APP1) and ignore others; otherwise, keep + * all sections. The last section with image data will have -1 length. + * + * @param is Input image data stream. + * @param readMetaOnly Whether only reads the metadata in jpg. + * @return The parse result. + */ + private static List<Section> parse(InputStream is, boolean readMetaOnly) { + try { + if (is.read() != 0xff || is.read() != M_SOI) { + return null; + } + List<Section> sections = new ArrayList<Section>(); + int c; + while ((c = is.read()) != -1) { + if (c != 0xff) { + return null; + } + // Skip padding bytes. + while ((c = is.read()) == 0xff) { + } + if (c == -1) { + return null; + } + int marker = c; + if (marker == M_SOS) { + // M_SOS indicates the image data will follow and no metadata after + // that, so read all data at one time. + if (!readMetaOnly) { + Section section = new Section(); + section.marker = marker; + section.length = -1; + section.data = new byte[is.available()]; + is.read(section.data, 0, section.data.length); + sections.add(section); + } + return sections; + } + int lh = is.read(); + int ll = is.read(); + if (lh == -1 || ll == -1) { + return null; + } + int length = lh << 8 | ll; + if (!readMetaOnly || c == M_APP1) { + Section section = new Section(); + section.marker = marker; + section.length = length; + section.data = new byte[length - 2]; + is.read(section.data, 0, length - 2); + sections.add(section); + } else { + // Skip this section since all exif/xmp meta will be in M_APP1 + // section. + is.skip(length - 2); + } + } + return sections; + } catch (IOException e) { + Log.d(TAG, "Could not parse file.", e); + return null; + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Ignore. + } + } + } + } + + private XmpUtil() {} +} |