Bring back wide angle panorama.

  bug:10293937

Change-Id: I23a977e87b7416f07ecac20025b6c142ae61be05
diff --git a/Android.mk b/Android.mk
index a3659d0..ef47728 100644
--- a/Android.mk
+++ b/Android.mk
@@ -26,6 +26,15 @@
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 
+# If this is an unbundled build (to install seprately) then include
+# 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
+else
+  LOCAL_REQUIRED_MODULES := libjni_mosaic
+endif
+
 include $(BUILD_PACKAGE)
 
 include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/res/drawable-hdpi/ic_switch_pan.png b/res/drawable-hdpi/ic_switch_pan.png
new file mode 100644
index 0000000..c8161be
--- /dev/null
+++ b/res/drawable-hdpi/ic_switch_pan.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_switch_pan.png b/res/drawable-mdpi/ic_switch_pan.png
new file mode 100644
index 0000000..e63b8e9
--- /dev/null
+++ b/res/drawable-mdpi/ic_switch_pan.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_switch_pan.png b/res/drawable-xhdpi/ic_switch_pan.png
new file mode 100644
index 0000000..f17ce2f
--- /dev/null
+++ b/res/drawable-xhdpi/ic_switch_pan.png
Binary files differ
diff --git a/res/drawable/pano_direction_left_indicator.xml b/res/drawable/pano_direction_left_indicator.xml
new file mode 100644
index 0000000..a0bfb0a
--- /dev/null
+++ b/res/drawable/pano_direction_left_indicator.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false"
+              android:drawable="@drawable/ic_pan_left_indicator" />
+    <item android:drawable="@drawable/ic_pan_left_indicator_fast" />
+</selector>
diff --git a/res/drawable/pano_direction_right_indicator.xml b/res/drawable/pano_direction_right_indicator.xml
new file mode 100644
index 0000000..c3ce377
--- /dev/null
+++ b/res/drawable/pano_direction_right_indicator.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false"
+              android:drawable="@drawable/ic_pan_right_indicator" />
+    <item android:drawable="@drawable/ic_pan_right_indicator_fast" />
+</selector>
diff --git a/res/layout-land/camera_controls.xml b/res/layout-land/camera_controls.xml
index d177240..1495332 100644
--- a/res/layout-land/camera_controls.xml
+++ b/res/layout-land/camera_controls.xml
@@ -40,7 +40,7 @@
             android:layout_gravity="right|top"
             android:layout_marginRight="2dip" />
 
-        <com.android.camera.ui.CameraSwitcher
+        <com.android.camera.ui.ModuleSwitcher
             android:id="@+id/camera_switcher"
             style="@style/SwitcherButton"
             android:layout_gravity="right|bottom"
diff --git a/res/layout-land/pano_module_capture.xml b/res/layout-land/pano_module_capture.xml
new file mode 100644
index 0000000..cb76026
--- /dev/null
+++ b/res/layout-land/pano_module_capture.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/panorama_capture_layout"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <!-- The top bar with capture indication -->
+        <FrameLayout style="@style/PanoViewHorizontalBar">
+            <TextView android:id="@+id/pano_capture_indicator"
+                    android:text="@string/pano_capture_indication"
+                    android:textAppearance="?android:textAppearanceMedium"
+                    android:layout_gravity="center"
+                    android:visibility="gone"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content" />
+        </FrameLayout>
+
+        <FrameLayout
+                android:layout_gravity="center"
+                android:layout_weight="6"
+                android:layout_width="match_parent"
+                android:layout_height="0dp">
+            <TextureView
+                    android:id="@+id/pano_preview_textureview"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent" />
+            <View
+                    android:id="@+id/pano_preview_area_border"
+                    android:visibility="gone"
+                    android:background="@drawable/ic_pan_border_fast"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent" />
+        </FrameLayout>
+
+        <!-- The bottom bar with progress bar and direction indicators -->
+        <RelativeLayout style="@style/PanoViewHorizontalBar">
+
+            <com.android.camera.PanoProgressBar
+                    android:id="@+id/pano_pan_progress_bar"
+                    android:visibility="gone"
+                    android:src="@drawable/ic_pan_progression"
+                    android:layout_centerInParent="true"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content" />
+            <ImageView
+                    android:id="@+id/pano_pan_left_indicator"
+                    android:src="@drawable/pano_direction_left_indicator"
+                    android:visibility="gone"
+                    android:layout_marginRight="5dp"
+                    android:layout_toLeftOf="@id/pano_pan_progress_bar"
+                    android:layout_centerVertical="true"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content" />
+
+            <ImageView
+                    android:id="@+id/pano_pan_right_indicator"
+                    android:src="@drawable/pano_direction_right_indicator"
+                    android:visibility="gone"
+                    android:layout_marginLeft="5dp"
+                    android:layout_toRightOf="@id/pano_pan_progress_bar"
+                    android:layout_centerVertical="true"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content" />
+        </RelativeLayout>
+
+    </LinearLayout>
+
+    <!-- The hint for "Too fast" text view -->
+    <TextView android:id="@+id/pano_capture_too_fast_textview"
+            android:text="@string/pano_too_fast_prompt"
+            android:textAppearance="?android:textAppearanceMedium"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:visibility="gone" />
+</FrameLayout>
diff --git a/res/layout-land/pano_module_review.xml b/res/layout-land/pano_module_review.xml
new file mode 100644
index 0000000..002d47a
--- /dev/null
+++ b/res/layout-land/pano_module_review.xml
@@ -0,0 +1,48 @@
+<?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"
+        android:id="@+id/pano_review_layout"
+        android:visibility="gone"
+        android:orientation="vertical"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent">
+
+    <TextView style="@style/PanoViewHorizontalBar"
+            android:text="@string/pano_review_rendering"
+            android:textAppearance="?android:textAppearanceMedium"
+            android:gravity="center" />
+
+    <ImageView android:id="@+id/pano_reviewarea"
+            android:scaleType="fitCenter"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/pano_mosaic_surface_height" />
+
+    <FrameLayout style="@style/PanoViewHorizontalBar">
+        <com.android.camera.PanoProgressBar
+                android:id="@+id/pano_saving_progress_bar"
+                android:src="@drawable/ic_pan_progression"
+                android:layout_gravity="center"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content" />
+
+        <com.android.camera.ui.RotateImageView android:id="@+id/pano_review_cancel_button"
+                style="@style/ReviewControlIcon"
+                android:contentDescription="@string/accessibility_review_cancel"
+                android:layout_gravity="center_vertical|right"
+                android:src="@drawable/ic_menu_cancel_holo_light" />
+    </FrameLayout>
+</LinearLayout>
diff --git a/res/layout-port/camera_controls.xml b/res/layout-port/camera_controls.xml
index 5f89830..03e896b 100644
--- a/res/layout-port/camera_controls.xml
+++ b/res/layout-port/camera_controls.xml
@@ -40,7 +40,7 @@
             android:layout_marginBottom="2dip"
             android:contentDescription="@string/accessibility_menu_button" />
 
-        <com.android.camera.ui.CameraSwitcher
+        <com.android.camera.ui.ModuleSwitcher
            android:id="@+id/camera_switcher"
            style="@style/SwitcherButton"
            android:layout_gravity="bottom|left"
@@ -67,4 +67,4 @@
             android:scaleType="centerInside"
             android:layout_gravity="top|right" />
 
-</com.android.camera.ui.CameraControls>
\ No newline at end of file
+</com.android.camera.ui.CameraControls>
diff --git a/res/layout-port/pano_module_capture.xml b/res/layout-port/pano_module_capture.xml
new file mode 100644
index 0000000..57c00cd
--- /dev/null
+++ b/res/layout-port/pano_module_capture.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/panorama_capture_layout"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <!-- The top bar with capture indication -->
+        <FrameLayout style="@style/PanoViewHorizontalBar">
+            <TextView android:id="@+id/pano_capture_indicator"
+                android:text="@string/pano_capture_indication"
+                android:textAppearance="?android:textAppearanceMedium"
+                android:layout_gravity="center"
+                android:visibility="gone"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+        </FrameLayout>
+
+        <FrameLayout
+            android:layout_gravity="center"
+            android:layout_weight="3"
+            android:layout_width="match_parent"
+            android:layout_height="0dp">
+            <TextureView
+                android:id="@+id/pano_preview_textureview"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+            <View
+                android:id="@+id/pano_preview_area_border"
+                android:visibility="gone"
+                android:background="@drawable/ic_pan_border_fast"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+        </FrameLayout>
+
+        <!-- The bottom bar with progress bar and direction indicators -->
+        <RelativeLayout style="@style/PanoViewHorizontalBar">
+
+            <com.android.camera.PanoProgressBar
+                android:id="@+id/pano_pan_progress_bar"
+                android:visibility="gone"
+                android:src="@drawable/ic_pan_progression"
+                android:layout_centerInParent="true"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+            <ImageView
+                android:id="@+id/pano_pan_left_indicator"
+                android:src="@drawable/pano_direction_left_indicator"
+                android:visibility="gone"
+                android:layout_marginRight="5dp"
+                android:layout_toLeftOf="@id/pano_pan_progress_bar"
+                android:layout_centerVertical="true"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+
+            <ImageView
+                android:id="@+id/pano_pan_right_indicator"
+                android:src="@drawable/pano_direction_right_indicator"
+                android:visibility="gone"
+                android:layout_marginLeft="5dp"
+                android:layout_toRightOf="@id/pano_pan_progress_bar"
+                android:layout_centerVertical="true"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+        </RelativeLayout>
+
+    </LinearLayout>
+
+    <!-- The hint for "Too fast" text view -->
+    <TextView android:id="@+id/pano_capture_too_fast_textview"
+        android:text="@string/pano_too_fast_prompt"
+        android:textAppearance="?android:textAppearanceMedium"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:visibility="gone" />
+</FrameLayout>
diff --git a/res/layout-port/pano_module_review.xml b/res/layout-port/pano_module_review.xml
new file mode 100644
index 0000000..3c5eb2c
--- /dev/null
+++ b/res/layout-port/pano_module_review.xml
@@ -0,0 +1,60 @@
+<?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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/pano_review_layout"
+        android:visibility="gone"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent">
+
+    <LinearLayout
+            android:orientation="vertical"
+            android:layout_height="match_parent"
+            android:layout_width="match_parent">
+        <TextView style="@style/PanoViewHorizontalBar"
+                android:text="@string/pano_review_rendering"
+                android:textAppearance="?android:textAppearanceMedium"
+                android:gravity="center" />
+
+        <ImageView android:id="@+id/pano_reviewarea"
+                   android:scaleType="fitCenter"
+                   android:layout_width="match_parent"
+                   android:layout_height="0dp"
+                   android:layout_weight="1.5" />
+
+        <View style="@style/PanoViewHorizontalBar"/>
+    </LinearLayout>
+
+    <LinearLayout
+            android:orientation="vertical"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:layout_gravity="center_horizontal|bottom">
+
+        <com.android.camera.PanoProgressBar
+                android:id="@+id/pano_saving_progress_bar"
+                android:src="@drawable/ic_pan_progression"
+                android:layout_gravity="center_horizontal"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content" />
+
+        <ImageView android:id="@id/pano_review_cancel_button"
+                style="@style/ReviewControlIcon"
+                android:contentDescription="@string/accessibility_review_cancel"
+                android:layout_gravity="center_horizontal"
+                android:src="@drawable/ic_menu_cancel_holo_light" />
+    </LinearLayout>
+</FrameLayout>
diff --git a/res/layout/panorama_module.xml b/res/layout/panorama_module.xml
new file mode 100644
index 0000000..64063a2
--- /dev/null
+++ b/res/layout/panorama_module.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/pano_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <include layout="@layout/pano_module_capture" />
+    <include layout="@layout/pano_module_review" />
+    <include layout="@layout/camera_controls"
+             android:layout_gravity="center"
+             style="@style/CameraControls"/>
+</FrameLayout>
diff --git a/res/values-land/styles.xml b/res/values-land/styles.xml
index 6ca7e91..69b524e 100644
--- a/res/values-land/styles.xml
+++ b/res/values-land/styles.xml
@@ -56,13 +56,6 @@
         <item name="android:layout_marginBottom">13dp</item>
         <item name="android:layout_marginTop">13dp</item>
     </style>
-        <style name="PanoViewHorizontalBar">
-        <item name="android:background">#000000</item>
-        <item name="android:alpha">1.0</item>
-        <item name="android:layout_height">0dp</item>
-        <item name="android:layout_width">match_parent</item>
-        <item name="android:layout_weight">1.5</item>
-    </style>
     <style name="SettingPopupWindow_xlarge">
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
diff --git a/res/values-port/styles.xml b/res/values-port/styles.xml
index 46871c6..830b0c7 100644
--- a/res/values-port/styles.xml
+++ b/res/values-port/styles.xml
@@ -47,13 +47,6 @@
         <item name="android:layout_marginLeft">13dp</item>
         <item name="android:layout_marginRight">13dp</item>
     </style>
-    <style name="PanoViewHorizontalBar">
-        <item name="android:background">#000000</item>
-        <item name="android:alpha">1.0</item>
-        <item name="android:layout_width">match_parent</item>
-        <item name="android:layout_height">0dp</item>
-        <item name="android:layout_weight">1</item>
-    </style>
     <style name="SettingPopupWindow_xlarge">
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 2fd7dbd..23a5f5c 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -264,4 +264,11 @@
         <item name="android:drawablePadding">8dp</item>
         <item name="android:background">@drawable/bg_pressed</item>
     </style>
+    <style name="PanoViewHorizontalBar">
+        <item name="android:background">#000000</item>
+        <item name="android:alpha">1.0</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">0dp</item>
+        <item name="android:layout_weight">1.5</item>
+    </style>
 </resources>
diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java
index 47a964f..ec8fc95 100644
--- a/src/com/android/camera/CameraActivity.java
+++ b/src/com/android/camera/CameraActivity.java
@@ -53,6 +53,8 @@
 import android.widget.ProgressBar;
 import android.widget.ShareActionProvider;
 
+import com.android.camera.app.AppManagerFactory;
+import com.android.camera.app.PanoramaStitchingManager;
 import com.android.camera.data.CameraDataAdapter;
 import com.android.camera.data.CameraPreviewData;
 import com.android.camera.data.FixedFirstDataAdapter;
@@ -61,8 +63,7 @@
 import com.android.camera.data.LocalDataAdapter;
 import com.android.camera.data.MediaDetails;
 import com.android.camera.data.SimpleViewData;
-import com.android.camera.ui.CameraSwitcher;
-import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.ModuleSwitcher;
 import com.android.camera.ui.DetailsDialog;
 import com.android.camera.ui.FilmStripView;
 import com.android.camera.util.ApiHelper;
@@ -72,7 +73,7 @@
 import com.android.camera2.R;
 
 public class CameraActivity extends Activity
-    implements CameraSwitchListener {
+    implements ModuleSwitcher.ModuleSwitchListener {
 
     private static final String TAG = "CAM_Activity";
 
@@ -128,7 +129,6 @@
     private boolean mSecureCamera;
     // This is a hack to speed up the start of SecureCamera.
     private static boolean sFirstStartAfterScreenOn = true;
-    private boolean mShowCameraPreview;
     private int mLastRawOrientation;
     private MyOrientationEventListener mOrientationListener;
     private Handler mMainHandler;
@@ -643,7 +643,8 @@
         mAboveFilmstripControlLayout.setSystemUiVisibility(
                 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
-        mPanoramaManager = new PanoramaStitchingManager(CameraActivity.this);
+        mPanoramaManager = AppManagerFactory.getInstance(this)
+                .getPanoramaStitchingManager();
         mPanoramaManager.addTaskListener(mStitchingListener);
         LayoutInflater inflater = getLayoutInflater();
         View rootLayout = inflater.inflate(R.layout.camera, null, false);
@@ -670,26 +671,26 @@
         int moduleIndex = -1;
         if (MediaStore.INTENT_ACTION_VIDEO_CAMERA.equals(getIntent().getAction())
                 || MediaStore.ACTION_VIDEO_CAPTURE.equals(getIntent().getAction())) {
-            moduleIndex = CameraSwitcher.VIDEO_MODULE_INDEX;
+            moduleIndex = ModuleSwitcher.VIDEO_MODULE_INDEX;
         } else if (MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA.equals(getIntent().getAction())
                 || MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(getIntent()
                         .getAction())
                 || MediaStore.ACTION_IMAGE_CAPTURE.equals(getIntent().getAction())
                 || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(getIntent().getAction())) {
-            moduleIndex = CameraSwitcher.PHOTO_MODULE_INDEX;
+            moduleIndex = ModuleSwitcher.PHOTO_MODULE_INDEX;
         } else {
             // If the activity has not been started using an explicit intent,
             // read the module index from the last time the user changed modes
             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
             moduleIndex = prefs.getInt(PREF_STARTUP_MODULE_INDEX, -1);
             if (moduleIndex < 0) {
-                moduleIndex = CameraSwitcher.PHOTO_MODULE_INDEX;
+                moduleIndex = ModuleSwitcher.PHOTO_MODULE_INDEX;
             }
         }
-        setModuleFromIndex(moduleIndex);
 
-        mCurrentModule.init(this, mCameraModuleRootView);
         mOrientationListener = new MyOrientationEventListener(this);
+        setModuleFromIndex(moduleIndex);
+        mCurrentModule.init(this, mCameraModuleRootView);
         mMainHandler = new Handler(getMainLooper());
 
         if (!mSecureCamera) {
@@ -788,9 +789,6 @@
                 || keyCode == KeyEvent.KEYCODE_MENU) {
             if (event.isLongPress()) return true;
         }
-        if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) {
-            return true;
-        }
 
         return super.onKeyDown(keyCode, event);
     }
@@ -798,9 +796,6 @@
     @Override
     public boolean onKeyUp(int keyCode, KeyEvent event) {
         if (mCurrentModule.onKeyUp(keyCode, event)) return true;
-        if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) {
-            return true;
-        }
         return super.onKeyUp(keyCode, event);
     }
 
@@ -887,7 +882,7 @@
     }
 
     @Override
-    public void onCameraSelected(int moduleIndex) {
+    public void onModuleSelected(int moduleIndex) {
         if (mCurrentModuleIndex == moduleIndex) return;
 
         CameraHolder.instance().keep();
@@ -913,15 +908,26 @@
     private void setModuleFromIndex(int moduleIndex) {
         mCurrentModuleIndex = moduleIndex;
         switch (moduleIndex) {
-            case CameraSwitcher.VIDEO_MODULE_INDEX:
+            case ModuleSwitcher.VIDEO_MODULE_INDEX: {
                 mCurrentModule = new VideoModule();
                 break;
-            case CameraSwitcher.PHOTO_MODULE_INDEX:
+            }
+
+            case ModuleSwitcher.PHOTO_MODULE_INDEX: {
                 mCurrentModule = new PhotoModule();
                 break;
-            case CameraSwitcher.LIGHTCYCLE_MODULE_INDEX:
+            }
+
+            case ModuleSwitcher.WIDE_ANGLE_PANO_MODULE_INDEX: {
+                mCurrentModule = new WideAnglePanoramaModule();
+                break;
+            }
+
+            case ModuleSwitcher.LIGHTCYCLE_MODULE_INDEX: {
                 mCurrentModule = PhotoSphereHelper.createPanoramaModule();
                 break;
+            }
+
             default:
                 break;
         }
diff --git a/src/com/android/camera/Mosaic.java b/src/com/android/camera/Mosaic.java
new file mode 100644
index 0000000..b1d10c0
--- /dev/null
+++ b/src/com/android/camera/Mosaic.java
@@ -0,0 +1,206 @@
+/*
+ * 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;
+
+/**
+ * The Java interface to JNI calls regarding mosaic stitching.
+ *
+ * A high-level usage is:
+ *
+ * Mosaic mosaic = new Mosaic();
+ * mosaic.setSourceImageDimensions(width, height);
+ * mosaic.reset(blendType);
+ *
+ * while ((pixels = hasNextImage()) != null) {
+ *    mosaic.setSourceImage(pixels);
+ * }
+ *
+ * mosaic.createMosaic(highRes);
+ * byte[] result = mosaic.getFinalMosaic();
+ *
+ */
+public class Mosaic {
+    /**
+     * In this mode, the images are stitched together in the same spatial arrangement as acquired
+     * i.e. if the user follows a curvy trajectory, the image boundary of the resulting mosaic will
+     * be curved in the same manner. This mode is useful if the user wants to capture a mosaic as
+     * if "painting" the scene using the smart-phone device and does not want any corrective warps
+     * to distort the captured images.
+     */
+    public static final int BLENDTYPE_FULL = 0;
+
+    /**
+     * This mode is the same as BLENDTYPE_FULL except that the resulting mosaic is rotated
+     * to balance the first and last images to be approximately at the same vertical offset in the
+     * output mosaic. This is useful when acquiring a mosaic by a typical panning-like motion to
+     * remove a one-sided curve in the mosaic (typically due to the camera not staying horizontal
+     * during the video capture) and convert it to a more symmetrical "smiley-face" like output.
+     */
+    public static final int BLENDTYPE_PAN = 1;
+
+    /**
+     * This mode compensates for typical "smiley-face" like output in longer mosaics and creates
+     * a rectangular mosaic with minimal black borders (by unwrapping the mosaic onto an imaginary
+     * cylinder). If the user follows a curved trajectory (instead of a perfect panning trajectory),
+     * the resulting mosaic here may suffer from some image distortions in trying to map the
+     * trajectory to a cylinder.
+     */
+    public static final int BLENDTYPE_CYLINDERPAN = 2;
+
+    /**
+     * This mode is basically BLENDTYPE_CYLINDERPAN plus doing a rectangle cropping before returning
+     * the mosaic. The mode is useful for making the resulting mosaic have a rectangle shape.
+     */
+    public static final int BLENDTYPE_HORIZONTAL =3;
+
+    /**
+     * This strip type will use the default thin strips where the strips are
+     * spaced according to the image capture rate.
+     */
+    public static final int STRIPTYPE_THIN = 0;
+
+    /**
+     * This strip type will use wider strips for blending. The strip separation
+     * is controlled by a threshold on the native side. Since the strips are
+     * wider, there is an additional cross-fade blending step to make the seam
+     * boundaries smoother. Since this mode uses lesser image frames, it is
+     * computationally more efficient than the thin strip mode.
+     */
+    public static final int STRIPTYPE_WIDE = 1;
+
+    /**
+     * Return flags returned by createMosaic() are one of the following.
+     */
+    public static final int MOSAIC_RET_OK = 1;
+    public static final int MOSAIC_RET_ERROR = -1;
+    public static final int MOSAIC_RET_CANCELLED = -2;
+    public static final int MOSAIC_RET_LOW_TEXTURE = -3;
+    public static final int MOSAIC_RET_FEW_INLIERS = 2;
+
+
+    static {
+        System.loadLibrary("jni_mosaic");
+    }
+
+    /**
+     * Allocate memory for the image frames at the given resolution.
+     *
+     * @param width width of the input frames in pixels
+     * @param height height of the input frames in pixels
+     */
+    public native void allocateMosaicMemory(int width, int height);
+
+    /**
+     * Free memory allocated by allocateMosaicMemory.
+     *
+     */
+    public native void freeMosaicMemory();
+
+    /**
+     * Pass the input image frame to the native layer. Each time the a new
+     * source image t is set, the transformation matrix from the first source
+     * image to t is computed and returned.
+     *
+     * @param pixels source image of NV21 format.
+     * @return Float array of length 11; first 9 entries correspond to the 3x3
+     *         transformation matrix between the first frame and the passed frame;
+     *         the 10th entry is the number of the passed frame, where the counting
+     *         starts from 1; and the 11th entry is the returning code, whose value
+     *         is one of those MOSAIC_RET_* returning flags defined above.
+     */
+    public native float[] setSourceImage(byte[] pixels);
+
+    /**
+     * This is an alternative to the setSourceImage function above. This should
+     * be called when the image data is already on the native side in a fixed
+     * byte array. In implementation, this array is filled by the GL thread
+     * using glReadPixels directly from GPU memory (where it is accessed by
+     * an associated SurfaceTexture).
+     *
+     * @return Float array of length 11; first 9 entries correspond to the 3x3
+     *         transformation matrix between the first frame and the passed frame;
+     *         the 10th entry is the number of the passed frame, where the counting
+     *         starts from 1; and the 11th entry is the returning code, whose value
+     *         is one of those MOSAIC_RET_* returning flags defined above.
+     */
+    public native float[] setSourceImageFromGPU();
+
+    /**
+     * Set the type of blending.
+     *
+     * @param type the blending type defined in the class. {BLENDTYPE_FULL,
+     *        BLENDTYPE_PAN, BLENDTYPE_CYLINDERPAN, BLENDTYPE_HORIZONTAL}
+     */
+    public native void setBlendingType(int type);
+
+    /**
+     * Set the type of strips to use for blending.
+     * @param type the blending strip type to use {STRIPTYPE_THIN,
+     * STRIPTYPE_WIDE}.
+     */
+    public native void setStripType(int type);
+
+    /**
+     * Tell the native layer to create the final mosaic after all the input frame
+     * data have been collected.
+     * The case of generating high-resolution mosaic may take dozens of seconds to finish.
+     *
+     * @param value True means generating a high-resolution mosaic -
+     *        which is based on the original images set in setSourceImage().
+     *        False means generating a low-resolution version -
+     *        which is based on 1/4 downscaled images from the original images.
+     * @return Returns a status code suggesting if the mosaic building was
+     *        successful, in error, or was cancelled by the user.
+     */
+    public native int createMosaic(boolean value);
+
+    /**
+     * Get the data for the created mosaic.
+     *
+     * @return Returns an integer array which contains the final mosaic in the ARGB_8888 format.
+     *         The first MosaicWidth*MosaicHeight values contain the image data, followed by 2
+     *         integers corresponding to the values MosaicWidth and MosaicHeight respectively.
+     */
+    public native int[] getFinalMosaic();
+
+    /**
+     * Get the data for the created mosaic.
+     *
+     * @return Returns a byte array which contains the final mosaic in the NV21 format.
+     *         The first MosaicWidth*MosaicHeight*1.5 values contain the image data, followed by
+     *         8 bytes which pack the MosaicWidth and MosaicHeight integers into 4 bytes each
+     *         respectively.
+     */
+    public native byte[] getFinalMosaicNV21();
+
+    /**
+     * Reset the state of the frame arrays which maintain the captured frame data.
+     * Also re-initializes the native mosaic object to make it ready for capturing a new mosaic.
+     */
+    public native void reset();
+
+    /**
+     * Get the progress status of the mosaic computation process.
+     * @param hires Boolean flag to select whether to report progress of the
+     *              low-res or high-res mosaicer.
+     * @param cancelComputation Boolean flag to allow cancelling the
+     *              mosaic computation when needed from the GUI end.
+     * @return Returns a number from 0-100 where 50 denotes that the mosaic
+     *          computation is 50% done.
+     */
+    public native int reportProgress(boolean hires, boolean cancelComputation);
+}
diff --git a/src/com/android/camera/MosaicFrameProcessor.java b/src/com/android/camera/MosaicFrameProcessor.java
new file mode 100644
index 0000000..cb30534
--- /dev/null
+++ b/src/com/android/camera/MosaicFrameProcessor.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2011 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;
+
+import android.util.Log;
+
+/**
+ * A singleton to handle the processing of each frame by {@link Mosaic}.
+ */
+public class MosaicFrameProcessor {
+    private static final String TAG = "MosaicFrameProcessor";
+    private static final int NUM_FRAMES_IN_BUFFER = 2;
+    private static final int MAX_NUMBER_OF_FRAMES = 100;
+    private static final int MOSAIC_RET_CODE_INDEX = 10;
+    private static final int FRAME_COUNT_INDEX = 9;
+    private static final int X_COORD_INDEX = 2;
+    private static final int Y_COORD_INDEX = 5;
+    private static final int HR_TO_LR_DOWNSAMPLE_FACTOR = 4;
+    private static final int WINDOW_SIZE = 3;
+
+    private Mosaic mMosaicer;
+    private boolean mIsMosaicMemoryAllocated = false;
+    private float mTranslationLastX;
+    private float mTranslationLastY;
+
+    private int mFillIn = 0;
+    private int mTotalFrameCount = 0;
+    private int mLastProcessFrameIdx = -1;
+    private int mCurrProcessFrameIdx = -1;
+    private boolean mFirstRun;
+
+    // Panning rate is in unit of percentage of image content translation per
+    // frame. Use moving average to calculate the panning rate.
+    private float mPanningRateX;
+    private float mPanningRateY;
+
+    private float[] mDeltaX = new float[WINDOW_SIZE];
+    private float[] mDeltaY = new float[WINDOW_SIZE];
+    private int mOldestIdx = 0;
+    private float mTotalTranslationX = 0f;
+    private float mTotalTranslationY = 0f;
+
+    private ProgressListener mProgressListener;
+
+    private int mPreviewWidth;
+    private int mPreviewHeight;
+    private int mPreviewBufferSize;
+
+    private static MosaicFrameProcessor sMosaicFrameProcessor; // singleton
+
+    public interface ProgressListener {
+        public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
+                float progressX, float progressY);
+    }
+
+    public static MosaicFrameProcessor getInstance() {
+        if (sMosaicFrameProcessor == null) {
+            sMosaicFrameProcessor = new MosaicFrameProcessor();
+        }
+        return sMosaicFrameProcessor;
+    }
+
+    private MosaicFrameProcessor() {
+        mMosaicer = new Mosaic();
+    }
+
+    public void setProgressListener(ProgressListener listener) {
+        mProgressListener = listener;
+    }
+
+    public int reportProgress(boolean hires, boolean cancel) {
+        return mMosaicer.reportProgress(hires, cancel);
+    }
+
+    public void initialize(int previewWidth, int previewHeight, int bufSize) {
+        mPreviewWidth = previewWidth;
+        mPreviewHeight = previewHeight;
+        mPreviewBufferSize = bufSize;
+        setupMosaicer(mPreviewWidth, mPreviewHeight, mPreviewBufferSize);
+        setStripType(Mosaic.STRIPTYPE_WIDE);
+        // no need to call reset() here. reset() should be called by the client
+        // after this initialization before calling other methods of this object.
+    }
+
+    public void clear() {
+        if (mIsMosaicMemoryAllocated) {
+            mMosaicer.freeMosaicMemory();
+            mIsMosaicMemoryAllocated = false;
+        }
+        synchronized (this) {
+            notify();
+        }
+    }
+
+    public boolean isMosaicMemoryAllocated() {
+        return mIsMosaicMemoryAllocated;
+    }
+
+    public void setStripType(int type) {
+        mMosaicer.setStripType(type);
+    }
+
+    private void setupMosaicer(int previewWidth, int previewHeight, int bufSize) {
+        Log.v(TAG, "setupMosaicer w, h=" + previewWidth + ',' + previewHeight + ',' + bufSize);
+
+        if (mIsMosaicMemoryAllocated) throw new RuntimeException("MosaicFrameProcessor in use!");
+        mIsMosaicMemoryAllocated = true;
+        mMosaicer.allocateMosaicMemory(previewWidth, previewHeight);
+    }
+
+    public void reset() {
+        // reset() can be called even if MosaicFrameProcessor is not initialized.
+        // Only counters will be changed.
+        mFirstRun = true;
+        mTotalFrameCount = 0;
+        mFillIn = 0;
+        mTotalTranslationX = 0;
+        mTranslationLastX = 0;
+        mTotalTranslationY = 0;
+        mTranslationLastY = 0;
+        mPanningRateX = 0;
+        mPanningRateY = 0;
+        mLastProcessFrameIdx = -1;
+        mCurrProcessFrameIdx = -1;
+        for (int i = 0; i < WINDOW_SIZE; ++i) {
+            mDeltaX[i] = 0f;
+            mDeltaY[i] = 0f;
+        }
+        mMosaicer.reset();
+    }
+
+    public int createMosaic(boolean highRes) {
+        return mMosaicer.createMosaic(highRes);
+    }
+
+    public byte[] getFinalMosaicNV21() {
+        return mMosaicer.getFinalMosaicNV21();
+    }
+
+    // Processes the last filled image frame through the mosaicer and
+    // updates the UI to show progress.
+    // When done, processes and displays the final mosaic.
+    public void processFrame() {
+        if (!mIsMosaicMemoryAllocated) {
+            // clear() is called and buffers are cleared, stop computation.
+            // This can happen when the onPause() is called in the activity, but still some frames
+            // are not processed yet and thus the callback may be invoked.
+            return;
+        }
+
+        mCurrProcessFrameIdx = mFillIn;
+        mFillIn = ((mFillIn + 1) % NUM_FRAMES_IN_BUFFER);
+
+        // Check that we are trying to process a frame different from the
+        // last one processed (useful if this class was running asynchronously)
+        if (mCurrProcessFrameIdx != mLastProcessFrameIdx) {
+            mLastProcessFrameIdx = mCurrProcessFrameIdx;
+
+            // TODO: make the termination condition regarding reaching
+            // MAX_NUMBER_OF_FRAMES solely determined in the library.
+            if (mTotalFrameCount < MAX_NUMBER_OF_FRAMES) {
+                // If we are still collecting new frames for the current mosaic,
+                // process the new frame.
+                calculateTranslationRate();
+
+                // Publish progress of the ongoing processing
+                if (mProgressListener != null) {
+                    mProgressListener.onProgress(false, mPanningRateX, mPanningRateY,
+                            mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth,
+                            mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight);
+                }
+            } else {
+                if (mProgressListener != null) {
+                    mProgressListener.onProgress(true, mPanningRateX, mPanningRateY,
+                            mTranslationLastX * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewWidth,
+                            mTranslationLastY * HR_TO_LR_DOWNSAMPLE_FACTOR / mPreviewHeight);
+                }
+            }
+        }
+    }
+
+    public void calculateTranslationRate() {
+        float[] frameData = mMosaicer.setSourceImageFromGPU();
+        int ret_code = (int) frameData[MOSAIC_RET_CODE_INDEX];
+        mTotalFrameCount  = (int) frameData[FRAME_COUNT_INDEX];
+        float translationCurrX = frameData[X_COORD_INDEX];
+        float translationCurrY = frameData[Y_COORD_INDEX];
+
+        if (mFirstRun) {
+            // First time: no need to update delta values.
+            mTranslationLastX = translationCurrX;
+            mTranslationLastY = translationCurrY;
+            mFirstRun = false;
+            return;
+        }
+
+        // Moving average: remove the oldest translation/deltaTime and
+        // add the newest translation/deltaTime in
+        int idx = mOldestIdx;
+        mTotalTranslationX -= mDeltaX[idx];
+        mTotalTranslationY -= mDeltaY[idx];
+        mDeltaX[idx] = Math.abs(translationCurrX - mTranslationLastX);
+        mDeltaY[idx] = Math.abs(translationCurrY - mTranslationLastY);
+        mTotalTranslationX += mDeltaX[idx];
+        mTotalTranslationY += mDeltaY[idx];
+
+        // The panning rate is measured as the rate of the translation percentage in
+        // image width/height. Take the horizontal panning rate for example, the image width
+        // used in finding the translation is (PreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR).
+        // To get the horizontal translation percentage, the horizontal translation,
+        // (translationCurrX - mTranslationLastX), is divided by the
+        // image width. We then get the rate by dividing the translation percentage with the
+        // number of frames.
+        mPanningRateX = mTotalTranslationX /
+                (mPreviewWidth / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE;
+        mPanningRateY = mTotalTranslationY /
+                (mPreviewHeight / HR_TO_LR_DOWNSAMPLE_FACTOR) / WINDOW_SIZE;
+
+        mTranslationLastX = translationCurrX;
+        mTranslationLastY = translationCurrY;
+        mOldestIdx = (mOldestIdx + 1) % WINDOW_SIZE;
+    }
+}
diff --git a/src/com/android/camera/MosaicPreviewRenderer.java b/src/com/android/camera/MosaicPreviewRenderer.java
new file mode 100644
index 0000000..e8c02db
--- /dev/null
+++ b/src/com/android/camera/MosaicPreviewRenderer.java
@@ -0,0 +1,190 @@
+/*
+ * 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;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import com.android.camera.util.ApiHelper;
+
+import javax.microedition.khronos.opengles.GL10;
+
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture
+public class MosaicPreviewRenderer {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "CAM_MosaicPreviewRenderer";
+
+    private int mWidth; // width of the view in UI
+    private int mHeight; // height of the view in UI
+
+    private boolean mIsLandscape = true;
+    private final float[] mTransformMatrix = new float[16];
+
+    private ConditionVariable mEglThreadBlockVar = new ConditionVariable();
+    private HandlerThread mEglThread;
+    private MyHandler mHandler;
+    private SurfaceTextureRenderer mSTRenderer;
+
+    private SurfaceTexture mInputSurfaceTexture;
+
+    private class MyHandler extends Handler {
+        public static final int MSG_INIT_SYNC = 0;
+        public static final int MSG_SHOW_PREVIEW_FRAME_SYNC = 1;
+        public static final int MSG_SHOW_PREVIEW_FRAME = 2;
+        public static final int MSG_ALIGN_FRAME_SYNC = 3;
+        public static final int MSG_RELEASE = 4;
+
+        public MyHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_INIT_SYNC:
+                    doInit();
+                    mEglThreadBlockVar.open();
+                    break;
+                case MSG_SHOW_PREVIEW_FRAME_SYNC:
+                    doShowPreviewFrame();
+                    mEglThreadBlockVar.open();
+                    break;
+                case MSG_SHOW_PREVIEW_FRAME:
+                    doShowPreviewFrame();
+                    break;
+                case MSG_ALIGN_FRAME_SYNC:
+                    doAlignFrame();
+                    mEglThreadBlockVar.open();
+                    break;
+                case MSG_RELEASE:
+                    doRelease();
+                    mEglThreadBlockVar.open();
+                    break;
+            }
+        }
+
+        private void doAlignFrame() {
+            mInputSurfaceTexture.updateTexImage();
+            mInputSurfaceTexture.getTransformMatrix(mTransformMatrix);
+
+            MosaicRenderer.setWarping(true);
+            // Call preprocess to render it to low-res and high-res RGB textures.
+            MosaicRenderer.preprocess(mTransformMatrix);
+            // Now, transfer the textures from GPU to CPU memory for processing
+            MosaicRenderer.transferGPUtoCPU();
+            MosaicRenderer.updateMatrix();
+            MosaicRenderer.step();
+        }
+
+        private void doShowPreviewFrame() {
+            mInputSurfaceTexture.updateTexImage();
+            mInputSurfaceTexture.getTransformMatrix(mTransformMatrix);
+
+            MosaicRenderer.setWarping(false);
+            // Call preprocess to render it to low-res and high-res RGB textures.
+            MosaicRenderer.preprocess(mTransformMatrix);
+            MosaicRenderer.updateMatrix();
+            MosaicRenderer.step();
+        }
+
+        private void doInit() {
+            mInputSurfaceTexture = new SurfaceTexture(MosaicRenderer.init());
+            MosaicRenderer.reset(mWidth, mHeight, mIsLandscape);
+        }
+
+        private void doRelease() {
+            releaseSurfaceTexture(mInputSurfaceTexture);
+            mEglThread.quit();
+        }
+
+        @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+        private void releaseSurfaceTexture(SurfaceTexture st) {
+            if (ApiHelper.HAS_RELEASE_SURFACE_TEXTURE) {
+                st.release();
+            }
+        }
+
+        // Should be called from other thread.
+        public void sendMessageSync(int msg) {
+            mEglThreadBlockVar.close();
+            sendEmptyMessage(msg);
+            mEglThreadBlockVar.block();
+        }
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param tex The {@link SurfaceTexture} for the final UI output.
+     * @param w The width of the UI view.
+     * @param h The height of the UI view.
+     * @param isLandscape The UI orientation. {@code true} if in landscape,
+     *                    false if in portrait.
+     */
+    public MosaicPreviewRenderer(SurfaceTexture tex, int w, int h, boolean isLandscape) {
+        mIsLandscape = isLandscape;
+
+        mEglThread = new HandlerThread("PanoramaRealtimeRenderer");
+        mEglThread.start();
+        mHandler = new MyHandler(mEglThread.getLooper());
+        mWidth = w;
+        mHeight = h;
+
+        SurfaceTextureRenderer.FrameDrawer dummy = new SurfaceTextureRenderer.FrameDrawer() {
+            @Override
+            public void onDrawFrame(GL10 gl) {
+                // nothing, we have our draw functions.
+            }
+        };
+        mSTRenderer = new SurfaceTextureRenderer(tex, mHandler, dummy);
+
+        // We need to sync this because the generation of surface texture for input is
+        // done here and the client will continue with the assumption that the
+        // generation is completed.
+        mHandler.sendMessageSync(MyHandler.MSG_INIT_SYNC);
+    }
+
+    public void release() {
+        mSTRenderer.release();
+        mHandler.sendMessageSync(MyHandler.MSG_RELEASE);
+    }
+
+    public void showPreviewFrameSync() {
+        mHandler.sendMessageSync(MyHandler.MSG_SHOW_PREVIEW_FRAME_SYNC);
+        mSTRenderer.draw(true);
+    }
+
+    public void showPreviewFrame() {
+        mHandler.sendEmptyMessage(MyHandler.MSG_SHOW_PREVIEW_FRAME);
+        mSTRenderer.draw(false);
+    }
+
+    public void alignFrameSync() {
+        mHandler.sendMessageSync(MyHandler.MSG_ALIGN_FRAME_SYNC);
+        mSTRenderer.draw(true);
+    }
+
+    public SurfaceTexture getInputSurfaceTexture() {
+        return mInputSurfaceTexture;
+    }
+}
diff --git a/src/com/android/camera/MosaicRenderer.java b/src/com/android/camera/MosaicRenderer.java
new file mode 100644
index 0000000..c50ca0d
--- /dev/null
+++ b/src/com/android/camera/MosaicRenderer.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * The Java interface to JNI calls regarding mosaic preview rendering.
+ *
+ */
+public class MosaicRenderer
+{
+     static
+     {
+         System.loadLibrary("jni_mosaic");
+     }
+
+     /**
+      * Function to be called in onSurfaceCreated() to initialize
+      * the GL context, load and link the shaders and create the
+      * program. Returns a texture ID to be used for SurfaceTexture.
+      *
+      * @return textureID the texture ID of the newly generated texture to
+      *          be assigned to the SurfaceTexture object.
+      */
+     public static native int init();
+
+     /**
+      * Pass the drawing surface's width and height to initialize the
+      * renderer viewports and FBO dimensions.
+      *
+      * @param width width of the drawing surface in pixels.
+      * @param height height of the drawing surface in pixels.
+      * @param isLandscapeOrientation is the orientation of the activity layout in landscape.
+      */
+     public static native void reset(int width, int height, boolean isLandscapeOrientation);
+
+     /**
+      * Calling this function will render the SurfaceTexture to a new 2D texture
+      * using the provided STMatrix.
+      *
+      * @param stMatrix texture coordinate transform matrix obtained from the
+      *        Surface texture
+      */
+     public static native void preprocess(float[] stMatrix);
+
+     /**
+      * This function calls glReadPixels to transfer both the low-res and high-res
+      * data from the GPU memory to the CPU memory for further processing by the
+      * mosaicing library.
+      */
+     public static native void transferGPUtoCPU();
+
+     /**
+      * Function to be called in onDrawFrame() to update the screen with
+      * the new frame data.
+      */
+     public static native void step();
+
+     /**
+      * Call this function when a new low-res frame has been processed by
+      * the mosaicing library. This will tell the renderer library to
+      * update its texture and warping transformation. Any calls to step()
+      * after this call will use the new image frame and transformation data.
+      */
+     public static native void updateMatrix();
+
+     /**
+      * This function allows toggling between showing the input image data
+      * (without applying any warp) and the warped image data. For running
+      * the renderer as a viewfinder, we set the flag to false. To see the
+      * preview mosaic, we set the flag to true.
+      *
+      * @param flag boolean flag to set the warping to true or false.
+      */
+     public static native void setWarping(boolean flag);
+}
diff --git a/src/com/android/camera/PanoProgressBar.java b/src/com/android/camera/PanoProgressBar.java
new file mode 100644
index 0000000..8dfb366
--- /dev/null
+++ b/src/com/android/camera/PanoProgressBar.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2011 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;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+class PanoProgressBar extends ImageView {
+    @SuppressWarnings("unused")
+    private static final String TAG = "PanoProgressBar";
+    public static final int DIRECTION_NONE = 0;
+    public static final int DIRECTION_LEFT = 1;
+    public static final int DIRECTION_RIGHT = 2;
+    private float mProgress = 0;
+    private float mMaxProgress = 0;
+    private float mLeftMostProgress = 0;
+    private float mRightMostProgress = 0;
+    private float mProgressOffset = 0;
+    private float mIndicatorWidth = 0;
+    private int mDirection = 0;
+    private final Paint mBackgroundPaint = new Paint();
+    private final Paint mDoneAreaPaint = new Paint();
+    private final Paint mIndicatorPaint = new Paint();
+    private float mWidth;
+    private float mHeight;
+    private RectF mDrawBounds;
+    private OnDirectionChangeListener mListener = null;
+
+    public interface OnDirectionChangeListener {
+        public void onDirectionChange(int direction);
+    }
+
+    public PanoProgressBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mDoneAreaPaint.setStyle(Paint.Style.FILL);
+        mDoneAreaPaint.setAlpha(0xff);
+
+        mBackgroundPaint.setStyle(Paint.Style.FILL);
+        mBackgroundPaint.setAlpha(0xff);
+
+        mIndicatorPaint.setStyle(Paint.Style.FILL);
+        mIndicatorPaint.setAlpha(0xff);
+
+        mDrawBounds = new RectF();
+    }
+
+    public void setOnDirectionChangeListener(OnDirectionChangeListener l) {
+        mListener = l;
+    }
+
+    private void setDirection(int direction) {
+        if (mDirection != direction) {
+            mDirection = direction;
+            if (mListener != null) {
+                mListener.onDirectionChange(mDirection);
+            }
+            invalidate();
+        }
+    }
+
+    public int getDirection() {
+        return mDirection;
+    }
+
+    @Override
+    public void setBackgroundColor(int color) {
+        mBackgroundPaint.setColor(color);
+        invalidate();
+    }
+
+    public void setDoneColor(int color) {
+        mDoneAreaPaint.setColor(color);
+        invalidate();
+    }
+
+    public void setIndicatorColor(int color) {
+        mIndicatorPaint.setColor(color);
+        invalidate();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        mWidth = w;
+        mHeight = h;
+        mDrawBounds.set(0, 0, mWidth, mHeight);
+    }
+
+    public void setMaxProgress(int progress) {
+        mMaxProgress = progress;
+    }
+
+    public void setIndicatorWidth(float w) {
+        mIndicatorWidth = w;
+        invalidate();
+    }
+
+    public void setRightIncreasing(boolean rightIncreasing) {
+        if (rightIncreasing) {
+            mLeftMostProgress = 0;
+            mRightMostProgress = 0;
+            mProgressOffset = 0;
+            setDirection(DIRECTION_RIGHT);
+        } else {
+            mLeftMostProgress = mWidth;
+            mRightMostProgress = mWidth;
+            mProgressOffset = mWidth;
+            setDirection(DIRECTION_LEFT);
+        }
+        invalidate();
+    }
+
+    public void setProgress(int progress) {
+        // The panning direction will be decided after user pan more than 10 degrees in one
+        // direction.
+        if (mDirection == DIRECTION_NONE) {
+            if (progress > 10) {
+                setRightIncreasing(true);
+            } else if (progress < -10) {
+                setRightIncreasing(false);
+            }
+        }
+        // mDirection might be modified by setRightIncreasing() above. Need to check again.
+        if (mDirection != DIRECTION_NONE) {
+            mProgress = progress * mWidth / mMaxProgress + mProgressOffset;
+            // value bounds.
+            mProgress = Math.min(mWidth, Math.max(0, mProgress));
+            if (mDirection == DIRECTION_RIGHT) {
+                // The right most progress is adjusted.
+                mRightMostProgress = Math.max(mRightMostProgress, mProgress);
+            }
+            if (mDirection == DIRECTION_LEFT) {
+                // The left most progress is adjusted.
+                mLeftMostProgress = Math.min(mLeftMostProgress, mProgress);
+            }
+            invalidate();
+        }
+    }
+
+    public void reset() {
+        mProgress = 0;
+        mProgressOffset = 0;
+        setDirection(DIRECTION_NONE);
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        // the background
+        canvas.drawRect(mDrawBounds, mBackgroundPaint);
+        if (mDirection != DIRECTION_NONE) {
+            // the progress area
+            canvas.drawRect(mLeftMostProgress, mDrawBounds.top, mRightMostProgress,
+                    mDrawBounds.bottom, mDoneAreaPaint);
+            // the indication bar
+            float l;
+            float r;
+            if (mDirection == DIRECTION_RIGHT) {
+                l = Math.max(mProgress - mIndicatorWidth, 0f);
+                r = mProgress;
+            } else {
+                l = mProgress;
+                r = Math.min(mProgress + mIndicatorWidth, mWidth);
+            }
+            canvas.drawRect(l, mDrawBounds.top, r, mDrawBounds.bottom, mIndicatorPaint);
+        }
+
+        // draw the mask image on the top for shaping.
+        super.onDraw(canvas);
+    }
+}
diff --git a/src/com/android/camera/PanoUtil.java b/src/com/android/camera/PanoUtil.java
new file mode 100644
index 0000000..e50eacc
--- /dev/null
+++ b/src/com/android/camera/PanoUtil.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 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;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class PanoUtil {
+    public static String createName(String format, long dateTaken) {
+        Date date = new Date(dateTaken);
+        SimpleDateFormat dateFormat = new SimpleDateFormat(format);
+        return dateFormat.format(date);
+    }
+
+    // TODO: Add comments about the range of these two arguments.
+    public static double calculateDifferenceBetweenAngles(double firstAngle,
+            double secondAngle) {
+        double difference1 = (secondAngle - firstAngle) % 360;
+        if (difference1 < 0) {
+            difference1 += 360;
+        }
+
+        double difference2 = (firstAngle - secondAngle) % 360;
+        if (difference2 < 0) {
+            difference2 += 360;
+        }
+
+        return Math.min(difference1, difference2);
+    }
+
+    public static void decodeYUV420SPQuarterRes(int[] rgb, byte[] yuv420sp, int width, int height) {
+        final int frameSize = width * height;
+
+        for (int j = 0, ypd = 0; j < height; j += 4) {
+            int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
+            for (int i = 0; i < width; i += 4, ypd++) {
+                int y = (0xff & (yuv420sp[j * width + i])) - 16;
+                if (y < 0) {
+                    y = 0;
+                }
+                if ((i & 1) == 0) {
+                    v = (0xff & yuv420sp[uvp++]) - 128;
+                    u = (0xff & yuv420sp[uvp++]) - 128;
+                    uvp += 2;  // Skip the UV values for the 4 pixels skipped in between
+                }
+                int y1192 = 1192 * y;
+                int r = (y1192 + 1634 * v);
+                int g = (y1192 - 833 * v - 400 * u);
+                int b = (y1192 + 2066 * u);
+
+                if (r < 0) {
+                    r = 0;
+                } else if (r > 262143) {
+                    r = 262143;
+                }
+                if (g < 0) {
+                    g = 0;
+                } else if (g > 262143) {
+                    g = 262143;
+                }
+                if (b < 0) {
+                    b = 0;
+                } else if (b > 262143) {
+                    b = 262143;
+                }
+
+                rgb[ypd] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) |
+                        ((b >> 10) & 0xff);
+            }
+        }
+    }
+}
diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java
index ea0037d..73840ab 100644
--- a/src/com/android/camera/PhotoUI.java
+++ b/src/com/android/camera/PhotoUI.java
@@ -26,10 +26,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.hardware.Camera;
 import android.hardware.Camera.Face;
-import android.hardware.Camera.Size;
 import android.os.AsyncTask;
-import android.os.Handler;
-import android.os.Message;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.TextureView;
@@ -45,12 +42,11 @@
 
 import com.android.camera.CameraPreference.OnPreferenceChangedListener;
 import com.android.camera.FocusOverlayManager.FocusUI;
+import com.android.camera.ui.ModuleSwitcher;
 import com.android.camera.util.ApiHelper;
 import com.android.camera.ui.AbstractSettingPopup;
 import com.android.camera.ui.CameraControls;
 import com.android.camera.ui.CameraRootView;
-import com.android.camera.ui.CameraSwitcher;
-import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
 import com.android.camera.ui.CountDownView;
 import com.android.camera.ui.CountDownView.OnCountDownFinishedListener;
 import com.android.camera.ui.FaceView;
@@ -92,7 +88,7 @@
 
     private View mMenuButton;
     private PhotoMenu mMenu;
-    private CameraSwitcher mSwitcher;
+    private ModuleSwitcher mSwitcher;
     private CameraControls mCameraControls;
     private AlertDialog mLocationDialog;
 
@@ -196,8 +192,8 @@
         initIndicators();
 
         mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
-        mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher);
-        mSwitcher.setCurrentIndex(CameraSwitcher.PHOTO_MODULE_INDEX);
+        mSwitcher = (ModuleSwitcher) mRootView.findViewById(R.id.camera_switcher);
+        mSwitcher.setCurrentIndex(ModuleSwitcher.PHOTO_MODULE_INDEX);
         mSwitcher.setSwitchListener(mActivity);
         mMenuButton = mRootView.findViewById(R.id.menu);
         if (ApiHelper.HAS_FACE_DETECTION) {
diff --git a/src/com/android/camera/PreviewFrameLayout.java b/src/com/android/camera/PreviewFrameLayout.java
deleted file mode 100644
index 2bdace6..0000000
--- a/src/com/android/camera/PreviewFrameLayout.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2009 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;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.RelativeLayout;
-import com.android.camera.util.ApiHelper;
-import com.android.camera.ui.LayoutChangeHelper;
-import com.android.camera.ui.LayoutChangeNotifier;
-import com.android.camera.util.CameraUtil;
-import com.android.camera2.R;
-
-/**
- * A layout which handles the preview aspect ratio.
- */
-public class PreviewFrameLayout extends RelativeLayout implements LayoutChangeNotifier {
-
-    private static final String TAG = "CAM_preview";
-
-    /** A callback to be invoked when the preview frame's size changes. */
-    public interface OnSizeChangedListener {
-        public void onSizeChanged(int width, int height);
-    }
-
-    private double mAspectRatio;
-    private View mBorder;
-    private OnSizeChangedListener mListener;
-    private LayoutChangeHelper mLayoutChangeHelper;
-
-    public PreviewFrameLayout(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        setAspectRatio(4.0 / 3.0);
-        mLayoutChangeHelper = new LayoutChangeHelper(this);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        mBorder = findViewById(R.id.preview_border);
-    }
-
-    public void setAspectRatio(double ratio) {
-        if (ratio <= 0.0) throw new IllegalArgumentException();
-
-        if (mAspectRatio != ratio) {
-            mAspectRatio = ratio;
-            requestLayout();
-        }
-    }
-
-    public void showBorder(boolean enabled) {
-        mBorder.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
-    }
-
-    public void fadeOutBorder() {
-        CameraUtil.fadeOut(mBorder);
-    }
-
-    @Override
-    protected void onMeasure(int widthSpec, int heightSpec) {
-        int previewWidth = MeasureSpec.getSize(widthSpec);
-        int previewHeight = MeasureSpec.getSize(heightSpec);
-
-        if (!ApiHelper.HAS_SURFACE_TEXTURE) {
-            // Get the padding of the border background.
-            int hPadding = getPaddingLeft() + getPaddingRight();
-            int vPadding = getPaddingTop() + getPaddingBottom();
-
-            // Resize the preview frame with correct aspect ratio.
-            previewWidth -= hPadding;
-            previewHeight -= vPadding;
-
-            boolean widthLonger = previewWidth > previewHeight;
-            int longSide = (widthLonger ? previewWidth : previewHeight);
-            int shortSide = (widthLonger ? previewHeight : previewWidth);
-            if (longSide > shortSide * mAspectRatio) {
-                longSide = (int) ((double) shortSide * mAspectRatio);
-            } else {
-                shortSide = (int) ((double) longSide / mAspectRatio);
-            }
-            if (widthLonger) {
-                previewWidth = longSide;
-                previewHeight = shortSide;
-            } else {
-                previewWidth = shortSide;
-                previewHeight = longSide;
-            }
-
-            // Add the padding of the border.
-            previewWidth += hPadding;
-            previewHeight += vPadding;
-        }
-
-        // Ask children to follow the new preview dimension.
-        super.onMeasure(MeasureSpec.makeMeasureSpec(previewWidth, MeasureSpec.EXACTLY),
-                MeasureSpec.makeMeasureSpec(previewHeight, MeasureSpec.EXACTLY));
-    }
-
-    public void setOnSizeChangedListener(OnSizeChangedListener listener) {
-        mListener = listener;
-    }
-
-    @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        if (mListener != null) mListener.onSizeChanged(w, h);
-    }
-
-    @Override
-    public void setOnLayoutChangeListener(
-            LayoutChangeNotifier.Listener listener) {
-        mLayoutChangeHelper.setOnLayoutChangeListener(listener);
-    }
-
-    @SuppressLint("WrongCall")
-	@Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-        // TODO: Suspicious call!
-        mLayoutChangeHelper.onLayout(changed, l, t, r, b);
-    }
-}
diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java
index ad42b1a..88a7b58 100644
--- a/src/com/android/camera/VideoUI.java
+++ b/src/com/android/camera/VideoUI.java
@@ -36,7 +36,6 @@
 import android.view.View.OnClickListener;
 import android.view.View.OnLayoutChangeListener;
 import android.view.ViewGroup;
-import android.widget.FrameLayout;
 import android.widget.FrameLayout.LayoutParams;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -47,8 +46,7 @@
 import com.android.camera.ui.AbstractSettingPopup;
 import com.android.camera.ui.CameraControls;
 import com.android.camera.ui.CameraRootView;
-import com.android.camera.ui.CameraSwitcher;
-import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.ModuleSwitcher;
 import com.android.camera.ui.PieRenderer;
 import com.android.camera.ui.RenderOverlay;
 import com.android.camera.ui.RotateLayout;
@@ -74,7 +72,7 @@
     private View mReviewDoneButton;
     private View mReviewPlayButton;
     private ShutterButton mShutterButton;
-    private CameraSwitcher mSwitcher;
+    private ModuleSwitcher mSwitcher;
     private TextView mRecordingTimeView;
     private LinearLayout mLabelsLinearLayout;
     private View mTimeLapseLabel;
@@ -172,9 +170,9 @@
         ((CameraRootView) mRootView).setDisplayChangeListener(this);
         mFlashOverlay = mRootView.findViewById(R.id.flash_overlay);
         mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
-        mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher);
-        mSwitcher.setCurrentIndex(CameraSwitcher.VIDEO_MODULE_INDEX);
-        mSwitcher.setSwitchListener((CameraSwitchListener) mActivity);
+        mSwitcher = (ModuleSwitcher) mRootView.findViewById(R.id.camera_switcher);
+        mSwitcher.setCurrentIndex(ModuleSwitcher.VIDEO_MODULE_INDEX);
+        mSwitcher.setSwitchListener(mActivity);
         initializeMiscControls();
         initializeControlByIntent();
         initializeOverlay();
diff --git a/src/com/android/camera/WideAnglePanoramaController.java b/src/com/android/camera/WideAnglePanoramaController.java
new file mode 100644
index 0000000..6ac7c51
--- /dev/null
+++ b/src/com/android/camera/WideAnglePanoramaController.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * The interface that controls the wide angle panorama module.
+ */
+public interface WideAnglePanoramaController {
+
+    public void onPreviewUIReady();
+
+    public void onPreviewUIDestroyed();
+
+    public void cancelHighResStitching();
+
+    public void onShutterButtonClick();
+
+    public void onPreviewUILayoutChange(int l, int t, int r, int b);
+}
diff --git a/src/com/android/camera/WideAnglePanoramaModule.java b/src/com/android/camera/WideAnglePanoramaModule.java
new file mode 100644
index 0000000..1756e4c
--- /dev/null
+++ b/src/com/android/camera/WideAnglePanoramaModule.java
@@ -0,0 +1,1071 @@
+/*
+ * 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;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.graphics.YuvImage;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.location.Location;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.OrientationEventListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.app.OrientationManager;
+import com.android.camera.exif.ExifInterface;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.util.CameraUtil;
+import com.android.camera.util.UsageStatistics;
+import com.android.camera2.R;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.TimeZone;
+
+/**
+ * Activity to handle panorama capturing.
+ */
+public class WideAnglePanoramaModule
+        implements CameraModule, WideAnglePanoramaController,
+        SurfaceTexture.OnFrameAvailableListener {
+
+    public static final int DEFAULT_SWEEP_ANGLE = 160;
+    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
+    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
+
+    private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
+    private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2;
+    private static final int MSG_END_DIALOG_RESET_TO_PREVIEW = 3;
+    private static final int MSG_CLEAR_SCREEN_DELAY = 4;
+    private static final int MSG_RESET_TO_PREVIEW = 5;
+
+    private static final int SCREEN_DELAY = 2 * 60 * 1000;
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "CAM_WidePanoModule";
+    private static final int PREVIEW_STOPPED = 0;
+    private static final int PREVIEW_ACTIVE = 1;
+    public static final int CAPTURE_STATE_VIEWFINDER = 0;
+    public static final int CAPTURE_STATE_MOSAIC = 1;
+
+    // The unit of speed is degrees per frame.
+    private static final float PANNING_SPEED_THRESHOLD = 2.5f;
+    private static final boolean DEBUG = false;
+
+    private ContentResolver mContentResolver;
+    private WideAnglePanoramaUI mUI;
+
+    private MosaicPreviewRenderer mMosaicPreviewRenderer;
+    private Object mRendererLock = new Object();
+    private Object mWaitObject = new Object();
+
+    private String mPreparePreviewString;
+    private String mDialogTitle;
+    private String mDialogOkString;
+    private String mDialogPanoramaFailedString;
+    private String mDialogWaitingPreviousString;
+
+    private int mPreviewUIWidth;
+    private int mPreviewUIHeight;
+    private boolean mUsingFrontCamera;
+    private int mCameraPreviewWidth;
+    private int mCameraPreviewHeight;
+    private int mCameraState;
+    private int mCaptureState;
+    private PowerManager.WakeLock mPartialWakeLock;
+    private MosaicFrameProcessor mMosaicFrameProcessor;
+    private boolean mMosaicFrameProcessorInitialized;
+    private AsyncTask <Void, Void, Void> mWaitProcessorTask;
+    private long mTimeTaken;
+    private Handler mMainHandler;
+    private SurfaceTexture mCameraTexture;
+    private boolean mThreadRunning;
+    private boolean mCancelComputation;
+    private float mHorizontalViewAngle;
+    private float mVerticalViewAngle;
+
+    // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
+    // getting a better image quality by the former.
+    private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
+
+    private PanoOrientationEventListener mOrientationEventListener;
+    // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
+    // respectively.
+    private int mDeviceOrientation;
+    private int mDeviceOrientationAtCapture;
+    private int mCameraOrientation;
+    private int mOrientationCompensation;
+
+    private SoundClips.Player mSoundPlayer;
+
+    private Runnable mOnFrameAvailableRunnable;
+
+    private CameraActivity mActivity;
+    private View mRootView;
+    private CameraProxy mCameraDevice;
+    private boolean mPaused;
+
+    private LocationManager mLocationManager;
+    private OrientationManager mOrientationManager;
+    private ComboPreferences mPreferences;
+    private boolean mMosaicPreviewConfigured;
+
+    @Override
+    public void onPreviewUIReady() {
+        configMosaicPreview();
+    }
+
+    @Override
+    public void onPreviewUIDestroyed() {
+
+    }
+
+    private class MosaicJpeg {
+        public MosaicJpeg(byte[] data, int width, int height) {
+            this.data = data;
+            this.width = width;
+            this.height = height;
+            this.isValid = true;
+        }
+
+        public MosaicJpeg() {
+            this.data = null;
+            this.width = 0;
+            this.height = 0;
+            this.isValid = false;
+        }
+
+        public final byte[] data;
+        public final int width;
+        public final int height;
+        public final boolean isValid;
+    }
+
+    private class PanoOrientationEventListener extends OrientationEventListener {
+        public PanoOrientationEventListener(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onOrientationChanged(int orientation) {
+            // 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;
+            mDeviceOrientation = CameraUtil.roundOrientation(orientation, mDeviceOrientation);
+            // When the screen is unlocked, display rotation may change. Always
+            // calculate the up-to-date orientationCompensation.
+            int orientationCompensation = mDeviceOrientation
+                    + CameraUtil.getDisplayRotation(mActivity) % 360;
+            if (mOrientationCompensation != orientationCompensation) {
+                mOrientationCompensation = orientationCompensation;
+            }
+        }
+    }
+
+    @Override
+    public void init(CameraActivity activity, View parent) {
+        mActivity = activity;
+        mRootView = parent;
+
+        mOrientationManager = new OrientationManager(activity);
+        mCaptureState = CAPTURE_STATE_VIEWFINDER;
+        mUI = new WideAnglePanoramaUI(mActivity, this, (ViewGroup) mRootView);
+        mUI.setCaptureProgressOnDirectionChangeListener(
+                new PanoProgressBar.OnDirectionChangeListener() {
+                    @Override
+                    public void onDirectionChange(int direction) {
+                        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
+                            mUI.showDirectionIndicators(direction);
+                        }
+                    }
+                });
+
+        mContentResolver = mActivity.getContentResolver();
+        // This runs in UI thread.
+        mOnFrameAvailableRunnable = new Runnable() {
+            @Override
+            public void run() {
+                // Frames might still be available after the activity is paused.
+                // If we call onFrameAvailable after pausing, the GL thread will crash.
+                if (mPaused) return;
+
+                MosaicPreviewRenderer renderer = null;
+                synchronized (mRendererLock) {
+                    if (mMosaicPreviewRenderer == null) {
+                        return;
+                    }
+                    renderer = mMosaicPreviewRenderer;
+                }
+                if (mRootView.getVisibility() != View.VISIBLE) {
+                    renderer.showPreviewFrameSync();
+                    mRootView.setVisibility(View.VISIBLE);
+                } else {
+                    if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
+                        renderer.showPreviewFrame();
+                    } else {
+                        renderer.alignFrameSync();
+                        mMosaicFrameProcessor.processFrame();
+                    }
+                }
+            }
+        };
+
+        PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
+        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama");
+
+        mOrientationEventListener = new PanoOrientationEventListener(mActivity);
+
+        mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
+
+        Resources appRes = mActivity.getResources();
+        mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview);
+        mDialogTitle = appRes.getString(R.string.pano_dialog_title);
+        mDialogOkString = appRes.getString(R.string.dialog_ok);
+        mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed);
+        mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous);
+
+        mPreferences = new ComboPreferences(mActivity);
+        CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
+        mLocationManager = new LocationManager(mActivity, null);
+
+        mMainHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_LOW_RES_FINAL_MOSAIC_READY:
+                        onBackgroundThreadFinished();
+                        showFinalMosaic((Bitmap) msg.obj);
+                        saveHighResMosaic();
+                        break;
+                    case MSG_GENERATE_FINAL_MOSAIC_ERROR:
+                        onBackgroundThreadFinished();
+                        if (mPaused) {
+                            resetToPreviewIfPossible();
+                        } else {
+                            mUI.showAlertDialog(
+                                    mDialogTitle, mDialogPanoramaFailedString,
+                                    mDialogOkString, new Runnable() {
+                                @Override
+                                public void run() {
+                                    resetToPreviewIfPossible();
+                                }
+                            });
+                        }
+                        clearMosaicFrameProcessorIfNeeded();
+                        break;
+                    case MSG_END_DIALOG_RESET_TO_PREVIEW:
+                        onBackgroundThreadFinished();
+                        resetToPreviewIfPossible();
+                        clearMosaicFrameProcessorIfNeeded();
+                        break;
+                    case MSG_CLEAR_SCREEN_DELAY:
+                        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.
+                                FLAG_KEEP_SCREEN_ON);
+                        break;
+                    case MSG_RESET_TO_PREVIEW:
+                        resetToPreviewIfPossible();
+                        break;
+                }
+            }
+        };
+    }
+
+    @Override
+    public void onSwitchMode(boolean toCamera) {
+        if (toCamera) {
+            mUI.showUI();
+        } else {
+            mUI.hideUI();
+        }
+    }
+
+    private void setupCamera() throws CameraHardwareException, CameraDisabledException {
+        openCamera();
+        Parameters parameters = mCameraDevice.getParameters();
+        setupCaptureParams(parameters);
+        configureCamera(parameters);
+    }
+
+    private void releaseCamera() {
+        if (mCameraDevice != null) {
+            CameraHolder.instance().release();
+            mCameraDevice = null;
+            mCameraState = PREVIEW_STOPPED;
+        }
+    }
+
+    private void openCamera() throws CameraHardwareException, CameraDisabledException {
+        int cameraId = CameraHolder.instance().getBackCameraId();
+        // If there is no back camera, use the first camera. Camera id starts
+        // from 0. Currently if a camera is not back facing, it is front facing.
+        // This is also forward compatible if we have a new facing other than
+        // back or front in the future.
+        if (cameraId == -1) cameraId = 0;
+        mCameraDevice = CameraUtil.openCamera(mActivity, cameraId);
+        mCameraOrientation = CameraUtil.getCameraOrientation(cameraId);
+        if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true;
+    }
+
+    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
+            boolean needSmaller) {
+        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
+        boolean hasFound = false;
+        for (Size size : supportedSizes) {
+            int h = size.height;
+            int w = size.width;
+            // we only want 4:3 format.
+            int d = DEFAULT_CAPTURE_PIXELS - h * w;
+            if (needSmaller && d < 0) { // no bigger preview than 960x720.
+                continue;
+            }
+            if (need4To3 && (h * 4 != w * 3)) {
+                continue;
+            }
+            d = Math.abs(d);
+            if (d < pixelsDiff) {
+                mCameraPreviewWidth = w;
+                mCameraPreviewHeight = h;
+                pixelsDiff = d;
+                hasFound = true;
+            }
+        }
+        return hasFound;
+    }
+
+    private void setupCaptureParams(Parameters parameters) {
+        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
+        if (!findBestPreviewSize(supportedSizes, true, true)) {
+            Log.w(TAG, "No 4:3 ratio preview size supported.");
+            if (!findBestPreviewSize(supportedSizes, false, true)) {
+                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
+                findBestPreviewSize(supportedSizes, false, false);
+            }
+        }
+        Log.d(TAG, "camera preview h = "
+                    + mCameraPreviewHeight + " , w = " + mCameraPreviewWidth);
+        parameters.setPreviewSize(mCameraPreviewWidth, mCameraPreviewHeight);
+
+        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
+        int last = frameRates.size() - 1;
+        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
+        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
+        parameters.setPreviewFpsRange(minFps, maxFps);
+        Log.d(TAG, "preview fps: " + minFps + ", " + maxFps);
+
+        List<String> supportedFocusModes = parameters.getSupportedFocusModes();
+        if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
+            parameters.setFocusMode(mTargetFocusMode);
+        } else {
+            // Use the default focus mode and log a message
+            Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
+                  " becuase the mode is not supported.");
+        }
+
+        parameters.set(CameraUtil.RECORDING_HINT, CameraUtil.FALSE);
+
+        mHorizontalViewAngle = parameters.getHorizontalViewAngle();
+        mVerticalViewAngle =  parameters.getVerticalViewAngle();
+    }
+
+    public int getPreviewBufSize() {
+        PixelFormat pixelInfo = new PixelFormat();
+        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
+        // TODO: remove this extra 32 byte after the driver bug is fixed.
+        return (mCameraPreviewWidth * mCameraPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
+    }
+
+    private void configureCamera(Parameters parameters) {
+        mCameraDevice.setParameters(parameters);
+    }
+
+    /**
+     * Configures the preview renderer according to the dimension defined by
+     * {@code mPreviewUIWidth} and {@code mPreviewUIHeight}.
+     * Will stop the camera preview first.
+     */
+    private void configMosaicPreview() {
+        if (mPreviewUIWidth == 0 || mPreviewUIHeight == 0
+                || mUI.getSurfaceTexture() == null) {
+            return;
+        }
+
+        stopCameraPreview();
+        synchronized (mRendererLock) {
+            if (mMosaicPreviewRenderer != null) {
+                mMosaicPreviewRenderer.release();
+            }
+            mMosaicPreviewRenderer = null;
+        }
+        final boolean isLandscape =
+                (mActivity.getResources().getConfiguration().orientation ==
+                        Configuration.ORIENTATION_LANDSCAPE);
+
+        MosaicPreviewRenderer renderer = new MosaicPreviewRenderer(
+                mUI.getSurfaceTexture(),
+                mPreviewUIWidth, mPreviewUIHeight, isLandscape);
+        synchronized (mRendererLock) {
+            mMosaicPreviewRenderer = renderer;
+            mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture();
+
+            if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) {
+                mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
+            }
+            mRendererLock.notifyAll();
+        }
+        mMosaicPreviewConfigured = true;
+        resetToPreviewIfPossible();
+    }
+
+    /**
+     * Receives the layout change event from the preview area. So we can
+     * initialize the mosaic preview renderer.
+     */
+    @Override
+    public void onPreviewUILayoutChange(int l, int t, int r, int b) {
+        Log.d(TAG, "layout change: " + (r - l) + "/" + (b - t));
+        mPreviewUIWidth = r - l;
+        mPreviewUIHeight = b - t;
+        configMosaicPreview();
+    }
+
+    @Override
+    public void onFrameAvailable(SurfaceTexture surface) {
+        /* This function may be called by some random thread,
+         * so let's be safe and jump back to ui thread.
+         * No OpenGL calls can be done here. */
+        mActivity.runOnUiThread(mOnFrameAvailableRunnable);
+    }
+
+    public void startCapture() {
+        // Reset values so we can do this again.
+        mCancelComputation = false;
+        mTimeTaken = System.currentTimeMillis();
+        mActivity.setSwipingEnabled(false);
+        mCaptureState = CAPTURE_STATE_MOSAIC;
+        mUI.onStartCapture();
+
+        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
+            @Override
+            public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
+                    float progressX, float progressY) {
+                float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
+                float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
+                if (isFinished
+                        || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
+                        || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
+                    stopCapture(false);
+                } else {
+                    float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
+                    float panningRateYInDegree = panningRateY * mVerticalViewAngle;
+                    mUI.updateCaptureProgress(panningRateXInDegree, panningRateYInDegree,
+                            accumulatedHorizontalAngle, accumulatedVerticalAngle,
+                            PANNING_SPEED_THRESHOLD);
+                }
+            }
+        });
+
+        mUI.resetCaptureProgress();
+        // TODO: calculate the indicator width according to different devices to reflect the actual
+        // angle of view of the camera device.
+        mUI.setMaxCaptureProgress(DEFAULT_SWEEP_ANGLE);
+        mUI.showCaptureProgress();
+        mDeviceOrientationAtCapture = mDeviceOrientation;
+        keepScreenOn();
+        // TODO: mActivity.getOrientationManager().lockOrientation();
+        mOrientationManager.lockOrientation();
+        int degrees = CameraUtil.getDisplayRotation(mActivity);
+        int cameraId = CameraHolder.instance().getBackCameraId();
+        int orientation = CameraUtil.getDisplayOrientation(degrees, cameraId);
+        mUI.setProgressOrientation(orientation);
+    }
+
+    private void stopCapture(boolean aborted) {
+        mCaptureState = CAPTURE_STATE_VIEWFINDER;
+        mUI.onStopCapture();
+
+        mMosaicFrameProcessor.setProgressListener(null);
+        stopCameraPreview();
+
+        mCameraTexture.setOnFrameAvailableListener(null);
+
+        if (!aborted && !mThreadRunning) {
+            mUI.showWaitingDialog(mPreparePreviewString);
+            // Hide shutter button, shutter icon, etc when waiting for
+            // panorama to stitch
+            mUI.hideUI();
+            runBackgroundThread(new Thread() {
+                @Override
+                public void run() {
+                    MosaicJpeg jpeg = generateFinalMosaic(false);
+
+                    if (jpeg != null && jpeg.isValid) {
+                        Bitmap bitmap = null;
+                        bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
+                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
+                                MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
+                    } else {
+                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
+                                MSG_END_DIALOG_RESET_TO_PREVIEW));
+                    }
+                }
+            });
+        }
+        keepScreenOnAwhile();
+    }
+
+    @Override
+    public void onShutterButtonClick() {
+        // If mCameraTexture == null then GL setup is not finished yet.
+        // No buttons can be pressed.
+        if (mPaused || mThreadRunning || mCameraTexture == null) return;
+        // Since this button will stay on the screen when capturing, we need to check the state
+        // right now.
+        switch (mCaptureState) {
+            case CAPTURE_STATE_VIEWFINDER:
+                if(mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) return;
+                mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
+                startCapture();
+                break;
+            case CAPTURE_STATE_MOSAIC:
+                mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
+                stopCapture(false);
+        }
+    }
+
+    public void reportProgress() {
+        mUI.resetSavingProgress();
+        Thread t = new Thread() {
+            @Override
+            public void run() {
+                while (mThreadRunning) {
+                    final int progress = mMosaicFrameProcessor.reportProgress(
+                            true, mCancelComputation);
+
+                    try {
+                        synchronized (mWaitObject) {
+                            mWaitObject.wait(50);
+                        }
+                    } catch (InterruptedException e) {
+                        throw new RuntimeException("Panorama reportProgress failed", e);
+                    }
+                    // Update the progress bar
+                    mActivity.runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            mUI.updateSavingProgress(progress);
+                        }
+                    });
+                }
+            }
+        };
+        t.start();
+    }
+
+    private int getCaptureOrientation() {
+        // The panorama image returned from the library is oriented based on the
+        // natural orientation of a camera. We need to set an orientation for the image
+        // in its EXIF header, so the image can be displayed correctly.
+        // The orientation is calculated from compensating the
+        // device orientation at capture and the camera orientation respective to
+        // the natural orientation of the device.
+        int orientation;
+        if (mUsingFrontCamera) {
+            // mCameraOrientation is negative with respect to the front facing camera.
+            // See document of android.hardware.Camera.Parameters.setRotation.
+            orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360;
+        } else {
+            orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
+        }
+        return orientation;
+    }
+
+    public void saveHighResMosaic() {
+        runBackgroundThread(new Thread() {
+            @Override
+            public void run() {
+                mPartialWakeLock.acquire();
+                MosaicJpeg jpeg;
+                try {
+                    jpeg = generateFinalMosaic(true);
+                } finally {
+                    mPartialWakeLock.release();
+                }
+
+                if (jpeg == null) {  // Cancelled by user.
+                    mMainHandler.sendEmptyMessage(MSG_END_DIALOG_RESET_TO_PREVIEW);
+                } else if (!jpeg.isValid) {  // Error when generating mosaic.
+                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
+                } else {
+                    int orientation = getCaptureOrientation();
+                    final Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
+                    if (uri != null) {
+                        mActivity.runOnUiThread(new Runnable() {
+                            @Override
+                            public void run() {
+                                mActivity.notifyNewMedia(uri);
+                            }
+                        });
+                    }
+                    mMainHandler.sendMessage(
+                            mMainHandler.obtainMessage(MSG_END_DIALOG_RESET_TO_PREVIEW));
+                }
+            }
+        });
+        reportProgress();
+    }
+
+    private void runBackgroundThread(Thread thread) {
+        mThreadRunning = true;
+        thread.start();
+    }
+
+    private void onBackgroundThreadFinished() {
+        mThreadRunning = false;
+        mUI.dismissAllDialogs();
+    }
+
+    private void cancelHighResComputation() {
+        mCancelComputation = true;
+        synchronized (mWaitObject) {
+            mWaitObject.notify();
+        }
+    }
+
+    // This function will be called upon the first camera frame is available.
+    private void reset() {
+        mCaptureState = CAPTURE_STATE_VIEWFINDER;
+
+        mOrientationManager.unlockOrientation();
+        mUI.reset();
+        mActivity.setSwipingEnabled(true);
+        // Orientation change will trigger onLayoutChange->configMosaicPreview->
+        // resetToPreview. Do not show the capture UI in film strip.
+        /* if (mActivity.mShowCameraAppView) {
+            mCaptureLayout.setVisibility(View.VISIBLE); */
+        mUI.showPreviewUI();
+        /*} else {
+        }*/
+        mMosaicFrameProcessor.reset();
+    }
+
+    private void resetToPreviewIfPossible() {
+        if (!mMosaicFrameProcessorInitialized
+                || mUI.getSurfaceTexture() == null
+                || !mMosaicPreviewConfigured) {
+            return;
+        }
+        reset();
+        if (!mPaused) startCameraPreview();
+    }
+
+    private void showFinalMosaic(Bitmap bitmap) {
+        mUI.showFinalMosaic(bitmap, getCaptureOrientation());
+    }
+
+    private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
+        if (jpegData != null) {
+            String filename = PanoUtil.createName(
+                    mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
+            String filepath = Storage.generateFilepath(filename);
+
+            Location loc = mLocationManager.getCurrentLocation();
+            ExifInterface exif = new ExifInterface();
+            try {
+                exif.readExif(jpegData);
+                exif.addGpsDateTimeStampTag(mTimeTaken);
+                exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, mTimeTaken,
+                        TimeZone.getDefault());
+                exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION,
+                        ExifInterface.getOrientationValueForRotation(orientation)));
+                writeLocation(loc, exif);
+                exif.writeExif(jpegData, filepath);
+            } catch (IOException e) {
+                Log.e(TAG, "Cannot set exif for " + filepath, e);
+                Storage.writeFile(filepath, jpegData);
+            }
+            int jpegLength = (int) (new File(filepath).length());
+            return Storage.addImage(mContentResolver, filename, mTimeTaken,
+                    loc, orientation, jpegLength, filepath, width, height);
+        }
+        return null;
+    }
+
+    private static void writeLocation(Location location, ExifInterface exif) {
+        if (location == null) {
+            return;
+        }
+        exif.addGpsTags(location.getLatitude(), location.getLongitude());
+        exif.setTag(exif.buildTag(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider()));
+    }
+
+    private void clearMosaicFrameProcessorIfNeeded() {
+        if (!mPaused || mThreadRunning) return;
+        // Only clear the processor if it is initialized by this activity
+        // instance. Other activity instances may be using it.
+        if (mMosaicFrameProcessorInitialized) {
+            mMosaicFrameProcessor.clear();
+            mMosaicFrameProcessorInitialized = false;
+        }
+    }
+
+    private void initMosaicFrameProcessorIfNeeded() {
+        if (mPaused || mThreadRunning) {
+            return;
+        }
+
+        mMosaicFrameProcessor.initialize(
+                mCameraPreviewWidth, mCameraPreviewHeight, getPreviewBufSize());
+        mMosaicFrameProcessorInitialized = true;
+    }
+
+    @Override
+    public void onPauseBeforeSuper() {
+        mPaused = true;
+        if (mLocationManager != null) mLocationManager.recordLocation(false);
+    }
+
+    @Override
+    public void onPauseAfterSuper() {
+        mOrientationEventListener.disable();
+        if (mCameraDevice == null) {
+            // Camera open failed. Nothing should be done here.
+            return;
+        }
+        // Stop the capturing first.
+        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
+            stopCapture(true);
+            reset();
+        }
+
+        releaseCamera();
+        synchronized (mRendererLock) {
+            mCameraTexture = null;
+
+            // The preview renderer might not have a chance to be initialized
+            // before onPause().
+            if (mMosaicPreviewRenderer != null) {
+                mMosaicPreviewRenderer.release();
+                mMosaicPreviewRenderer = null;
+            }
+        }
+
+        clearMosaicFrameProcessorIfNeeded();
+        if (mWaitProcessorTask != null) {
+            mWaitProcessorTask.cancel(true);
+            mWaitProcessorTask = null;
+        }
+        resetScreenOn();
+        if (mSoundPlayer != null) {
+            mSoundPlayer.release();
+            mSoundPlayer = null;
+        }
+        System.gc();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        mUI.onConfigurationChanged(newConfig, mThreadRunning);
+    }
+
+    @Override
+    public void onOrientationChanged(int orientation) {
+    }
+
+    @Override
+    public void onResumeBeforeSuper() {
+        mPaused = false;
+    }
+
+    @Override
+    public void onResumeAfterSuper() {
+        mOrientationEventListener.enable();
+
+        mCaptureState = CAPTURE_STATE_VIEWFINDER;
+
+        try {
+            setupCamera();
+        } catch (CameraHardwareException e) {
+            CameraUtil.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+            return;
+        } catch (CameraDisabledException e) {
+            CameraUtil.showErrorAndFinish(mActivity, R.string.camera_disabled);
+            return;
+        }
+
+        // Set up sound playback for shutter button
+        mSoundPlayer = SoundClips.getPlayer(mActivity);
+
+        // Check if another panorama instance is using the mosaic frame processor.
+        mUI.dismissAllDialogs();
+        if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
+            mUI.showWaitingDialog(mDialogWaitingPreviousString);
+            // If stitching is still going on, make sure switcher and shutter button
+            // are not showing
+            mUI.hideUI();
+            mWaitProcessorTask = new WaitProcessorTask().execute();
+        } else {
+            // Camera must be initialized before MosaicFrameProcessor is
+            // initialized. The preview size has to be decided by camera device.
+            initMosaicFrameProcessorIfNeeded();
+            Point size = mUI.getPreviewAreaSize();
+            mPreviewUIWidth = size.x;
+            mPreviewUIHeight = size.y;
+            configMosaicPreview();
+        }
+        keepScreenOnAwhile();
+
+        // Initialize location service.
+        boolean recordLocation = RecordLocationPreference.get(mPreferences,
+                mContentResolver);
+        mLocationManager.recordLocation(recordLocation);
+
+        // Dismiss open menu if exists.
+        PopupManager.getInstance(mActivity).notifyShowPopup(null);
+        UsageStatistics.onContentViewChanged(
+                UsageStatistics.COMPONENT_CAMERA, "PanoramaModule");
+    }
+
+    /**
+     * Generate the final mosaic image.
+     *
+     * @param highRes flag to indicate whether we want to get a high-res version.
+     * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
+     *         process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
+     *         is an error in generating the final mosaic.
+     */
+    public MosaicJpeg generateFinalMosaic(boolean highRes) {
+        int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
+        if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
+            return null;
+        } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
+            return new MosaicJpeg();
+        }
+
+        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
+        if (imageData == null) {
+            Log.e(TAG, "getFinalMosaicNV21() returned null.");
+            return new MosaicJpeg();
+        }
+
+        int len = imageData.length - 8;
+        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
+                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
+        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
+                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
+        Log.d(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
+
+        if (width <= 0 || height <= 0) {
+            // TODO: pop up an error message indicating that the final result is not generated.
+            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
+                    height);
+            return new MosaicJpeg();
+        }
+
+        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
+        try {
+            out.close();
+        } catch (Exception e) {
+            Log.e(TAG, "Exception in storing final mosaic", e);
+            return new MosaicJpeg();
+        }
+        return new MosaicJpeg(out.toByteArray(), width, height);
+    }
+
+    private void startCameraPreview() {
+        if (mCameraDevice == null) {
+            // Camera open failed. Return.
+            return;
+        }
+
+        if (mUI.getSurfaceTexture() == null) {
+            // UI is not ready.
+            return;
+        }
+
+        // This works around a driver issue. startPreview may fail if
+        // stopPreview/setPreviewTexture/startPreview are called several times
+        // in a row. mCameraTexture can be null after pressing home during
+        // mosaic generation and coming back. Preview will be started later in
+        // onLayoutChange->configMosaicPreview. This also reduces the latency.
+        synchronized (mRendererLock) {
+            if (mCameraTexture == null) return;
+
+            // If we're previewing already, stop the preview first (this will
+            // blank the screen).
+            if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
+
+            // Set the display orientation to 0, so that the underlying mosaic
+            // library can always get undistorted mCameraPreviewWidth x mCameraPreviewHeight
+            // image data from SurfaceTexture.
+            mCameraDevice.setDisplayOrientation(0);
+
+            mCameraTexture.setOnFrameAvailableListener(this);
+            mCameraDevice.setPreviewTexture(mCameraTexture);
+        }
+        mCameraDevice.startPreview();
+        mCameraState = PREVIEW_ACTIVE;
+    }
+
+    private void stopCameraPreview() {
+        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
+            mCameraDevice.stopPreview();
+        }
+        mCameraState = PREVIEW_STOPPED;
+    }
+
+    @Override
+    public void onUserInteraction() {
+        if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
+    }
+
+    @Override
+    public boolean onBackPressed() {
+        // If panorama is generating low res or high res mosaic, ignore back
+        // key. So the activity will not be destroyed.
+        if (mThreadRunning) return true;
+        return false;
+    }
+
+    private void resetScreenOn() {
+        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private void keepScreenOnAwhile() {
+        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+    }
+
+    private void keepScreenOn() {
+        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
+        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    private class WaitProcessorTask extends AsyncTask<Void, Void, Void> {
+        @Override
+        protected Void doInBackground(Void... params) {
+            synchronized (mMosaicFrameProcessor) {
+                while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
+                    try {
+                        mMosaicFrameProcessor.wait();
+                    } catch (Exception e) {
+                        // ignore
+                    }
+                }
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            mWaitProcessorTask = null;
+            mUI.dismissAllDialogs();
+            // TODO (shkong): mGLRootView.setVisibility(View.VISIBLE);
+            initMosaicFrameProcessorIfNeeded();
+            Point size = mUI.getPreviewAreaSize();
+            mPreviewUIWidth = size.x;
+            mPreviewUIHeight = size.y;
+            configMosaicPreview();
+            resetToPreviewIfPossible();
+        }
+    }
+
+    @Override
+    public void cancelHighResStitching() {
+        if (mPaused || mCameraTexture == null) return;
+        cancelHighResComputation();
+    }
+
+    @Override
+    public void onStop() {
+    }
+
+    @Override
+    public void installIntentFilter() {
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+    }
+
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public void onSingleTapUp(View view, int x, int y) {
+    }
+
+    @Override
+    public void onPreviewTextureCopied() {
+    }
+
+    @Override
+    public void onCaptureTextureCopied() {
+    }
+
+    @Override
+    public boolean updateStorageHintOnResume() {
+        return false;
+    }
+
+    @Override
+    public void updateCameraAppView() {
+    }
+
+    @Override
+    public void onShowSwitcherPopup() {
+    }
+
+    @Override
+    public void onMediaSaveServiceConnected(MediaSaveService s) {
+        // do nothing.
+    }
+}
diff --git a/src/com/android/camera/WideAnglePanoramaUI.java b/src/com/android/camera/WideAnglePanoramaUI.java
new file mode 100644
index 0000000..0ce9d62
--- /dev/null
+++ b/src/com/android/camera/WideAnglePanoramaUI.java
@@ -0,0 +1,461 @@
+/*
+ * 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;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.camera.ui.CameraControls;
+import com.android.camera.ui.CameraRootView;
+import com.android.camera.ui.ModuleSwitcher;
+import com.android.camera2.R;
+
+/**
+ * The UI of {@link WideAnglePanoramaModule}.
+ */
+public class WideAnglePanoramaUI implements
+        TextureView.SurfaceTextureListener,
+        ShutterButton.OnShutterButtonListener,
+        View.OnLayoutChangeListener {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "CAM_WidePanoramaUI";
+
+    private CameraActivity mActivity;
+    private WideAnglePanoramaController mController;
+
+    private ViewGroup mRootView;
+    private ModuleSwitcher mSwitcher;
+    private ViewGroup mPanoLayout;
+    private FrameLayout mCaptureLayout;
+    private View mReviewLayout;
+    private ImageView mReview;
+    private View mPreviewBorder;
+    private View mLeftIndicator;
+    private View mRightIndicator;
+    private View mCaptureIndicator;
+    private PanoProgressBar mCaptureProgressBar;
+    private PanoProgressBar mSavingProgressBar;
+    private TextView mTooFastPrompt;
+    private TextureView mTextureView;
+    private ShutterButton mShutterButton;
+    private CameraControls mCameraControls;
+
+    private Matrix mProgressDirectionMatrix = new Matrix();
+    private float[] mProgressAngle = new float[2];
+
+    private DialogHelper mDialogHelper;
+
+    // Color definitions.
+    private int mIndicatorColor;
+    private int mIndicatorColorFast;
+    private int mReviewBackground;
+    private SurfaceTexture mSurfaceTexture;
+
+    /** Constructor. */
+    public WideAnglePanoramaUI(
+            CameraActivity activity,
+            WideAnglePanoramaController controller,
+            ViewGroup root) {
+        mActivity = activity;
+        mController = controller;
+        mRootView = root;
+
+        createContentView();
+        mSwitcher = (ModuleSwitcher) mRootView.findViewById(R.id.camera_switcher);
+        Log.v(TAG, "setting CurrentIndex:" + ModuleSwitcher.WIDE_ANGLE_PANO_MODULE_INDEX);
+        mSwitcher.setCurrentIndex(ModuleSwitcher.WIDE_ANGLE_PANO_MODULE_INDEX);
+        mSwitcher.setSwitchListener(mActivity);
+    }
+
+    public void onStartCapture() {
+        hideSwitcher();
+        mShutterButton.setImageResource(R.drawable.btn_shutter_recording);
+        mCaptureIndicator.setVisibility(View.VISIBLE);
+        showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
+    }
+
+    public void showPreviewUI() {
+        mCaptureLayout.setVisibility(View.VISIBLE);
+        showUI();
+    }
+
+    public void onStopCapture() {
+        mCaptureIndicator.setVisibility(View.INVISIBLE);
+        hideTooFastIndication();
+        hideDirectionIndicators();
+    }
+
+    public void hideSwitcher() {
+        mSwitcher.closePopup();
+        mSwitcher.setVisibility(View.INVISIBLE);
+    }
+
+    public void hideUI() {
+        hideSwitcher();
+        mCameraControls.setVisibility(View.INVISIBLE);
+    }
+
+    public void showUI() {
+        showSwitcher();
+        mCameraControls.setVisibility(View.VISIBLE);
+    }
+
+    public void showSwitcher() {
+        mSwitcher.setVisibility(View.VISIBLE);
+    }
+
+    public void setCaptureProgressOnDirectionChangeListener(
+            PanoProgressBar.OnDirectionChangeListener listener) {
+        mCaptureProgressBar.setOnDirectionChangeListener(listener);
+    }
+
+    public void resetCaptureProgress() {
+        mCaptureProgressBar.reset();
+    }
+
+    public void setMaxCaptureProgress(int max) {
+        mCaptureProgressBar.setMaxProgress(max);
+    }
+
+    public void showCaptureProgress() {
+        mCaptureProgressBar.setVisibility(View.VISIBLE);
+    }
+
+    public void updateCaptureProgress(
+            float panningRateXInDegree, float panningRateYInDegree,
+            float progressHorizontalAngle, float progressVerticalAngle,
+            float maxPanningSpeed) {
+
+        if ((Math.abs(panningRateXInDegree) > maxPanningSpeed)
+                || (Math.abs(panningRateYInDegree) > maxPanningSpeed)) {
+            showTooFastIndication();
+        } else {
+            hideTooFastIndication();
+        }
+
+        // progressHorizontalAngle and progressVerticalAngle are relative to the
+        // camera. Convert them to UI direction.
+        mProgressAngle[0] = progressHorizontalAngle;
+        mProgressAngle[1] = progressVerticalAngle;
+        mProgressDirectionMatrix.mapPoints(mProgressAngle);
+
+        int angleInMajorDirection =
+                (Math.abs(mProgressAngle[0]) > Math.abs(mProgressAngle[1]))
+                        ? (int) mProgressAngle[0]
+                        : (int) mProgressAngle[1];
+        mCaptureProgressBar.setProgress((angleInMajorDirection));
+    }
+
+    public void setProgressOrientation(int orientation) {
+        mProgressDirectionMatrix.reset();
+        mProgressDirectionMatrix.postRotate(orientation);
+    }
+
+    public void showDirectionIndicators(int direction) {
+        switch (direction) {
+            case PanoProgressBar.DIRECTION_NONE:
+                mLeftIndicator.setVisibility(View.VISIBLE);
+                mRightIndicator.setVisibility(View.VISIBLE);
+                break;
+            case PanoProgressBar.DIRECTION_LEFT:
+                mLeftIndicator.setVisibility(View.VISIBLE);
+                mRightIndicator.setVisibility(View.INVISIBLE);
+                break;
+            case PanoProgressBar.DIRECTION_RIGHT:
+                mLeftIndicator.setVisibility(View.INVISIBLE);
+                mRightIndicator.setVisibility(View.VISIBLE);
+                break;
+        }
+    }
+
+    public SurfaceTexture getSurfaceTexture() {
+        return mSurfaceTexture;
+    }
+
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) {
+        mSurfaceTexture = surfaceTexture;
+        mController.onPreviewUIReady();
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) {
+
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+        mController.onPreviewUIDestroyed();
+        mSurfaceTexture = null;
+        Log.d(TAG, "surfaceTexture is destroyed");
+        return true;
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+    }
+
+    private void hideDirectionIndicators() {
+        mLeftIndicator.setVisibility(View.INVISIBLE);
+        mRightIndicator.setVisibility(View.INVISIBLE);
+    }
+
+    public Point getPreviewAreaSize() {
+        return new Point(
+                mTextureView.getWidth(), mTextureView.getHeight());
+    }
+
+    public void reset() {
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
+        mReviewLayout.setVisibility(View.GONE);
+        mCaptureProgressBar.setVisibility(View.INVISIBLE);
+    }
+
+    public void showFinalMosaic(Bitmap bitmap, int orientation) {
+        if (bitmap != null && orientation != 0) {
+            Matrix rotateMatrix = new Matrix();
+            rotateMatrix.setRotate(orientation);
+            bitmap = Bitmap.createBitmap(
+                    bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
+                    rotateMatrix, false);
+        }
+
+        mReview.setImageBitmap(bitmap);
+        mCaptureLayout.setVisibility(View.GONE);
+        mReviewLayout.setVisibility(View.VISIBLE);
+    }
+
+    public void onConfigurationChanged(
+            Configuration newConfig, boolean threadRunning) {
+        Drawable lowResReview = null;
+        if (threadRunning) lowResReview = mReview.getDrawable();
+
+        // Change layout in response to configuration change
+        /* TODO (shkong):mCaptureLayout.setOrientation(
+                newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
+                        ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);*/
+        LayoutInflater inflater = (LayoutInflater)
+                mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        mPanoLayout.removeView(mReviewLayout);
+        inflater.inflate(R.layout.pano_module_review, mPanoLayout);
+
+        mPanoLayout.bringChildToFront(mCameraControls);
+        setViews(mActivity.getResources());
+        if (threadRunning) {
+            mReview.setImageDrawable(lowResReview);
+            mCaptureLayout.setVisibility(View.GONE);
+            mReviewLayout.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public void resetSavingProgress() {
+        mSavingProgressBar.reset();
+        mSavingProgressBar.setRightIncreasing(true);
+    }
+
+    public void updateSavingProgress(int progress) {
+        mSavingProgressBar.setProgress(progress);
+    }
+
+    @Override
+    public void onShutterButtonFocus(boolean pressed) {
+        // Do nothing.
+    }
+
+    @Override
+    public void onShutterButtonClick() {
+        mController.onShutterButtonClick();
+    }
+
+    @Override
+    public void onLayoutChange(
+            View v, int l, int t, int r, int b,
+            int oldl, int oldt, int oldr, int oldb) {
+        mController.onPreviewUILayoutChange(l, t, r, b);
+    }
+
+    public void showAlertDialog(
+            String title, String failedString,
+            String OKString, Runnable runnable) {
+        mDialogHelper.showAlertDialog(title, failedString, OKString, runnable);
+    }
+
+    public void showWaitingDialog(String title) {
+        mDialogHelper.showWaitingDialog(title);
+    }
+
+    public void dismissAllDialogs() {
+        mDialogHelper.dismissAll();
+    }
+
+    private void createContentView() {
+        LayoutInflater inflator = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        inflator.inflate(R.layout.panorama_module, mRootView, true);
+        mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout);
+        Resources appRes = mActivity.getResources();
+        mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
+        mReviewBackground = appRes.getColor(R.color.review_background);
+        mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
+        mDialogHelper = new DialogHelper();
+        setViews(appRes);
+    }
+
+    private void setViews(Resources appRes) {
+        mCaptureLayout = (FrameLayout) mRootView.findViewById(R.id.panorama_capture_layout);
+        // TODO (shkong): set display change listener properly.
+        ((CameraRootView) mRootView).setDisplayChangeListener(null);
+        mTextureView = (TextureView) mRootView.findViewById(R.id.pano_preview_textureview);
+        mTextureView.setSurfaceTextureListener(this);
+        mTextureView.addOnLayoutChangeListener(this);
+        mCameraControls = (CameraControls) mRootView.findViewById(R.id.camera_controls);
+        mCaptureProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar);
+        mCaptureProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
+        mCaptureProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
+        mCaptureProgressBar.setIndicatorColor(mIndicatorColor);
+        mCaptureProgressBar.setIndicatorWidth(20);
+
+        mPreviewBorder = mCaptureLayout.findViewById(R.id.pano_preview_area_border);
+
+        mLeftIndicator = mRootView.findViewById(R.id.pano_pan_left_indicator);
+        mRightIndicator = mRootView.findViewById(R.id.pano_pan_right_indicator);
+        mLeftIndicator.setEnabled(false);
+        mRightIndicator.setEnabled(false);
+        mTooFastPrompt = (TextView) mRootView.findViewById(R.id.pano_capture_too_fast_textview);
+
+        mSavingProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_saving_progress_bar);
+        mSavingProgressBar.setIndicatorWidth(0);
+        mSavingProgressBar.setMaxProgress(100);
+        mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
+        mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
+
+        mCaptureIndicator = mRootView.findViewById(R.id.pano_capture_indicator);
+
+        mReviewLayout = mRootView.findViewById(R.id.pano_review_layout);
+        mReview = (ImageView) mRootView.findViewById(R.id.pano_reviewarea);
+        mReview.setBackgroundColor(mReviewBackground);
+        View cancelButton = mRootView.findViewById(R.id.pano_review_cancel_button);
+        cancelButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View arg0) {
+                mController.cancelHighResStitching();
+            }
+        });
+
+        mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
+        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
+        mShutterButton.setOnShutterButtonListener(this);
+    }
+
+    private void showTooFastIndication() {
+        mTooFastPrompt.setVisibility(View.VISIBLE);
+        // The PreviewArea also contains the border for "too fast" indication.
+        mPreviewBorder.setVisibility(View.VISIBLE);
+        mCaptureProgressBar.setIndicatorColor(mIndicatorColorFast);
+        mLeftIndicator.setEnabled(true);
+        mRightIndicator.setEnabled(true);
+    }
+
+    private void hideTooFastIndication() {
+        mTooFastPrompt.setVisibility(View.GONE);
+        mPreviewBorder.setVisibility(View.INVISIBLE);
+        mCaptureProgressBar.setIndicatorColor(mIndicatorColor);
+        mLeftIndicator.setEnabled(false);
+        mRightIndicator.setEnabled(false);
+    }
+
+    private class DialogHelper {
+        private ProgressDialog mProgressDialog;
+        private AlertDialog mAlertDialog;
+
+        DialogHelper() {
+            mProgressDialog = null;
+            mAlertDialog = null;
+        }
+
+        public void dismissAll() {
+            if (mAlertDialog != null) {
+                mAlertDialog.dismiss();
+                mAlertDialog = null;
+            }
+            if (mProgressDialog != null) {
+                mProgressDialog.dismiss();
+                mProgressDialog = null;
+            }
+        }
+
+        public void showAlertDialog(
+                CharSequence title, CharSequence message,
+                CharSequence buttonMessage, final Runnable buttonRunnable) {
+            dismissAll();
+            mAlertDialog = (new AlertDialog.Builder(mActivity))
+                    .setTitle(title)
+                    .setMessage(message)
+                    .setNeutralButton(buttonMessage, new DialogInterface.OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialogInterface, int i) {
+                            buttonRunnable.run();
+                        }
+                    })
+                    .show();
+        }
+
+        public void showWaitingDialog(CharSequence message) {
+            dismissAll();
+            mProgressDialog = ProgressDialog.show(mActivity, null, message, true, false);
+        }
+    }
+
+    private static class FlipBitmapDrawable extends BitmapDrawable {
+
+        public FlipBitmapDrawable(Resources res, Bitmap bitmap) {
+            super(res, bitmap);
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            Rect bounds = getBounds();
+            int cx = bounds.centerX();
+            int cy = bounds.centerY();
+            canvas.save(Canvas.MATRIX_SAVE_FLAG);
+            canvas.rotate(180, cx, cy);
+            super.draw(canvas);
+            canvas.restore();
+        }
+    }
+}
diff --git a/src/com/android/camera/app/AppManagerFactory.java b/src/com/android/camera/app/AppManagerFactory.java
new file mode 100644
index 0000000..9c047aa
--- /dev/null
+++ b/src/com/android/camera/app/AppManagerFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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.app;
+
+import android.app.Application;
+import android.content.Context;
+
+/**
+ * A singleton class which provides application level utility
+ * classes.
+ */
+public class AppManagerFactory {
+
+    private static AppManagerFactory sFactory;
+
+    public static synchronized AppManagerFactory getInstance(Context ctx) {
+        if (sFactory == null) {
+            sFactory = new AppManagerFactory(ctx);
+        }
+        return sFactory;
+    }
+
+    private PanoramaStitchingManager mPanoramaStitchingManager;
+
+    /** No public constructor. */
+    private AppManagerFactory(Context ctx) {
+        mPanoramaStitchingManager = new PanoramaStitchingManager(ctx);
+    }
+
+    public PanoramaStitchingManager getPanoramaStitchingManager() {
+        return mPanoramaStitchingManager;
+    }
+}
diff --git a/src/com/android/camera/app/CameraApp.java b/src/com/android/camera/app/CameraApp.java
index e4da414..2e61fa9 100644
--- a/src/com/android/camera/app/CameraApp.java
+++ b/src/com/android/camera/app/CameraApp.java
@@ -28,3 +28,4 @@
         CameraUtil.initialize(this);
     }
 }
+
diff --git a/src/com/android/camera/app/OrientationManager.java b/src/com/android/camera/app/OrientationManager.java
index 412be30..7bf9242 100644
--- a/src/com/android/camera/app/OrientationManager.java
+++ b/src/com/android/camera/app/OrientationManager.java
@@ -1,5 +1,20 @@
-package com.android.camera.app;
+/*
+ * 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.app;
 
 import android.app.Activity;
 import android.content.ContentResolver;
@@ -13,8 +28,8 @@
 
 import com.android.camera.util.ApiHelper;
 
-public class OrientationManager implements OrientationSource {
-    private static final String TAG = "OrientationManager";
+public class OrientationManager {
+    private static final String TAG = "CAM_OrientationManager";
 
     // Orientation hysteresis amount used in rounding, in degrees
     private static final int ORIENTATION_HYSTERESIS = 5;
@@ -112,12 +127,10 @@
         }
     }
 
-    @Override
     public int getDisplayRotation() {
         return getDisplayRotation(mActivity);
     }
 
-    @Override
     public int getCompensation() {
         return 0;
     }
@@ -148,4 +161,4 @@
         }
         return 0;
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/camera/app/OrientationSource.java b/src/com/android/camera/app/OrientationSource.java
deleted file mode 100644
index 57dcfeb..0000000
--- a/src/com/android/camera/app/OrientationSource.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.android.camera.app;
-
-public interface OrientationSource {
-    public int getDisplayRotation();
-    public int getCompensation();
-}
diff --git a/src/com/android/camera/ui/CameraRootView.java b/src/com/android/camera/ui/CameraRootView.java
index 48f24e4..75d0842 100644
--- a/src/com/android/camera/ui/CameraRootView.java
+++ b/src/com/android/camera/ui/CameraRootView.java
@@ -77,7 +77,9 @@
 
                 @Override
                 public void onDisplayChanged(int arg0) {
-                    mListener.onDisplayChanged();
+                    if (mListener != null) {
+                        mListener.onDisplayChanged();
+                    }
                 }
 
                 @Override
diff --git a/src/com/android/camera/ui/LayoutChangeHelper.java b/src/com/android/camera/ui/LayoutChangeHelper.java
deleted file mode 100644
index ef4eb6a..0000000
--- a/src/com/android/camera/ui/LayoutChangeHelper.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.camera.ui;
-
-import android.view.View;
-
-public class LayoutChangeHelper implements LayoutChangeNotifier {
-    private LayoutChangeNotifier.Listener mListener;
-    private boolean mFirstTimeLayout;
-    private View mView;
-
-    public LayoutChangeHelper(View v) {
-        mView = v;
-        mFirstTimeLayout = true;
-    }
-
-    @Override
-    public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener) {
-        mListener = listener;
-    }
-
-    public void onLayout(boolean changed, int l, int t, int r, int b) {
-        if (mListener == null) return;
-        if (mFirstTimeLayout || changed) {
-            mFirstTimeLayout = false;
-            mListener.onLayoutChange(mView, l, t, r, b);
-        }
-    }
-}
diff --git a/src/com/android/camera/ui/LayoutChangeNotifier.java b/src/com/android/camera/ui/LayoutChangeNotifier.java
deleted file mode 100644
index 6261d34..0000000
--- a/src/com/android/camera/ui/LayoutChangeNotifier.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.camera.ui;
-
-import android.view.View;
-
-public interface LayoutChangeNotifier {
-    public interface Listener {
-        // Invoked only when the layout has changed or it is the first layout.
-        public void onLayoutChange(View v, int l, int t, int r, int b);
-    }
-
-    public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener);
-}
diff --git a/src/com/android/camera/ui/LayoutNotifyView.java b/src/com/android/camera/ui/LayoutNotifyView.java
deleted file mode 100644
index 6e118fc..0000000
--- a/src/com/android/camera/ui/LayoutNotifyView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.camera.ui;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-
-/*
- * Customized view to support onLayoutChange() at or before API 10.
- */
-public class LayoutNotifyView extends View implements LayoutChangeNotifier {
-    private LayoutChangeHelper mLayoutChangeHelper = new LayoutChangeHelper(this);
-
-    public LayoutNotifyView(Context context) {
-        super(context);
-    }
-
-    public LayoutNotifyView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    public void setOnLayoutChangeListener(
-            LayoutChangeNotifier.Listener listener) {
-        mLayoutChangeHelper.setOnLayoutChangeListener(listener);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-        mLayoutChangeHelper.onLayout(changed, l, t, r, b);
-    }
-}
diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/ModuleSwitcher.java
similarity index 93%
rename from src/com/android/camera/ui/CameraSwitcher.java
rename to src/com/android/camera/ui/ModuleSwitcher.java
index aaa9cda..5eb316c 100644
--- a/src/com/android/camera/ui/CameraSwitcher.java
+++ b/src/com/android/camera/ui/ModuleSwitcher.java
@@ -34,13 +34,13 @@
 import android.widget.FrameLayout.LayoutParams;
 import android.widget.LinearLayout;
 
+import com.android.camera.util.ApiHelper;
 import com.android.camera.util.CameraUtil;
 import com.android.camera.util.PhotoSphereHelper;
 import com.android.camera.util.UsageStatistics;
 import com.android.camera2.R;
-import com.android.camera.util.ApiHelper;
 
-public class CameraSwitcher extends RotateImageView
+public class ModuleSwitcher extends RotateImageView
         implements OnClickListener, OnTouchListener {
 
     @SuppressWarnings("unused")
@@ -49,20 +49,22 @@
 
     public static final int PHOTO_MODULE_INDEX = 0;
     public static final int VIDEO_MODULE_INDEX = 1;
-    public static final int LIGHTCYCLE_MODULE_INDEX = 2;
+    public static final int WIDE_ANGLE_PANO_MODULE_INDEX = 2;
+    public static final int LIGHTCYCLE_MODULE_INDEX = 3;
     private static final int[] DRAW_IDS = {
             R.drawable.ic_switch_camera,
             R.drawable.ic_switch_video,
+            R.drawable.ic_switch_pan,
             R.drawable.ic_switch_photosphere,
     };
 
-    public interface CameraSwitchListener {
-        public void onCameraSelected(int i);
+    public interface ModuleSwitchListener {
+        public void onModuleSelected(int i);
 
         public void onShowSwitcherPopup();
     }
 
-    private CameraSwitchListener mListener;
+    private ModuleSwitchListener mListener;
     private int mCurrentIndex;
     private int[] mModuleIds;
     private int[] mDrawIds;
@@ -79,12 +81,12 @@
     private AnimatorListener mHideAnimationListener;
     private AnimatorListener mShowAnimationListener;
 
-    public CameraSwitcher(Context context) {
+    public ModuleSwitcher(Context context) {
         super(context);
         init(context);
     }
 
-    public CameraSwitcher(Context context, AttributeSet attrs) {
+    public ModuleSwitcher(Context context, AttributeSet attrs) {
         super(context, attrs);
         init(context);
     }
@@ -126,7 +128,7 @@
         setImageResource(mDrawIds[i]);
     }
 
-    public void setSwitchListener(CameraSwitchListener l) {
+    public void setSwitchListener(ModuleSwitchListener l) {
         mListener = l;
     }
 
@@ -136,14 +138,14 @@
         mListener.onShowSwitcherPopup();
     }
 
-    private void onCameraSelected(int ix) {
+    private void onModuleSelected(int ix) {
         hidePopup();
         if ((ix != mCurrentIndex) && (mListener != null)) {
             UsageStatistics.onEvent("CameraModeSwitch", null, null);
             UsageStatistics.setPendingTransitionCause(
                     UsageStatistics.TRANSITION_MENU_TAP);
             setCurrentIndex(ix);
-            mListener.onCameraSelected(mModuleIds[ix]);
+            mListener.onModuleSelected(mModuleIds[ix]);
         }
     }
 
@@ -178,7 +180,7 @@
                 @Override
                 public void onClick(View v) {
                     if (showsPopup()) {
-                        onCameraSelected(index);
+                        onModuleSelected(index);
                     }
                 }
             });
@@ -191,6 +193,10 @@
                     item.setContentDescription(getContext().getResources().getString(
                             R.string.accessibility_switch_to_video));
                     break;
+                case R.drawable.ic_switch_pan:
+                    item.setContentDescription(getContext().getResources().getString(
+                            R.string.accessibility_switch_to_panorama));
+                    break;
                 case R.drawable.ic_switch_photosphere:
                     item.setContentDescription(getContext().getResources().getString(
                             R.string.accessibility_switch_to_photo_sphere));
diff --git a/src_pd/com/android/camera/PanoramaStitchingManager.java b/src_pd/com/android/camera/PanoramaStitchingManager.java
deleted file mode 100644
index 48f67ce..0000000
--- a/src_pd/com/android/camera/PanoramaStitchingManager.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.android.camera;
-
-import android.content.Context;
-import android.net.Uri;
-
-class PanoramaStitchingManager implements ImageTaskManager {
-
-    public PanoramaStitchingManager(Context ctx) {
-    }
-
-    @Override
-    public void addTaskListener(TaskListener l) {
-        // do nothing.
-    }
-
-    @Override
-    public void removeTaskListener(TaskListener l) {
-        // do nothing.
-    }
-
-    @Override
-    public int getTaskProgress(Uri uri) {
-        return -1;
-    }
-}
diff --git a/src_pd/com/android/camera/app/PanoramaStitchingManager.java b/src_pd/com/android/camera/app/PanoramaStitchingManager.java
new file mode 100644
index 0000000..9d6e796
--- /dev/null
+++ b/src_pd/com/android/camera/app/PanoramaStitchingManager.java
@@ -0,0 +1,43 @@
+/*
+ * 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.app;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.camera.ImageTaskManager;
+
+public class PanoramaStitchingManager implements ImageTaskManager {
+
+    public PanoramaStitchingManager(Context ctx) {
+    }
+
+    @Override
+    public void addTaskListener(TaskListener l) {
+        // do nothing.
+    }
+
+    @Override
+    public void removeTaskListener(TaskListener l) {
+        // do nothing.
+    }
+
+    @Override
+    public int getTaskProgress(Uri uri) {
+        return -1;
+    }
+}