(this);
+ private GLThread mGLThread;
+ private Renderer mRenderer;
+ private boolean mDetached;
+ private EGLConfigChooser mEGLConfigChooser;
+ private EGLContextFactory mEGLContextFactory;
+ private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
+ private GLWrapper mGLWrapper;
+ private int mDebugFlags;
+ private int mEGLContextClientVersion;
+ private boolean mPreserveEGLContextOnPause;
+ private boolean mPreserveGlThreadOnDetachedFromWindow;
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/CardboardLayout.java b/java_api/src/main/java/com/google/cardboard/sdk/CardboardLayout.java
new file mode 100644
index 00000000..26c8654f
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/CardboardLayout.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+
+/**
+ * This class holds a view which results from inflating {@code R.layout.ui_layer}. Provides an easy
+ * way to set and control the Cardboard layout. This class also includes some methods to set the
+ * visibility of the inner views (back and settings buttons, and alignment marker), making it
+ * suitable for both monocular and stereo layouts. Setters for the buttons' callbacks are also
+ * provided within this class.
+ */
+public class CardboardLayout {
+ private final Handler handler;
+ private final FrameLayout alignmentMarker;
+ private final ImageButton backButton;
+ private final ImageButton settingsButton;
+ private final View view;
+
+ /**
+ * Creates a new Cardboard Layout object.
+ *
+ * Inflates and adds the {@code R.layout.ui_layer}. It also loads all the inner views.
+ *
+ * @param[in] context Used to build a LayoutInflater and inflate the layout.
+ * @param[in] parent A ViewGroup to get the LayoutParams from. It won't be attached to it.
+ */
+ public CardboardLayout(Context context, ViewGroup parent) {
+ handler = new Handler(Looper.getMainLooper());
+
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = inflater.inflate(R.layout.cardboard_ui, parent, false);
+
+ alignmentMarker = (FrameLayout) view.findViewById(R.id.ui_alignment_marker);
+ backButton = (ImageButton) view.findViewById(R.id.ui_back_button);
+ settingsButton = (ImageButton) view.findViewById(R.id.ui_settings_button);
+ }
+
+ public View getView() {
+ return view;
+ }
+
+ /** Shows all widgets from the layout. */
+ public void showAll() {
+ showAlignmentMarker();
+ showBackButton();
+ showSettingsButton();
+ }
+
+ /** Hides all widgets from the layout. */
+ public void hideAll() {
+ hideAlignmentMarker();
+ hideBackButton();
+ hideSettingsButton();
+ }
+
+ /** Shows alignment marker widget. */
+ public void showAlignmentMarker() {
+ handler.post(
+ () -> {
+ alignmentMarker.setVisibility(View.VISIBLE);
+ alignmentMarker.bringToFront();
+ });
+ }
+
+ /** Hides alignment marker widget. */
+ public void hideAlignmentMarker() {
+ handler.post(() -> alignmentMarker.setVisibility(View.INVISIBLE));
+ }
+
+ /** Shows back button (X) widget. */
+ public void showBackButton() {
+ handler.post(
+ () -> {
+ backButton.setVisibility(View.VISIBLE);
+ backButton.bringToFront();
+ });
+ }
+
+ /** Hides back button (X) widget. */
+ public void hideBackButton() {
+ handler.post(() -> backButton.setVisibility(View.INVISIBLE));
+ }
+
+ /** Shows settings button (gear) widget. */
+ public void showSettingsButton() {
+ handler.post(
+ () -> {
+ settingsButton.setVisibility(View.VISIBLE);
+ settingsButton.bringToFront();
+ });
+ }
+
+ /** Hides settings button (gear) widget. */
+ public void hideSettingsButton() {
+ handler.post(() -> settingsButton.setVisibility(View.INVISIBLE));
+ }
+
+ /**
+ * Sets a {@code Runnable} as the back button event handler.
+ *
+ * @param[in] listener A {@code Runnable} to be executed when the back button is clicked.
+ */
+ public void setOnBackButtonClick(Runnable listener) {
+ backButton.setOnClickListener((View v) -> listener.run());
+ }
+
+ /**
+ * Sets a {@code Runnable} as the settings button event handler.
+ *
+ * @param[in] listener A {@code Runnable} to be executed when the settings button is clicked.
+ */
+ public void setOnSettingskButtonClick(Runnable listener) {
+ settingsButton.setOnClickListener((View v) -> listener.run());
+ }
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/CardboardView.java b/java_api/src/main/java/com/google/cardboard/sdk/CardboardView.java
new file mode 100644
index 00000000..f50bed9c
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/CardboardView.java
@@ -0,0 +1,1201 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import static android.opengl.GLU.gluErrorString;
+
+import android.content.Context;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+import android.opengl.GLSurfaceView.EGLWindowSurfaceFactory;
+import android.opengl.Matrix;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.MotionEvent;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import com.google.cardboard.sdk.nativetypes.EyeTextureDescription;
+import com.google.cardboard.sdk.nativetypes.EyeType;
+import com.google.cardboard.sdk.nativetypes.Mesh;
+import com.google.cardboard.sdk.utils.MathUtils;
+import java.nio.IntBuffer;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * Wrapper around {@code CardboardViewApi} and {@code GLSurfaceView}. Cardboard SDK provides stereo
+ * rendering only but this class offers stereo and monocular rendering seamlessly by reusing the
+ * same {@code GLSurfaceView}.
+ */
+public class CardboardView extends FrameLayout {
+ /** Defines an interface of an Eye, a useful bundle to the renderer when drawing. */
+ public static class Eye {
+ public static final int LEFT = EyeType.LEFT;
+ public static final int RIGHT = EyeType.RIGHT;
+ public static final int MONOCULAR = 2;
+
+ /** Default vertical FOV for monocular mode. */
+ static final float MONOCULAR_FOV_Y_DEGREES = 45.0f;
+
+ /**
+ * Holds the eye type, one value in the set {{@code LEFT}, {@code RIGHT}, {@code MONOCULAR}}.
+ */
+ private final int eyeType;
+
+ /** Holds a reference to {@code CardboardViewApi} to compute eye matrices. */
+ private final CardboardViewApi cardboardViewApi;
+
+ /** Holds the Transformation of the from the head after the head was rotated */
+ private final float[] eyeViewMatrix;
+
+ /** Holds {@code LensDistortion} eye from head matrix for a device */
+ private final float[] eyeFromHeadMatrix = new float[16];
+
+ /** Holds {@code LensDistortion} eye projection matrix for a device */
+ private final float[] projectionMatrix = new float[16];
+
+ public Eye(int eyeType, CardboardViewApi cardboardViewApi) {
+ this.eyeType = eyeType;
+ eyeViewMatrix = new float[16];
+ this.cardboardViewApi = cardboardViewApi;
+ resetEyeFromHeadMatrix();
+ }
+
+ public void resetEyeFromHeadMatrix() {
+ if (this.cardboardViewApi == null) {
+ return;
+ }
+
+ if (this.eyeType == MONOCULAR) {
+ this.cardboardViewApi.getEyeFromHeadMatrix(EyeType.LEFT, eyeFromHeadMatrix);
+ } else {
+ this.cardboardViewApi.getEyeFromHeadMatrix(this.eyeType, eyeFromHeadMatrix);
+ }
+ }
+
+ /**
+ * Gets the eye type.
+ *
+ * @return An integer describing the which eye this class represents. It can be one of {{@code
+ * LEFT}, {@code RIGHT}, {@code MONOCULAR}}.
+ */
+ public int getEyeType() {
+ return eyeType;
+ }
+
+ /**
+ * Gets the eye from head matrix.
+ *
+ * @return A float 4x4 matrix representing the eye from head matrix.
+ */
+ public float[] getEyeFromHead() {
+ return eyeFromHeadMatrix;
+ }
+
+ public void applyHeadView(float[] headView) {
+// resetEyeFromHeadMatrix();
+ Matrix.multiplyMM(eyeViewMatrix, 0, eyeFromHeadMatrix, 0, headView, 0);
+ }
+
+ /**
+ * Returns a matrix that transforms from the camera to the current eye. This transform is the
+ * multiplication of the head rotation and the eye from head transformation.
+ *
+ * @return A 4x4 column-major representing the transformation from the camera to this eye.
+ */
+ public float[] getEyeView() {
+ return eyeViewMatrix;
+ }
+
+ /**
+ * Gets the perspective matrix for the input z-axis clipping planes.
+ *
+ * @param[in] zNear Near clip plane z-axis coordinate.
+ * @param[in] zFar Far clip plane z-axis coordinate.
+ * @return A float 4x4 matrix representing the perspective matrix.
+ */
+ public float[] getPerspective(float zNear, float zFar) {
+ if (eyeType == MONOCULAR) {
+ computeMonocularPerspectiveMatrix(zNear, zFar);
+ } else {
+ cardboardViewApi.getEyeProjectionMatrix(eyeType, zNear, zFar, projectionMatrix);
+ }
+ return projectionMatrix;
+ }
+
+ /**
+ * Constructs a float array with device's field of view angles.
+ *
+ * @return A float array whose elements are "[left, right, bottom, top]" angles.
+ */
+ public float[] getFieldOfView() {
+ final float[] fov = new float[4];
+ fov[0] = cardboardViewApi.getFieldOfViewParamsLeft();
+ fov[1] = cardboardViewApi.getFieldOfViewParamsRight();
+ fov[2] = cardboardViewApi.getFieldOfViewParamsBottom();
+ fov[3] = cardboardViewApi.getFieldOfViewParamsTop();
+ return fov;
+ }
+
+ /**
+ * Computes in {@code projectionMatrix} the projection matrix for monocular rendering mode.
+ *
+ *
Screen size is extracted from {@code cardboardViewApi} and from it, the viewport aspect
+ * ratio is derived. A fixed vertical angular span is used in conjunction with the z-axis
+ * clipping planes.
+ *
+ * @param zNear Near clip plane z-axis coordinate.
+ * @param zFar Far clip plane z-axis coordinate.
+ */
+ private void computeMonocularPerspectiveMatrix(float zNear, float zFar) {
+ // For monocular rendering always use a fixed vertical FOV and calculate the horizontal FOV
+ // correspondingly using the aspect ratio of the screen.
+ float aspectRatio =
+ cardboardViewApi.getScreenParamsWidth()
+ / (float) cardboardViewApi.getScreenParamsHeight();
+ Matrix.perspectiveM(projectionMatrix, 0, MONOCULAR_FOV_Y_DEGREES, aspectRatio, zNear, zFar);
+ }
+ }
+
+ /**
+ * Defines a rendering interface to be consumed by the {@code GLSurfaceView} when set.
+ *
+ *
Note: all callbacks will be executed in the rendering thread.
+ */
+ public interface Renderer {
+ /**
+ * Called when a new frame is about to be drawn.
+ *
+ *
Allows to differenciate between rendering eye views and different frames. Any per-frame
+ * operations not specific to a single view should happen here.
+ *
+ * @param headTransform The head transformation in the new frame. It is a 4x4 matrix.
+ */
+ public void onNewFrame(HeadTransform headTransform);
+
+ /**
+ * Requests to draw the contents from the point of view of an eye.
+ *
+ *
When monocular rendering mode is set, {@code onDrawEye()} will be called once per frame
+ * with monocular eye information. When stereo rendering mode is set, this method is called for
+ * the left eye first and then for the right eye.
+ *
+ * @param eye The eye type and matrix information.
+ */
+ public void onDrawEye(Eye eye);
+
+ /** Called before a frame is finished. */
+ public void onFinishFrame(Viewport viewport);
+
+ /**
+ * Called when there is a change in the surface dimensions.
+ *
+ * @param width New width of the surface for a single eye in pixels.
+ * @param height New height of the surface for a single eye in pixels.
+ */
+ public void onSurfaceChanged(int width, int height);
+
+ /**
+ * Called when the surface is created or recreated.
+ *
+ * @param config The EGL configuration used when creating the surface.
+ */
+ public void onSurfaceCreated(EGLConfig config);
+
+ /**
+ * Called on the renderer thread when the thread is shutting down.
+ *
+ *
Allows releasing GL resources and performing shutdown operations in the renderer thread.
+ * Called only if there was a previous call to {@link #onSurfaceCreated(EGLConfig)
+ * onSurfaceCreated}.
+ */
+ public void onRendererShutdown();
+ }
+
+ /** Aggregate type to hold eye specific stereo eye data. */
+ private static class EyeData {
+ /** Eye configuration. */
+ public final Eye eye;
+
+ /** Cardboard SDK eye textrure description. */
+ public final EyeTextureDescription textureDescription;
+
+ public EyeData(int eyeType, CardboardViewApi cardboardViewApi) {
+ eye = new Eye(eyeType, cardboardViewApi);
+ textureDescription = new EyeTextureDescription();
+ }
+ }
+
+ /**
+ * Interface of a safe surface view.
+ *
+ *
This interface is two fold: provides by means of the adapter pattern a common interface to
+ * interact with {@code CardboardGLSurfaceView} and {@code GLSurfaceView}, and subclasses them to
+ * call a shutdown runnable when detached from the window.
+ */
+ private static interface SafeSurfaceView {
+ /**
+ * The following list of methods are almost all that {@code GLView} offers. {@code
+ * GLSurfaceView} implements such interface, but {@code CardboardGLSurfaceView} doesn't.
+ */
+ public void setEGLContextClientVersion(int version);
+
+ public void setRenderer(android.opengl.GLSurfaceView.Renderer renderer);
+
+ public void setRenderMode(int mode);
+
+ public void onPause();
+
+ public void onResume();
+
+ public void setPreserveEGLContextOnPause(boolean preserveOnPause);
+
+ public void setOnTouchListener(android.view.View.OnTouchListener listener);
+
+ public void setLayoutParams(ViewGroup.LayoutParams glViewLayoutParams);
+ /** The following list of methods are part of the {@code GLSurfaceView} public API. */
+ public void setEGLConfigChooser(
+ int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize);
+
+ public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory);
+
+ public void queueEvent(Runnable r);
+ /**
+ * The following two methods conform the extended "safe" interface of the surface to execute a
+ * runnable when the view is detached.
+ */
+ public void setOnViewDetachedRunnable(Runnable onViewDetachedRunnable);
+
+ public boolean isViewAttached();
+
+ public void requestRender();
+ }
+
+ /**
+ * Implementation of {@code SafeSurfaceView} for {@code CardboardGLSurfaceView}.
+ *
+ *
Most of the methods forwards calls to {@code CardboardGLSurfaceView} implementation. The
+ * rest offers mechanisms to call a runnable when the view is detached from the window.
+ */
+ private static class SafeCardboardGLSurfaceView extends CardboardGLSurfaceView
+ implements SafeSurfaceView {
+ private boolean isViewAttached;
+ private Runnable onViewDetachedRunnable;
+
+ /**
+ * Constructs a SafeCardboardGLSurfaceView.
+ *
+ * @param[in] context Unused parameter within this class, required by parent class. It is
+ * generally an Activity instance or wraps one.
+ */
+ public SafeCardboardGLSurfaceView(Context context) {
+ super(context);
+ isViewAttached = false;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (onViewDetachedRunnable != null) {
+ onViewDetachedRunnable.run();
+ }
+ isViewAttached = false;
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ isViewAttached = true;
+ }
+
+ @Override
+ public void setOnViewDetachedRunnable(Runnable onViewDetachedRunnable) {
+ this.onViewDetachedRunnable = onViewDetachedRunnable;
+ }
+
+ @Override
+ public boolean isViewAttached() {
+ return isViewAttached;
+ }
+ }
+
+ /**
+ * Implementation of {@code SafeSurfaceView} for {@code GLSurfaceView}.
+ *
+ *
Most of the methods forwards calls to {@code GLSurfaceView} implementation. The rest offers
+ * mechanisms to call a runnable when the view is detached from the window.
+ */
+ private static class SafeGLSurfaceView extends GLSurfaceView implements SafeSurfaceView {
+ private boolean isViewAttached;
+ private Runnable onViewDetachedRunnable;
+
+ /**
+ * Constructs a SafeGLSurfaceView.
+ *
+ * @param[in] context Unused parameter within this class, required by parent class. It is
+ * generally an Activity instance or wraps one.
+ */
+ public SafeGLSurfaceView(Context context) {
+ super(context);
+ isViewAttached = false;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (onViewDetachedRunnable != null) {
+ onViewDetachedRunnable.run();
+ }
+ isViewAttached = false;
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ isViewAttached = true;
+ }
+
+ @Override
+ public void setOnViewDetachedRunnable(Runnable onViewDetachedRunnable) {
+ this.onViewDetachedRunnable = onViewDetachedRunnable;
+ }
+
+ @Override
+ public boolean isViewAttached() {
+ return isViewAttached;
+ }
+ }
+
+ /** Thread with its own Looper to serve Choreographer. */
+ private static class ChoreographerThread extends Thread {
+ private volatile Handler handler;
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ handler =
+ new Handler(Looper.myLooper()) {
+ @Override
+ public void handleMessage(Message msg) {}
+ };
+ Looper.loop();
+ }
+
+ /**
+ * Posts a message to Choreographer to request the rendering of @p view upon the next VSYNC
+ * callback.
+ *
+ * @param[in] view A SafeSurfaceView to request the rendering.
+ */
+ public synchronized void requestRenderOnVsyncEvent(final SafeSurfaceView view) {
+ if (handler != null) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ handler.post(
+ () -> Choreographer.getInstance().postFrameCallback((t) -> view.requestRender()));
+ } else {
+ handler.post(
+ () -> Choreographer.getInstance().postVsyncCallback((t) -> view.requestRender()));
+ }
+ }
+ }
+
+ /**
+ * When {@code handler} is not null, quits the Looper making it not possible to continue sending
+ * messages.
+ */
+ public synchronized void stopLooper() {
+ if (handler != null) {
+ handler.getLooper().quit();
+ handler = null;
+ }
+ }
+ }
+
+ private static final String TAG = CardboardView.class.getSimpleName();
+
+ /** Timeout in milliseconds to wait for the Choreographer thread to join. */
+ private static final int CHOREOGRAPHER_THREAD_JOIN_TIMEOUT_MS = 100;
+
+ /** Tracks when screen parameters changed. */
+ private boolean screenParamsChanged;
+
+ /** Tracks when device parameters changed. */
+ private boolean deviceParamsChanged;
+
+ /** Holds left eye specific data. */
+ private EyeData leftEyeData;
+
+ /** Holds right eye specific data. */
+ private EyeData rightEyeData;
+
+ /** Holds monocular eye specific data. */
+ private Eye monocularEye;
+
+ /** OpenGL depth render buffer. */
+ private final int[] depthRenderBuffer = new int[1];
+
+ /** OpenGL frame buffer. */
+ private final int[] framebuffer = new int[1];
+
+ /** OpenGL texture. */
+ private final int[] texture = new int[1];
+
+ /** Cardboard SDK API. */
+ private CardboardViewApi cardboardViewApi;
+
+ /** OpenGL Surface. */
+ private SafeSurfaceView glView;
+
+ /** Custom client renderer. */
+ private Renderer renderer;
+
+ /** Stereo rendering mode status. */
+ private boolean stereoRenderMode;
+
+ /** Cardboard layout manager. */
+ private CardboardLayout cardboardLayout;
+
+ /**
+ * Alias of {@code stereoRenderMode} updated when the frame is rendered. It avoids any change in
+ * the rendering mode while the frame is drawn.
+ */
+ private boolean currentStereoRenderMode;
+
+ private long lastLogTime;
+
+ /** Tracks whether a shutdown sequence has started. */
+ private volatile boolean shutdownCalled = false;
+
+ /** Tracks whether to use Gvr GlSurfaceView or Android's. */
+ private static boolean useCardboardGlSurfaceView = false;
+
+ /** Thread to execute Choreographer VSync callbacks. */
+ private ChoreographerThread choreographerThread;
+
+ /**
+ * Selects which GlSurfaceView to use.
+ *
+ *
Consumers of this class should call this class method before constructing an instance. By
+ * default, the Android GlSurfaceView will be preferred (wrapped with the SafeGlSurfaceView class)
+ * if this function is not called.
+ *
+ * @param[in] useCardboardGlSurfaceView True when Gvr GlSurfaceView must be used, else false.
+ */
+ public static void setUseCardboardGlSurfaceView(boolean useCardboardGlSurfaceView) {
+ CardboardView.useCardboardGlSurfaceView = useCardboardGlSurfaceView;
+ Log.w(TAG, "CardboardSV(" + useCardboardGlSurfaceView + ")");
+ }
+
+ /**
+ * Constructs a CardboardView.
+ *
+ * @param[in] context The current Context. It is generally an Activity instance or wraps one. The
+ * following is the list of methods that will be queried: getFilesDir(), getPackageManager(),
+ * getResources(), getSystemService(Context.WINDOW_SERVICE), startActivity(Intent),
+ * getContentResolver(), getDisplay().
+ */
+ public CardboardView(Context context) {
+ super(context);
+ Log.w(TAG, "CW(C)");
+ initialize(context);
+ }
+
+ /**
+ * Constructs a CardboardView.
+ *
+ * @param[in] context The current Context. It is generally an Activity instance or wraps one. The
+ * following is the list of methods that will be queried: getFilesDir(), getPackageManager(),
+ * getResources(), getSystemService(Context.WINDOW_SERVICE), startActivity(Intent),
+ * getContentResolver(), getDisplay().
+ * @param[in] attrs The custom AttributeSet.
+ */
+ public CardboardView(Context context, AttributeSet attributeSet) {
+ super(context, attributeSet);
+ Log.w(TAG, "CW(C,A)");
+ initialize(context);
+ }
+
+ /**
+ * Initializes a CardboardView.
+ *
+ *
Constructs the {@code CardboardViewApi} and the {@code GLSurfaceView}. For the latter, a
+ * custom {@code GLSurfaceView.Renderer} is created to forward calls to private methods that will
+ * handle the management of the GL resources and {@code CardboardViewApi}.
+ *
+ *
Monocular rendering is configured, meaning that {@code stereoRenderMode} is initialized with
+ * false.
+ *
+ * @param[in] context The current Context. It is generally an Activity instance or wraps one. See
+ * {@code CardboardView(Context)} for more details.
+ */
+ private void initialize(Context context) {
+ cardboardViewApi = new CardboardViewApi(context);
+
+ renderer = null;
+
+ // Sets and adds GLSurfaceView to CardboardView layout.
+ if (useCardboardGlSurfaceView) {
+ Log.w(TAG, "Using Cardboard Surface");
+ glView = new SafeCardboardGLSurfaceView(context);
+ } else {
+ Log.w(TAG, "Using Android Surface");
+ glView = new SafeGLSurfaceView(context);
+ }
+
+ glView.setEGLContextClientVersion(2);
+ // The following line makes the surface to preserve the context between resumed-paused state
+ // transitions.
+ // GvrView sets it which makes dependent and already integrated code assume that the Gl
+ // context will be preserved.
+ // When it is set to false (or default is used, i.e. false), client code will experiment calls
+ // to Renderer::onSurfaceCreated() after resuming the GlSurfaceView from a prior pause state.
+ // The callback is used to request a Gl context creation because the surface has been created
+ // for the first time or re-created.
+ //
+ // See b/186201897 to understand what this line could cause if not set.
+ // See {@code GlSurfaceView::setPreserveEGLContextOnPause()} for further details.
+ glView.setPreserveEGLContextOnPause(true);
+ ViewGroup.LayoutParams glViewLayoutParams =
+ new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ glView.setLayoutParams(glViewLayoutParams);
+ addView((SurfaceView) glView);
+
+ // Sets and adds CardboardLayout to CardboardView layout.
+ cardboardLayout = new CardboardLayout(context, this);
+ setStereoRenderMode(false);
+ screenParamsChanged = false;
+ deviceParamsChanged = false;
+
+ addView(cardboardLayout.getView());
+ }
+
+ /**
+ * Sets the renderer that will be called from {@code GLSurfaceView}'s callbacks.
+ *
+ * @param[in] renderer The CardboardView.Renderer implementation to be consumed by GLSurfaceView.
+ */
+ public void setRenderer(Renderer renderer) {
+ Log.w(TAG, "SR");
+ this.renderer = renderer;
+ lastLogTime = SystemClock.uptimeMillis();
+ glView.setRenderer(
+ new GLSurfaceView.Renderer() {
+ @Override
+ public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
+ CardboardView.this.onSurfaceCreated(eglConfig);
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl10, int width, int height) {
+ CardboardView.this.onSurfaceChanged(width, height);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl10) {
+ CardboardView.this.onDrawFrame();
+ if (!shutdownCalled && choreographerThread != null) {
+ choreographerThread.requestRenderOnVsyncEvent(glView);
+ }
+ }
+ });
+ glView.setRenderMode(CardboardGLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ choreographerThread = new ChoreographerThread();
+ choreographerThread.start();
+ choreographerThread.requestRenderOnVsyncEvent(glView);
+ }
+
+ /**
+ * Gets the enabled rendering mode.
+ *
+ * @return true When stereo rendering mode is enabled. Otherwise, monocular rendering is enabled.
+ */
+ public boolean isStereoRenderModeEnabled() {
+ return stereoRenderMode;
+ }
+
+ /**
+ * Sets the enabled rendering mode.
+ *
+ *
This method can be called from any thread. However, the change (if any) will be effective
+ * next time the {@code GLSurfaceView} in the rendering thead executes {@code onDrawFrame()}
+ * callback.
+ *
+ * @param[in] enable When true, stereo rendering mode will be enabled. Otherwise, monocular
+ * rendering becomes effective.
+ */
+ public void setStereoRenderMode(boolean enable) {
+ Log.w(TAG, "SSRMode(" + enable + ")");
+
+ stereoRenderMode = enable;
+ screenParamsChanged = true;
+
+ // Setups the layout according to the selected mode.
+ if (enable) {
+ cardboardLayout.showAll();
+ } else {
+ cardboardLayout.hideAll();
+ }
+ }
+
+ /**
+ * Pauses the {@code HeadTracker} module and the {@code GLSurfaceView}.
+ *
+ *
Generally, this method should be called from the Activity that contains it at {@code
+ * onPause()} callback.
+ *
+ *
Preconditions: Must not be called before a renderer has been set as it forwards the call to
+ * the {@code GLSurfaceView}.
+ */
+ public void onPause() {
+ Log.w(TAG, "OP");
+ cardboardViewApi.pauseHeadTracker();
+ glView.onPause();
+ }
+
+ /**
+ * Resumes the {@code HeadTracker} module and the {@code GLSurfaceView}.
+ *
+ *
Generally, this method should be called from the Activity that contains it at {@code
+ * onResume()} callback.
+ *
+ *
Preconditions: Must not be called before a renderer has been set as it forwards the call to
+ * the {@code GLSurfaceView}.
+ */
+ public void onResume() {
+ Log.w(TAG, "OR()");
+ // Parameters may have changed.
+ deviceParamsChanged = true;
+ glView.onResume();
+ cardboardViewApi.resumeHeadTracker();
+ }
+
+ /**
+ * Loads device parameter configuration. See {@code CardboardViewApi::loadDeviceParams()} for an
+ * in detail explanation of how and where parameters are obtained.
+ */
+ public void loadDeviceParams() {
+ Log.w(TAG, "LDP");
+ // Parameters may have changed.
+ deviceParamsChanged = true;
+ cardboardViewApi.loadDeviceParams();
+ }
+
+ /** Settings (gear) button callback. */
+ public void onSettingsButtonClick() {
+ Log.w(TAG, "OSBC");
+ cardboardViewApi.scanViewerQrCode();
+ }
+
+ /** Scans viewer parameters. */
+ public void scanViewerQrCode() {
+ Log.w(TAG, "SVQRC");
+ cardboardViewApi.scanViewerQrCode();
+ }
+
+ /**
+ * Closes all taken resources by {@code CardboardViewApi}.
+ *
+ *
Generally, this method should be called from the Activity that contains it at {@code
+ * onDestroy()} callback.
+ */
+ public void onDestroy() {
+ Log.w(TAG, "OD");
+ shutdownCalled();
+ if (choreographerThread != null) {
+ try {
+ choreographerThread.stopLooper();
+ choreographerThread.join(CHOREOGRAPHER_THREAD_JOIN_TIMEOUT_MS);
+ } catch (SecurityException e) {
+ Log.e(TAG, "OD-SE");
+ } catch (InterruptedException e) {
+ Log.e(TAG, "OD-IE");
+ } finally {
+ choreographerThread = null;
+ }
+ }
+ cardboardViewApi.close();
+ }
+
+ /** Forwards the call to the underlying GL view. */
+ public void setEGLConfigChooser(
+ int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) {
+ glView.setEGLConfigChooser(redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize);
+ }
+
+ /** Forwards the call to the underlying GL view. */
+ public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) {
+ glView.setEGLWindowSurfaceFactory(factory);
+ }
+
+ /** Returns whether the GL view is attached. */
+ public boolean isGlViewAttached() {
+ return glView.isViewAttached();
+ }
+
+ /** Sets a runnable that should be executed when the GL view is detached. */
+ public void setOnViewDetachedRunnable(Runnable onViewDetachedRunnable) {
+ Log.w(TAG, "SOVDR");
+ glView.setOnViewDetachedRunnable(onViewDetachedRunnable);
+ }
+
+ /**
+ * Sets a {@code Runnable} to be executed in {@code GLSurfaceView}'s GL Thread.
+ *
+ *
Preconditions: Must not be called before a renderer has been set as it forwards the call to
+ * the {@code GLSurfaceView}.
+ *
+ * @param[in] runnable Runnable to execute.
+ */
+ public void queueEvent(Runnable runnable) {
+ glView.queueEvent(runnable);
+ }
+
+ /**
+ * Sets a {@code Runnable} as the back button event handler.
+ *
+ * @param[in] listener A {@code Runnable} to be executed when the back button is clicked.
+ */
+ public void setOnBackButtonClick(Runnable listener) {
+ Log.w(TAG, "SOBBC");
+ cardboardLayout.setOnBackButtonClick(listener);
+ }
+
+ /**
+ * Sets a {@code Runnable} as the settings button event.
+ *
+ * @param[in] listener A {@code Runnable} to be executed when the settings button is clicked.
+ */
+ public void setOnSettingsButtonClick(Runnable listener) {
+ Log.w(TAG, "SOSBC");
+ cardboardLayout.setOnSettingskButtonClick(listener);
+ }
+
+ /**
+ * Sets a {@code Runnable} to be executed when the viewer's trigger is pressed (screen is
+ * touched).
+ *
+ * @param[in] runnable Runnable to execute.
+ */
+ public void setOnTriggerEvent(Runnable runnable) {
+ Log.w(TAG, "SOTE");
+ glView.setOnTouchListener(
+ (v, event) -> {
+ if (runnable != null && event.getAction() == MotionEvent.ACTION_DOWN) {
+ runnable.run();
+ v.performClick();
+ return true;
+ }
+ return false;
+ });
+ }
+
+ /**
+ * Flags from *any* thread that the shutdown sequence has started.
+ *
+ *
This method does not replace {@code CardboardView::onDestroy()} which must be still called
+ * to properly close resources. However, application code can use this flag to prevent {@code
+ * onSurfaceCreated()}, {@code onSurfaceChanged()} and {@code onDrawFrame()} callbacks from
+ * running next time the GlSurfaceView executes them before the enqueued run ({@code
+ * CardboardView::onDestroy()} is typically enqueued in the Gl Thread).
+ */
+ public void shutdownCalled() {
+ shutdownCalled = true;
+ }
+
+ /** Updates device parameters and GL resources if needed. */
+ private boolean updateDeviceParams() {
+ // Checks if screen or device parameters changed.
+ if (!screenParamsChanged && !deviceParamsChanged) {
+ return true;
+ }
+
+ // Turns down both flags. Note when they change:
+ // - screenParamsChanged will be set when the surface changes ({@code onSurfaceChange()}) or
+ // when the stereoscopic view changes ({@code setStereoRenderMode()}).
+ // - deviceParamsChanged will be set when the application resumes after a pause state
+ // ({@code onResume()}) under the assumption that parameters may have changed by other
+ // application (Android R or below) and when a request to {@code loadDeviceParams()} is
+ // performed.
+ screenParamsChanged = false;
+ deviceParamsChanged = false;
+
+ // Gets saved device parameters.
+ // Note: the following call does not touch {@code deviceParamsChange}.
+ cardboardViewApi.loadDeviceParams();
+
+ // When using monocular rendering, we must ensure to teardown previously configured GL resources
+ // for stereo rendering. Monocular rendering setup is trivial, so no configuration is needed.
+ if (!currentStereoRenderMode) {
+ glTeardown();
+ // Recreates the eye data for monocular rendering.
+ monocularEye = new Eye(Eye.MONOCULAR, cardboardViewApi);
+ return true;
+ }
+
+ // Gets distortion meshes.
+ Mesh leftMesh = cardboardViewApi.getDistortionMesh(EyeType.LEFT);
+ Mesh rightMesh = cardboardViewApi.getDistortionMesh(EyeType.RIGHT);
+
+ // Recreates the eye data.
+ leftEyeData = new EyeData(Eye.LEFT, cardboardViewApi);
+ rightEyeData = new EyeData(Eye.RIGHT, cardboardViewApi);
+
+ glSetup();
+
+ cardboardViewApi.initializeRenderThread();
+
+ // Sets distortion meshes.
+ cardboardViewApi.setMesh(leftMesh, EyeType.LEFT);
+ cardboardViewApi.setMesh(rightMesh, EyeType.RIGHT);
+
+ checkGlError("updateDeviceParams");
+
+ return true;
+ }
+
+ /**
+ * Setups GLES20 components.
+ *
+ *
Modifies the OpenGL global state. In particular: - glGet(GL_TEXTURE_BINDING_2D) -
+ * glGet(GL_FRAMEBUFFER_BINDING)
+ */
+ private void glSetup() {
+ if (framebuffer[0] != 0) {
+ glTeardown();
+ }
+
+ // Creates render texture.
+ GLES20.glGenTextures(1, IntBuffer.wrap(texture));
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexImage2D(
+ GLES20.GL_TEXTURE_2D,
+ 0,
+ GLES20.GL_RGB,
+ cardboardViewApi.getScreenParamsWidth(),
+ cardboardViewApi.getScreenParamsHeight(),
+ 0,
+ GLES20.GL_RGB,
+ GLES20.GL_UNSIGNED_BYTE,
+ null);
+
+ // Sets eyes texture description.
+ leftEyeData.textureDescription.texture = (long) texture[0];
+ leftEyeData.textureDescription.leftU = 0f;
+ leftEyeData.textureDescription.rightU = 0.5f;
+ leftEyeData.textureDescription.topV = 1f;
+ leftEyeData.textureDescription.bottomV = 0f;
+
+ rightEyeData.textureDescription.texture = (long) texture[0];
+ rightEyeData.textureDescription.leftU = 0.5f;
+ rightEyeData.textureDescription.rightU = 1f;
+ rightEyeData.textureDescription.topV = 1f;
+ rightEyeData.textureDescription.bottomV = 0f;
+
+ // Generate depth buffer to perform depth test.
+ GLES20.glGenRenderbuffers(1, IntBuffer.wrap(depthRenderBuffer));
+ GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, depthRenderBuffer[0]);
+ GLES20.glRenderbufferStorage(
+ GLES20.GL_RENDERBUFFER,
+ GLES20.GL_DEPTH_COMPONENT16,
+ cardboardViewApi.getScreenParamsWidth(),
+ cardboardViewApi.getScreenParamsHeight());
+ checkGlError("Create Render buffer");
+
+ // Creates the render target.
+ GLES20.glGenFramebuffers(1, IntBuffer.wrap(framebuffer));
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer[0]);
+ GLES20.glFramebufferTexture2D(
+ GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, texture[0], 0);
+ GLES20.glFramebufferRenderbuffer(
+ GLES20.GL_FRAMEBUFFER,
+ GLES20.GL_DEPTH_ATTACHMENT,
+ GLES20.GL_RENDERBUFFER,
+ depthRenderBuffer[0]);
+
+ checkGlError("glSetup");
+ }
+
+ /** Teardowns GLES20 components. */
+ private void glTeardown() {
+ if (framebuffer[0] == 0) {
+ return;
+ }
+
+ GLES20.glDeleteRenderbuffers(1, IntBuffer.wrap(depthRenderBuffer));
+ depthRenderBuffer[0] = 0;
+ GLES20.glDeleteFramebuffers(1, IntBuffer.wrap(framebuffer));
+ framebuffer[0] = 0;
+ GLES20.glDeleteTextures(1, IntBuffer.wrap(texture));
+ texture[0] = 0;
+
+ checkGlError("glTeardown");
+ }
+
+ /**
+ * Gets the pose matrix.
+ *
+ *
If the head tracker query results in a no-op because it is not initialized, this function
+ * will return an identity transform.
+ *
+ * @return 4x4 pose matrix as a float array.
+ */
+ private float[] getPose() {
+ float[] posePosition = {0.0f, 0.0f, 0.0f};
+ float[] poseOrientation = {0.0f, 0.0f, 0.0f, 1.0f};
+ cardboardViewApi.getPose(posePosition, poseOrientation);
+ return MathUtils.getMatrixFromPose(posePosition, poseOrientation);
+ }
+
+ /** Inner callback that just redirects to {@code renderer} the call. */
+ private void onSurfaceCreated(EGLConfig eglConfig) {
+ if (shutdownCalled) {
+ Log.w(TAG, "OSCRS");
+ return;
+ }
+ if (renderer != null) {
+ renderer.onSurfaceCreated(eglConfig);
+ }
+ }
+
+ /**
+ * Inner callback that updates {@code cardboardViewApi} screen size and forwards the call to the
+ * {@code renderer}.
+ *
+ * @param width New width of the surface in pixels.
+ * @param height New height of the surface in pixels.
+ */
+ private void onSurfaceChanged(int width, int height) {
+ Log.w(TAG, "OSC(" + width + ", " + height + ")");
+ if (shutdownCalled) {
+ Log.w(TAG, "OSCS");
+ return;
+ }
+ cardboardViewApi.setScreenParams(width, height);
+ screenParamsChanged = true;
+
+ if (renderer != null) {
+ try {
+ renderer.onSurfaceChanged(width, height);
+ } catch (Throwable throwable) {
+ errorLogCurrentState();
+ throw throwable;
+ }
+ }
+ }
+
+ /** Logs every at roughly 1Hz that the renderer is active and the selected renderer mode. */
+ private void logOnDrawFrame() {
+ final long currentTime = SystemClock.uptimeMillis();
+ if ((currentTime - lastLogTime) < 1000) {
+ return;
+ }
+ lastLogTime = currentTime;
+ Log.w(TAG, "DF");
+ }
+
+ /**
+ * Inner callback that manages the draw frame event.
+ *
+ *
Saves the value of {@code stereoRenderMode} in {@code currentStereoRenderMode} and uses the
+ * latter during the span of this method and preserve the rendering mode while this method is
+ * executed. Note that it can be changed from the GUI thread while this one runs on the rendering
+ * one.
+ *
+ *
Updates device parameters if necessary and retrieves the head pose to call {@code
+ * Renderer.onNewFrame()}. Then, the frame buffer is cleared, viewports are set and the client
+ * rendering via {@code Render.onDrawEye()} happens. Finally, {@code Renderer.onFinishFrame()} is
+ * called to free any GL resources.
+ *
+ *
When {@code currentStereoRenderMode} is true, left and right eyes are rendered using
+ * Cardboard SDK. Otherwise, calls to Open GL are done to clean the viewport and channels and only
+ * left eye is rendered.
+ */
+ private void onDrawFrame() {
+ if (shutdownCalled) {
+ Log.w(TAG, "DFS");
+ return;
+ }
+ // Guarantees that stereoRenderMode is preserved while onDrawFrame() is executed.
+ currentStereoRenderMode = stereoRenderMode;
+
+ logOnDrawFrame();
+
+ if (!updateDeviceParams()) {
+ return;
+ }
+
+ // Updates Head Pose.
+ if (renderer != null) {
+ try {
+ HeadTransform headTransform = new HeadTransform();
+ System.arraycopy(getPose(), 0, headTransform.getHeadView(), 0, 16);
+
+ renderer.onNewFrame(headTransform);
+ } catch (Throwable throwable) {
+ errorLogCurrentState();
+ throw throwable;
+ }
+ }
+
+ // Draws the frames.
+ if (currentStereoRenderMode) {
+ onRenderStereoFrame();
+ } else {
+ onRenderMonocularFrame();
+ }
+
+ // Finishes the frame.
+ if (renderer != null) {
+ try {
+ Viewport viewport = new Viewport();
+ viewport.setViewport(
+ 0,
+ 0,
+ cardboardViewApi.getScreenParamsWidth(),
+ cardboardViewApi.getScreenParamsHeight());
+ renderer.onFinishFrame(viewport);
+ } catch (Throwable throwable) {
+ errorLogCurrentState();
+ throw throwable;
+ }
+ }
+ checkGlError("onDrawFrame");
+ }
+
+ /**
+ * Renders left and right eye using Cardboard SDK.
+ *
+ *
When {@code currentStereoRenderMode} is false, it does nothing.
+ *
+ *
Modifies the OpenGL global state. In particular: - glGet(GL_FRAMEBUFFER_BINDING) -
+ * glGet(GL_VIEWPORT)
+ */
+ private void onRenderStereoFrame() {
+ if (!currentStereoRenderMode) {
+ return;
+ }
+
+ // Binds framebuffer.
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer[0]);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
+
+ // Draws eyes views.
+ if (renderer != null) {
+ GLES20.glViewport(
+ 0,
+ 0,
+ cardboardViewApi.getScreenParamsWidth() / 2,
+ cardboardViewApi.getScreenParamsHeight());
+ try {
+ renderer.onDrawEye(leftEyeData.eye);
+ } catch (Throwable throwable) {
+ errorLogCurrentState();
+ throw throwable;
+ }
+
+ GLES20.glViewport(
+ cardboardViewApi.getScreenParamsWidth() / 2,
+ 0,
+ cardboardViewApi.getScreenParamsWidth() / 2,
+ cardboardViewApi.getScreenParamsHeight());
+ try {
+ renderer.onDrawEye(rightEyeData.eye);
+ } catch (Throwable throwable) {
+ errorLogCurrentState();
+ throw throwable;
+ }
+ }
+
+ // Renders eye textures to display.
+ cardboardViewApi.renderEyeToDisplay(
+ leftEyeData.textureDescription, rightEyeData.textureDescription);
+ }
+
+ /**
+ * Renders left eye without any distortion.
+ *
+ *
When {@code currentStereoRenderMode} is true, it does nothing.
+ *
+ *
Modifies the OpenGL global state. In particular: - glGet(GL_CLEAR_COLOR_VALUE) -
+ * glGet(GL_VIEWPORT)
+ */
+ private void onRenderMonocularFrame() {
+ if (currentStereoRenderMode) {
+ return;
+ }
+
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
+ GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+
+ if (renderer != null) {
+ GLES20.glViewport(
+ 0, 0, cardboardViewApi.getScreenParamsWidth(), cardboardViewApi.getScreenParamsHeight());
+ try {
+ renderer.onDrawEye(monocularEye);
+ } catch (Throwable throwable) {
+ errorLogCurrentState();
+ throw throwable;
+ }
+ }
+ }
+
+ /**
+ * Checks GLES20.glGetError and fails quickly if the state isn't GL_NO_ERROR.
+ *
+ * @param label Label to report in case of error.
+ */
+ private static void checkGlError(String label) {
+ int error = GLES20.glGetError();
+ int lastError;
+ if (error != GLES20.GL_NO_ERROR) {
+ do {
+ lastError = error;
+ Log.w(TAG, label + ": glError " + gluErrorString(lastError));
+ error = GLES20.glGetError();
+ } while (error != GLES20.GL_NO_ERROR);
+
+ throw new RuntimeException("glError " + gluErrorString(lastError));
+ }
+ }
+
+ /** Logs the state of this class instance. */
+ private void errorLogCurrentState() {
+ Log.w(
+ TAG,
+ "screenParamsChanged: "
+ + screenParamsChanged
+ + " | deviceParamsChanged: "
+ + deviceParamsChanged
+ + " | stereoRenderMode: "
+ + stereoRenderMode
+ + " | currentStereoRenderMode: "
+ + currentStereoRenderMode
+ + " | renderer != null: "
+ + (renderer != null));
+ }
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/CardboardViewApi.java b/java_api/src/main/java/com/google/cardboard/sdk/CardboardViewApi.java
new file mode 100644
index 00000000..b3c01387
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/CardboardViewApi.java
@@ -0,0 +1,713 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.hardware.display.DisplayManager;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.Log;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+import androidx.core.app.ActivityCompat;
+import com.google.cardboard.proto.CardboardDevice;
+import com.google.cardboard.sdk.deviceparams.CardboardV1DeviceParams;
+import com.google.cardboard.sdk.deviceparams.DeviceParamsUtils;
+import com.google.cardboard.sdk.nativetypes.EyeTextureDescription;
+import com.google.cardboard.sdk.nativetypes.EyeType;
+import com.google.cardboard.sdk.nativetypes.Mesh;
+import com.google.cardboard.sdk.nativetypes.UvPoint;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Wrapper around {@code DistortionRenderer}, {@code HeadTracker}, {@code LensDistortion}, {@code
+ * QrCode} and {@code Initialize}.
+ */
+public class CardboardViewApi {
+ /**
+ * Enum that corresponds to the values of CardboardViewportOrientation in
+ * google3/third_party/cardboard_oss/sdk/include/cardboard.h
+ */
+ private enum CardboardViewportOrientation {
+ // Corresponds to CardboardViewportOrientation::kLandscapeLeft
+ LANDSCAPE_LEFT(0),
+ // Corresponds to CardboardViewportOrientation::kLandscapeRight
+ LANDSCAPE_RIGHT(1),
+ // Corresponds to CardboardViewportOrientation::kPortrait
+ PORTRAIT(2),
+ // Corresponds to CardboardViewportOrientation::kPortraitUpsideDown
+ PORTRAIT_REVERSED(3);
+
+ private final int value;
+
+ private CardboardViewportOrientation(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+ }
+
+ /** Holds the screen size information in pixels. */
+ public static class ScreenSize {
+ final int width;
+ final int height;
+
+ /**
+ * Constructs a ScreenSize.
+ *
+ * @param[in] width The screen width in pixels. It must be positive.
+ * @param[in] height The screen height in pixels. It must be positive.
+ */
+ ScreenSize(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+ }
+
+ /** Holds device's field of view. */
+ public static class FieldOfView {
+ final float left;
+ final float top;
+ final float right;
+ final float bottom;
+
+ /**
+ * Constructs a FieldOfView.
+ *
+ * @param[in] left The left angle in degrees of the field of view. It must be positive.
+ * @param[in] right The right angle in degrees of the field of view. It must be positive.
+ * @param[in] bottom The bottom angle in degrees of the field of view. It must be positive.
+ * @param[in] top The top angle in degrees of the field of view. It must be positive.
+ */
+ public FieldOfView(float left, float right, float bottom, float top) {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+ }
+
+ /**
+ * Default delta time in nanoseconds to get the future pose estimate from the {@code HeadTracker}.
+ */
+ private static final long PREDICTION_TIME_WITHOUT_VSYNC_NANOS = 50000000;
+
+ /** Default display value to forward to {@code DistortionRenderer}. */
+ private static final long NATIVE_TARGET_DISPLAY = 0;
+
+ /** Holds the screen size information. */
+ private ScreenSize screenSize;
+
+ /** Holds the {@code DistortionRenderer} object. */
+ private DistortionRenderer distortionRenderer;
+
+ /** Holds the {@code LensDistortion} object. */
+ private LensDistortion lensDistortion;
+
+ /** Holds the {@code HeadTracker} object. */
+ private HeadTracker headTracker;
+
+ /** Holds device's field of view. */
+ private FieldOfView fieldOfView;
+
+ /** Delta time in nanoseconds to obtain the pose from {@code HeadTracker}. */
+ private long deltaTimeInNs;
+
+ /** The current Context. It is generally an Activity instance or wraps one. */
+ private final Context context;
+
+ /** The current display. Used to get the display rotation. */
+ private final Display display;
+
+ /** The current display rotation. */
+ private int displayRotation;
+
+ /** The cached display rotation from the previous call to {@code getPose()}. */
+ private int cachedDisplayRotation;
+
+ /** The current device orientation. */
+ private CardboardViewportOrientation deviceOrientation;
+
+ private static final String CARDBOARD_CONFIGURE_ACTION =
+ "com.google.vrtoolkit.cardboard.CONFIGURE";
+
+ /** Tag for logging purposes. */
+ private static final String TAG = CardboardViewApi.class.getSimpleName();
+
+ /**
+ * Constructs a CardboardViewApi.
+ *
+ *
It initializes the JNI modules and loads the {@code HeadTracker}. For the pose estimation, a
+ * default positive delta time is assigned which leads to obtaining a future pose every time it is
+ * queried.
+ *
+ *
Consumers of this object should refer to {@code loadDeviceParams()} and {@code
+ * initializeRenderThread()} to fully initialize this object with the device (viewer) information.
+ *
+ * @param[in] context The current Context. It is generally an Activity instance or wraps one. The
+ * following is the list of methods that will be queried: getFilesDir(), getPackageManager(),
+ * getResources(), getSystemService(Context.WINDOW_SERVICE), startActivity(Intent),
+ * getContentResolver(), getDisplay().
+ */
+ @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16.
+ public CardboardViewApi(Context context) {
+ // Guarantees proper initialization of the JavaVM and Android context.
+ Initialize.initialize(context);
+
+ this.context = context;
+ distortionRenderer = null;
+ lensDistortion = null;
+ headTracker = new HeadTracker();
+
+ screenSize = new ScreenSize(0, 0);
+ fieldOfView = new FieldOfView(0f, 0f, 0f, 0f);
+
+ deltaTimeInNs = PREDICTION_TIME_WITHOUT_VSYNC_NANOS;
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+ DisplayManager displayManager =
+ (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ } else {
+ // This is necessary because our minimum API level is 16
+ display =
+ ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+ }
+ displayRotation = 0;
+ cachedDisplayRotation = -1;
+ deviceOrientation = CardboardViewportOrientation.LANDSCAPE_LEFT;
+ }
+
+ /**
+ * Releases resources taken at construct time and after initializing device parameters.
+ *
+ *
Preconditions: it must be called from the rendering thread.
+ */
+ public void close() {
+ if (distortionRenderer != null) {
+ distortionRenderer.close();
+ distortionRenderer = null;
+ }
+ if (lensDistortion != null) {
+ lensDistortion.close();
+ lensDistortion = null;
+ }
+ if (headTracker != null) {
+ headTracker.close();
+ headTracker = null;
+ }
+ }
+
+ /**
+ * Forwards a call to {@code close()} and is provided as a safe-guard in case it is not called.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ // TODO(b/146156526): Remove finalize() usage from Java API.
+ close();
+ super.finalize();
+ }
+
+ /**
+ * Sets current screen parameters.
+ *
+ * @param[in] width The screen width in pixels.
+ * @param[in] height The screen height in pixels.
+ */
+ public void setScreenParams(int width, int height) {
+ screenSize = new ScreenSize(width, height);
+ }
+
+ /**
+ * Getter of the configured width property of the screen parameters.
+ *
+ * @return The width in pixels of the screen.
+ */
+ public int getScreenParamsWidth() {
+ return screenSize.width;
+ }
+
+ /**
+ * Getter of the configured height property of the screen parameters.
+ *
+ * @return The height in pixels of the screen.
+ */
+ public int getScreenParamsHeight() {
+ return screenSize.height;
+ }
+
+ /**
+ * Getter of the device's field of view top angle.
+ *
+ * @return The device's field of view top angle
+ */
+ public float getFieldOfViewParamsTop() {
+ return fieldOfView.top;
+ }
+
+ /**
+ * Getter of the device's field of view left angle.
+ *
+ * @return The device's field of view left angle.
+ */
+ public float getFieldOfViewParamsLeft() {
+ return fieldOfView.left;
+ }
+
+ /**
+ * Getter of the device's field of view right angle.
+ *
+ * @return The device's field of view right angle.
+ */
+ public float getFieldOfViewParamsRight() {
+ return fieldOfView.right;
+ }
+
+ /**
+ * Getter of the device's field of view bottom angle.
+ *
+ * @return The device's field of view bottom angle.
+ */
+ public float getFieldOfViewParamsBottom() {
+ return fieldOfView.bottom;
+ }
+
+ /**
+ * Computes the eye from head matrix.
+ *
+ *
Preconditions: {@code LensDistortion} must be previously and successfully initialized. When
+ * it is unmet, a call to this function results in a no-op.
+ *
+ * @param[in] eye Desired eye.
+ * @param[out] eyeFromHeadMatrix 4x4 float eye from head matrix.
+ * @throws IllegalArgumentException When @p eye is not a valid eye type.
+ */
+ public void getEyeFromHeadMatrix(int eye, float[] eyeFromHeadMatrix) {
+ if (!isValidEyeType(eye)) {
+ throw new IllegalArgumentException("eye is invalid.");
+ }
+ if (lensDistortion == null) {
+ Log.w(TAG, "Tried to getEyeFromHeadMatrix but LensDistortion is not initialized.");
+ return;
+ }
+ lensDistortion.getEyeFromHeadMatrix(eye, eyeFromHeadMatrix);
+ }
+
+ /**
+ * Computes the projection matrix for an eye.
+ *
+ *
Preconditions: {@code LensDistortion} must be previously and successfully initialized. When
+ * it is unmet, a call to this function results in a no-op.
+ *
+ * @param[in] eye Desired eye.
+ * @param[in] zNear Near clip plane z-axis coordinate.
+ * @param[in] zFar Far clip plane z-axis coordinate.
+ * @param[out] projectionMatrix 4x4 float ideal projection matrix.
+ * @throws IllegalArgumentException When @p eye is not a valid eye type.
+ */
+ public void getEyeProjectionMatrix(int eye, float zNear, float zFar, float[] projectionMatrix) {
+ if (!isValidEyeType(eye)) {
+ throw new IllegalArgumentException("eye is invalid.");
+ }
+ if (lensDistortion == null) {
+ Log.w(TAG, "Tried to getEyeProjectionMatrix but LensDistortion is not initialized.");
+ return;
+ }
+ lensDistortion.getEyeProjectionMatrix(eye, zNear, zFar, projectionMatrix);
+ }
+
+ /**
+ * Computes the distortion {@code Mesh} for an eye.
+ *
+ *
Preconditions: {@code LensDistortion} must be previously and successfully initialized.
+ *
+ *
Important: The distorsion {@code Mesh} that is returned by this function becomes invalid if
+ * {@code LensDistortion} is destroyed.
+ *
+ * @param[in] eye Desired eye.
+ * @return The distortion {@code Mesh}.
+ * @throws IllegalStateException When this method is called before a successful initialization of
+ * {@code LensDistortion}.
+ */
+ public Mesh getDistortionMesh(int eye) {
+ if (lensDistortion == null) {
+ throw new IllegalStateException("LensDistortion has not been initialized yet.");
+ }
+ if (!isValidEyeType(eye)) {
+ throw new IllegalArgumentException("eye is invalid.");
+ }
+ return lensDistortion.getDistortionMesh(eye);
+ }
+
+ /**
+ * Undistorts a {@code UvPoint} for an eye.
+ *
+ *
Preconditions: {@code LensDistortion} must be previously and successfully initialized.
+ *
+ * @param[in] distortedUv A distorted {@code UvPoint} point to undistort.
+ * @param[in] eye Desired eye.
+ * @return An undistorted {@code UvPoint}.
+ * @throws IllegalStateException When this method is called before a successful initialization of
+ * {@code LensDistortion}.
+ */
+ UvPoint undistortedUvForDistortedUv(UvPoint distortedUv, int eye) {
+ if (lensDistortion == null) {
+ throw new IllegalStateException("LensDistortion has not been initialized yet.");
+ }
+ if (!isValidEyeType(eye)) {
+ throw new IllegalArgumentException("eye is invalid.");
+ }
+ return lensDistortion.undistortedUvForDistortedUv(distortedUv, eye);
+ }
+
+ /**
+ * Distorts a {@code UvPoint} for an eye.
+ *
+ *
Preconditions: {@code LensDistortion} must be previously and successfully initialized.
+ *
+ * @param[in] undistortedUv An undistorted {@code UvPoint} point to distort.
+ * @param[in] eye Desired eye.
+ * @return A distorted {@code UvPoint}.
+ * @throws IllegalStateException When this method is called before a successful initialization of
+ * {@code LensDistortion}.
+ */
+ UvPoint distortedUvForUndistortedUv(UvPoint undistortedUv, int eye) {
+ if (lensDistortion == null) {
+ throw new IllegalStateException("LensDistortion has not been initialized yet.");
+ }
+ if (!isValidEyeType(eye)) {
+ throw new IllegalArgumentException("eye is invalid.");
+ }
+ return lensDistortion.distortedUvForUndistortedUv(undistortedUv, eye);
+ }
+
+ /**
+ * Initializes all GL-related modules.
+ *
+ *
Preconditions: must be called from the rendering thread.
+ */
+ public void initializeRenderThread() {
+ if (distortionRenderer != null) {
+ distortionRenderer.close();
+ }
+ distortionRenderer = new DistortionRenderer();
+ }
+
+ /**
+ * Sets a {@code Mesh} for an eye.
+ *
+ *
Preconditions: {@code DistortionRenderer} must be previously and successfully initialized.
+ * When it is unment, a call to this function results in a no-op.
+ *
+ *
Preconditions: must be called from the rendering thread.
+ *
+ * @param[in] mesh The {@code Mesh} to set.
+ * @param[in] eye Desired eye.
+ */
+ public void setMesh(Mesh mesh, int eye) {
+ if (distortionRenderer == null) {
+ Log.w(TAG, "Tried to setMesh but DistortionRenderer is not initialized.");
+ return;
+ }
+ if (!isValidEyeType(eye)) {
+ throw new IllegalArgumentException("eye is invalid.");
+ }
+ distortionRenderer.setMesh(mesh, eye);
+ }
+
+ /**
+ * Renders left and right eye textures to display.
+ *
+ *
Preconditions: {@code DistortionRenderer} must be previously and successfully initialized.
+ * When it is unment, a call to this function results in a no-op.
+ *
+ *
Preconditions: must be called from the rendering thread.
+ *
+ * @param[in] leftEyeTextureDescription Left eye texture description.
+ * @param[in] rightEyeTextureDescription Right eye texture description.
+ */
+ public void renderEyeToDisplay(
+ EyeTextureDescription leftEyeTextureDescription,
+ EyeTextureDescription rightEyeTextureDescription) {
+ if (distortionRenderer == null) {
+ Log.w(TAG, "Tried to renderEyeToDisplay but DistortionRenderer is not initialized.");
+ return;
+ }
+ // TODO(b/156227563): Unhardcode rect information.
+ distortionRenderer.renderEyeToDisplay(
+ NATIVE_TARGET_DISPLAY,
+ 0,
+ 0,
+ screenSize.width,
+ screenSize.height,
+ leftEyeTextureDescription,
+ rightEyeTextureDescription);
+ }
+
+ /**
+ * Pauses the {@code HeadTracker} pose tracking.
+ *
+ *
Precondition: the {@code HeadTracker} is initialized. When it is unmet, a call to this
+ * function results in a no-op.
+ */
+ public void pauseHeadTracker() {
+ if (headTracker != null) {
+ headTracker.pause();
+ } else {
+ Log.w(TAG, "Tried to pause the HeadTracker but it is not initialized.");
+ }
+ }
+
+ /**
+ * Resumes the {@code HeadTracker} pose tracking.
+ *
+ *
Precondition: the {@code HeadTracker} is initialized. When it is unmet, a call to this
+ * function results in a no-op.
+ */
+ public void resumeHeadTracker() {
+ if (headTracker != null) {
+ headTracker.resume();
+ } else {
+ Log.w(TAG, "Tried to resume the HeadTracker but it is not initialized.");
+ }
+ }
+
+ /**
+ * Sets the delta time in nanoseconds used to compute the pose at some point in time with respect
+ * to the call time of {@code getPose()}.
+ */
+ public void setDeltaTimeInNs(long deltaTimeInNs) {
+ this.deltaTimeInNs = deltaTimeInNs;
+ }
+
+ /**
+ * Gets the value of {@code deltaTimeInNs}.
+ *
+ * @return The time in nanoseconds used to compute the pose at some point in time with respect to
+ * the call time of {@code getPose()}.
+ */
+ public long getDeltaTimeInNs() {
+ return deltaTimeInNs;
+ }
+
+ /**
+ * Gets the pose of the {@code HeadTracker}.
+ *
+ *
Precondition: the {@code HeadTracker} is initialized. When it is unmet, a call to this
+ * function results in a no-op.
+ *
+ * @param[out] position 3 floats for (x, y, z).
+ * @param[out] orientation 4 floats for quaternion.
+ */
+ public void getPose(float[] position, float[] orientation) {
+ displayRotation = display.getRotation();
+
+ if (cachedDisplayRotation != displayRotation) {
+ setDeviceOrientation(displayRotation);
+ }
+
+ if (headTracker != null) {
+ headTracker.getPose(deltaTimeInNs, deviceOrientation.getValue(), position, orientation);
+ } else {
+ Log.w(TAG, "Tried to get the pose from HeadTracker but it is not initialized.");
+ }
+ }
+
+ /**
+ * Checks for device parameters existence and initializes {@code LensDistortion} module if they
+ * exist.
+ *
+ *
Loading flow: - If the application is run in Android P or below, the READ_EXTERNAL_STORAGE
+ * permission is checked. If granted, parameters are read from external storage. - If the
+ * application is run in Android Q or above, device parameters are read from scoped storage. - If
+ * the previous step failed, it uses Cardboard V1 device parameters.
+ */
+ public void loadDeviceParams() {
+ byte[] deviceParams = null;
+
+ if ((VERSION.SDK_INT <= VERSION_CODES.P && isReadExternalStoragePermissionEnabled())
+ || VERSION.SDK_INT > VERSION_CODES.P) {
+ deviceParams = QrCode.getSavedDeviceParams();
+ }
+
+ if (deviceParams == null) {
+ deviceParams = QrCode.getCardboardV1DeviceParams();
+ }
+
+ initializeFieldOfView(deviceParams);
+ if (lensDistortion != null) {
+ lensDistortion.close();
+ }
+ lensDistortion = new LensDistortion(deviceParams, screenSize.width, screenSize.height);
+ }
+
+ /**
+ * Checks if a Cardboard viewer has previously been scanned and its parameters saved.
+ *
+ * @return true if a Cardboard viewer has previously been scanned and its parameters saved.
+ */
+ public boolean hasSavedDeviceParams() {
+ if (VERSION.SDK_INT <= VERSION_CODES.P && !isReadExternalStoragePermissionEnabled()) {
+ return false;
+ }
+ return QrCode.getSavedDeviceParams() != null;
+ }
+
+ /**
+ * Returns the device parameters of the Cardboard viewer.
+ *
+ * @return the device parameters of the Cardboard viewer.
+ */
+ public CardboardDevice.DeviceParams getSavedDeviceParams() {
+ return DeviceParamsUtils.parseCardboardDeviceParams(QrCode.getSavedDeviceParams());
+ }
+
+ /** Scans a Cardboard QR code from a viewer and saves the obtained parameters. */
+ public void scanViewerQrCode() {
+ QrCode.scanQrCodeAndSaveDeviceParams();
+ }
+
+ /** Launches VRCore Settings Activity. */
+ public void launchVrCoreSettingsActivity() {
+ PackageManager pm = context.getPackageManager();
+ Intent settingsIntent = new Intent();
+ settingsIntent.setAction(CARDBOARD_CONFIGURE_ACTION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Grab the settings intent handlers whose package begins with 'com.google.'
+ // Note that the result should be ordered by *descending* priority, but we make
+ // no such assumption.
+ // TODO: b/441496226 - Add declaration to manifest.
+ List resolveInfos = pm.queryIntentActivities(settingsIntent, 0 /* flags */);
+ List googleIntentsWithHighestPriority = new ArrayList<>();
+ Integer highestGoogleIntentPriority = null;
+ for (ResolveInfo info : resolveInfos) {
+ String pkgName = info.activityInfo.packageName;
+ if (!PackageUtils.isGooglePackage(pkgName)) {
+ continue;
+ }
+
+ // With N+, apps are no longer allowed to give >0 priority to intent-filters.
+ // Prioritizing system apps avoids the disambiguation dialog when
+ // legacy Cardboard apps (with a priority of 0) are installed alongside a
+ // system VrCore installation.
+ int priority = info.priority;
+ if (PackageUtils.isSystemPackage(context, pkgName)) {
+ ++priority;
+ }
+
+ if (highestGoogleIntentPriority == null) {
+ highestGoogleIntentPriority = priority;
+ } else if (priority > highestGoogleIntentPriority.intValue()) {
+ // If the intent has a higher priority, accept it and ignore all lower priority matches.
+ highestGoogleIntentPriority = priority;
+ googleIntentsWithHighestPriority.clear();
+ } else if (priority < highestGoogleIntentPriority.intValue()) {
+ // If the intent has a lesser priority, ignore it.
+ continue;
+ }
+
+ Intent intent = new Intent(settingsIntent);
+ intent.setClassName(pkgName, info.activityInfo.name);
+ googleIntentsWithHighestPriority.add(intent);
+ }
+
+ if (!googleIntentsWithHighestPriority.isEmpty()) {
+ // If there is only one matching Google-owned activity with the highest priority, have the
+ // dialog directly launch it and avoid offering a chooser for any third-party apps that
+ // intercept the intent. Otherwise, go through intent resolution as usual. Note this may show
+ // third-party apps that intercept the intent, which is fine since the list is guaranteed to
+ // also contain at least 2 Google-owned Cardboard configuration Activities.
+ Intent intent =
+ googleIntentsWithHighestPriority.size() == 1
+ ? googleIntentsWithHighestPriority.get(0)
+ : settingsIntent;
+ // Go straight to the activity.
+ context.startActivity(intent);
+ }
+ }
+
+ /**
+ * Checks if reading external storage permission is granted.
+ *
+ * @return whether the permission is already granted.
+ */
+ private boolean isReadExternalStoragePermissionEnabled() {
+ return ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * Determines whether {@code eye} is equal to either {@code EyeType.LEFT} or {@code
+ * EyeType.RIGHT}.
+ *
+ * @param eye An integer pointing to either the left or right eye.
+ * @return true When {@code eye} is equal to either {@code EyeType.LEFT} or {@code EyeType.RIGHT}.
+ */
+ private static boolean isValidEyeType(int eye) {
+ return eye == EyeType.LEFT || eye == EyeType.RIGHT;
+ }
+
+ /**
+ * Initializes the field of view form device parameters.
+ *
+ * Left eye field of view is an optional paramater. When it is provided, we set the custom
+ * device field of view. Otherwise, Cardboard V1 device field of view is set.
+ *
+ * @param[in] deviceParamsBuffer A byte buffer with the serialized device parameters.
+ */
+ private void initializeFieldOfView(byte[] deviceParamsBuffer) {
+ final CardboardDevice.DeviceParams deviceParams =
+ DeviceParamsUtils.parseCardboardDeviceParams(deviceParamsBuffer);
+ if (deviceParams.getLeftEyeFieldOfViewAnglesCount() == 4) {
+ fieldOfView =
+ new FieldOfView(
+ deviceParams.getLeftEyeFieldOfViewAngles(0),
+ deviceParams.getLeftEyeFieldOfViewAngles(1),
+ deviceParams.getLeftEyeFieldOfViewAngles(2),
+ deviceParams.getLeftEyeFieldOfViewAngles(3));
+ } else {
+ fieldOfView =
+ new FieldOfView(
+ CardboardV1DeviceParams.CARDBOARD_V1_FOV_ANGLES[0],
+ CardboardV1DeviceParams.CARDBOARD_V1_FOV_ANGLES[1],
+ CardboardV1DeviceParams.CARDBOARD_V1_FOV_ANGLES[2],
+ CardboardV1DeviceParams.CARDBOARD_V1_FOV_ANGLES[3]);
+ }
+ }
+
+ private void setDeviceOrientation(int displayRotation) {
+ if (displayRotation == Surface.ROTATION_0) {
+ // This corresponds to CardboardViewportOrientation::kPortrait
+ deviceOrientation = CardboardViewportOrientation.PORTRAIT;
+ } else if (displayRotation == Surface.ROTATION_180) {
+ // This corresponds to CardboardViewportOrientation::kPortrait
+ deviceOrientation = CardboardViewportOrientation.PORTRAIT_REVERSED;
+ } else if (displayRotation == Surface.ROTATION_90) {
+ // This corresponds to CardboardViewportOrientation::kLandscapeLeft
+ deviceOrientation = CardboardViewportOrientation.LANDSCAPE_LEFT;
+ } else if (displayRotation == Surface.ROTATION_270) {
+ // This corresponds to CardboardViewportOrientation::kLandscapeRight
+ deviceOrientation = CardboardViewportOrientation.LANDSCAPE_RIGHT;
+ }
+ }
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/DistortionRenderer.java b/java_api/src/main/java/com/google/cardboard/sdk/DistortionRenderer.java
new file mode 100644
index 00000000..c6da8293
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/DistortionRenderer.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import com.google.cardboard.sdk.nativetypes.EyeTextureDescription;
+import com.google.cardboard.sdk.nativetypes.Mesh;
+
+/** Cardboard SDK Distortion Renderer API. */
+public class DistortionRenderer {
+
+ // Opaque native pointer to the native DistortionRenderer instance.
+ // This object is owned by this class instance and passed to the native methods.
+ private long nativeDistortionRenderer;
+
+ /**
+ * Creates a new distortion renderer object.
+ *
+ *
Preconditions: Must be called from the rendering thread.
+ */
+ public DistortionRenderer() {
+ nativeDistortionRenderer = nativeDistortionRendererCreate();
+ }
+
+ /**
+ * Releases memory created at construct time by the native distortion renderer object. This method
+ * should be executed just once after construct time, then it will not have effect and a new
+ * instance must be instantiated. If this function is not called, additional resources will be
+ * consumed.
+ *
+ *
Preconditions: Must be called from the rendering thread.
+ */
+ public void close() {
+ nativeDistortionRendererDestroy(nativeDistortionRenderer);
+ nativeDistortionRenderer = 0;
+ }
+
+ /**
+ * Calls {@code close()} in case that method is mistakenly not called.
+ *
+ *
Preconditions: Must be called from the rendering thread.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ // TODO(b/146156526): Remove finalize() usage from Java API.
+ close();
+ super.finalize();
+ }
+
+ /**
+ * Sets Distortion Mesh for a particular eye.
+ *
+ *
Preconditions: Must be called from the rendering thread.
+ *
+ * @param[in] mesh Distortion mesh.
+ * @param[in] eye Desired eye.
+ */
+ public void setMesh(Mesh mesh, int eye) {
+ nativeDistortionRendererSetMesh(nativeDistortionRenderer, mesh, eye);
+ }
+
+ /**
+ * Renders eye textures to display.
+ *
+ *
Preconditions: Must be called from the rendering thread.
+ *
+ * @param[in] targetDisplay Target display.
+ * @param[in] x x coordinate of the rectangle's lower left corner to draw into.
+ * @param[in] y y coordinate of the rectangle's lower left corner to draw into.
+ * @param[in] width Size in pixels of rectangle's width to draw into.
+ * @param[in] height Size in pixels of rectangle's height to draw into.
+ * @param[in] leftEye Left eye texture description.
+ * @param[in] rightEye Right eye texture description.
+ */
+ public void renderEyeToDisplay(
+ long targetDisplay,
+ int x,
+ int y,
+ int width,
+ int height,
+ EyeTextureDescription leftEye,
+ EyeTextureDescription rightEye) {
+ nativeDistortionRendererRenderEyeToDisplay(
+ nativeDistortionRenderer, targetDisplay, x, y, width, height, leftEye, rightEye);
+ }
+
+ private native long nativeDistortionRendererCreate();
+
+ private native void nativeDistortionRendererDestroy(long nativeDistortionRenderer);
+
+ private native void nativeDistortionRendererSetMesh(
+ long nativeDistortionRenderer, Mesh mesh, int eye);
+
+ private native void nativeDistortionRendererRenderEyeToDisplay(
+ long nativeDistortionRenderer,
+ long targetDisplay,
+ int x,
+ int y,
+ int width,
+ int height,
+ EyeTextureDescription leftEye,
+ EyeTextureDescription rightEye);
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/EglFactory.java b/java_api/src/main/java/com/google/cardboard/sdk/EglFactory.java
new file mode 100644
index 00000000..409f52a2
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/EglFactory.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2016 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import android.opengl.EGL14;
+import android.opengl.GLSurfaceView.EGLContextFactory;
+import android.opengl.GLSurfaceView.EGLWindowSurfaceFactory;
+import android.os.Build;
+import android.util.Log;
+
+import java.nio.IntBuffer;
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+
+/**
+ * @hide
+ * Factory class for creating contexts and surfaces.
+ */
+public class EglFactory implements EGLContextFactory, EGLWindowSurfaceFactory {
+ private static final String TAG = "GvrEglFactory";
+ public static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+ private static final int EGL_CONTEXT_PRIORITY_LEVEL = 0x3100;
+ private static final int EGL_CONTEXT_PRIORITY_HIGH = 0x3101;
+ private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
+ private static final int EGL_CONTEXT_OPENGL_NO_ERROR_KHR = 0x31B3;
+ private static final int EGL_CONTEXT_FLAGS_KHR = 0x30FC;
+ private static final int EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR = 0x00000001;
+ private static final int MIN_REQUIRED_CONTEXT_CLIENT_VERSION = 2;
+
+ private boolean usePriority = false;
+ private boolean useProtected = false;
+ private boolean useDebug = false;
+ private boolean errorReportingEnabled = true;
+ private int eglContextClientVersion = 2;
+ private EGLContext sharedContext = EGL10.EGL_NO_CONTEXT;
+
+ public EglFactory() {
+ }
+
+ /**
+ * Sets whether priority contexts should be created.
+ *
+ *
Disabled by default. This must be called before calling setRenderer.
+ *
+ * @param enabled Whether to create priority contexts.
+ */
+ public void setUsePriorityContext(boolean enabled) {
+ usePriority = enabled;
+ }
+
+ /**
+ * Sets whether protected buffers should be used.
+ *
+ *
Disabled by default. This must be called before calling setRenderer.
+ *
+ * @param enabled Whether to use protected buffers.
+ * @throws RuntimeException if EGL 1.4 is unavailable.
+ */
+ public void setUseProtectedBuffers(boolean enabled) {
+ if (enabled && (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) {
+ throw new RuntimeException(
+ "Protected buffer support requires EGL 1.4, available only on Jelly Bean MR1 and later.");
+ }
+ useProtected = enabled;
+ }
+
+ /**
+ * Sets whether KHR_debug logging should be enabled during context creation.
+ *
+ *
Disabled by default. This must be called before calling setRenderer.
+ *
+ * @param enabled Whether to use KHR_debug.
+ */
+ public void setUseDebug(boolean enabled) {
+ useDebug = enabled;
+ }
+
+ /**
+ * Sets up OpenGL context sharing.
+ *
+ *
EGL_NO_CONTEXT by default. This must be called before createContext to enable sharing.
+ *
+ * @param context The existing EGL context with which to share OpenGL resources.
+ */
+ public void setSharedContext(EGLContext context) {
+ sharedContext = context;
+ }
+
+ /**
+ * Enables or disables error reporting for the created context.
+ *
+ *
When set to false, the context will be created with EGL_CONTEXT_OPENGL_NO_ERROR_KHR
+ * attribute set to true, which will cause all glGetError calls to return GL_NO_ERROR.
+ * The default is to report errors normally.
+ *
+ * @param enabled Whether to enable error reporting.
+ */
+ public void setErrorReportingEnabled(boolean enabled) {
+ errorReportingEnabled = enabled;
+ }
+
+ @Override
+ public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
+ IntBuffer attribs = IntBuffer.allocate(10);
+
+ attribs.put(EGL_CONTEXT_CLIENT_VERSION);
+ attribs.put(eglContextClientVersion);
+
+ // Note that Adreno drivers do not require this flag because glEnable(GL_DEBUG_OUTPUT) is
+ // sufficient, however Mali drivers require the context flag *and* the OpenGL state.
+ if (useDebug) {
+ attribs.put(EGL_CONTEXT_FLAGS_KHR);
+ attribs.put(EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR);
+ }
+
+ if (usePriority) {
+ if (Build.FINGERPRINT.contains("generic_x86")) {
+ Log.w(TAG, "EGL_CONTEXT_PRIORITY_LEVEL not supported on emulators.");
+ } else {
+ attribs.put(EGL_CONTEXT_PRIORITY_LEVEL);
+ attribs.put(EGL_CONTEXT_PRIORITY_HIGH);
+ }
+ }
+ if (useProtected && supportsProtectedContent(egl, display)) {
+ attribs.put(EGL_PROTECTED_CONTENT_EXT);
+ attribs.put(EGL14.EGL_TRUE);
+ }
+ if (!errorReportingEnabled) {
+ final String eglExtensionString = egl.eglQueryString(display, EGL10.EGL_EXTENSIONS);
+ if (eglExtensionString.contains("EGL_KHR_create_context_no_error")) {
+ attribs.put(EGL_CONTEXT_OPENGL_NO_ERROR_KHR);
+ attribs.put(EGL14.EGL_TRUE);
+ }
+ }
+ while (attribs.hasRemaining()) {
+ attribs.put(EGL10.EGL_NONE);
+ }
+
+ EGLContext context = egl.eglCreateContext(display, eglConfig, sharedContext, attribs.array());
+ if ((context == null || context == EGL10.EGL_NO_CONTEXT)
+ && eglContextClientVersion > MIN_REQUIRED_CONTEXT_CLIENT_VERSION) {
+ Log.w(
+ TAG,
+ "Failed to create EGL context with version "
+ + eglContextClientVersion
+ + ", will try "
+ + MIN_REQUIRED_CONTEXT_CLIENT_VERSION);
+
+ attribs.array()[1] = MIN_REQUIRED_CONTEXT_CLIENT_VERSION;
+ context = egl.eglCreateContext(display, eglConfig, sharedContext, attribs.array());
+ }
+
+ return context;
+ }
+
+ @Override
+ public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
+ egl.eglDestroyContext(display, context);
+ }
+
+ @Override
+ public EGLSurface createWindowSurface(
+ EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) {
+ EGLSurface result = null;
+ int[] attribsList = null;
+ if (useProtected && supportsProtectedContent(egl, display)) {
+ attribsList = new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE};
+ }
+ try {
+ result = egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList);
+ } catch (IllegalArgumentException e) {
+ // This exception indicates that the surface flinger surface
+ // is not valid. This can happen if the surface flinger surface has
+ // been torn down, but the application has not yet been
+ // notified via SurfaceTexture.Callback.surfaceDestroyed.
+ // In theory the application should be notified first,
+ // but in practice sometimes it is not. See b/4588890.
+ Log.e(TAG, "eglCreateWindowSurface", e);
+ }
+ return result;
+ }
+
+ @Override
+ public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
+ egl.eglDestroySurface(display, surface);
+ }
+
+ /**
+ * Sets the EGL version to use.
+ */
+ public void setEGLContextClientVersion(int version) {
+ eglContextClientVersion = version;
+ }
+
+ /**
+ * Returns whether the EGL driver supports EGL_EXT_protected_content.
+ */
+ private boolean supportsProtectedContent(EGL10 egl, EGLDisplay display) {
+ String extensions = egl.eglQueryString(display, EGL10.EGL_EXTENSIONS);
+ return extensions.contains("EGL_EXT_protected_content");
+ }
+
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/FieldOfView.java b/java_api/src/main/java/com/google/cardboard/sdk/FieldOfView.java
new file mode 100644
index 00000000..c2e51a6c
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/FieldOfView.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import android.opengl.Matrix;
+import java.util.Arrays;
+import java.util.List;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Encapsulates a field of view composed of 4 half angles (left, right, bottom, top) as would be
+ * passed to glFrustum.
+ */
+public class FieldOfView {
+
+ // Default (Cardboard V2.2) half angles in degrees for the maximum field of view.
+ // A consequence of using left/right terminology over inner/outer is that
+ // left and right default angles must be identical.
+ private static final float CARDBOARD_V2_2_MAX_FOV_LEFT_RIGHT = 60.0f;
+ private static final float CARDBOARD_V2_2_MAX_FOV_BOTTOM = 60.0f;
+ private static final float CARDBOARD_V2_2_MAX_FOV_TOP = 60.0f;
+
+ // Parameters for Carboard V1.0.
+ private static final float CARDBOARD_V1_MAX_FOV_LEFT_RIGHT = 40.0f;
+ private static final float CARDBOARD_V1_MAX_FOV_BOTTOM = 40.0f;
+ private static final float CARDBOARD_V1_MAX_FOV_TOP = 40.0f;
+
+ private float left;
+ private float right;
+ private float bottom;
+ private float top;
+
+ public FieldOfView() {
+ left = CARDBOARD_V2_2_MAX_FOV_LEFT_RIGHT;
+ right = CARDBOARD_V2_2_MAX_FOV_LEFT_RIGHT;
+ bottom = CARDBOARD_V2_2_MAX_FOV_BOTTOM;
+ top = CARDBOARD_V2_2_MAX_FOV_TOP;
+ }
+
+ /** Returns parameters for Cardboard V1.0.0 */
+ public static FieldOfView cardboardV1FieldOfView() {
+ FieldOfView params = new FieldOfView();
+ params.setAngles(
+ CARDBOARD_V1_MAX_FOV_LEFT_RIGHT,
+ CARDBOARD_V1_MAX_FOV_LEFT_RIGHT,
+ CARDBOARD_V1_MAX_FOV_BOTTOM,
+ CARDBOARD_V1_MAX_FOV_TOP);
+
+ return params;
+ }
+
+ /**
+ * Creates a new field of view object with the provided params.
+ *
+ * @param left The left field of view half-angle in degrees.
+ * @param right The right field of view half-angle in degrees.
+ * @param bottom The bottom field of view half-angle in degrees.
+ * @param top The top field of view half-angle in degrees.
+ */
+ public FieldOfView(float left, float right, float bottom, float top) {
+ setAngles(left, right, bottom, top);
+ }
+
+ /**
+ * Constructs a new field of view object copying the contents from another.
+ *
+ * @param other The other FieldOfView to copy from.
+ */
+ public FieldOfView(FieldOfView other) {
+ copy(other);
+ }
+
+ /**
+ * @hide Creates a field of view object from the contents of protocol buffer field.
+ * @param angles Angle array from protocol buffer field.
+ * @return The new field of view object or {@code null} in case of error.
+ */
+ public static @Nullable FieldOfView parseFromProtobuf(float[] angles) {
+ if (angles.length != 4) {
+ return null;
+ }
+
+ return new FieldOfView(angles[0], angles[1], angles[2], angles[3]);
+ }
+
+ /**
+ * @hide Creates a field of view object from the contents of protocol buffer field.
+ * @param angles Angle array from protocol buffer field.
+ * @return The new field of view object or {@code null} in case of error.
+ */
+ public static @Nullable FieldOfView parseFromProtobuf(List angles) {
+ if (angles.size() != 4) {
+ return null;
+ }
+
+ return new FieldOfView(angles.get(0), angles.get(1), angles.get(2), angles.get(3));
+ }
+
+ /**
+ * @hide Encodes the current field of view information as array for protocol buffer field.
+ * @return Angle array.
+ */
+ public float[] toProtobuf() {
+ return new float[] {left, right, bottom, top};
+ }
+
+ /**
+ * @hide Encodes the current field of view information as list for protocol buffer field.
+ * @return Angle list.
+ */
+ public List toProtobufAsList() {
+ return Arrays.asList(left, right, bottom, top);
+ }
+
+ /**
+ * Copies the contents of another FieldOfView into this one.
+ *
+ * @param other The FieldOfView object to copy from.
+ */
+ public void copy(FieldOfView other) {
+ this.left = other.left;
+ this.right = other.right;
+ this.bottom = other.bottom;
+ this.top = other.top;
+ }
+
+ /**
+ * Sets the four half-angles of the field of view.
+ *
+ * @param left The left field of view half-angle in degrees.
+ * @param right The right field of view half-angle in degrees.
+ * @param bottom The bottom field of view half-angle in degrees.
+ * @param top The top field of view half-angle in degrees.
+ */
+ public void setAngles(float left, float right, float bottom, float top) {
+ this.left = left;
+ this.right = right;
+ this.bottom = bottom;
+ this.top = top;
+ }
+
+ /**
+ * Sets the left field of view half-angle in degrees.
+ *
+ * @param left The left field of view half-angle in degrees.
+ */
+ public void setLeft(float left) {
+ this.left = left;
+ }
+
+ /**
+ * Returns the left field of view half-angle in degrees.
+ *
+ * @return The left field of view half-angle in degrees.
+ */
+ public float getLeft() {
+ return left;
+ }
+
+ /**
+ * Sets the right field of view half-angle in degrees.
+ *
+ * @param right The right field of view half-angle in degrees.
+ */
+ public void setRight(float right) {
+ this.right = right;
+ }
+
+ /**
+ * Returns the right field of view half-angle in degrees.
+ *
+ * @return The right field of view half-angle in degrees.
+ */
+ public float getRight() {
+ return right;
+ }
+
+ /**
+ * Sets the bottom field of view half-angle in degrees.
+ *
+ * @param bottom The bottom field of view half-angle in degrees.
+ */
+ public void setBottom(float bottom) {
+ this.bottom = bottom;
+ }
+
+ /**
+ * Returns the bottom field of view half-angle in degrees.
+ *
+ * @return The bottom field of view half-angle in degrees.
+ */
+ public float getBottom() {
+ return bottom;
+ }
+
+ /**
+ * Sets the top field of view half-angle in degrees.
+ *
+ * @param top The top field of view half-angle in degrees.
+ */
+ public void setTop(float top) {
+ this.top = top;
+ }
+
+ /**
+ * Returns the top field of view half-angle in degrees.
+ *
+ * @return The top field of view half-angle in degrees.
+ */
+ public float getTop() {
+ return top;
+ }
+
+ /**
+ * Generates a perspective projection matrix from this object.
+ *
+ * @param near The near plane.
+ * @param far The far plane.
+ * @param perspective The perspective matrix to fill.
+ * @param offset The offset into the perspective array to write the matrix to.
+ * @throws IllegalArgumentException If there is not enough space to write the result.
+ */
+ public void toPerspectiveMatrix(float near, float far, float[] perspective, int offset) {
+ // Ensure the result fits.
+ if (offset + 16 > perspective.length) {
+ throw new IllegalArgumentException("Not enough space to write the result");
+ }
+
+ float l = (float) (-Math.tan(Math.toRadians(left)) * near);
+ float r = (float) (Math.tan(Math.toRadians(right)) * near);
+ float b = (float) (-Math.tan(Math.toRadians(bottom)) * near);
+ float t = (float) (Math.tan(Math.toRadians(top)) * near);
+ Matrix.frustumM(perspective, offset, l, r, b, t, near, far);
+ }
+
+ /**
+ * Compares this instance with the specified object and indicates if they are equal.
+ *
+ * @param other The object to compare this instance with.
+ * @return {@code true} if the objects are equal, {@code false} otherwise.
+ */
+ // TODO(b/37774152): implement hashCode() (go/equals-hashcode-lsc)
+ @SuppressWarnings("EqualsHashCode")
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) {
+ return false;
+ }
+
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof FieldOfView)) {
+ return false;
+ }
+
+ FieldOfView o = (FieldOfView) other;
+ return (this.left == o.left && right == o.right && bottom == o.bottom && top == o.top);
+ }
+
+ /**
+ * Returns a string containing a concise, human-readable description of this object.
+ *
+ * @return A printable representation of this object.
+ */
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("{\n")
+ .append(" left: " + left + ",\n")
+ .append(" right: " + right + ",\n")
+ .append(" bottom: " + bottom + ",\n")
+ .append(" top: " + top + ",\n")
+ .append("}")
+ .toString();
+ }
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/HeadTracker.java b/java_api/src/main/java/com/google/cardboard/sdk/HeadTracker.java
new file mode 100644
index 00000000..d2e6dc9c
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/HeadTracker.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+/** Cardboard SDK Head Tracker API. */
+public class HeadTracker {
+
+ // Opaque native pointer to the native HeadTracker instance.
+ // This object is owned by this class instance and passed to the native methods.
+ private long nativeHeadTracker;
+
+ /** Creates a new head tracker object. */
+ public HeadTracker() {
+ nativeHeadTracker = nativeHeadTrackerCreate();
+ }
+
+ /**
+ * Releases memory created at construct time by the native head tracker object. This method should
+ * be executed just once after construct time, then it will not have effect and a new instance
+ * must be instantiated. If this function is not called, additional resources will be consumed.
+ */
+ public void close() {
+ nativeHeadTrackerDestroy(nativeHeadTracker);
+ nativeHeadTracker = 0;
+ }
+
+ /** Calls {@code close()} in case that method is mistakenly not called. */
+ @Override
+ protected void finalize() throws Throwable {
+ // TODO(b/146156526): Remove finalize() usage from Java API.
+ close();
+ super.finalize();
+ }
+
+ /** Pauses head tracker and underlying device sensors. */
+ public void pause() {
+ nativeHeadTrackerPause(nativeHeadTracker);
+ }
+
+ /** Resumes head tracker and underlying device sensors. */
+ public void resume() {
+ nativeHeadTrackerResume(nativeHeadTracker);
+ }
+
+ /**
+ * Gets the predicted head pose for a given timestamp.
+ *
+ * Preconditions: position Must not be null. Preconditions: orientation Must not be null.
+ *
+ * @param[in] deltaTimeNs Delta time in nanoseconds from current system boot time to obtain the
+ * pose.
+ * @param[in] deviceOrientation Orientation of the device.
+ * @param[out] position 3 floats for (x, y, z).
+ * @param[out] orientation 4 floats for quaternion.
+ */
+ public void getPose(
+ long deltaTimeNs, int deviceOrientation, float[] position, float[] orientation) {
+ nativeHeadTrackerGetPose(
+ nativeHeadTracker, deltaTimeNs, deviceOrientation, position, orientation);
+ }
+
+ private native long nativeHeadTrackerCreate();
+
+ private native void nativeHeadTrackerDestroy(long nativeHeadTracker);
+
+ private native void nativeHeadTrackerPause(long nativeHeadTracker);
+
+ private native void nativeHeadTrackerResume(long nativeHeadTracker);
+
+ private native void nativeHeadTrackerGetPose(
+ long nativeHeadTracker,
+ long deltaTimeNs,
+ int deviceOrientation,
+ float[] position,
+ float[] orientation);
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/HeadTransform.java b/java_api/src/main/java/com/google/cardboard/sdk/HeadTransform.java
new file mode 100644
index 00000000..921a136e
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/HeadTransform.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import android.opengl.Matrix;
+
+/** Describes the head transform independently of any eye parameters. */
+public class HeadTransform {
+
+ /** Epsilon value used to detect Gimbal lock situations when computing Euler angles. */
+ private static final float GIMBAL_LOCK_EPSILON = 1e-2f;
+
+ private final float[] headView;
+
+ public HeadTransform() {
+ headView = new float[16];
+ Matrix.setIdentityM(headView, 0);
+ }
+
+ /**
+ * @hide Returns the array where the camera to head matrix transform is stored. For internal use
+ * only.
+ * @return The array where the headView matrix is stored.
+ */
+ public float[] getHeadView() {
+ return headView;
+ }
+
+ /**
+ * A matrix representing the transform from the camera to the head.
+ *
+ *
Head origin is defined as the center point between the two eyes.
+ *
+ * @param headView Array where the 4x4 column-major transformation matrix will be written to.
+ * @param offset Offset in the array where data should be written.
+ * @throws IllegalArgumentException If there is not enough space to write the result.
+ */
+ public void getHeadView(float[] headView, int offset) {
+ // Ensure the result fits.
+ if (offset + 16 > headView.length) {
+ throw new IllegalArgumentException("Not enough space to write the result");
+ }
+
+ System.arraycopy(this.headView, 0, headView, offset, 16);
+ }
+
+ /**
+ * Provides the direction the head is looking towards as a 3x1 unit vector.
+ *
+ *
Note that in OpenGL the forward vector points into the -Z direction. Make sure to invert it
+ * if ever used to compute the basis of a right-handed system.
+ *
+ * @param forward Array where the forward vector will be written to.
+ * @param offset Offset in the array where data should be written.
+ * @throws IllegalArgumentException If there is not enough space to write the result.
+ */
+ public void getForwardVector(float[] forward, int offset) {
+ // Ensure the result fits.
+ if (offset + 3 > forward.length) {
+ throw new IllegalArgumentException("Not enough space to write the result");
+ }
+
+ // Same as multiplying the inverse of the rotation component of the matrix by (0, 0, -1, 0).
+ for (int i = 0; i < 3; ++i) {
+ forward[i + offset] = -headView[2 + i * 4];
+ }
+ }
+
+ /**
+ * Provides the upwards direction of the head as a 3x1 unit vector.
+ *
+ * @param up Array where the up vector will be written to.
+ * @param offset Offset in the array where data should be written.
+ * @throws IllegalArgumentException If there is not enough space to write the result.
+ */
+ public void getUpVector(float[] up, int offset) {
+ // Ensure the result fits.
+ if (offset + 3 > up.length) {
+ throw new IllegalArgumentException("Not enough space to write the result");
+ }
+
+ // Same as multiplying the inverse of the rotation component of the matrix by (0, 1, 0, 0).
+ for (int i = 0; i < 3; ++i) {
+ up[i + offset] = headView[1 + i * 4];
+ }
+ }
+
+ /**
+ * Provides the rightwards direction of the head as a 3x1 unit vector.
+ *
+ * @param right Array where the right vector will be written to.
+ * @param offset Offset in the array where data should be written.
+ * @throws IllegalArgumentException If there is not enough space to write the result.
+ */
+ public void getRightVector(float[] right, int offset) {
+ // Ensure the result fits.
+ if (offset + 3 > right.length) {
+ throw new IllegalArgumentException("Not enough space to write the result");
+ }
+
+ // Same as multiplying the inverse of the rotation component of the matrix by (1, 0, 0, 0).
+ for (int i = 0; i < 3; ++i) {
+ right[i + offset] = headView[i * 4];
+ }
+ }
+
+ /**
+ * Provides the quaternion representing the head rotation.
+ *
+ * @param quaternion Array where the quaternion (x, y, z, w) will be written to.
+ * @param offset Offset in the array where data should be written.
+ * @throws IllegalArgumentException If there is not enough space to write the result.
+ */
+ public void getQuaternion(float[] quaternion, int offset) {
+ // Ensure the result fits.
+ if (offset + 4 > quaternion.length) {
+ throw new IllegalArgumentException("Not enough space to write the result");
+ }
+
+ // Based on the code from http://www.cs.ucr.edu/~vbz/resources/quatut.pdf.
+ float[] m = headView;
+ float t = m[0] + m[5] + m[10];
+ float x;
+ float y;
+ float z;
+ float w;
+
+ if (t >= 0) {
+ float s = (float) Math.sqrt(t + 1);
+ w = 0.5f * s;
+ s = 0.5f / s;
+ x = (m[9] - m[6]) * s;
+ y = (m[2] - m[8]) * s;
+ z = (m[4] - m[1]) * s;
+
+ } else if ((m[0] > m[5]) && (m[0] > m[10])) {
+ float s = (float) Math.sqrt(1.0f + m[0] - m[5] - m[10]);
+ x = s * 0.5f;
+ s = 0.5f / s;
+ y = (m[4] + m[1]) * s;
+ z = (m[2] + m[8]) * s;
+ w = (m[9] - m[6]) * s;
+
+ } else if (m[5] > m[10]) {
+ float s = (float) Math.sqrt(1.0f + m[5] - m[0] - m[10]);
+ y = s * 0.5f;
+ s = 0.5f / s;
+ x = (m[4] + m[1]) * s;
+ z = (m[9] + m[6]) * s;
+ w = (m[2] - m[8]) * s;
+
+ } else {
+ float s = (float) Math.sqrt(1.0f + m[10] - m[0] - m[5]);
+ z = s * 0.5f;
+ s = 0.5f / s;
+ x = (m[2] + m[8]) * s;
+ y = (m[9] + m[6]) * s;
+ w = (m[4] - m[1]) * s;
+ }
+
+ quaternion[offset + 0] = x;
+ quaternion[offset + 1] = y;
+ quaternion[offset + 2] = z;
+ quaternion[offset + 3] = w;
+ }
+
+ /**
+ * Provides the Euler angles representation of the head rotation.
+ *
+ *
Use of Euler angles is discouraged as they might be subject to Gimbal lock situations. Use
+ * quaternions or rotation matrices instead whenever possible.
+ *
+ *
The provided values represent the viewport rotation as pitch, yaw and roll angles where the
+ * matrix R = Rz(roll) * Rx(pitch) * Ry(yaw) represents the full rotation. This rotation matrix
+ * order ensures both yaw and roll are in the full [-pi, pi] interval.
+ *
+ *
The angles are provided in radians, in this order and within the following intervals:
+ *
+ *
+ * - Pitch (X axis): [-pi/2, pi/2]
+ *
- Yaw (Y axis): [-pi, pi]
+ *
- Roll (Z axis): [-pi, pi]
+ *
+ *
+ * The X-Y-Z axes are the basis of a right-handed OpenGL-style coordinate system. During Gimbal
+ * lock this method enforces yaw to 0 and provides a valid roll angle.
+ *
+ * @param eulerAngles Array where the 3 angles will be written to.
+ * @param offset Offset in the array where data should be written.
+ * @throws IllegalArgumentException If there is not enough space to write the result.
+ */
+ public void getEulerAngles(float[] eulerAngles, int offset) {
+ // Ensure the result fits.
+ if (offset + 3 > eulerAngles.length) {
+ throw new IllegalArgumentException("Not enough space to write the result");
+ }
+
+ // Compute pitch.
+ float pitch = (float) Math.asin(headView[6]);
+ float yaw;
+ float roll;
+
+ // Check the absolute value of the pitch cosine to detect Gimbal lock situations.
+ if (Math.sqrt(1.0f - headView[6] * headView[6]) >= GIMBAL_LOCK_EPSILON) {
+
+ // No Gimbal lock.
+ yaw = (float) Math.atan2(-headView[2], headView[10]);
+ roll = (float) Math.atan2(-headView[4], headView[5]);
+
+ } else {
+ // Gimbal lock detected.
+ yaw = 0.0f;
+ roll = (float) Math.atan2(headView[1], headView[0]);
+ }
+
+ eulerAngles[offset + 0] = -pitch;
+ eulerAngles[offset + 1] = -yaw;
+ eulerAngles[offset + 2] = -roll;
+ }
+
+ /**
+ * Provides the relative translation of the head as a 3x1 vector.
+ *
+ * @param translation Array where the translation vector will be written to.
+ * @param offset Offset in the array where data should be written.
+ * @throws IllegalArgumentException If there is not enough space to write the result.
+ */
+ public void getTranslation(float[] translation, int offset) {
+ // Ensure the result fits.
+ if (offset + 3 > translation.length) {
+ throw new IllegalArgumentException("Not enough space to write the result");
+ }
+
+ // Same as multiplying the matrix by (0, 0, 0, 1).
+ for (int i = 0; i < 3; ++i) {
+ translation[i + offset] = headView[12 + i];
+ }
+ }
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/Initialize.java b/java_api/src/main/java/com/google/cardboard/sdk/Initialize.java
new file mode 100644
index 00000000..dfdce8a3
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/Initialize.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import android.content.Context;
+
+/**
+ * Cardboard SDK Initialize API.
+ *
+ *
It is only needed to be called once per Activity. Code within sdk package is not guaranteed to
+ * work from multiple Activities. Every time an Activity is destroyed where references to these
+ * classes live, all destroy() methods should be called before de-referencing them. Every time an
+ * Activity is created and the SDK JNI objects instantiated, a first call to initialize() is needed
+ * and then other objects can be constructed.
+ */
+public class Initialize {
+ static {
+ System.loadLibrary("cardboard_sdk_jni");
+ }
+
+ /** Class only contains static methods. */
+ private Initialize() {}
+
+ /**
+ * Initializes JavaVM and Android context.
+ *
+ * @param[in] context The current Context. It is generally an Activity instance or wraps one. The
+ * following is the list of methods that will be queried: getFilesDir(),
+ * getResources(), getSystemService(Context.WINDOW_SERVICE), startActivity(Intent),
+ * getDisplay().
+ */
+ public static void initialize(Context context) {
+ nativeInitialize(context);
+ }
+
+ private static native void nativeInitialize(Context context);
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/LensDistortion.java b/java_api/src/main/java/com/google/cardboard/sdk/LensDistortion.java
new file mode 100644
index 00000000..19541290
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/LensDistortion.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import androidx.annotation.Nullable;
+import com.google.cardboard.sdk.nativetypes.Mesh;
+import com.google.cardboard.sdk.nativetypes.UvPoint;
+
+/** Cardboard SDK Lens Distortion API. */
+public class LensDistortion {
+
+ // Opaque native pointer to the native LensDistortion instance.
+ // This object is owned by this class instance and passed to the native methods.
+ private long nativeLensDistortion;
+
+ /**
+ * Creates a new lens distortion object.
+ *
+ *
Preconditions: deviceParams Must not be null.
+ *
+ * @param[in] deviceParams The device parameters serialized using cardboard_device.proto.
+ * @param[in] displayWidth Size in pixels of display width.
+ * @param[in] displayHeight Size in pixels of display height.
+ */
+ public LensDistortion(byte[] deviceParams, int displayWidth, int displayHeight) {
+ nativeLensDistortion = nativeLensDistortionCreate(deviceParams, displayWidth, displayHeight);
+ }
+
+ /**
+ * Releases memory created at construct time by the native lens distortion object. This method
+ * should be executed just once after construct time, then it will not have effect and a new
+ * instance must be instantiated.
+ */
+ public void close() {
+ nativeLensDistortionDestroy(nativeLensDistortion);
+ nativeLensDistortion = 0;
+ }
+
+ /** Calls {@code close()} in case that method is mistakenly not called. */
+ @Override
+ protected void finalize() throws Throwable {
+ // TODO(b/146156526): Remove finalize() usage from Java API.
+ close();
+ super.finalize();
+ }
+
+ /**
+ * Gets the eyeFromHead matrix for a particular eye.
+ *
+ *
Preconditions: eyeFromHeadMatrix Must not be null.
+ *
+ * @param[in] eye Desired eye.
+ * @param[out] eyeFromHeadMatrix 4x4 float eye from head matrix.
+ */
+ public void getEyeFromHeadMatrix(int eye, float[] eyeFromHeadMatrix) {
+ nativeLensDistortionGetEyeFromHeadMatrix(nativeLensDistortion, eye, eyeFromHeadMatrix);
+ }
+
+ /**
+ * Gets the ideal projection matrix for a particular eye.
+ *
+ *
Preconditions: z_near and z_far must be postive and z_near must be smaller than z_far.
+ * Preconditions: projectionMatrix Must not be null.
+ *
+ * @param[in] eye Desired eye.
+ * @param[in] zNear Near clip plane z-axis coordinate.
+ * @param[in] zFar Far clip plane z-axis coordinate.
+ * @param[out] projectionMatrix 4x4 float ideal projection matrix.
+ */
+ public void getEyeProjectionMatrix(int eye, float zNear, float zFar, float[] projectionMatrix) {
+ nativeLensDistortionGetEyeProjectionMatrix(
+ nativeLensDistortion, eye, zNear, zFar, projectionMatrix);
+ }
+
+ /**
+ * Gets the distortion mesh for a particular eye.
+ *
+ *
Important: The distorsion {@code Mesh} that is returned by this function becomes invalid if
+ * {@code LensDistortion} is destroyed.
+ *
+ * @param[in] eye Desired eye.
+ * @return A distorsion mesh. When {@code destroy()} has been called, this method returns null.
+ */
+ @Nullable
+ public Mesh getDistortionMesh(int eye) {
+ return nativeLensDistortionGetDistortionMesh(nativeLensDistortion, eye);
+ }
+
+ /**
+ * Applies lens inverse distortion function to a point normalized [0,1] in pre-distortion (eye
+ * texture) space.
+ *
+ * @param[in] distortedUv Distorted UV point.
+ * @param[in] eye Desired eye.
+ * @return A point normalized [0,1] in the screen post distort space. When {@code destroy()} has
+ * been called, this method returns null.
+ */
+ @Nullable
+ public UvPoint undistortedUvForDistortedUv(UvPoint distortedUv, int eye) {
+ return nativeLensDistortionUndistortedUvForDistortedUv(nativeLensDistortion, distortedUv, eye);
+ }
+
+ /**
+ * Applies lens distortion function to a point normalized [0,1] in the screen post-distortion
+ * space.
+ *
+ * @param[in] undistortedUv Undistorted UV point.
+ * @param[in] eye Desired eye.
+ * @return A point normalized [0,1] in pre distort space (eye texture space). When {@code
+ * destroy()} has been called, this method returns null.
+ */
+ @Nullable
+ public UvPoint distortedUvForUndistortedUv(UvPoint undistortedUv, int eye) {
+ return nativeLensDistortionDistortedUvForUndistortedUv(
+ nativeLensDistortion, undistortedUv, eye);
+ }
+
+ private native long nativeLensDistortionCreate(
+ byte[] deviceParams, int displayWidth, int displayHeight);
+
+ private native void nativeLensDistortionDestroy(long nativeLensDistortion);
+
+ private native void nativeLensDistortionGetEyeFromHeadMatrix(
+ long nativeLensDistortion, int eye, float[] eyeFromHeadMatrix);
+
+ private native void nativeLensDistortionGetEyeProjectionMatrix(
+ long nativeLensDistortion, int eye, float zNear, float zFar, float[] projectionMatrix);
+
+ private native Mesh nativeLensDistortionGetDistortionMesh(long nativeLensDistortion, int eye);
+
+ private native UvPoint nativeLensDistortionUndistortedUvForDistortedUv(
+ long nativeLensDistortion, UvPoint distortedUv, int eye);
+
+ private native UvPoint nativeLensDistortionDistortedUvForUndistortedUv(
+ long nativeLensDistortion, UvPoint undistortedUv, int eye);
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/PackageUtils.java b/java_api/src/main/java/com/google/cardboard/sdk/PackageUtils.java
new file mode 100644
index 00000000..3f824c95
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/PackageUtils.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+
+/**
+ * Helper class for querying Android package properties.
+ */
+public class PackageUtils {
+ private static final String GOOGLE_PACKAGE_PREFIX = "com.google.";
+
+ /** Class only contains static methods. */
+ private PackageUtils() {}
+
+ /**
+ * Reports whether a given package is a "Google" package.
+ *
+ * @param packageName the name of the package in question
+ * @return whether the specified package is a "Google" package
+ */
+ public static boolean isGooglePackage(String packageName) {
+ return packageName != null && packageName.startsWith(GOOGLE_PACKAGE_PREFIX);
+ }
+
+ /**
+ * Reports whether a given package is a "system" package.
+ *
+ * @param context The current Context. It is generally an Activity instance or wraps one.
+ * {@code Context.getPackageManager()} will be invoked.
+ * @param packageName the name of the package in question
+ * @return whether the specified package is a "system" package
+ */
+ public static boolean isSystemPackage(Context context, String packageName) {
+ try {
+ ApplicationInfo applicationInfo =
+ context.getPackageManager().getApplicationInfo(packageName, 0);
+ int applicationFlags = applicationInfo != null ? applicationInfo.flags : 0;
+ if (((applicationFlags & ApplicationInfo.FLAG_SYSTEM) != 0)
+ || ((applicationFlags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)) {
+ return true;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ return false;
+ }
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/QrCode.java b/java_api/src/main/java/com/google/cardboard/sdk/QrCode.java
new file mode 100644
index 00000000..5ff26d26
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/QrCode.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import androidx.annotation.Nullable;
+
+/** Cardboard SDK QR Code API. */
+public class QrCode {
+
+ /** Class only contains static methods. */
+ private QrCode() {}
+
+ /**
+ * Gets currently saved device parameters.
+ *
+ * @return device parameters serialized using cardboard_device.proto. If there are not saved
+ * device parameters, this method returns null.
+ */
+ @Nullable
+ public static byte[] getSavedDeviceParams() {
+ return nativeQrCodeGetSavedDeviceParams();
+ }
+
+ /**
+ * Gets Cardboard V1 device parameters.
+ *
+ *
Note that a call to {@code nativeQrCodeGetCardboardV1DeviceParams()} might lead to a
+ * RuntimeException (which is intentionally uncaught). When this very unlikely event happens,
+ * code is thought to be in a severe and faulty state and signalling it within Java provides more
+ * tools to analyze the call graph.
+ *
+ * @return Cardboard V1 device parameters serialized using cardboard_device.proto.
+ */
+ public static byte[] getCardboardV1DeviceParams() {
+ return nativeQrCodeGetCardboardV1DeviceParams();
+ }
+
+ /** Scans a QR code and saves the encoded device parameters. */
+ public static void scanQrCodeAndSaveDeviceParams() {
+ nativeQrCodeScanQrCodeAndSaveDeviceParams();
+ }
+
+ private static native byte[] nativeQrCodeGetSavedDeviceParams();
+
+ private static native byte[] nativeQrCodeGetCardboardV1DeviceParams();
+
+ private static native void nativeQrCodeScanQrCodeAndSaveDeviceParams();
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/Viewport.java b/java_api/src/main/java/com/google/cardboard/sdk/Viewport.java
new file mode 100644
index 00000000..1988eda7
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/Viewport.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.google.cardboard.sdk;
+
+import android.opengl.GLES20;
+
+/** Defines a viewport rectangle. */
+public class Viewport {
+ public int x;
+ public int y;
+ public int width;
+ public int height;
+
+ /**
+ * Sets a new viewport based on the provided coordinates and dimensions.
+ *
+ * @param x Horizontal pixel coordinate of the bottom-left corner of the viewport.
+ * @param y Vertical pixel coordinate of the bottom-left corner of the viewport.
+ * @param width Width of the viewport in pixels.
+ * @param height Height of the viewport in pixels.
+ */
+ public void setViewport(int x, int y, int width, int height) {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ }
+
+ /** Sets the current OpenGL viewport based on the current viewport settings. */
+ public void setGLViewport() {
+ GLES20.glViewport(x, y, width, height);
+ }
+
+ /** Sets the current OpenGL scissor rect based on the current viewport settings. */
+ public void setGLScissor() {
+ GLES20.glScissor(x, y, width, height);
+ }
+
+ /**
+ * Stores the viewport parameters into an int array.
+ *
+ * @param array Array to store values in.
+ * @param offset Offset into the array to start writing values in.
+ * @throws IllegalArgumentException If there is not enough space to write the result.
+ */
+ public void getAsArray(int[] array, int offset) {
+ // Ensure the result fits.
+ if (offset + 4 > array.length) {
+ throw new IllegalArgumentException("Not enough space to write the result");
+ }
+
+ array[offset] = x;
+ array[offset + 1] = y;
+ array[offset + 2] = width;
+ array[offset + 3] = height;
+ }
+
+ /**
+ * Returns a string containing a concise, human-readable description of this object.
+ *
+ * @return A printable representation of this object.
+ */
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("{\n")
+ .append(" x: " + x + ",\n")
+ .append(" y: " + y + ",\n")
+ .append(" width: " + width + ",\n")
+ .append(" height: " + height + ",\n")
+ .append("}")
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof Viewport)) {
+ return false;
+ }
+ Viewport other = (Viewport) obj;
+ return x == other.x && y == other.y && width == other.width && height == other.height;
+ }
+
+ @Override
+ public int hashCode() {
+ return Integer.valueOf(x).hashCode()
+ ^ Integer.valueOf(y).hashCode()
+ ^ Integer.valueOf(width).hashCode()
+ ^ Integer.valueOf(height).hashCode();
+ }
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeTextureDescription.java b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeTextureDescription.java
new file mode 100644
index 00000000..0a13be14
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeTextureDescription.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk.nativetypes;
+
+/** Cardboard SDK Eye Texture Description type. */
+public class EyeTextureDescription {
+ // The texture with eye pixels.
+ public long texture;
+ // u coordinate of the left side of the eye.
+ public float leftU;
+ // u coordinate of the right side of the eye.
+ public float rightU;
+ // v coordinate of the top side of the eye.
+ public float topV;
+ // v coordinate of the bottom side of the eye.
+ public float bottomV;
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeType.java b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeType.java
new file mode 100644
index 00000000..b593cc49
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/EyeType.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk.nativetypes;
+
+/** Cardboard SDK Eye type. */
+public abstract class EyeType {
+
+ private EyeType() {}
+
+ // Left eye.
+ public static final int LEFT = 0;
+ // Right eye.
+ public static final int RIGHT = 1;
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/Mesh.java b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/Mesh.java
new file mode 100644
index 00000000..e0d03d67
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/Mesh.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk.nativetypes;
+
+/** Cardboard SDK Mesh type. */
+public class Mesh {
+ // Indices buffer opaque native pointer.
+ public long indices;
+ // Number of indices.
+ public int nIndices;
+ // Vertices buffer opaque native pointer. 2 floats per vertex: x, y.
+ public long vertices;
+ // UV coordinates buffer opaque native pointer. 2 floats per uv: u, v.
+ public long uvs;
+ // Number of vertices.
+ public int nVertices;
+
+ /** Creates a new mesh object. */
+ public Mesh(long indices, int nIndices, long vertices, long uvs, int nVertices) {
+ this.indices = indices;
+ this.nIndices = nIndices;
+ this.vertices = vertices;
+ this.uvs = uvs;
+ this.nVertices = nVertices;
+ }
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/UvPoint.java b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/UvPoint.java
new file mode 100644
index 00000000..b1fd1994
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/nativetypes/UvPoint.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk.nativetypes;
+
+/** Cardboard SDK UV Point type. */
+public class UvPoint {
+ // u coordinate.
+ public float u;
+ // v coordinate.
+ public float v;
+
+ /** Creates a new UV point object. */
+ public UvPoint(float u, float v) {
+ this.u = u;
+ this.v = v;
+ }
+}
diff --git a/java_api/src/main/java/com/google/cardboard/sdk/utils/MathUtils.java b/java_api/src/main/java/com/google/cardboard/sdk/utils/MathUtils.java
new file mode 100644
index 00000000..6d2fc997
--- /dev/null
+++ b/java_api/src/main/java/com/google/cardboard/sdk/utils/MathUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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.google.cardboard.sdk.utils;
+
+import android.opengl.Matrix;
+
+/** Math utility functions. */
+public class MathUtils {
+
+ private static final String TAG = MathUtils.class.getSimpleName();
+
+ /** Class only contains static methods. */
+ private MathUtils() {}
+
+ /**
+ * Gets isometry matrix from pose position and orientation.
+ *
+ * @param[in] posePosition Pose position.
+ * @param[in] poseOrientation Pose orientation.
+ * @return 4x4 isometry matrix as a float array.
+ */
+ public static float[] getMatrixFromPose(float[] posePosition, float[] poseOrientation) {
+ float[] translationMatrix = new float[16];
+ Matrix.setIdentityM(translationMatrix, 0);
+ Matrix.translateM(translationMatrix, 0, posePosition[0], posePosition[1], posePosition[2]);
+ float[] rotationMatrix = MathUtils.getRotationMatrixFromQuaternion(poseOrientation);
+ float[] result = new float[16];
+ Matrix.multiplyMM(result, 0, translationMatrix, 0, rotationMatrix, 0);
+ return result;
+ }
+
+ /**
+ * Gets rotation matrix from quaternion.
+ *
+ * @param[in] orientationQuaternion Orientation quaternion.
+ * @return 4x4 rotation matrix as a float array.
+ */
+ private static float[] getRotationMatrixFromQuaternion(float[] orientationQuaternion) {
+ float x = orientationQuaternion[0];
+ float y = orientationQuaternion[1];
+ float z = orientationQuaternion[2];
+ float w = orientationQuaternion[3];
+
+ float xx = 2 * x * x;
+ float yy = 2 * y * y;
+ float zz = 2 * z * z;
+
+ float xy = 2 * x * y;
+ float xz = 2 * x * z;
+ float yz = 2 * y * z;
+
+ float xw = 2 * x * w;
+ float yw = 2 * y * w;
+ float zw = 2 * z * w;
+
+ float[] result = new float[16];
+ result[0] = 1 - yy - zz;
+ result[1] = xy + zw;
+ result[2] = xz - yw;
+ result[3] = 0;
+ result[4] = xy - zw;
+ result[5] = 1 - xx - zz;
+ result[6] = yz + xw;
+ result[7] = 0;
+ result[8] = xz + yw;
+ result[9] = yz - xw;
+ result[10] = 1 - xx - yy;
+ result[11] = 0;
+ result[12] = 0;
+ result[13] = 0;
+ result[14] = 0;
+ result[15] = 1;
+
+ return result;
+ }
+}
diff --git a/java_api/src/main/jni/cardboard_jni.cc b/java_api/src/main/jni/cardboard_jni.cc
new file mode 100644
index 00000000..d800aaed
--- /dev/null
+++ b/java_api/src/main/jni/cardboard_jni.cc
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include "include/cardboard.h"
+#include "jni_utils/android/jni_utils.h"
+
+#define JNI_METHOD(return_type, clazz, method_name) \
+ JNIEXPORT return_type JNICALL \
+ Java_com_google_cardboard_sdk_##clazz##_##method_name
+
+namespace {
+
+JavaVM* java_vm_;
+jclass mesh_class_;
+jclass uv_point_class_;
+jclass eye_texture_description_class_;
+
+constexpr const char* kConstructorMethod = "";
+constexpr const char* kJniIntSignature = "I";
+constexpr const char* kJniFloatSignature = "F";
+constexpr const char* kJniLongSignature = "J";
+
+constexpr const char* kEyeTextureDescriptionTexture = "texture";
+constexpr const char* kEyeTextureDescriptionLeftU = "leftU";
+constexpr const char* kEyeTextureDescriptionRightU = "rightU";
+constexpr const char* kEyeTextureDescriptionTopV = "topV";
+constexpr const char* kEyeTextureDescriptionBottomV = "bottomV";
+
+constexpr const char* kUvPointU = "u";
+constexpr const char* kUvPointV = "v";
+
+constexpr const char* kMeshIndices = "indices";
+constexpr const char* kMeshNIndices = "nIndices";
+constexpr const char* kMeshVertices = "vertices";
+constexpr const char* kMeshUvs = "uvs";
+constexpr const char* kMeshNVertices = "nVertices";
+
+constexpr uint64_t kNanosInSeconds = 1000000000;
+
+// Type conversion between native code pointers to intptr_t which are implicitly
+// converted into jlongs afterwards if required.
+// @param ptr A pointer.
+// @tparam T Any native type.
+// @return The address that @p ptr holds converted to an intptr_t.
+template
+constexpr intptr_t ToJniPointer(const T* ptr) {
+ return reinterpret_cast(ptr);
+}
+
+// Finds and creates global references to java classes used within this module.
+// TODO(b/180938531): Release these global references.
+void LoadJNIResources(JNIEnv* env) {
+ mesh_class_ =
+ reinterpret_cast(env->NewGlobalRef(cardboard::jni::LoadJClass(
+ env, "com/google/cardboard/sdk/nativetypes/Mesh")));
+ uv_point_class_ =
+ reinterpret_cast(env->NewGlobalRef(cardboard::jni::LoadJClass(
+ env, "com/google/cardboard/sdk/nativetypes/UvPoint")));
+ eye_texture_description_class_ =
+ reinterpret_cast(env->NewGlobalRef(cardboard::jni::LoadJClass(
+ env, "com/google/cardboard/sdk/nativetypes/EyeTextureDescription")));
+}
+
+} // anonymous namespace
+
+extern "C" {
+
+// Creates a CardboardEyeTextureDescription out of @p eye.
+//
+// @param env The JNI environment.
+// @param eye A Java EyeTextureDescription reference.
+// @return A CardboardEyeTextureDescription instance with the same contents than
+// @p eye.
+CardboardEyeTextureDescription CardboardEyeTextureDescriptionFromJobject(
+ JNIEnv* env, jobject eye) {
+ const jlong java_texture = env->GetLongField(
+ eye, env->GetFieldID(eye_texture_description_class_,
+ kEyeTextureDescriptionTexture, kJniLongSignature));
+ const jfloat java_left_u = env->GetFloatField(
+ eye, env->GetFieldID(eye_texture_description_class_,
+ kEyeTextureDescriptionLeftU, kJniFloatSignature));
+ const jfloat java_right_u = env->GetFloatField(
+ eye, env->GetFieldID(eye_texture_description_class_,
+ kEyeTextureDescriptionRightU, kJniFloatSignature));
+ const jfloat java_top_v = env->GetFloatField(
+ eye, env->GetFieldID(eye_texture_description_class_,
+ kEyeTextureDescriptionTopV, kJniFloatSignature));
+ const jfloat java_bottom_v = env->GetFloatField(
+ eye, env->GetFieldID(eye_texture_description_class_,
+ kEyeTextureDescriptionBottomV, kJniFloatSignature));
+ return CardboardEyeTextureDescription{
+ static_cast(java_texture), static_cast(java_left_u),
+ static_cast(java_right_u), static_cast(java_top_v),
+ static_cast(java_bottom_v)};
+}
+
+// Converts @p eye into a CardboardEye enumeration.
+// @param eye It could be 0 or 1 which represents CardboardEye::kLeft and
+// CardboardEye::kRight respectively.
+// @return CardboardEye::kLeft when @p eye is 0. Otherwise CardboardEye::kRight.
+constexpr CardboardEye CardboardEyeFromJint(jint eye) {
+ return eye == 0 ? kLeft : kRight;
+}
+
+// @return The time since the boot in nano seconds.
+int64_t GetBootTimeNano() {
+ struct timespec res;
+ clock_gettime(CLOCK_BOOTTIME, &res);
+ return (res.tv_sec * kNanosInSeconds) + res.tv_nsec;
+}
+
+// Library entry point.
+JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
+ java_vm_ = vm;
+ return JNI_VERSION_1_6;
+}
+
+// Initialization API
+
+JNI_METHOD(void, Initialize, nativeInitialize)
+(JNIEnv* env, jobject, jobject context) {
+ Cardboard_initializeAndroid(java_vm_, context);
+ LoadJNIResources(env);
+}
+
+// Lens Distortion API
+
+JNI_METHOD(jlong, LensDistortion, nativeLensDistortionCreate)
+(JNIEnv* env, jobject, jbyteArray device_params, jint display_width,
+ jint display_height) {
+ jbyte* device_params_array = env->GetByteArrayElements(device_params, 0);
+ const jsize size = env->GetArrayLength(device_params);
+ const jlong lens_distortion = ToJniPointer(CardboardLensDistortion_create(
+ reinterpret_cast(device_params_array), size,
+ display_width, display_height));
+ env->ReleaseByteArrayElements(device_params, device_params_array, 0);
+ return lens_distortion;
+}
+
+JNI_METHOD(void, LensDistortion, nativeLensDistortionDestroy)
+(JNIEnv*, jobject, jlong lens_distortion) {
+ if (lens_distortion == 0) {
+ return;
+ }
+ CardboardLensDistortion_destroy(
+ reinterpret_cast(lens_distortion));
+}
+
+JNI_METHOD(void, LensDistortion, nativeLensDistortionGetEyeFromHeadMatrix)
+(JNIEnv* env, jobject, jlong lens_distortion, jint eye,
+ jfloatArray eye_from_head_matrix) {
+ if (lens_distortion == 0) {
+ return;
+ }
+ jfloat* eye_from_head_matrix_array =
+ env->GetFloatArrayElements(eye_from_head_matrix, 0);
+ CardboardLensDistortion_getEyeFromHeadMatrix(
+ reinterpret_cast(lens_distortion),
+ CardboardEyeFromJint(eye), eye_from_head_matrix_array);
+ env->ReleaseFloatArrayElements(eye_from_head_matrix,
+ eye_from_head_matrix_array, 0);
+}
+
+JNI_METHOD(void, LensDistortion, nativeLensDistortionGetEyeProjectionMatrix)
+(JNIEnv* env, jobject, jlong lens_distortion, jint eye, jfloat z_near,
+ jfloat z_far, jfloatArray projection_matrix) {
+ if (lens_distortion == 0) {
+ return;
+ }
+ jfloat* projection_matrix_array =
+ env->GetFloatArrayElements(projection_matrix, 0);
+ CardboardLensDistortion_getProjectionMatrix(
+ reinterpret_cast(lens_distortion),
+ CardboardEyeFromJint(eye), z_near, z_far, projection_matrix_array);
+ env->ReleaseFloatArrayElements(projection_matrix, projection_matrix_array, 0);
+}
+
+JNI_METHOD(jobject, LensDistortion, nativeLensDistortionGetDistortionMesh)
+(JNIEnv* env, jobject, jlong lens_distortion, jint eye) {
+ if (lens_distortion == 0) {
+ return NULL;
+ }
+ CardboardMesh mesh;
+ CardboardLensDistortion_getDistortionMesh(
+ reinterpret_cast(lens_distortion),
+ CardboardEyeFromJint(eye), &mesh);
+ // Get constructor method
+ constexpr const char* kConstructorSignature = "(JIJJI)V";
+ const jmethodID meshConstructor =
+ env->GetMethodID(mesh_class_, kConstructorMethod, kConstructorSignature);
+ // Return built object
+ const jlong java_indices = ToJniPointer(mesh.indices);
+ const jint java_n_indices = static_cast(mesh.n_indices);
+ const jlong java_vertices = ToJniPointer(mesh.vertices);
+ const jlong java_uvs = ToJniPointer(mesh.uvs);
+ const jint java_n_vertices = static_cast(mesh.n_vertices);
+ return env->NewObject(mesh_class_, meshConstructor, java_indices,
+ java_n_indices, java_vertices, java_uvs,
+ java_n_vertices);
+}
+
+JNI_METHOD(jobject, LensDistortion,
+ nativeLensDistortionUndistortedUvForDistortedUv)
+(JNIEnv* env, jobject, jlong lens_distortion, jobject distorted_uv,
+ jint eye) {
+ if (lens_distortion == 0) {
+ return NULL;
+ }
+ // Load native structure
+ const CardboardUv native_distorted_uv{
+ env->GetFloatField(
+ distorted_uv,
+ env->GetFieldID(uv_point_class_, kUvPointU, kJniFloatSignature)),
+ env->GetFloatField(
+ distorted_uv,
+ env->GetFieldID(uv_point_class_, kUvPointV, kJniFloatSignature))};
+ const CardboardUv native_undistorted_uv =
+ CardboardLensDistortion_undistortedUvForDistortedUv(
+ reinterpret_cast(lens_distortion),
+ &native_distorted_uv, CardboardEyeFromJint(eye));
+ // Get constructor method
+ constexpr const char* kConstructorSignature = "(FF)V";
+ const jmethodID uvPointConstructor = env->GetMethodID(
+ uv_point_class_, kConstructorMethod, kConstructorSignature);
+ // Return built object
+ return env->NewObject(uv_point_class_, uvPointConstructor,
+ static_cast(native_undistorted_uv.u),
+ static_cast(native_undistorted_uv.v));
+}
+
+JNI_METHOD(jobject, LensDistortion,
+ nativeLensDistortionDistortedUvForUndistortedUv)
+(JNIEnv* env, jobject, jlong lens_distortion, jobject undistorted_uv,
+ jint eye) {
+ if (lens_distortion == 0) {
+ return NULL;
+ }
+ // Load native structure
+ const CardboardUv native_undistorted_uv{
+ env->GetFloatField(
+ undistorted_uv,
+ env->GetFieldID(uv_point_class_, kUvPointU, kJniFloatSignature)),
+ env->GetFloatField(
+ undistorted_uv,
+ env->GetFieldID(uv_point_class_, kUvPointV, kJniFloatSignature))};
+ const CardboardUv native_distorted_uv =
+ CardboardLensDistortion_distortedUvForUndistortedUv(
+ reinterpret_cast(lens_distortion),
+ &native_undistorted_uv, CardboardEyeFromJint(eye));
+ // Get constructor method
+ constexpr const char* kConstructorSignature = "(FF)V";
+ const jmethodID uvPointConstructor = env->GetMethodID(
+ uv_point_class_, kConstructorMethod, kConstructorSignature);
+ // Return built object
+ return env->NewObject(uv_point_class_, uvPointConstructor,
+ static_cast(native_distorted_uv.u),
+ static_cast(native_distorted_uv.v));
+}
+
+// Distortion Renderer API
+
+JNI_METHOD(jlong, DistortionRenderer, nativeDistortionRendererCreate)
+(JNIEnv*, jobject ) {
+ CardboardOpenGlEsDistortionRendererConfig config{kGlTexture2D};
+ return ToJniPointer(CardboardOpenGlEs2DistortionRenderer_create(&config));
+}
+
+JNI_METHOD(void, DistortionRenderer, nativeDistortionRendererDestroy)
+(JNIEnv*, jobject, jlong distortion_renderer) {
+ if (distortion_renderer == 0) {
+ return;
+ }
+ CardboardDistortionRenderer_destroy(
+ reinterpret_cast(distortion_renderer));
+}
+
+JNI_METHOD(void, DistortionRenderer, nativeDistortionRendererSetMesh)
+(JNIEnv* env, jobject, jlong distortion_renderer, jobject mesh, jint eye) {
+ if (distortion_renderer == 0) {
+ return;
+ }
+ // Load native structure
+ const CardboardMesh native_mesh{
+ reinterpret_cast(env->GetLongField(
+ mesh, env->GetFieldID(mesh_class_, kMeshIndices, kJniLongSignature))),
+ static_cast(env->GetIntField(
+ mesh, env->GetFieldID(mesh_class_, kMeshNIndices, kJniIntSignature))),
+ reinterpret_cast(env->GetLongField(
+ mesh,
+ env->GetFieldID(mesh_class_, kMeshVertices, kJniLongSignature))),
+ reinterpret_cast(env->GetLongField(
+ mesh, env->GetFieldID(mesh_class_, kMeshUvs, kJniLongSignature))),
+ static_cast(env->GetIntField(
+ mesh,
+ env->GetFieldID(mesh_class_, kMeshNVertices, kJniIntSignature)))};
+ CardboardDistortionRenderer_setMesh(
+ reinterpret_cast(distortion_renderer),
+ &native_mesh, CardboardEyeFromJint(eye));
+}
+
+JNI_METHOD(void, DistortionRenderer, nativeDistortionRendererRenderEyeToDisplay)
+(JNIEnv* env, jobject, jlong distortion_renderer, jlong target_display,
+ jint x, jint y, jint width, jint height, jobject left_eye, jobject right_eye) {
+ if (distortion_renderer == 0) {
+ return;
+ }
+ // Load native structures
+ CardboardEyeTextureDescription native_left_eye =
+ CardboardEyeTextureDescriptionFromJobject(env, left_eye);
+ CardboardEyeTextureDescription native_right_eye =
+ CardboardEyeTextureDescriptionFromJobject(env, right_eye);
+
+ CardboardDistortionRenderer_renderEyeToDisplay(
+ reinterpret_cast(distortion_renderer),
+ static_cast(target_display), x, y, width, height,
+ &native_left_eye, &native_right_eye);
+}
+
+// Head Tracker API
+
+JNI_METHOD(jlong, HeadTracker, nativeHeadTrackerCreate)
+(JNIEnv*, jobject) {
+ return ToJniPointer(CardboardHeadTracker_create());
+}
+
+JNI_METHOD(void, HeadTracker, nativeHeadTrackerDestroy)
+(JNIEnv* , jobject , jlong head_tracker) {
+ if (head_tracker == 0) {
+ return;
+ }
+ CardboardHeadTracker_destroy(
+ reinterpret_cast(head_tracker));
+}
+
+JNI_METHOD(void, HeadTracker, nativeHeadTrackerPause)
+(JNIEnv*, jobject, jlong head_tracker) {
+ if (head_tracker == 0) {
+ return;
+ }
+ CardboardHeadTracker_pause(
+ reinterpret_cast(head_tracker));
+}
+
+JNI_METHOD(void, HeadTracker, nativeHeadTrackerResume)
+(JNIEnv*, jobject , jlong head_tracker) {
+ if (head_tracker == 0) {
+ return;
+ }
+ CardboardHeadTracker_resume(
+ reinterpret_cast(head_tracker));
+}
+
+JNI_METHOD(void, HeadTracker, nativeHeadTrackerGetPose)
+(JNIEnv* env, jobject , jlong head_tracker, jlong delta_time_ns,
+ jint deviceOrientation, jfloatArray position, jfloatArray orientation) {
+ if (head_tracker == 0) {
+ return;
+ }
+ jfloat* position_array = env->GetFloatArrayElements(position, 0);
+ jfloat* orientation_array = env->GetFloatArrayElements(orientation, 0);
+ const int64_t timestamp_ns = GetBootTimeNano() + delta_time_ns;
+ CardboardHeadTracker_getPose(
+ reinterpret_cast(head_tracker), timestamp_ns,
+ static_cast(deviceOrientation),
+ position_array, orientation_array);
+ env->ReleaseFloatArrayElements(position, position_array, 0);
+ env->ReleaseFloatArrayElements(orientation, orientation_array, 0);
+}
+
+// QR Code Scanner API
+
+JNI_METHOD(jbyteArray, QrCode, nativeQrCodeGetSavedDeviceParams)
+(JNIEnv* env, jobject) {
+ uint8_t* buffer{};
+ int size{};
+ CardboardQrCode_getSavedDeviceParams(&buffer, &size);
+ if (size == 0) {
+ return NULL;
+ }
+ jbyteArray device_params = env->NewByteArray(size);
+ env->SetByteArrayRegion(device_params, 0, size, (jbyte*)buffer);
+ CardboardQrCode_destroy(buffer);
+ return device_params;
+}
+
+JNI_METHOD(jbyteArray, QrCode, nativeQrCodeGetCardboardV1DeviceParams)
+(JNIEnv* env, jobject) {
+ uint8_t* buffer{};
+ int size{};
+ CardboardQrCode_getCardboardV1DeviceParams(&buffer, &size);
+ if (size == 0) {
+ cardboard::jni::ThrowJavaRuntimeException(
+ env,
+ "An error occured while retrieving Cardboard V1 device parameters.");
+ return NULL;
+ }
+ jbyteArray device_params = env->NewByteArray(size);
+ env->SetByteArrayRegion(device_params, 0, size, (jbyte*)buffer);
+ return device_params;
+}
+
+JNI_METHOD(void, QrCode, nativeQrCodeScanQrCodeAndSaveDeviceParams)
+(JNIEnv*, jobject ) { CardboardQrCode_scanQrCodeAndSaveDeviceParams(); }
+
+} // extern "C"
diff --git a/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_close_white_24.png b/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_close_white_24.png
new file mode 100644
index 00000000..d5fd569a
Binary files /dev/null and b/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_close_white_24.png differ
diff --git a/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_settings_white_24.png b/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_settings_white_24.png
new file mode 100644
index 00000000..ac490c09
Binary files /dev/null and b/java_api/src/main/res/drawable-xxhdpi/cardboard_oss_quantum_ic_settings_white_24.png differ
diff --git a/java_api/src/main/res/drawable-xxhdpi/rippleable.xml b/java_api/src/main/res/drawable-xxhdpi/rippleable.xml
new file mode 100644
index 00000000..261cc2d9
--- /dev/null
+++ b/java_api/src/main/res/drawable-xxhdpi/rippleable.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java_api/src/main/res/layout/cardboard_ui.xml b/java_api/src/main/res/layout/cardboard_ui.xml
new file mode 100644
index 00000000..b1b1eacb
--- /dev/null
+++ b/java_api/src/main/res/layout/cardboard_ui.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/java_api/src/main/res/values/colors.xml b/java_api/src/main/res/values/colors.xml
new file mode 100644
index 00000000..f781fef9
--- /dev/null
+++ b/java_api/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #b4b4b4
+ #4cFFFFFF
+
diff --git a/java_api/src/main/res/values/dimens.xml b/java_api/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..c51d551a
--- /dev/null
+++ b/java_api/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 24mm
+ 2dip
+
\ No newline at end of file
diff --git a/java_api/src/main/res/values/strings.xml b/java_api/src/main/res/values/strings.xml
new file mode 100644
index 00000000..be5c3c75
--- /dev/null
+++ b/java_api/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ back
+ settings
+
diff --git a/java_api/src/main/res/values/styles.xml b/java_api/src/main/res/values/styles.xml
new file mode 100644
index 00000000..ac52f1dc
--- /dev/null
+++ b/java_api/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/paperscope/AndroidManifest.xml b/paperscope/AndroidManifest.xml
new file mode 100644
index 00000000..9ef7c4a7
--- /dev/null
+++ b/paperscope/AndroidManifest.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/paperscope/CMakeLists.txt b/paperscope/CMakeLists.txt
new file mode 100644
index 00000000..978b6332
--- /dev/null
+++ b/paperscope/CMakeLists.txt
@@ -0,0 +1,124 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+# https://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.
+
+cmake_minimum_required(VERSION 3.22.1)
+project(paperscope VERSION 1.0.0 LANGUAGES CXX)
+
+include(FetchContent)
+
+# C++ flags.
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED True)
+add_compile_options(-Wall -Wextra)
+
+# dependencies
+find_library(ANDROID_LIB android)
+find_library(GLESv2_LIB GLESv2)
+find_library(GLESv3_LIB GLESv3)
+find_library(LOG_LIB log)
+
+FetchContent_Declare(
+ freetype
+ GIT_REPOSITORY https://gitlab.freedesktop.org/freetype/freetype.git
+ GIT_TAG VER-2-13-2
+)
+
+FetchContent_MakeAvailable(freetype)
+
+set(CARDBOARD_SDK_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../sdk)
+set(CARDBOARD_JAVA_API_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../java_api)
+set(PAPERSCOPE_LAUNCHER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/paperscope_launcher)
+
+# Path to the prebuilt Cardboard SDK library extracted by Gradle
+set(CARDBOARD_LIB ${CMAKE_CURRENT_SOURCE_DIR}/libraries/jni/${ANDROID_ABI}/libGfxPluginCardboard.so)
+
+# Generate localization strings
+set(GENSTRINGS_SCRIPT "${PAPERSCOPE_LAUNCHER_DIR}/common/tools/genstrings.py")
+set(RESOURCE_FILE "${PAPERSCOPE_LAUNCHER_DIR}/resources/strings/en.lproj/cardboard.strings")
+set(NAMESPACE "cardboard")
+set(OUT_DIR "${PAPERSCOPE_LAUNCHER_DIR}/generated_strings")
+
+file(MAKE_DIRECTORY ${OUT_DIR})
+
+add_custom_command(
+ OUTPUT
+ ${OUT_DIR}/${NAMESPACE}_strings.h
+ ${OUT_DIR}/${NAMESPACE}_strings.cc
+ COMMAND python3 ${GENSTRINGS_SCRIPT}
+ --resource_file=${RESOURCE_FILE}
+ --namespace=${NAMESPACE}
+ --out_dir=${OUT_DIR}
+ --language=cc
+ DEPENDS
+ ${GENSTRINGS_SCRIPT}
+ ${RESOURCE_FILE}
+ COMMENT "Generating ${NAMESPACE}_strings C++ files..."
+)
+
+# Explicitly set this to STATIC so CMake knows how to build it
+add_library(cardboard_strings STATIC
+ ${OUT_DIR}/${NAMESPACE}_strings.cc
+ ${OUT_DIR}/${NAMESPACE}_strings.h
+)
+target_include_directories(cardboard_strings PUBLIC ${OUT_DIR})
+
+# Sources
+file(GLOB PAPERSCOPE_SOURCES "${PAPERSCOPE_LAUNCHER_DIR}/src/*.cc")
+
+add_library(cardboard_jni SHARED
+ ${PAPERSCOPE_SOURCES}
+ ${PAPERSCOPE_LAUNCHER_DIR}/common/strings/localized_strings.cc
+ ${CARDBOARD_SDK_DIR}/util/matrix_4x4.cc
+)
+
+# Includes
+target_include_directories(cardboard_jni PRIVATE
+ ${PAPERSCOPE_LAUNCHER_DIR}/src
+ ${PAPERSCOPE_LAUNCHER_DIR}/common/strings
+ ${CARDBOARD_SDK_DIR}
+ ${CARDBOARD_SDK_DIR}/include
+ ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/glm
+)
+
+add_library(cardboard_sdk_jni SHARED
+ ${CARDBOARD_JAVA_API_DIR}/src/main/jni/cardboard_jni.cc
+ ${CARDBOARD_SDK_DIR}/jni_utils/android/jni_utils.cc
+)
+
+target_include_directories(cardboard_sdk_jni PRIVATE
+ ${CARDBOARD_SDK_DIR}
+)
+
+# Link the actual Cardboard library and Android system libraries
+target_link_libraries(cardboard_sdk_jni PRIVATE
+ ${CARDBOARD_LIB}
+ ${ANDROID_LIB}
+ ${LOG_LIB}
+ ${GLESv2_LIB}
+)
+
+# Build
+target_link_libraries(cardboard_jni PRIVATE
+ ${ANDROID_LIB}
+ ${GLESv2_LIB}
+ ${GLESv3_LIB}
+ ${LOG_LIB}
+ ${CARDBOARD_LIB}
+ cardboard_sdk_jni
+ cardboard_strings
+ freetype
+)
+
+target_link_options(cardboard_sdk_jni PRIVATE "-Wl,-z,max-page-size=16384")
+target_link_options(cardboard_jni PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/paperscope/DummyManifest.xml b/paperscope/DummyManifest.xml
new file mode 100644
index 00000000..9a40236b
--- /dev/null
+++ b/paperscope/DummyManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/paperscope/README.md b/paperscope/README.md
new file mode 100644
index 00000000..adbfe02e
--- /dev/null
+++ b/paperscope/README.md
@@ -0,0 +1,39 @@
+# Cardboard Demo App (Paperscope)
+
+This app is the Android demo application for Google Cardboard. It is based on the public [Cardboard Demo App](https://play.google.com/store/apps/details?id=com.google.samples.apps.cardboarddemo) available on the Google Play Store, but without the Exhibit demo.
+
+## Features
+
+The Demo App showcases various capabilities of the Cardboard SDK, such as:
+
+* **Photosphere**: Immersive 360-degree photo viewer.
+* **Magic Window**: A window into a virtual world using the device's sensors.
+* **My Videos**: Media viewer for immersive/stereoscopic video content.
+
+## Requirements
+
+* **OpenJDK 17** (`openjdk@17`)
+* **Android Studio** or Gradle
+* **ADB** (Android Debug Bridge) available in your `PATH`
+* A physical Android device (recommended for optimal head tracking and VR rendering)
+
+## Building and Running
+
+### Using Android Studio
+1. Open Android Studio.
+2. Select **Open an existing project** and choose the directory containing the cloned Cardboard repository.
+3. Wait for the project to sync with the Gradle files.
+4. From the run configurations dropdown, select the **paperscope** configuration.
+5. Connect your Android device via USB (with USB Debugging enabled).
+6. Click **Run** or press `Shift+F10` to install and launch the app on your device.
+
+### Using Gradle (Command Line)
+To build and install the app using the Gradle wrapper from the command line,
+run:
+
+```shell
+./gradlew :sdk:assembleDebug :paperscope:assembleDebug
+adb install -r paperscope/build/outputs/apk/debug/paperscope-debug.apk
+```
+
+Make sure `adb` is connected and authorized on your device.
diff --git a/paperscope/about/AboutActivity.java b/paperscope/about/AboutActivity.java
new file mode 100644
index 00000000..66fb942a
--- /dev/null
+++ b/paperscope/about/AboutActivity.java
@@ -0,0 +1,151 @@
+package com.google.vr.cardboard.paperscope.demo.about;
+
+import com.google.vr.cardboard.paperscope.demo.R;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.google.common.flogger.FluentLogger;
+
+/** Activity for the About page. */
+public class AboutActivity extends AppCompatActivity {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final String MARKET_DETAIL = "market://details?id=";
+
+ private static final String PLAYSTORE_LISTING = "http://play.google.com/store/apps/details?id=";
+
+ private static final String PROD_PACKAGE_NAME = "com.google.samples.apps.cardboarddemo";
+
+ private static final Uri GOOGLE_TOS_URL = Uri.parse("http://www.google.com/accounts/TOS");
+
+ private static final Uri CARDBOARD_SAFETY_INFORMATION_URL =
+ Uri.parse("http://www.google.com/get/cardboard/product-safety/");
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.about);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ // Rate the app.
+ checkNotNull((View) findViewById(R.id.rate_the_app))
+ .setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // First try to go directly to Play Store page using a playstore specific intent.
+ // We don't use getPackageName() here since the dev package name is not present
+ // in Play Store.
+ Uri uri = Uri.parse(MARKET_DETAIL + PROD_PACKAGE_NAME);
+ Intent goToPlayStore = new Intent(Intent.ACTION_VIEW, uri);
+ try {
+ startActivity(goToPlayStore);
+ } catch (ActivityNotFoundException e) {
+ // If that fails, fall back to a broader view webpage intent.
+ openWebPageOrShowErrorToast(
+ AboutActivity.this, Uri.parse(PLAYSTORE_LISTING + PROD_PACKAGE_NAME));
+ }
+ }
+ });
+
+ // Terms of service.
+ checkNotNull((View) findViewById(R.id.terms_of_service))
+ .setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ openWebPageOrShowErrorToast(AboutActivity.this, GOOGLE_TOS_URL);
+ }
+ });
+
+ // Safety.
+ checkNotNull((View) findViewById(R.id.product_safety))
+ .setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ openWebPageOrShowErrorToast(AboutActivity.this, CARDBOARD_SAFETY_INFORMATION_URL);
+ }
+ });
+
+ // Set app version.
+ TextView appVersionString = checkNotNull((TextView) findViewById(R.id.app_version));
+ appVersionString.setText(getVersionName());
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ // This is the "Up" button
+ finish();
+ return true; // Event is handled
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /** Opens up the given web page or shows a toast on error. */
+ public void openWebPageOrShowErrorToast(Context context, Uri uri) {
+ try {
+ context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(context, context.getString(R.string.view_web_page_failed), Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ /**
+ * Returns the version name of the application, or an empty string on error. For dogfood or debug
+ * builds, this method truncates the version name to the "X.Y.Z" pattern, removing any additional
+ * suffixes.
+ */
+ private String getVersionName() {
+ String appVersion = "";
+ String packageName = getPackageName();
+ PackageManager pm = getPackageManager();
+ try {
+ PackageInfo info;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ info = pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
+ } else {
+ info = pm.getPackageInfo(packageName, 0);
+ }
+ if (info != null) {
+ appVersion = info.versionName;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ logger.atSevere().withCause(e).log("Failed to get package info for %s", packageName);
+ }
+
+ // Production version strings follow a traditional "X.Y.Z" pattern.
+ // Dogfood / debug builds follow the following pattern:
+ // "X.Y.Z.Dogfood long_string_such_as_release_branch_name".
+ // In order to display something that doesn't overflow for these builds, we strip out everything
+ // after the first whitespace, if any.
+ // Note that the full version name can always be accessed from the Android "App info" page.
+ int spaceIndex = appVersion.indexOf(" ");
+ if (spaceIndex == -1) {
+ return appVersion;
+ }
+ return appVersion.substring(0, spaceIndex);
+ }
+}
diff --git a/paperscope/about/AndroidManifest.xml b/paperscope/about/AndroidManifest.xml
new file mode 100644
index 00000000..a9c04324
--- /dev/null
+++ b/paperscope/about/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/paperscope/about/res/layout/about.xml b/paperscope/about/res/layout/about.xml
new file mode 100644
index 00000000..aa4b82fe
--- /dev/null
+++ b/paperscope/about/res/layout/about.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/paperscope/assets/IconMesh.obj b/paperscope/assets/IconMesh.obj
new file mode 100644
index 00000000..d9bf27bb
--- /dev/null
+++ b/paperscope/assets/IconMesh.obj
@@ -0,0 +1,15 @@
+# Blender 4.2.2 LTS
+# www.blender.org
+o Icon_mesh
+v 0.350000 -0.350000 0.000000
+v -0.350000 -0.350000 0.000000
+v 0.350000 0.350000 -0.000000
+v -0.350000 0.350000 -0.000000
+vn -0.0000 -0.0000 -1.0000
+vt 1.000000 0.000000
+vt 0.000000 1.000000
+vt 0.000000 0.000000
+vt 1.000000 1.000000
+s 0
+f 2/1/1 3/2/1 1/3/1
+f 2/1/1 4/4/1 3/2/1
diff --git a/paperscope/assets/Roboto-Regular.ttf b/paperscope/assets/Roboto-Regular.ttf
new file mode 100644
index 00000000..4c420003
Binary files /dev/null and b/paperscope/assets/Roboto-Regular.ttf differ
diff --git a/paperscope/assets/Tuffy.ttf b/paperscope/assets/Tuffy.ttf
new file mode 100644
index 00000000..ade553a6
Binary files /dev/null and b/paperscope/assets/Tuffy.ttf differ
diff --git a/paperscope/assets/World.obj b/paperscope/assets/World.obj
new file mode 100644
index 00000000..f4dd61d5
--- /dev/null
+++ b/paperscope/assets/World.obj
@@ -0,0 +1,2039 @@
+# Blender 4.2.2 LTS
+# www.blender.org
+o Sphere
+v 34.047318 83.618576 -28.569096
+v 50.955414 61.546619 -42.756672
+v 60.106007 32.708229 -50.434933
+v 61.283554 17.101002 -51.423012
+v 60.106007 1.493776 -50.434933
+v 50.955414 -27.344616 -42.756672
+v 13.683271 95.563828 -7.506901
+v 26.840706 91.011368 -14.725317
+v 38.966663 83.618576 -21.377846
+v 49.595150 73.669540 -27.208832
+v 58.317726 61.546619 -31.994205
+v 64.799187 47.715679 -35.550053
+v 68.790451 32.708229 -37.739735
+v 70.138138 17.101002 -38.479103
+v 68.790451 1.493776 -37.739735
+v 64.799187 -13.513676 -35.550053
+v 58.317726 -27.344616 -31.994205
+v 49.595150 -39.467541 -27.208832
+v 38.966663 -49.416569 -21.377846
+v 26.840706 -56.809361 -14.725317
+v 13.683271 -61.361820 -7.506901
+v 14.884875 95.563828 -4.693184
+v 29.197735 91.011368 -9.206014
+v 42.388538 83.618576 -13.365054
+v 53.950375 73.669540 -17.010490
+v 63.438927 61.546619 -20.002216
+v 70.489563 47.715679 -22.225279
+v 74.831322 32.708229 -23.594225
+v 76.297348 17.101002 -24.056463
+v 74.831322 1.493776 -23.594225
+v 70.489563 -13.513676 -22.225279
+v 63.438927 -27.344616 -20.002216
+v 53.950375 -39.467541 -17.010490
+v 42.388538 -49.416569 -13.365054
+v 29.197735 -56.809361 -9.206014
+v 14.884875 -61.361820 -4.693184
+v 15.514460 95.563828 -1.699110
+v 30.432713 91.011368 -3.332925
+v 44.181442 83.618576 -4.838654
+v 56.232319 73.669540 -6.158440
+v 66.122200 61.546619 -7.241560
+v 73.471062 47.715679 -8.046392
+v 77.996460 32.708229 -8.542004
+v 79.524506 17.101002 -8.709347
+v 77.996460 1.493776 -8.542004
+v 73.471062 -13.513676 -8.046392
+v 66.122200 -27.344616 -7.241560
+v 56.232319 -39.467541 -6.158440
+v 44.181442 -49.416569 -4.838654
+v 30.432713 -56.809361 -3.332925
+v 15.514460 -61.361820 -1.699110
+v 15.547832 95.563828 1.360259
+v 30.498175 91.011368 2.668244
+v 44.276482 83.618576 3.873693
+v 56.353283 73.669540 4.930271
+v 66.264435 61.546619 5.797390
+v 73.629105 47.715679 6.441704
+v 78.164246 32.708229 6.838482
+v 79.695564 17.101002 6.972466
+v 78.164246 1.493776 6.838482
+v 73.629105 -13.513676 6.441704
+v 66.264435 -27.344616 5.797390
+v 56.353283 -39.467541 4.930271
+v 44.276482 -49.416569 3.873693
+v 30.498175 -56.809361 2.668244
+v 15.547832 -61.361820 1.360259
+v 14.983713 95.563828 4.367354
+v 29.391613 91.011368 8.566874
+v 42.670006 83.618576 12.437172
+v 54.308620 73.669540 15.829517
+v 63.860172 61.546619 18.613546
+v 70.957626 47.715679 20.682255
+v 75.328217 32.708229 21.956173
+v 76.803970 17.101002 22.386328
+v 75.328217 1.493776 21.956173
+v 70.957626 -13.513676 20.682255
+v 63.860172 -27.344616 18.613546
+v 54.308620 -39.467541 15.829517
+v 42.670006 -49.416569 12.437172
+v 29.391613 -56.809361 8.566874
+v 14.983713 -61.361820 4.367354
+v 0.000000 97.100998 0.000000
+v 13.843775 95.563828 7.206614
+v 27.155546 91.011368 14.136281
+v 39.423740 83.618576 20.522701
+v 50.176910 73.669540 26.120441
+v 59.001793 61.546619 30.714394
+v 65.559288 47.715679 34.127998
+v 69.597359 32.708229 36.230095
+v 70.960846 17.101002 36.939888
+v 69.597359 1.493776 36.230095
+v 65.559288 -13.513676 34.127998
+v 59.001793 -27.344616 30.714394
+v 50.176910 -39.467541 26.120441
+v 39.423740 -49.416569 20.522701
+v 27.155546 -56.809361 14.136281
+v 13.843775 -61.361820 7.206614
+v 12.171830 95.563828 9.768927
+v 23.875908 91.011368 19.162441
+v 34.662445 83.618576 27.819553
+v 44.116928 73.669540 35.407574
+v 51.876007 61.546619 41.634899
+v 57.641541 47.715679 46.262222
+v 61.191925 32.708229 49.111717
+v 62.390739 17.101002 50.073875
+v 61.191925 1.493776 49.111717
+v 57.641541 -13.513676 46.262222
+v 51.876007 -27.344616 41.634899
+v 44.116928 -39.467541 35.407574
+v 34.662445 -49.416569 27.819553
+v 23.875908 -56.809361 19.162441
+v 12.171830 -61.361820 9.768927
+v 10.032128 95.563828 11.955826
+v 19.678730 91.011368 23.452200
+v 28.569086 83.618576 34.047318
+v 36.361557 73.669540 43.334011
+v 42.756657 61.546619 50.955406
+v 47.508667 47.715679 56.618610
+v 50.434921 32.708229 60.106003
+v 51.422985 17.101002 61.283543
+v 50.434921 1.493776 60.106003
+v 47.508667 -13.513676 56.618610
+v 42.756657 -27.344616 50.955406
+v 36.361557 -39.467541 43.334011
+v 28.569086 -49.416569 34.047318
+v 19.678730 -56.809361 23.452200
+v 10.032128 -61.361820 11.955826
+v 7.506898 95.563828 13.683268
+v 14.725313 91.011368 26.840704
+v 21.377838 83.618576 38.966656
+v 27.208836 73.669540 49.595150
+v 31.994190 61.546619 58.317719
+v 35.550053 47.715679 64.799179
+v 37.739727 32.708229 68.790443
+v 38.479080 17.101002 70.138123
+v 37.739727 1.493776 68.790443
+v 35.550053 -13.513676 64.799179
+v 31.994190 -27.344616 58.317719
+v 27.208836 -39.467541 49.595150
+v 21.377838 -49.416569 38.966656
+v 14.725313 -56.809361 26.840704
+v 7.506898 -61.361820 13.683268
+v 4.693181 95.563828 14.884871
+v 9.206010 91.011368 29.197731
+v 13.365048 83.618576 42.388531
+v 17.010490 73.669540 53.950367
+v 20.002211 61.546619 63.438919
+v 22.225279 47.715679 70.489555
+v 23.594215 32.708229 74.831314
+v 24.056442 17.101002 76.297333
+v 23.594215 1.493776 74.831314
+v 22.225279 -13.513676 70.489555
+v 20.002211 -27.344616 63.438919
+v 17.010490 -39.467541 53.950367
+v 13.365048 -49.416569 42.388531
+v 9.206010 -56.809361 29.197731
+v 4.693181 -61.361820 14.884871
+v 1.699109 95.563828 15.514455
+v 3.332922 91.011368 30.432707
+v 4.838650 83.618576 44.181435
+v 6.158442 73.669540 56.232311
+v 7.241556 61.546619 66.122192
+v 8.046392 47.715679 73.471054
+v 8.541996 32.708229 77.996445
+v 8.709332 17.101002 79.524475
+v 8.541996 1.493776 77.996445
+v 8.046392 -13.513676 73.471054
+v 7.241556 -27.344616 66.122192
+v 6.158442 -39.467541 56.232311
+v 4.838650 -49.416569 44.181435
+v 3.332922 -56.809361 30.432707
+v 1.699109 -61.361820 15.514455
+v -1.360259 95.563828 15.547828
+v -2.668245 91.011368 30.498169
+v -3.873697 83.618576 44.276474
+v -4.930271 73.669540 56.353275
+v -5.797392 61.546619 66.264427
+v -6.441704 47.715679 73.629097
+v -6.838490 32.708229 78.164230
+v -6.972477 17.101002 79.695541
+v -6.838490 1.493776 78.164230
+v -6.441704 -13.513676 73.629097
+v -5.797392 -27.344616 66.264427
+v -4.930271 -39.467541 56.353275
+v -3.873697 -49.416569 44.276474
+v -2.668245 -56.809361 30.498169
+v -1.360259 -61.361820 15.547828
+v -4.367352 95.563828 14.983707
+v -8.566875 91.011368 29.391609
+v -12.437177 83.618576 42.669991
+v -15.829517 73.669540 54.308617
+v -18.613544 61.546619 63.860161
+v -20.682253 47.715679 70.957619
+v -21.956171 32.708229 75.328201
+v -22.386332 17.101002 76.803940
+v -21.956171 1.493776 75.328201
+v -20.682253 -13.513676 70.957619
+v -18.613544 -27.344616 63.860161
+v -15.829517 -39.467541 54.308617
+v -12.437177 -49.416569 42.669991
+v -8.566875 -56.809361 29.391609
+v -4.367352 -61.361820 14.983707
+v -7.206611 95.563828 13.843771
+v -14.136282 91.011368 27.155540
+v -20.522699 83.618576 39.423729
+v -26.120441 73.669540 50.176903
+v -30.714390 61.546619 59.001785
+v -34.127995 47.715679 65.559280
+v -36.230095 32.708229 69.597351
+v -36.939888 17.101002 70.960815
+v -36.230095 1.493776 69.597351
+v -34.127995 -13.513676 65.559280
+v -30.714390 -27.344616 59.001785
+v -26.120441 -39.467541 50.176903
+v -20.522699 -49.416569 39.423729
+v -14.136282 -56.809361 27.155540
+v -7.206611 -61.361820 13.843771
+v -9.768923 95.563828 12.171825
+v -19.162441 91.011368 23.875900
+v -27.819546 83.618576 34.662430
+v -35.407574 73.669540 44.116924
+v -41.634899 61.546619 51.875999
+v -46.262215 47.715679 57.641537
+v -49.111710 32.708229 61.191914
+v -50.073868 17.101002 62.390709
+v -49.111710 1.493776 61.191914
+v -46.262215 -13.513676 57.641537
+v -41.634899 -27.344616 51.875999
+v -35.407574 -39.467541 44.116924
+v -27.819546 -49.416569 34.662430
+v -19.162441 -56.809361 23.875900
+v -9.768923 -61.361820 12.171825
+v -11.955820 95.563828 10.032125
+v -23.452196 91.011368 19.678726
+v -34.047306 83.618576 28.569075
+v -43.334007 73.669540 36.361553
+v -50.955399 61.546619 42.756649
+v -56.618607 47.715679 47.508656
+v -60.105995 32.708229 50.434910
+v -61.283527 17.101002 51.422958
+v -60.105995 1.493776 50.434910
+v -56.618607 -13.513676 47.508656
+v -50.955399 -27.344616 42.756649
+v -43.334007 -39.467541 36.361553
+v -34.047306 -49.416569 28.569075
+v -23.452196 -56.809361 19.678726
+v -11.955820 -61.361820 10.032125
+v -13.683263 95.563828 7.506895
+v -26.840700 91.011368 14.725308
+v -38.966644 83.618576 21.377827
+v -49.595146 73.669540 27.208830
+v -58.317715 61.546619 31.994186
+v -64.799171 47.715679 35.550049
+v -68.790436 32.708229 37.739716
+v -70.138100 17.101002 38.479053
+v -68.790436 1.493776 37.739716
+v -64.799171 -13.513676 35.550049
+v -58.317715 -27.344616 31.994186
+v -49.595146 -39.467541 27.208830
+v -38.966644 -49.416569 21.377827
+v -26.840700 -56.809361 14.725308
+v -13.683263 -61.361820 7.506895
+v -14.884865 95.563828 4.693180
+v -29.197727 91.011368 9.206006
+v -42.388519 83.618576 13.365041
+v -53.950363 73.669540 17.010487
+v -63.438911 61.546619 20.002205
+v -70.489540 47.715679 22.225275
+v -74.831299 32.708229 23.594206
+v -76.297310 17.101002 24.056421
+v -74.831299 1.493776 23.594206
+v -70.489540 -13.513676 22.225275
+v -63.438911 -27.344616 20.002205
+v -53.950363 -39.467541 17.010487
+v -42.388519 -49.416569 13.365041
+v -29.197727 -56.809361 9.206006
+v -14.884865 -61.361820 4.693180
+v -15.514451 95.563828 1.699109
+v -30.432703 91.011368 3.332921
+v -44.181419 83.618576 4.838644
+v -56.232307 73.669540 6.158438
+v -66.122185 61.546619 7.241550
+v -73.471039 47.715679 8.046396
+v -77.996445 32.708229 8.541988
+v -79.524452 17.101002 8.709312
+v -77.996445 1.493776 8.541988
+v -73.471039 -13.513676 8.046396
+v -66.122185 -27.344616 7.241550
+v -56.232307 -39.467541 6.158438
+v -44.181419 -49.416569 4.838644
+v -30.432703 -56.809361 3.332921
+v -15.514451 -61.361820 1.699109
+v -15.547823 95.563828 -1.360258
+v -30.498165 91.011368 -2.668245
+v -44.276459 83.618576 -3.873697
+v -56.353271 73.669540 -4.930271
+v -66.264420 61.546619 -5.797394
+v -73.629089 47.715679 -6.441704
+v -78.164215 32.708229 -6.838493
+v -79.695511 17.101002 -6.972481
+v -78.164215 1.493776 -6.838493
+v -73.629089 -13.513676 -6.441704
+v -66.264420 -27.344616 -5.797394
+v -56.353271 -39.467541 -4.930271
+v -44.276459 -49.416569 -3.873697
+v -30.498165 -56.809361 -2.668245
+v -15.547823 -61.361820 -1.360258
+v 0.000000 -62.898998 0.000000
+v -14.983704 95.563828 -4.367352
+v -29.391602 91.011368 -8.566873
+v -42.669975 83.618576 -12.437175
+v -54.308609 73.669540 -15.829515
+v -63.860153 61.546619 -18.613546
+v -70.957611 47.715679 -20.682251
+v -75.328186 32.708229 -21.956177
+v -76.803909 17.101002 -22.386332
+v -75.328186 1.493776 -21.956177
+v -70.957611 -13.513676 -20.682251
+v -63.860153 -27.344616 -18.613546
+v -54.308609 -39.467541 -15.829515
+v -42.669975 -49.416569 -12.437175
+v -29.391602 -56.809361 -8.566873
+v -14.983704 -61.361820 -4.367352
+v -13.843766 95.563828 -7.206610
+v -27.155537 91.011368 -14.136279
+v -39.423714 83.618576 -20.522694
+v -50.176895 73.669540 -26.120438
+v -59.001770 61.546619 -30.714388
+v -65.559265 47.715679 -34.127991
+v -69.597336 32.708229 -36.230103
+v -70.960785 17.101002 -36.939884
+v -69.597336 1.493776 -36.230103
+v -65.559265 -13.513676 -34.127991
+v -59.001770 -27.344616 -30.714388
+v -50.176895 -39.467541 -26.120438
+v -39.423714 -49.416569 -20.522694
+v -27.155537 -56.809361 -14.136279
+v -13.843766 -61.361820 -7.206610
+v -12.171822 95.563828 -9.768921
+v -23.875898 91.011368 -19.162437
+v -34.662415 83.618576 -27.819540
+v -44.116920 73.669540 -35.407574
+v -51.875984 61.546619 -41.634895
+v -57.641525 47.715679 -46.262211
+v -61.191902 32.708229 -49.111713
+v -62.390678 17.101002 -50.073853
+v -61.191902 1.493776 -49.111713
+v -57.641525 -13.513676 -46.262211
+v -51.875984 -27.344616 -41.634895
+v -44.116920 -39.467541 -35.407574
+v -34.662415 -49.416569 -27.819540
+v -23.875898 -56.809361 -19.162437
+v -12.171822 -61.361820 -9.768921
+v -10.032122 95.563828 -11.955818
+v -19.678720 91.011368 -23.452192
+v -28.569061 83.618576 -34.047298
+v -36.361549 73.669540 -43.334003
+v -42.756638 61.546619 -50.955395
+v -47.508648 47.715679 -56.618599
+v -50.434895 32.708229 -60.105995
+v -51.422935 17.101002 -61.283520
+v -50.434895 1.493776 -60.105995
+v -47.508648 -13.513676 -56.618599
+v -42.756638 -27.344616 -50.955395
+v -36.361549 -39.467541 -43.334003
+v -28.569061 -49.416569 -34.047298
+v -19.678720 -56.809361 -23.452192
+v -10.032122 -61.361820 -11.955818
+v -7.506893 95.563828 -13.683261
+v -14.725306 91.011368 -26.840694
+v -21.377817 83.618576 -38.966637
+v -27.208824 73.669540 -49.595142
+v -31.994171 61.546619 -58.317703
+v -35.550037 47.715679 -64.799164
+v -37.739700 32.708229 -68.790428
+v -38.479034 17.101002 -70.138092
+v -37.739700 1.493776 -68.790428
+v -35.550037 -13.513676 -64.799164
+v -31.994171 -27.344616 -58.317703
+v -27.208824 -39.467541 -49.595142
+v -21.377817 -49.416569 -38.966637
+v -14.725306 -56.809361 -26.840694
+v -7.506893 -61.361820 -13.683261
+v -4.693178 95.563828 -14.884863
+v -9.206003 91.011368 -29.197723
+v -13.365032 83.618576 -42.388504
+v -17.010483 73.669540 -53.950359
+v -20.002197 61.546619 -63.438896
+v -22.225267 47.715679 -70.489532
+v -23.594194 32.708229 -74.831291
+v -24.056406 17.101002 -76.297287
+v -23.594194 1.493776 -74.831291
+v -22.225267 -13.513676 -70.489532
+v -20.002197 -27.344616 -63.438896
+v -17.010483 -39.467541 -53.950359
+v -13.365032 -49.416569 -42.388504
+v -9.206003 -56.809361 -29.197723
+v -4.693178 -61.361820 -14.884863
+v -1.699107 95.563828 -15.514448
+v -3.332918 91.011368 -30.432697
+v -4.838636 83.618576 -44.181408
+v -6.158438 73.669540 -56.232300
+v -7.241543 61.546619 -66.122169
+v -8.046389 47.715679 -73.471031
+v -8.541981 32.708229 -77.996429
+v -8.709301 17.101002 -79.524429
+v -8.541981 1.493776 -77.996429
+v -8.046389 -13.513676 -73.471031
+v -7.241543 -27.344616 -66.122169
+v -6.158438 -39.467541 -56.232300
+v -4.838636 -49.416569 -44.181408
+v -3.332918 -56.809361 -30.432697
+v -1.699107 -61.361820 -15.514448
+v 1.360259 95.563828 -15.547821
+v 2.668244 91.011368 -30.498161
+v 3.873703 83.618576 -44.276443
+v 4.930273 73.669540 -56.353264
+v 5.797400 61.546619 -66.264404
+v 6.441704 47.715679 -73.629074
+v 6.838501 32.708229 -78.164200
+v 6.972488 17.101002 -79.695488
+v 6.838501 1.493776 -78.164200
+v 6.441704 -13.513676 -73.629074
+v 5.797400 -27.344616 -66.264404
+v 4.930273 -39.467541 -56.353264
+v 3.873703 -49.416569 -44.276443
+v 2.668244 -56.809361 -30.498161
+v 1.360259 -61.361820 -15.547821
+v 4.367352 95.563828 -14.983700
+v 8.566874 91.011368 -29.391598
+v 12.437177 83.618576 -42.669964
+v 15.829519 73.669540 -54.308601
+v 18.613546 61.546619 -63.860134
+v 20.682255 47.715679 -70.957596
+v 21.956179 32.708229 -75.328171
+v 22.386330 17.101002 -76.803894
+v 21.956179 1.493776 -75.328171
+v 20.682255 -13.513676 -70.957596
+v 18.613546 -27.344616 -63.860134
+v 15.829519 -39.467541 -54.308601
+v 12.437177 -49.416569 -42.669964
+v 8.566874 -56.809361 -29.391598
+v 4.367352 -61.361820 -14.983700
+v 7.206608 95.563828 -13.843763
+v 14.136278 91.011368 -27.155533
+v 20.522694 83.618576 -39.423695
+v 26.120441 73.669540 -50.176895
+v 30.714386 61.546619 -59.001755
+v 34.127991 47.715679 -65.559265
+v 36.230095 32.708229 -69.597321
+v 36.939873 17.101002 -70.960770
+v 36.230095 1.493776 -69.597321
+v 34.127991 -13.513676 -65.559265
+v 30.714386 -27.344616 -59.001755
+v 26.120441 -39.467541 -50.176895
+v 20.522694 -49.416569 -39.423695
+v 14.136278 -56.809361 -27.155533
+v 7.206608 -61.361820 -13.843763
+v 9.768919 95.563828 -12.171820
+v 19.162437 91.011368 -23.875895
+v 27.819538 83.618576 -34.662399
+v 35.407578 73.669540 -44.116917
+v 41.634888 61.546619 -51.875973
+v 46.262207 47.715679 -57.641518
+v 49.111706 32.708229 -61.191887
+v 50.073845 17.101002 -62.390663
+v 49.111706 1.493776 -61.191887
+v 46.262207 -13.513676 -57.641518
+v 41.634888 -27.344616 -51.875973
+v 35.407578 -39.467541 -44.116917
+v 27.819538 -49.416569 -34.662399
+v 19.162437 -56.809361 -23.875895
+v 9.768919 -61.361820 -12.171820
+v 11.955815 95.563828 -10.032120
+v 23.452192 91.011368 -19.678720
+v 43.334007 73.669540 -36.361546
+v 56.618595 47.715679 -47.508636
+v 56.618595 -13.513676 -47.508636
+v 43.334007 -39.467541 -36.361546
+v 34.047287 -49.416569 -28.569046
+v 23.452192 -56.809361 -19.678720
+v 11.955815 -61.361820 -10.032120
+vn -0.4288 -0.8286 0.3598
+vn -0.6384 -0.5528 0.5357
+vn -0.7515 -0.1939 0.6306
+vn -0.7660 -0.0000 0.6428
+vn -0.7515 0.1939 0.6306
+vn -0.6384 0.5528 0.5357
+vn -0.1762 -0.9796 0.0967
+vn -0.3401 -0.9217 0.1866
+vn -0.4908 -0.8286 0.2692
+vn -0.6226 -0.7041 0.3416
+vn -0.7306 -0.5528 0.4008
+vn -0.8108 -0.3805 0.4448
+vn -0.8601 -0.1939 0.4719
+vn -0.8767 -0.0000 0.4810
+vn -0.8601 0.1939 0.4719
+vn -0.8108 0.3805 0.4448
+vn -0.7306 0.5528 0.4008
+vn -0.6226 0.7041 0.3416
+vn -0.4908 0.8286 0.2692
+vn -0.3401 0.9217 0.1866
+vn -0.1762 0.9796 0.0967
+vn -0.1917 -0.9796 0.0604
+vn -0.3699 -0.9217 0.1166
+vn -0.5339 -0.8286 0.1683
+vn -0.6773 -0.7041 0.2135
+vn -0.7948 -0.5528 0.2506
+vn -0.8820 -0.3805 0.2781
+vn -0.9356 -0.1939 0.2950
+vn -0.9537 -0.0000 0.3007
+vn -0.9356 0.1939 0.2950
+vn -0.8820 0.3805 0.2781
+vn -0.7948 0.5528 0.2506
+vn -0.6773 0.7041 0.2135
+vn -0.5339 0.8286 0.1683
+vn -0.3699 0.9217 0.1166
+vn -0.1917 0.9796 0.0604
+vn -0.1998 -0.9796 0.0219
+vn -0.3856 -0.9217 0.0422
+vn -0.5564 -0.8286 0.0609
+vn -0.7059 -0.7041 0.0773
+vn -0.8284 -0.5528 0.0907
+vn -0.9193 -0.3805 0.1007
+vn -0.9752 -0.1939 0.1068
+vn -0.9941 -0.0000 0.1089
+vn -0.9752 0.1939 0.1068
+vn -0.9193 0.3805 0.1007
+vn -0.8284 0.5528 0.0907
+vn -0.7059 0.7041 0.0773
+vn -0.5564 0.8286 0.0609
+vn -0.3856 0.9217 0.0422
+vn -0.1998 0.9796 0.0219
+vn -0.2002 -0.9796 -0.0175
+vn -0.3864 -0.9217 -0.0338
+vn -0.5576 -0.8286 -0.0488
+vn -0.7074 -0.7041 -0.0619
+vn -0.8302 -0.5528 -0.0726
+vn -0.9212 -0.3805 -0.0806
+vn -0.9773 -0.1939 -0.0855
+vn -0.9962 -0.0000 -0.0872
+vn -0.9773 0.1939 -0.0855
+vn -0.9212 0.3805 -0.0806
+vn -0.8302 0.5528 -0.0726
+vn -0.7074 0.7041 -0.0619
+vn -0.5576 0.8286 -0.0488
+vn -0.3864 0.9217 -0.0338
+vn -0.2002 0.9796 -0.0175
+vn -0.1929 -0.9796 -0.0562
+vn -0.3724 -0.9217 -0.1085
+vn -0.5374 -0.8286 -0.1566
+vn -0.6818 -0.7041 -0.1987
+vn -0.8000 -0.5528 -0.2332
+vn -0.8878 -0.3805 -0.2588
+vn -0.9418 -0.1939 -0.2745
+vn -0.9600 -0.0000 -0.2798
+vn -0.9418 0.1939 -0.2745
+vn -0.8878 0.3805 -0.2588
+vn -0.8000 0.5528 -0.2332
+vn -0.6818 0.7041 -0.1987
+vn -0.5374 0.8286 -0.1566
+vn -0.3724 0.9217 -0.1085
+vn -0.1929 0.9796 -0.0562
+vn -0.0000 -1.0000 -0.0000
+vn -0.1783 -0.9796 -0.0928
+vn -0.3441 -0.9217 -0.1791
+vn -0.4965 -0.8286 -0.2585
+vn -0.6299 -0.7041 -0.3279
+vn -0.7392 -0.5528 -0.3848
+vn -0.8203 -0.3805 -0.4270
+vn -0.8702 -0.1939 -0.4530
+vn -0.8870 -0.0000 -0.4617
+vn -0.8702 0.1939 -0.4530
+vn -0.8203 0.3805 -0.4270
+vn -0.7392 0.5528 -0.3848
+vn -0.6299 0.7041 -0.3279
+vn -0.4965 0.8286 -0.2585
+vn -0.3441 0.9217 -0.1791
+vn -0.1783 0.9796 -0.0928
+vn -0.1567 -0.9796 -0.1258
+vn -0.3025 -0.9217 -0.2428
+vn -0.4366 -0.8286 -0.3504
+vn -0.5538 -0.7041 -0.4445
+vn -0.6499 -0.5528 -0.5216
+vn -0.7212 -0.3805 -0.5788
+vn -0.7651 -0.1939 -0.6140
+vn -0.7799 -0.0000 -0.6259
+vn -0.7651 0.1939 -0.6140
+vn -0.7212 0.3805 -0.5788
+vn -0.6499 0.5528 -0.5216
+vn -0.5538 0.7041 -0.4445
+vn -0.4366 0.8286 -0.3504
+vn -0.3025 0.9217 -0.2428
+vn -0.1567 0.9796 -0.1258
+vn -0.1292 -0.9796 -0.1539
+vn -0.2493 -0.9217 -0.2971
+vn -0.3598 -0.8286 -0.4288
+vn -0.4565 -0.7041 -0.5440
+vn -0.5357 -0.5528 -0.6384
+vn -0.5944 -0.3805 -0.7084
+vn -0.6306 -0.1939 -0.7515
+vn -0.6428 -0.0000 -0.7660
+vn -0.6306 0.1939 -0.7515
+vn -0.5944 0.3805 -0.7084
+vn -0.5357 0.5528 -0.6384
+vn -0.4565 0.7041 -0.5440
+vn -0.3598 0.8286 -0.4288
+vn -0.2493 0.9217 -0.2971
+vn -0.1292 0.9796 -0.1539
+vn -0.0967 -0.9796 -0.1762
+vn -0.1866 -0.9217 -0.3401
+vn -0.2692 -0.8286 -0.4908
+vn -0.3416 -0.7041 -0.6226
+vn -0.4008 -0.5528 -0.7306
+vn -0.4448 -0.3805 -0.8108
+vn -0.4719 -0.1939 -0.8601
+vn -0.4810 -0.0000 -0.8767
+vn -0.4719 0.1939 -0.8601
+vn -0.4448 0.3805 -0.8108
+vn -0.4008 0.5528 -0.7306
+vn -0.3416 0.7041 -0.6226
+vn -0.2692 0.8286 -0.4908
+vn -0.1866 0.9217 -0.3401
+vn -0.0967 0.9796 -0.1762
+vn -0.0604 -0.9796 -0.1917
+vn -0.1166 -0.9217 -0.3699
+vn -0.1683 -0.8286 -0.5339
+vn -0.2135 -0.7041 -0.6773
+vn -0.2506 -0.5528 -0.7948
+vn -0.2781 -0.3805 -0.8820
+vn -0.2950 -0.1939 -0.9356
+vn -0.3007 -0.0000 -0.9537
+vn -0.2950 0.1939 -0.9356
+vn -0.2781 0.3805 -0.8820
+vn -0.2506 0.5528 -0.7948
+vn -0.2135 0.7041 -0.6773
+vn -0.1683 0.8286 -0.5339
+vn -0.1166 0.9217 -0.3699
+vn -0.0604 0.9796 -0.1917
+vn -0.0219 -0.9796 -0.1998
+vn -0.0422 -0.9217 -0.3856
+vn -0.0609 -0.8286 -0.5564
+vn -0.0773 -0.7041 -0.7059
+vn -0.0907 -0.5528 -0.8284
+vn -0.1007 -0.3805 -0.9193
+vn -0.1068 -0.1939 -0.9752
+vn -0.1089 -0.0000 -0.9941
+vn -0.1068 0.1939 -0.9752
+vn -0.1007 0.3805 -0.9193
+vn -0.0907 0.5528 -0.8284
+vn -0.0773 0.7041 -0.7059
+vn -0.0609 0.8286 -0.5564
+vn -0.0422 0.9217 -0.3856
+vn -0.0219 0.9796 -0.1998
+vn 0.0175 -0.9796 -0.2002
+vn 0.0338 -0.9217 -0.3864
+vn 0.0488 -0.8286 -0.5576
+vn 0.0619 -0.7041 -0.7074
+vn 0.0726 -0.5528 -0.8302
+vn 0.0806 -0.3805 -0.9212
+vn 0.0855 -0.1939 -0.9773
+vn 0.0872 -0.0000 -0.9962
+vn 0.0855 0.1939 -0.9773
+vn 0.0806 0.3805 -0.9212
+vn 0.0726 0.5528 -0.8302
+vn 0.0619 0.7041 -0.7074
+vn 0.0488 0.8286 -0.5576
+vn 0.0338 0.9217 -0.3864
+vn 0.0175 0.9796 -0.2002
+vn 0.0562 -0.9796 -0.1929
+vn 0.1085 -0.9217 -0.3724
+vn 0.1566 -0.8286 -0.5374
+vn 0.1987 -0.7041 -0.6818
+vn 0.2332 -0.5528 -0.8000
+vn 0.2588 -0.3805 -0.8878
+vn 0.2745 -0.1939 -0.9418
+vn 0.2798 -0.0000 -0.9600
+vn 0.2745 0.1939 -0.9418
+vn 0.2588 0.3805 -0.8878
+vn 0.2332 0.5528 -0.8000
+vn 0.1987 0.7041 -0.6818
+vn 0.1566 0.8286 -0.5374
+vn 0.1085 0.9217 -0.3724
+vn 0.0562 0.9796 -0.1929
+vn 0.0928 -0.9796 -0.1783
+vn 0.1791 -0.9217 -0.3441
+vn 0.2585 -0.8286 -0.4965
+vn 0.3279 -0.7041 -0.6299
+vn 0.3848 -0.5528 -0.7392
+vn 0.4270 -0.3805 -0.8203
+vn 0.4530 -0.1939 -0.8702
+vn 0.4617 -0.0000 -0.8870
+vn 0.4530 0.1939 -0.8702
+vn 0.4270 0.3805 -0.8203
+vn 0.3848 0.5528 -0.7392
+vn 0.3279 0.7041 -0.6299
+vn 0.2585 0.8286 -0.4965
+vn 0.1791 0.9217 -0.3441
+vn 0.0928 0.9796 -0.1783
+vn 0.1258 -0.9796 -0.1567
+vn 0.2428 -0.9217 -0.3025
+vn 0.3504 -0.8286 -0.4366
+vn 0.4445 -0.7041 -0.5538
+vn 0.5216 -0.5528 -0.6499
+vn 0.5788 -0.3805 -0.7212
+vn 0.6140 -0.1939 -0.7651
+vn 0.6259 -0.0000 -0.7799
+vn 0.6140 0.1939 -0.7651
+vn 0.5788 0.3805 -0.7212
+vn 0.5216 0.5528 -0.6499
+vn 0.4445 0.7041 -0.5538
+vn 0.3504 0.8286 -0.4366
+vn 0.2428 0.9217 -0.3025
+vn 0.1258 0.9796 -0.1567
+vn 0.1539 -0.9796 -0.1292
+vn 0.2971 -0.9217 -0.2493
+vn 0.4288 -0.8286 -0.3598
+vn 0.5440 -0.7041 -0.4565
+vn 0.6384 -0.5528 -0.5357
+vn 0.7084 -0.3805 -0.5944
+vn 0.7515 -0.1939 -0.6306
+vn 0.7660 -0.0000 -0.6428
+vn 0.7515 0.1939 -0.6306
+vn 0.7084 0.3805 -0.5944
+vn 0.6384 0.5528 -0.5357
+vn 0.5440 0.7041 -0.4565
+vn 0.4288 0.8286 -0.3598
+vn 0.2971 0.9217 -0.2493
+vn 0.1539 0.9796 -0.1292
+vn 0.1762 -0.9796 -0.0967
+vn 0.3401 -0.9217 -0.1866
+vn 0.4908 -0.8286 -0.2692
+vn 0.6226 -0.7041 -0.3416
+vn 0.7306 -0.5528 -0.4008
+vn 0.8108 -0.3805 -0.4448
+vn 0.8601 -0.1939 -0.4719
+vn 0.8767 -0.0000 -0.4810
+vn 0.8601 0.1939 -0.4719
+vn 0.8108 0.3805 -0.4448
+vn 0.7306 0.5528 -0.4008
+vn 0.6226 0.7041 -0.3416
+vn 0.4908 0.8286 -0.2692
+vn 0.3401 0.9217 -0.1866
+vn 0.1762 0.9796 -0.0967
+vn 0.1917 -0.9796 -0.0604
+vn 0.3699 -0.9217 -0.1166
+vn 0.5339 -0.8286 -0.1683
+vn 0.6773 -0.7041 -0.2135
+vn 0.7948 -0.5528 -0.2506
+vn 0.8820 -0.3805 -0.2781
+vn 0.9356 -0.1939 -0.2950
+vn 0.9537 -0.0000 -0.3007
+vn 0.9356 0.1939 -0.2950
+vn 0.8820 0.3805 -0.2781
+vn 0.7948 0.5528 -0.2506
+vn 0.6773 0.7041 -0.2135
+vn 0.5339 0.8286 -0.1683
+vn 0.3699 0.9217 -0.1166
+vn 0.1917 0.9796 -0.0604
+vn 0.1998 -0.9796 -0.0219
+vn 0.3856 -0.9217 -0.0422
+vn 0.5564 -0.8286 -0.0609
+vn 0.7059 -0.7041 -0.0773
+vn 0.8284 -0.5528 -0.0907
+vn 0.9193 -0.3805 -0.1007
+vn 0.9752 -0.1939 -0.1068
+vn 0.9941 -0.0000 -0.1089
+vn 0.9752 0.1939 -0.1068
+vn 0.9193 0.3805 -0.1007
+vn 0.8284 0.5528 -0.0907
+vn 0.7059 0.7041 -0.0773
+vn 0.5564 0.8286 -0.0609
+vn 0.3856 0.9217 -0.0422
+vn 0.1998 0.9796 -0.0219
+vn 0.2002 -0.9796 0.0175
+vn 0.3864 -0.9217 0.0338
+vn 0.5576 -0.8286 0.0488
+vn 0.7074 -0.7041 0.0619
+vn 0.8302 -0.5528 0.0726
+vn 0.9212 -0.3805 0.0806
+vn 0.9773 -0.1939 0.0855
+vn 0.9962 -0.0000 0.0872
+vn 0.9773 0.1939 0.0855
+vn 0.9212 0.3805 0.0806
+vn 0.8302 0.5528 0.0726
+vn 0.7074 0.7041 0.0619
+vn 0.5576 0.8286 0.0488
+vn 0.3864 0.9217 0.0338
+vn 0.2002 0.9796 0.0175
+vn -0.0000 1.0000 -0.0000
+vn 0.1929 -0.9796 0.0562
+vn 0.3724 -0.9217 0.1085
+vn 0.5374 -0.8286 0.1566
+vn 0.6818 -0.7041 0.1987
+vn 0.8000 -0.5528 0.2332
+vn 0.8878 -0.3805 0.2588
+vn 0.9418 -0.1939 0.2745
+vn 0.9600 -0.0000 0.2798
+vn 0.9418 0.1939 0.2745
+vn 0.8878 0.3805 0.2588
+vn 0.8000 0.5528 0.2332
+vn 0.6818 0.7041 0.1987
+vn 0.5374 0.8286 0.1566
+vn 0.3724 0.9217 0.1085
+vn 0.1929 0.9796 0.0562
+vn 0.1783 -0.9796 0.0928
+vn 0.3441 -0.9217 0.1791
+vn 0.4965 -0.8286 0.2585
+vn 0.6299 -0.7041 0.3279
+vn 0.7392 -0.5528 0.3848
+vn 0.8203 -0.3805 0.4270
+vn 0.8702 -0.1939 0.4530
+vn 0.8870 -0.0000 0.4617
+vn 0.8702 0.1939 0.4530
+vn 0.8203 0.3805 0.4270
+vn 0.7392 0.5528 0.3848
+vn 0.6299 0.7041 0.3279
+vn 0.4965 0.8286 0.2585
+vn 0.3441 0.9217 0.1791
+vn 0.1783 0.9796 0.0928
+vn 0.1567 -0.9796 0.1258
+vn 0.3025 -0.9217 0.2428
+vn 0.4366 -0.8286 0.3504
+vn 0.5538 -0.7041 0.4445
+vn 0.6499 -0.5528 0.5216
+vn 0.7212 -0.3805 0.5788
+vn 0.7651 -0.1939 0.6140
+vn 0.7799 -0.0000 0.6259
+vn 0.7651 0.1939 0.6140
+vn 0.7212 0.3805 0.5788
+vn 0.6499 0.5528 0.5216
+vn 0.5538 0.7041 0.4445
+vn 0.4366 0.8286 0.3504
+vn 0.3025 0.9217 0.2428
+vn 0.1567 0.9796 0.1258
+vn 0.1292 -0.9796 0.1539
+vn 0.2493 -0.9217 0.2971
+vn 0.3598 -0.8286 0.4288
+vn 0.4565 -0.7041 0.5440
+vn 0.5357 -0.5528 0.6384
+vn 0.5944 -0.3805 0.7084
+vn 0.6306 -0.1939 0.7515
+vn 0.6428 -0.0000 0.7660
+vn 0.6306 0.1939 0.7515
+vn 0.5944 0.3805 0.7084
+vn 0.5357 0.5528 0.6384
+vn 0.4565 0.7041 0.5440
+vn 0.3598 0.8286 0.4288
+vn 0.2493 0.9217 0.2971
+vn 0.1292 0.9796 0.1539
+vn 0.0967 -0.9796 0.1762
+vn 0.1866 -0.9217 0.3401
+vn 0.2692 -0.8286 0.4908
+vn 0.3416 -0.7041 0.6226
+vn 0.4008 -0.5528 0.7306
+vn 0.4448 -0.3805 0.8108
+vn 0.4719 -0.1939 0.8601
+vn 0.4810 -0.0000 0.8767
+vn 0.4719 0.1939 0.8601
+vn 0.4448 0.3805 0.8108
+vn 0.4008 0.5528 0.7306
+vn 0.3416 0.7041 0.6226
+vn 0.2692 0.8286 0.4908
+vn 0.1866 0.9217 0.3401
+vn 0.0967 0.9796 0.1762
+vn 0.0604 -0.9796 0.1917
+vn 0.1166 -0.9217 0.3699
+vn 0.1683 -0.8286 0.5339
+vn 0.2135 -0.7041 0.6773
+vn 0.2506 -0.5528 0.7948
+vn 0.2781 -0.3805 0.8820
+vn 0.2950 -0.1939 0.9356
+vn 0.3007 -0.0000 0.9537
+vn 0.2950 0.1939 0.9356
+vn 0.2781 0.3805 0.8820
+vn 0.2506 0.5528 0.7948
+vn 0.2135 0.7041 0.6773
+vn 0.1683 0.8286 0.5339
+vn 0.1166 0.9217 0.3699
+vn 0.0604 0.9796 0.1917
+vn 0.0219 -0.9796 0.1998
+vn 0.0422 -0.9217 0.3856
+vn 0.0609 -0.8286 0.5564
+vn 0.0773 -0.7041 0.7059
+vn 0.0907 -0.5528 0.8284
+vn 0.1007 -0.3805 0.9193
+vn 0.1068 -0.1939 0.9752
+vn 0.1089 -0.0000 0.9941
+vn 0.1068 0.1939 0.9752
+vn 0.1007 0.3805 0.9193
+vn 0.0907 0.5528 0.8284
+vn 0.0773 0.7041 0.7059
+vn 0.0609 0.8286 0.5564
+vn 0.0422 0.9217 0.3856
+vn 0.0219 0.9796 0.1998
+vn -0.0175 -0.9796 0.2002
+vn -0.0338 -0.9217 0.3864
+vn -0.0488 -0.8286 0.5576
+vn -0.0619 -0.7041 0.7074
+vn -0.0726 -0.5528 0.8302
+vn -0.0806 -0.3805 0.9212
+vn -0.0855 -0.1939 0.9773
+vn -0.0872 -0.0000 0.9962
+vn -0.0855 0.1939 0.9773
+vn -0.0806 0.3805 0.9212
+vn -0.0726 0.5528 0.8302
+vn -0.0619 0.7041 0.7074
+vn -0.0488 0.8286 0.5576
+vn -0.0338 0.9217 0.3864
+vn -0.0175 0.9796 0.2002
+vn -0.0562 -0.9796 0.1929
+vn -0.1085 -0.9217 0.3724
+vn -0.1566 -0.8286 0.5374
+vn -0.1987 -0.7041 0.6818
+vn -0.2332 -0.5528 0.8000
+vn -0.2588 -0.3805 0.8878
+vn -0.2745 -0.1939 0.9418
+vn -0.2798 -0.0000 0.9600
+vn -0.2745 0.1939 0.9418
+vn -0.2588 0.3805 0.8878
+vn -0.2332 0.5528 0.8000
+vn -0.1987 0.7041 0.6818
+vn -0.1566 0.8286 0.5374
+vn -0.1085 0.9217 0.3724
+vn -0.0562 0.9796 0.1929
+vn -0.0928 -0.9796 0.1783
+vn -0.1791 -0.9217 0.3441
+vn -0.2585 -0.8286 0.4965
+vn -0.3279 -0.7041 0.6299
+vn -0.3848 -0.5528 0.7392
+vn -0.4270 -0.3805 0.8203
+vn -0.4530 -0.1939 0.8702
+vn -0.4617 -0.0000 0.8870
+vn -0.4530 0.1939 0.8702
+vn -0.4270 0.3805 0.8203
+vn -0.3848 0.5528 0.7392
+vn -0.3279 0.7041 0.6299
+vn -0.2585 0.8286 0.4965
+vn -0.1791 0.9217 0.3441
+vn -0.0928 0.9796 0.1783
+vn -0.1258 -0.9796 0.1567
+vn -0.2428 -0.9217 0.3025
+vn -0.3504 -0.8286 0.4366
+vn -0.4445 -0.7041 0.5538
+vn -0.5216 -0.5528 0.6499
+vn -0.5788 -0.3805 0.7212
+vn -0.6140 -0.1939 0.7651
+vn -0.6259 -0.0000 0.7799
+vn -0.6140 0.1939 0.7651
+vn -0.5788 0.3805 0.7212
+vn -0.5216 0.5528 0.6499
+vn -0.4445 0.7041 0.5538
+vn -0.3504 0.8286 0.4366
+vn -0.2428 0.9217 0.3025
+vn -0.1258 0.9796 0.1567
+vn -0.1539 -0.9796 0.1292
+vn -0.2971 -0.9217 0.2493
+vn -0.5440 -0.7041 0.4565
+vn -0.7084 -0.3805 0.5944
+vn -0.7084 0.3805 0.5944
+vn -0.5440 0.7041 0.4565
+vn -0.4288 0.8286 0.3598
+vn -0.2971 0.9217 0.2493
+vn -0.1539 0.9796 0.1292
+vt 0.750000 0.812500
+vt 0.718750 0.812500
+vt 0.718750 0.875000
+vt 0.750000 0.875000
+vt 0.750000 0.312500
+vt 0.718750 0.312500
+vt 0.718750 0.375000
+vt 0.750000 0.375000
+vt 0.750000 0.750000
+vt 0.718750 0.750000
+vt 0.750000 0.250000
+vt 0.718750 0.250000
+vt 0.750000 0.687500
+vt 0.718750 0.687500
+vt 0.750000 0.187500
+vt 0.718750 0.187500
+vt 0.750000 0.625000
+vt 0.718750 0.625000
+vt 0.750000 0.125000
+vt 0.718750 0.125000
+vt 0.750000 0.562500
+vt 0.718750 0.562500
+vt 0.750000 0.062500
+vt 0.718750 0.062500
+vt 0.750000 0.500000
+vt 0.718750 0.500000
+vt 0.750000 0.937500
+vt 0.718750 0.937500
+vt 0.734375 1.000000
+vt 0.734375 0.000000
+vt 0.750000 0.437500
+vt 0.718750 0.437500
+vt 0.687500 0.875000
+vt 0.687500 0.937500
+vt 0.687500 0.375000
+vt 0.687500 0.437500
+vt 0.687500 0.812500
+vt 0.687500 0.312500
+vt 0.687500 0.750000
+vt 0.687500 0.250000
+vt 0.687500 0.687500
+vt 0.687500 0.187500
+vt 0.687500 0.625000
+vt 0.687500 0.125000
+vt 0.687500 0.562500
+vt 0.687500 0.062500
+vt 0.687500 0.500000
+vt 0.703125 1.000000
+vt 0.703125 0.000000
+vt 0.656250 0.625000
+vt 0.656250 0.687500
+vt 0.656250 0.125000
+vt 0.656250 0.187500
+vt 0.656250 0.562500
+vt 0.656250 0.062500
+vt 0.656250 0.500000
+vt 0.656250 0.937500
+vt 0.671875 1.000000
+vt 0.671875 0.000000
+vt 0.656250 0.437500
+vt 0.656250 0.875000
+vt 0.656250 0.375000
+vt 0.656250 0.812500
+vt 0.656250 0.312500
+vt 0.656250 0.750000
+vt 0.656250 0.250000
+vt 0.625000 0.375000
+vt 0.625000 0.437500
+vt 0.625000 0.812500
+vt 0.625000 0.875000
+vt 0.625000 0.312500
+vt 0.625000 0.750000
+vt 0.625000 0.250000
+vt 0.625000 0.687500
+vt 0.625000 0.187500
+vt 0.625000 0.625000
+vt 0.625000 0.125000
+vt 0.625000 0.562500
+vt 0.625000 0.062500
+vt 0.625000 0.500000
+vt 0.625000 0.937500
+vt 0.640625 1.000000
+vt 0.640625 0.000000
+vt 0.593750 0.125000
+vt 0.593750 0.187500
+vt 0.593750 0.562500
+vt 0.593750 0.625000
+vt 0.593750 0.062500
+vt 0.593750 0.500000
+vt 0.593750 0.937500
+vt 0.609375 1.000000
+vt 0.609375 0.000000
+vt 0.593750 0.437500
+vt 0.593750 0.875000
+vt 0.593750 0.375000
+vt 0.593750 0.812500
+vt 0.593750 0.312500
+vt 0.593750 0.750000
+vt 0.593750 0.250000
+vt 0.593750 0.687500
+vt 0.562500 0.812500
+vt 0.562500 0.875000
+vt 0.562500 0.312500
+vt 0.562500 0.375000
+vt 0.562500 0.750000
+vt 0.562500 0.250000
+vt 0.562500 0.687500
+vt 0.562500 0.187500
+vt 0.562500 0.625000
+vt 0.562500 0.125000
+vt 0.562500 0.562500
+vt 0.562500 0.062500
+vt 0.562500 0.500000
+vt 0.562500 0.937500
+vt 0.578125 1.000000
+vt 0.578125 0.000000
+vt 0.562500 0.437500
+vt 0.531250 0.562500
+vt 0.531250 0.625000
+vt 0.531250 0.062500
+vt 0.531250 0.125000
+vt 0.531250 0.500000
+vt 0.531250 0.937500
+vt 0.546875 1.000000
+vt 0.546875 0.000000
+vt 0.531250 0.437500
+vt 0.531250 0.875000
+vt 0.531250 0.375000
+vt 0.531250 0.812500
+vt 0.531250 0.312500
+vt 0.531250 0.750000
+vt 0.531250 0.250000
+vt 0.531250 0.687500
+vt 0.531250 0.187500
+vt 0.500000 0.312500
+vt 0.500000 0.375000
+vt 0.500000 0.750000
+vt 0.500000 0.812500
+vt 0.500000 0.250000
+vt 0.500000 0.687500
+vt 0.500000 0.187500
+vt 0.500000 0.625000
+vt 0.500000 0.125000
+vt 0.500000 0.562500
+vt 0.500000 0.062500
+vt 0.500000 0.500000
+vt 0.500000 0.937500
+vt 0.515625 1.000000
+vt 0.515625 0.000000
+vt 0.500000 0.437500
+vt 0.500000 0.875000
+vt 0.468750 0.062500
+vt 0.468750 0.125000
+vt 0.468750 0.500000
+vt 0.468750 0.562500
+vt 0.468750 0.937500
+vt 0.484375 1.000000
+vt 0.484375 0.000000
+vt 0.468750 0.437500
+vt 0.468750 0.875000
+vt 0.468750 0.375000
+vt 0.468750 0.812500
+vt 0.468750 0.312500
+vt 0.468750 0.750000
+vt 0.468750 0.250000
+vt 0.468750 0.687500
+vt 0.468750 0.187500
+vt 0.468750 0.625000
+vt 0.437500 0.250000
+vt 0.437500 0.312500
+vt 0.437500 0.687500
+vt 0.437500 0.750000
+vt 0.437500 0.187500
+vt 0.437500 0.625000
+vt 0.437500 0.125000
+vt 0.437500 0.562500
+vt 0.437500 0.062500
+vt 0.437500 0.500000
+vt 0.437500 0.937500
+vt 0.453125 1.000000
+vt 0.453125 0.000000
+vt 0.437500 0.437500
+vt 0.437500 0.875000
+vt 0.437500 0.375000
+vt 0.437500 0.812500
+vt 0.406250 0.937500
+vt 0.421875 1.000000
+vt 0.421875 0.000000
+vt 0.406250 0.062500
+vt 0.406250 0.437500
+vt 0.406250 0.500000
+vt 0.406250 0.875000
+vt 0.406250 0.375000
+vt 0.406250 0.812500
+vt 0.406250 0.312500
+vt 0.406250 0.750000
+vt 0.406250 0.250000
+vt 0.406250 0.687500
+vt 0.406250 0.187500
+vt 0.406250 0.625000
+vt 0.406250 0.125000
+vt 0.406250 0.562500
+vt 0.375000 0.687500
+vt 0.375000 0.750000
+vt 0.375000 0.187500
+vt 0.375000 0.250000
+vt 0.375000 0.625000
+vt 0.375000 0.125000
+vt 0.375000 0.562500
+vt 0.375000 0.062500
+vt 0.375000 0.500000
+vt 0.375000 0.937500
+vt 0.390625 1.000000
+vt 0.390625 0.000000
+vt 0.375000 0.437500
+vt 0.375000 0.875000
+vt 0.375000 0.375000
+vt 0.375000 0.812500
+vt 0.375000 0.312500
+vt 0.343750 0.437500
+vt 0.343750 0.500000
+vt 0.343750 0.875000
+vt 0.343750 0.937500
+vt 0.343750 0.375000
+vt 0.343750 0.812500
+vt 0.343750 0.312500
+vt 0.343750 0.750000
+vt 0.343750 0.250000
+vt 0.343750 0.687500
+vt 0.343750 0.187500
+vt 0.343750 0.625000
+vt 0.343750 0.125000
+vt 0.343750 0.562500
+vt 0.343750 0.062500
+vt 0.359375 1.000000
+vt 0.359375 0.000000
+vt 0.312500 0.187500
+vt 0.312500 0.250000
+vt 0.312500 0.625000
+vt 0.312500 0.687500
+vt 0.312500 0.125000
+vt 0.312500 0.562500
+vt 0.312500 0.062500
+vt 0.312500 0.500000
+vt 0.312500 0.937500
+vt 0.328125 1.000000
+vt 0.328125 0.000000
+vt 0.312500 0.437500
+vt 0.312500 0.875000
+vt 0.312500 0.375000
+vt 0.312500 0.812500
+vt 0.312500 0.312500
+vt 0.312500 0.750000
+vt 0.281250 0.875000
+vt 0.281250 0.937500
+vt 0.281250 0.375000
+vt 0.281250 0.437500
+vt 0.281250 0.812500
+vt 0.281250 0.312500
+vt 0.281250 0.750000
+vt 0.281250 0.250000
+vt 0.281250 0.687500
+vt 0.281250 0.187500
+vt 0.281250 0.625000
+vt 0.281250 0.125000
+vt 0.281250 0.562500
+vt 0.281250 0.062500
+vt 0.281250 0.500000
+vt 0.296875 1.000000
+vt 0.296875 0.000000
+vt 0.250000 0.625000
+vt 0.250000 0.687500
+vt 0.250000 0.125000
+vt 0.250000 0.187500
+vt 0.250000 0.562500
+vt 0.250000 0.062500
+vt 0.250000 0.500000
+vt 0.250000 0.937500
+vt 0.265625 1.000000
+vt 0.265625 0.000000
+vt 0.250000 0.437500
+vt 0.250000 0.875000
+vt 0.250000 0.375000
+vt 0.250000 0.812500
+vt 0.250000 0.312500
+vt 0.250000 0.750000
+vt 0.250000 0.250000
+vt 0.218750 0.375000
+vt 0.218750 0.437500
+vt 0.218750 0.812500
+vt 0.218750 0.875000
+vt 0.218750 0.312500
+vt 0.218750 0.750000
+vt 0.218750 0.250000
+vt 0.218750 0.687500
+vt 0.218750 0.187500
+vt 0.218750 0.625000
+vt 0.218750 0.125000
+vt 0.218750 0.562500
+vt 0.218750 0.062500
+vt 0.218750 0.500000
+vt 0.218750 0.937500
+vt 0.234375 1.000000
+vt 0.234375 0.000000
+vt 0.187500 0.125000
+vt 0.187500 0.187500
+vt 0.187500 0.562500
+vt 0.187500 0.625000
+vt 0.187500 0.062500
+vt 0.187500 0.500000
+vt 0.187500 0.937500
+vt 0.203125 1.000000
+vt 0.203125 0.000000
+vt 0.187500 0.437500
+vt 0.187500 0.875000
+vt 0.187500 0.375000
+vt 0.187500 0.812500
+vt 0.187500 0.312500
+vt 0.187500 0.750000
+vt 0.187500 0.250000
+vt 0.187500 0.687500
+vt 0.156250 0.312500
+vt 0.156250 0.375000
+vt 0.156250 0.750000
+vt 0.156250 0.812500
+vt 0.156250 0.250000
+vt 0.156250 0.687500
+vt 0.156250 0.187500
+vt 0.156250 0.625000
+vt 0.156250 0.125000
+vt 0.156250 0.562500
+vt 0.156250 0.062500
+vt 0.156250 0.500000
+vt 0.156250 0.937500
+vt 0.171875 1.000000
+vt 0.171875 0.000000
+vt 0.156250 0.437500
+vt 0.156250 0.875000
+vt 0.125000 0.062500
+vt 0.125000 0.125000
+vt 0.125000 0.500000
+vt 0.125000 0.562500
+vt 0.125000 0.937500
+vt 0.140625 1.000000
+vt 0.140625 0.000000
+vt 0.125000 0.437500
+vt 0.125000 0.875000
+vt 0.125000 0.375000
+vt 0.125000 0.812500
+vt 0.125000 0.312500
+vt 0.125000 0.750000
+vt 0.125000 0.250000
+vt 0.125000 0.687500
+vt 0.125000 0.187500
+vt 0.125000 0.625000
+vt 0.093750 0.750000
+vt 0.093750 0.812500
+vt 0.093750 0.250000
+vt 0.093750 0.312500
+vt 0.093750 0.687500
+vt 0.093750 0.187500
+vt 0.093750 0.625000
+vt 0.093750 0.125000
+vt 0.093750 0.562500
+vt 0.093750 0.062500
+vt 0.093750 0.500000
+vt 0.093750 0.937500
+vt 0.109375 1.000000
+vt 0.109375 0.000000
+vt 0.093750 0.437500
+vt 0.093750 0.875000
+vt 0.093750 0.375000
+vt 0.062500 0.500000
+vt 0.062500 0.562500
+vt 0.062500 0.937500
+vt 0.078125 1.000000
+vt 0.078125 0.000000
+vt 0.062500 0.062500
+vt 0.062500 0.437500
+vt 0.062500 0.875000
+vt 0.062500 0.375000
+vt 0.062500 0.812500
+vt 0.062500 0.312500
+vt 0.062500 0.750000
+vt 0.062500 0.250000
+vt 0.062500 0.687500
+vt 0.062500 0.187500
+vt 0.062500 0.625000
+vt 0.062500 0.125000
+vt 0.031250 0.250000
+vt 0.031250 0.312500
+vt 0.031250 0.687500
+vt 0.031250 0.750000
+vt 0.031250 0.187500
+vt 0.031250 0.625000
+vt 0.031250 0.125000
+vt 0.031250 0.562500
+vt 0.031250 0.062500
+vt 0.031250 0.500000
+vt 0.031250 0.937500
+vt 0.046875 1.000000
+vt 0.046875 0.000000
+vt 0.031250 0.437500
+vt 0.031250 0.875000
+vt 0.031250 0.375000
+vt 0.031250 0.812500
+vt 0.000000 0.937500
+vt 0.015625 1.000000
+vt 0.015625 0.000000
+vt 0.000000 0.062500
+vt 0.000000 0.437500
+vt 0.000000 0.500000
+vt 0.000000 0.875000
+vt 0.000000 0.375000
+vt 0.000000 0.812500
+vt 0.000000 0.312500
+vt 0.000000 0.750000
+vt 0.000000 0.250000
+vt 0.000000 0.687500
+vt 0.000000 0.187500
+vt 0.000000 0.625000
+vt 0.000000 0.125000
+vt 0.000000 0.562500
+vt 1.000000 0.687500
+vt 0.968750 0.687500
+vt 0.968750 0.750000
+vt 1.000000 0.750000
+vt 1.000000 0.187500
+vt 0.968750 0.187500
+vt 0.968750 0.250000
+vt 1.000000 0.250000
+vt 1.000000 0.625000
+vt 0.968750 0.625000
+vt 1.000000 0.125000
+vt 0.968750 0.125000
+vt 1.000000 0.562500
+vt 0.968750 0.562500
+vt 1.000000 0.062500
+vt 0.968750 0.062500
+vt 1.000000 0.500000
+vt 0.968750 0.500000
+vt 1.000000 0.937500
+vt 0.968750 0.937500
+vt 0.984375 1.000000
+vt 0.984375 0.000000
+vt 1.000000 0.437500
+vt 0.968750 0.437500
+vt 1.000000 0.875000
+vt 0.968750 0.875000
+vt 1.000000 0.375000
+vt 0.968750 0.375000
+vt 1.000000 0.812500
+vt 0.968750 0.812500
+vt 1.000000 0.312500
+vt 0.968750 0.312500
+vt 0.937500 0.437500
+vt 0.937500 0.500000
+vt 0.937500 0.875000
+vt 0.937500 0.937500
+vt 0.937500 0.375000
+vt 0.937500 0.812500
+vt 0.937500 0.312500
+vt 0.937500 0.750000
+vt 0.937500 0.250000
+vt 0.937500 0.687500
+vt 0.937500 0.187500
+vt 0.937500 0.625000
+vt 0.937500 0.125000
+vt 0.937500 0.562500
+vt 0.937500 0.062500
+vt 0.953125 1.000000
+vt 0.953125 0.000000
+vt 0.906250 0.187500
+vt 0.906250 0.250000
+vt 0.906250 0.625000
+vt 0.906250 0.687500
+vt 0.906250 0.125000
+vt 0.906250 0.562500
+vt 0.906250 0.062500
+vt 0.906250 0.500000
+vt 0.906250 0.937500
+vt 0.921875 1.000000
+vt 0.921875 0.000000
+vt 0.906250 0.437500
+vt 0.906250 0.875000
+vt 0.906250 0.375000
+vt 0.906250 0.812500
+vt 0.906250 0.312500
+vt 0.906250 0.750000
+vt 0.875000 0.375000
+vt 0.875000 0.437500
+vt 0.875000 0.812500
+vt 0.875000 0.875000
+vt 0.875000 0.312500
+vt 0.875000 0.750000
+vt 0.875000 0.250000
+vt 0.875000 0.687500
+vt 0.875000 0.187500
+vt 0.875000 0.625000
+vt 0.875000 0.125000
+vt 0.875000 0.562500
+vt 0.875000 0.062500
+vt 0.875000 0.500000
+vt 0.875000 0.937500
+vt 0.890625 1.000000
+vt 0.890625 0.000000
+vt 0.843750 0.125000
+vt 0.843750 0.187500
+vt 0.843750 0.562500
+vt 0.843750 0.625000
+vt 0.843750 0.062500
+vt 0.843750 0.500000
+vt 0.843750 0.937500
+vt 0.859375 1.000000
+vt 0.859375 0.000000
+vt 0.843750 0.437500
+vt 0.843750 0.875000
+vt 0.843750 0.375000
+vt 0.843750 0.812500
+vt 0.843750 0.312500
+vt 0.843750 0.750000
+vt 0.843750 0.250000
+vt 0.843750 0.687500
+vt 0.812500 0.812500
+vt 0.812500 0.875000
+vt 0.812500 0.312500
+vt 0.812500 0.375000
+vt 0.812500 0.750000
+vt 0.812500 0.250000
+vt 0.812500 0.687500
+vt 0.812500 0.187500
+vt 0.812500 0.625000
+vt 0.812500 0.125000
+vt 0.812500 0.562500
+vt 0.812500 0.062500
+vt 0.812500 0.500000
+vt 0.812500 0.937500
+vt 0.828125 1.000000
+vt 0.828125 0.000000
+vt 0.812500 0.437500
+vt 0.781250 0.562500
+vt 0.781250 0.625000
+vt 0.781250 0.062500
+vt 0.781250 0.125000
+vt 0.781250 0.500000
+vt 0.781250 0.937500
+vt 0.796875 1.000000
+vt 0.796875 0.000000
+vt 0.781250 0.437500
+vt 0.781250 0.875000
+vt 0.781250 0.375000
+vt 0.781250 0.812500
+vt 0.781250 0.312500
+vt 0.781250 0.750000
+vt 0.781250 0.250000
+vt 0.781250 0.687500
+vt 0.781250 0.187500
+vt 0.765625 1.000000
+vt 0.765625 0.000000
+s 1
+f 1/1/1 9/2/9 8/3/8 475/4/475
+f 6/5/6 17/6/17 16/7/16 478/8/478
+f 476/9/476 10/10/10 9/2/9 1/1/1
+f 479/11/479 18/12/18 17/6/17 6/5/6
+f 2/13/2 11/14/11 10/10/10 476/9/476
+f 480/15/480 19/16/19 18/12/18 479/11/479
+f 477/17/477 12/18/12 11/14/11 2/13/2
+f 481/19/481 20/20/20 19/16/19 480/15/480
+f 3/21/3 13/22/13 12/18/12 477/17/477
+f 482/23/482 21/24/21 20/20/20 481/19/481
+f 4/25/4 14/26/14 13/22/13 3/21/3
+f 474/27/474 7/28/7 82/29/82
+f 308/30/308 21/24/21 482/23/482
+f 5/31/5 15/32/15 14/26/14 4/25/4
+f 475/4/475 8/3/8 7/28/7 474/27/474
+f 478/8/478 16/7/16 15/32/15 5/31/5
+f 8/3/8 23/33/23 22/34/22 7/28/7
+f 16/7/16 31/35/31 30/36/30 15/32/15
+f 9/2/9 24/37/24 23/33/23 8/3/8
+f 17/6/17 32/38/32 31/35/31 16/7/16
+f 10/10/10 25/39/25 24/37/24 9/2/9
+f 18/12/18 33/40/33 32/38/32 17/6/17
+f 11/14/11 26/41/26 25/39/25 10/10/10
+f 19/16/19 34/42/34 33/40/33 18/12/18
+f 12/18/12 27/43/27 26/41/26 11/14/11
+f 20/20/20 35/44/35 34/42/34 19/16/19
+f 13/22/13 28/45/28 27/43/27 12/18/12
+f 21/24/21 36/46/36 35/44/35 20/20/20
+f 14/26/14 29/47/29 28/45/28 13/22/13
+f 7/28/7 22/34/22 82/48/82
+f 308/49/308 36/46/36 21/24/21
+f 15/32/15 30/36/30 29/47/29 14/26/14
+f 27/43/27 42/50/42 41/51/41 26/41/26
+f 35/44/35 50/52/50 49/53/49 34/42/34
+f 28/45/28 43/54/43 42/50/42 27/43/27
+f 36/46/36 51/55/51 50/52/50 35/44/35
+f 29/47/29 44/56/44 43/54/43 28/45/28
+f 22/34/22 37/57/37 82/58/82
+f 308/59/308 51/55/51 36/46/36
+f 30/36/30 45/60/45 44/56/44 29/47/29
+f 23/33/23 38/61/38 37/57/37 22/34/22
+f 31/35/31 46/62/46 45/60/45 30/36/30
+f 24/37/24 39/63/39 38/61/38 23/33/23
+f 32/38/32 47/64/47 46/62/46 31/35/31
+f 25/39/25 40/65/40 39/63/39 24/37/24
+f 33/40/33 48/66/48 47/64/47 32/38/32
+f 26/41/26 41/51/41 40/65/40 25/39/25
+f 34/42/34 49/53/49 48/66/48 33/40/33
+f 46/62/46 61/67/61 60/68/60 45/60/45
+f 39/63/39 54/69/54 53/70/53 38/61/38
+f 47/64/47 62/71/62 61/67/61 46/62/46
+f 40/65/40 55/72/55 54/69/54 39/63/39
+f 48/66/48 63/73/63 62/71/62 47/64/47
+f 41/51/41 56/74/56 55/72/55 40/65/40
+f 49/53/49 64/75/64 63/73/63 48/66/48
+f 42/50/42 57/76/57 56/74/56 41/51/41
+f 50/52/50 65/77/65 64/75/64 49/53/49
+f 43/54/43 58/78/58 57/76/57 42/50/42
+f 51/55/51 66/79/66 65/77/65 50/52/50
+f 44/56/44 59/80/59 58/78/58 43/54/43
+f 37/57/37 52/81/52 82/82/82
+f 308/83/308 66/79/66 51/55/51
+f 45/60/45 60/68/60 59/80/59 44/56/44
+f 38/61/38 53/70/53 52/81/52 37/57/37
+f 65/77/65 80/84/80 79/85/79 64/75/64
+f 58/78/58 73/86/73 72/87/72 57/76/57
+f 66/79/66 81/88/81 80/84/80 65/77/65
+f 59/80/59 74/89/74 73/86/73 58/78/58
+f 52/81/52 67/90/67 82/91/82
+f 308/92/308 81/88/81 66/79/66
+f 60/68/60 75/93/75 74/89/74 59/80/59
+f 53/70/53 68/94/68 67/90/67 52/81/52
+f 61/67/61 76/95/76 75/93/75 60/68/60
+f 54/69/54 69/96/69 68/94/68 53/70/53
+f 62/71/62 77/97/77 76/95/76 61/67/61
+f 55/72/55 70/98/70 69/96/69 54/69/54
+f 63/73/63 78/99/78 77/97/77 62/71/62
+f 56/74/56 71/100/71 70/98/70 55/72/55
+f 64/75/64 79/85/79 78/99/78 63/73/63
+f 57/76/57 72/87/72 71/100/71 56/74/56
+f 69/96/69 85/101/85 84/102/84 68/94/68
+f 77/97/77 93/103/93 92/104/92 76/95/76
+f 70/98/70 86/105/86 85/101/85 69/96/69
+f 78/99/78 94/106/94 93/103/93 77/97/77
+f 71/100/71 87/107/87 86/105/86 70/98/70
+f 79/85/79 95/108/95 94/106/94 78/99/78
+f 72/87/72 88/109/88 87/107/87 71/100/71
+f 80/84/80 96/110/96 95/108/95 79/85/79
+f 73/86/73 89/111/89 88/109/88 72/87/72
+f 81/88/81 97/112/97 96/110/96 80/84/80
+f 74/89/74 90/113/90 89/111/89 73/86/73
+f 67/90/67 83/114/83 82/115/82
+f 308/116/308 97/112/97 81/88/81
+f 75/93/75 91/117/91 90/113/90 74/89/74
+f 68/94/68 84/102/84 83/114/83 67/90/67
+f 76/95/76 92/104/92 91/117/91 75/93/75
+f 89/111/89 104/118/104 103/119/103 88/109/88
+f 97/112/97 112/120/112 111/121/111 96/110/96
+f 90/113/90 105/122/105 104/118/104 89/111/89
+f 83/114/83 98/123/98 82/124/82
+f 308/125/308 112/120/112 97/112/97
+f 91/117/91 106/126/106 105/122/105 90/113/90
+f 84/102/84 99/127/99 98/123/98 83/114/83
+f 92/104/92 107/128/107 106/126/106 91/117/91
+f 85/101/85 100/129/100 99/127/99 84/102/84
+f 93/103/93 108/130/108 107/128/107 92/104/92
+f 86/105/86 101/131/101 100/129/100 85/101/85
+f 94/106/94 109/132/109 108/130/108 93/103/93
+f 87/107/87 102/133/102 101/131/101 86/105/86
+f 95/108/95 110/134/110 109/132/109 94/106/94
+f 88/109/88 103/119/103 102/133/102 87/107/87
+f 96/110/96 111/121/111 110/134/110 95/108/95
+f 108/130/108 123/135/123 122/136/122 107/128/107
+f 101/131/101 116/137/116 115/138/115 100/129/100
+f 109/132/109 124/139/124 123/135/123 108/130/108
+f 102/133/102 117/140/117 116/137/116 101/131/101
+f 110/134/110 125/141/125 124/139/124 109/132/109
+f 103/119/103 118/142/118 117/140/117 102/133/102
+f 111/121/111 126/143/126 125/141/125 110/134/110
+f 104/118/104 119/144/119 118/142/118 103/119/103
+f 112/120/112 127/145/127 126/143/126 111/121/111
+f 105/122/105 120/146/120 119/144/119 104/118/104
+f 98/123/98 113/147/113 82/148/82
+f 308/149/308 127/145/127 112/120/112
+f 106/126/106 121/150/121 120/146/120 105/122/105
+f 99/127/99 114/151/114 113/147/113 98/123/98
+f 107/128/107 122/136/122 121/150/121 106/126/106
+f 100/129/100 115/138/115 114/151/114 99/127/99
+f 127/145/127 142/152/142 141/153/141 126/143/126
+f 120/146/120 135/154/135 134/155/134 119/144/119
+f 113/147/113 128/156/128 82/157/82
+f 308/158/308 142/152/142 127/145/127
+f 121/150/121 136/159/136 135/154/135 120/146/120
+f 114/151/114 129/160/129 128/156/128 113/147/113
+f 122/136/122 137/161/137 136/159/136 121/150/121
+f 115/138/115 130/162/130 129/160/129 114/151/114
+f 123/135/123 138/163/138 137/161/137 122/136/122
+f 116/137/116 131/164/131 130/162/130 115/138/115
+f 124/139/124 139/165/139 138/163/138 123/135/123
+f 117/140/117 132/166/132 131/164/131 116/137/116
+f 125/141/125 140/167/140 139/165/139 124/139/124
+f 118/142/118 133/168/133 132/166/132 117/140/117
+f 126/143/126 141/153/141 140/167/140 125/141/125
+f 119/144/119 134/155/134 133/168/133 118/142/118
+f 139/165/139 154/169/154 153/170/153 138/163/138
+f 132/166/132 147/171/147 146/172/146 131/164/131
+f 140/167/140 155/173/155 154/169/154 139/165/139
+f 133/168/133 148/174/148 147/171/147 132/166/132
+f 141/153/141 156/175/156 155/173/155 140/167/140
+f 134/155/134 149/176/149 148/174/148 133/168/133
+f 142/152/142 157/177/157 156/175/156 141/153/141
+f 135/154/135 150/178/150 149/176/149 134/155/134
+f 128/156/128 143/179/143 82/180/82
+f 308/181/308 157/177/157 142/152/142
+f 136/159/136 151/182/151 150/178/150 135/154/135
+f 129/160/129 144/183/144 143/179/143 128/156/128
+f 137/161/137 152/184/152 151/182/151 136/159/136
+f 130/162/130 145/185/145 144/183/144 129/160/129
+f 138/163/138 153/170/153 152/184/152 137/161/137
+f 131/164/131 146/172/146 145/185/145 130/162/130
+f 143/179/143 158/186/158 82/187/82
+f 308/188/308 172/189/172 157/177/157
+f 151/182/151 166/190/166 165/191/165 150/178/150
+f 144/183/144 159/192/159 158/186/158 143/179/143
+f 152/184/152 167/193/167 166/190/166 151/182/151
+f 145/185/145 160/194/160 159/192/159 144/183/144
+f 153/170/153 168/195/168 167/193/167 152/184/152
+f 146/172/146 161/196/161 160/194/160 145/185/145
+f 154/169/154 169/197/169 168/195/168 153/170/153
+f 147/171/147 162/198/162 161/196/161 146/172/146
+f 155/173/155 170/199/170 169/197/169 154/169/154
+f 148/174/148 163/200/163 162/198/162 147/171/147
+f 156/175/156 171/201/171 170/199/170 155/173/155
+f 149/176/149 164/202/164 163/200/163 148/174/148
+f 157/177/157 172/189/172 171/201/171 156/175/156
+f 150/178/150 165/191/165 164/202/164 149/176/149
+f 162/198/162 177/203/177 176/204/176 161/196/161
+f 170/199/170 185/205/185 184/206/184 169/197/169
+f 163/200/163 178/207/178 177/203/177 162/198/162
+f 171/201/171 186/208/186 185/205/185 170/199/170
+f 164/202/164 179/209/179 178/207/178 163/200/163
+f 172/189/172 187/210/187 186/208/186 171/201/171
+f 165/191/165 180/211/180 179/209/179 164/202/164
+f 158/186/158 173/212/173 82/213/82
+f 308/214/308 187/210/187 172/189/172
+f 166/190/166 181/215/181 180/211/180 165/191/165
+f 159/192/159 174/216/174 173/212/173 158/186/158
+f 167/193/167 182/217/182 181/215/181 166/190/166
+f 160/194/160 175/218/175 174/216/174 159/192/159
+f 168/195/168 183/219/183 182/217/182 167/193/167
+f 161/196/161 176/204/176 175/218/175 160/194/160
+f 169/197/169 184/206/184 183/219/183 168/195/168
+f 181/215/181 196/220/196 195/221/195 180/211/180
+f 174/216/174 189/222/189 188/223/188 173/212/173
+f 182/217/182 197/224/197 196/220/196 181/215/181
+f 175/218/175 190/225/190 189/222/189 174/216/174
+f 183/219/183 198/226/198 197/224/197 182/217/182
+f 176/204/176 191/227/191 190/225/190 175/218/175
+f 184/206/184 199/228/199 198/226/198 183/219/183
+f 177/203/177 192/229/192 191/227/191 176/204/176
+f 185/205/185 200/230/200 199/228/199 184/206/184
+f 178/207/178 193/231/193 192/229/192 177/203/177
+f 186/208/186 201/232/201 200/230/200 185/205/185
+f 179/209/179 194/233/194 193/231/193 178/207/178
+f 187/210/187 202/234/202 201/232/201 186/208/186
+f 180/211/180 195/221/195 194/233/194 179/209/179
+f 173/212/173 188/223/188 82/235/82
+f 308/236/308 202/234/202 187/210/187
+f 200/230/200 215/237/215 214/238/214 199/228/199
+f 193/231/193 208/239/208 207/240/207 192/229/192
+f 201/232/201 216/241/216 215/237/215 200/230/200
+f 194/233/194 209/242/209 208/239/208 193/231/193
+f 202/234/202 217/243/217 216/241/216 201/232/201
+f 195/221/195 210/244/210 209/242/209 194/233/194
+f 188/223/188 203/245/203 82/246/82
+f 308/247/308 217/243/217 202/234/202
+f 196/220/196 211/248/211 210/244/210 195/221/195
+f 189/222/189 204/249/204 203/245/203 188/223/188
+f 197/224/197 212/250/212 211/248/211 196/220/196
+f 190/225/190 205/251/205 204/249/204 189/222/189
+f 198/226/198 213/252/213 212/250/212 197/224/197
+f 191/227/191 206/253/206 205/251/205 190/225/190
+f 199/228/199 214/238/214 213/252/213 198/226/198
+f 192/229/192 207/240/207 206/253/206 191/227/191
+f 204/249/204 219/254/219 218/255/218 203/245/203
+f 212/250/212 227/256/227 226/257/226 211/248/211
+f 205/251/205 220/258/220 219/254/219 204/249/204
+f 213/252/213 228/259/228 227/256/227 212/250/212
+f 206/253/206 221/260/221 220/258/220 205/251/205
+f 214/238/214 229/261/229 228/259/228 213/252/213
+f 207/240/207 222/262/222 221/260/221 206/253/206
+f 215/237/215 230/263/230 229/261/229 214/238/214
+f 208/239/208 223/264/223 222/262/222 207/240/207
+f 216/241/216 231/265/231 230/263/230 215/237/215
+f 209/242/209 224/266/224 223/264/223 208/239/208
+f 217/243/217 232/267/232 231/265/231 216/241/216
+f 210/244/210 225/268/225 224/266/224 209/242/209
+f 203/245/203 218/255/218 82/269/82
+f 308/270/308 232/267/232 217/243/217
+f 211/248/211 226/257/226 225/268/225 210/244/210
+f 223/264/223 238/271/238 237/272/237 222/262/222
+f 231/265/231 246/273/246 245/274/245 230/263/230
+f 224/266/224 239/275/239 238/271/238 223/264/223
+f 232/267/232 247/276/247 246/273/246 231/265/231
+f 225/268/225 240/277/240 239/275/239 224/266/224
+f 218/255/218 233/278/233 82/279/82
+f 308/280/308 247/276/247 232/267/232
+f 226/257/226 241/281/241 240/277/240 225/268/225
+f 219/254/219 234/282/234 233/278/233 218/255/218
+f 227/256/227 242/283/242 241/281/241 226/257/226
+f 220/258/220 235/284/235 234/282/234 219/254/219
+f 228/259/228 243/285/243 242/283/242 227/256/227
+f 221/260/221 236/286/236 235/284/235 220/258/220
+f 229/261/229 244/287/244 243/285/243 228/259/228
+f 222/262/222 237/272/237 236/286/236 221/260/221
+f 230/263/230 245/274/245 244/287/244 229/261/229
+f 242/283/242 257/288/257 256/289/256 241/281/241
+f 235/284/235 250/290/250 249/291/249 234/282/234
+f 243/285/243 258/292/258 257/288/257 242/283/242
+f 236/286/236 251/293/251 250/290/250 235/284/235
+f 244/287/244 259/294/259 258/292/258 243/285/243
+f 237/272/237 252/295/252 251/293/251 236/286/236
+f 245/274/245 260/296/260 259/294/259 244/287/244
+f 238/271/238 253/297/253 252/295/252 237/272/237
+f 246/273/246 261/298/261 260/296/260 245/274/245
+f 239/275/239 254/299/254 253/297/253 238/271/238
+f 247/276/247 262/300/262 261/298/261 246/273/246
+f 240/277/240 255/301/255 254/299/254 239/275/239
+f 233/278/233 248/302/248 82/303/82
+f 308/304/308 262/300/262 247/276/247
+f 241/281/241 256/289/256 255/301/255 240/277/240
+f 234/282/234 249/291/249 248/302/248 233/278/233
+f 261/298/261 276/305/276 275/306/275 260/296/260
+f 254/299/254 269/307/269 268/308/268 253/297/253
+f 262/300/262 277/309/277 276/305/276 261/298/261
+f 255/301/255 270/310/270 269/307/269 254/299/254
+f 248/302/248 263/311/263 82/312/82
+f 308/313/308 277/309/277 262/300/262
+f 256/289/256 271/314/271 270/310/270 255/301/255
+f 249/291/249 264/315/264 263/311/263 248/302/248
+f 257/288/257 272/316/272 271/314/271 256/289/256
+f 250/290/250 265/317/265 264/315/264 249/291/249
+f 258/292/258 273/318/273 272/316/272 257/288/257
+f 251/293/251 266/319/266 265/317/265 250/290/250
+f 259/294/259 274/320/274 273/318/273 258/292/258
+f 252/295/252 267/321/267 266/319/266 251/293/251
+f 260/296/260 275/306/275 274/320/274 259/294/259
+f 253/297/253 268/308/268 267/321/267 252/295/252
+f 273/318/273 288/322/288 287/323/287 272/316/272
+f 266/319/266 281/324/281 280/325/280 265/317/265
+f 274/320/274 289/326/289 288/322/288 273/318/273
+f 267/321/267 282/327/282 281/324/281 266/319/266
+f 275/306/275 290/328/290 289/326/289 274/320/274
+f 268/308/268 283/329/283 282/327/282 267/321/267
+f 276/305/276 291/330/291 290/328/290 275/306/275
+f 269/307/269 284/331/284 283/329/283 268/308/268
+f 277/309/277 292/332/292 291/330/291 276/305/276
+f 270/310/270 285/333/285 284/331/284 269/307/269
+f 263/311/263 278/334/278 82/335/82
+f 308/336/308 292/332/292 277/309/277
+f 271/314/271 286/337/286 285/333/285 270/310/270
+f 264/315/264 279/338/279 278/334/278 263/311/263
+f 272/316/272 287/323/287 286/337/286 271/314/271
+f 265/317/265 280/325/280 279/338/279 264/315/264
+f 292/332/292 307/339/307 306/340/306 291/330/291
+f 285/333/285 300/341/300 299/342/299 284/331/284
+f 278/334/278 293/343/293 82/344/82
+f 308/345/308 307/339/307 292/332/292
+f 286/337/286 301/346/301 300/341/300 285/333/285
+f 279/338/279 294/347/294 293/343/293 278/334/278
+f 287/323/287 302/348/302 301/346/301 286/337/286
+f 280/325/280 295/349/295 294/347/294 279/338/279
+f 288/322/288 303/350/303 302/348/302 287/323/287
+f 281/324/281 296/351/296 295/349/295 280/325/280
+f 289/326/289 304/352/304 303/350/303 288/322/288
+f 282/327/282 297/353/297 296/351/296 281/324/281
+f 290/328/290 305/354/305 304/352/304 289/326/289
+f 283/329/283 298/355/298 297/353/297 282/327/282
+f 291/330/291 306/340/306 305/354/305 290/328/290
+f 284/331/284 299/342/299 298/355/298 283/329/283
+f 296/351/296 312/356/312 311/357/311 295/349/295
+f 304/352/304 320/358/320 319/359/319 303/350/303
+f 297/353/297 313/360/313 312/356/312 296/351/296
+f 305/354/305 321/361/321 320/358/320 304/352/304
+f 298/355/298 314/362/314 313/360/313 297/353/297
+f 306/340/306 322/363/322 321/361/321 305/354/305
+f 299/342/299 315/364/315 314/362/314 298/355/298
+f 307/339/307 323/365/323 322/363/322 306/340/306
+f 300/341/300 316/366/316 315/364/315 299/342/299
+f 293/343/293 309/367/309 82/368/82
+f 308/369/308 323/365/323 307/339/307
+f 301/346/301 317/370/317 316/366/316 300/341/300
+f 294/347/294 310/371/310 309/367/309 293/343/293
+f 302/348/302 318/372/318 317/370/317 301/346/301
+f 295/349/295 311/357/311 310/371/310 294/347/294
+f 303/350/303 319/359/319 318/372/318 302/348/302
+f 316/366/316 331/373/331 330/374/330 315/364/315
+f 309/367/309 324/375/324 82/376/82
+f 308/377/308 338/378/338 323/365/323
+f 317/370/317 332/379/332 331/373/331 316/366/316
+f 310/371/310 325/380/325 324/375/324 309/367/309
+f 318/372/318 333/381/333 332/379/332 317/370/317
+f 311/357/311 326/382/326 325/380/325 310/371/310
+f 319/359/319 334/383/334 333/381/333 318/372/318
+f 312/356/312 327/384/327 326/382/326 311/357/311
+f 320/358/320 335/385/335 334/383/334 319/359/319
+f 313/360/313 328/386/328 327/384/327 312/356/312
+f 321/361/321 336/387/336 335/385/335 320/358/320
+f 314/362/314 329/388/329 328/386/328 313/360/313
+f 322/363/322 337/389/337 336/387/336 321/361/321
+f 315/364/315 330/374/330 329/388/329 314/362/314
+f 323/365/323 338/378/338 337/389/337 322/363/322
+f 335/385/335 350/390/350 349/391/349 334/383/334
+f 328/386/328 343/392/343 342/393/342 327/384/327
+f 336/387/336 351/394/351 350/390/350 335/385/335
+f 329/388/329 344/395/344 343/392/343 328/386/328
+f 337/389/337 352/396/352 351/394/351 336/387/336
+f 330/374/330 345/397/345 344/395/344 329/388/329
+f 338/378/338 353/398/353 352/396/352 337/389/337
+f 331/373/331 346/399/346 345/397/345 330/374/330
+f 324/375/324 339/400/339 82/401/82
+f 308/402/308 353/398/353 338/378/338
+f 332/379/332 347/403/347 346/399/346 331/373/331
+f 325/380/325 340/404/340 339/400/339 324/375/324
+f 333/381/333 348/405/348 347/403/347 332/379/332
+f 326/382/326 341/406/341 340/404/340 325/380/325
+f 334/383/334 349/391/349 348/405/348 333/381/333
+f 327/384/327 342/393/342 341/406/341 326/382/326
+f 339/400/339 354/407/354 82/408/82
+f 308/409/308 368/410/368 353/398/353
+f 347/403/347 362/411/362 361/412/361 346/399/346
+f 340/404/340 355/413/355 354/407/354 339/400/339
+f 348/405/348 363/414/363 362/411/362 347/403/347
+f 341/406/341 356/415/356 355/413/355 340/404/340
+f 349/391/349 364/416/364 363/414/363 348/405/348
+f 342/393/342 357/417/357 356/415/356 341/406/341
+f 350/390/350 365/418/365 364/416/364 349/391/349
+f 343/392/343 358/419/358 357/417/357 342/393/342
+f 351/394/351 366/420/366 365/418/365 350/390/350
+f 344/395/344 359/421/359 358/419/358 343/392/343
+f 352/396/352 367/422/367 366/420/366 351/394/351
+f 345/397/345 360/423/360 359/421/359 344/395/344
+f 353/398/353 368/410/368 367/422/367 352/396/352
+f 346/399/346 361/412/361 360/423/360 345/397/345
+f 358/424/358 373/425/373 372/426/372 357/427/357
+f 366/428/366 381/429/381 380/430/380 365/431/365
+f 359/432/359 374/433/374 373/425/373 358/424/358
+f 367/434/367 382/435/382 381/429/381 366/428/366
+f 360/436/360 375/437/375 374/433/374 359/432/359
+f 368/438/368 383/439/383 382/435/382 367/434/367
+f 361/440/361 376/441/376 375/437/375 360/436/360
+f 354/442/354 369/443/369 82/444/82
+f 308/445/308 383/439/383 368/438/368
+f 362/446/362 377/447/377 376/441/376 361/440/361
+f 355/448/355 370/449/370 369/443/369 354/442/354
+f 363/450/363 378/451/378 377/447/377 362/446/362
+f 356/452/356 371/453/371 370/449/370 355/448/355
+f 364/454/364 379/455/379 378/451/378 363/450/363
+f 357/427/357 372/426/372 371/453/371 356/452/356
+f 365/431/365 380/430/380 379/455/379 364/454/364
+f 377/447/377 392/456/392 391/457/391 376/441/376
+f 370/449/370 385/458/385 384/459/384 369/443/369
+f 378/451/378 393/460/393 392/456/392 377/447/377
+f 371/453/371 386/461/386 385/458/385 370/449/370
+f 379/455/379 394/462/394 393/460/393 378/451/378
+f 372/426/372 387/463/387 386/461/386 371/453/371
+f 380/430/380 395/464/395 394/462/394 379/455/379
+f 373/425/373 388/465/388 387/463/387 372/426/372
+f 381/429/381 396/466/396 395/464/395 380/430/380
+f 374/433/374 389/467/389 388/465/388 373/425/373
+f 382/435/382 397/468/397 396/466/396 381/429/381
+f 375/437/375 390/469/390 389/467/389 374/433/374
+f 383/439/383 398/470/398 397/468/397 382/435/382
+f 376/441/376 391/457/391 390/469/390 375/437/375
+f 369/443/369 384/459/384 82/471/82
+f 308/472/308 398/470/398 383/439/383
+f 396/466/396 411/473/411 410/474/410 395/464/395
+f 389/467/389 404/475/404 403/476/403 388/465/388
+f 397/468/397 412/477/412 411/473/411 396/466/396
+f 390/469/390 405/478/405 404/475/404 389/467/389
+f 398/470/398 413/479/413 412/477/412 397/468/397
+f 391/457/391 406/480/406 405/478/405 390/469/390
+f 384/459/384 399/481/399 82/482/82
+f 308/483/308 413/479/413 398/470/398
+f 392/456/392 407/484/407 406/480/406 391/457/391
+f 385/458/385 400/485/400 399/481/399 384/459/384
+f 393/460/393 408/486/408 407/484/407 392/456/392
+f 386/461/386 401/487/401 400/485/400 385/458/385
+f 394/462/394 409/488/409 408/486/408 393/460/393
+f 387/463/387 402/489/402 401/487/401 386/461/386
+f 395/464/395 410/474/410 409/488/409 394/462/394
+f 388/465/388 403/476/403 402/489/402 387/463/387
+f 408/486/408 423/490/423 422/491/422 407/484/407
+f 401/487/401 416/492/416 415/493/415 400/485/400
+f 409/488/409 424/494/424 423/490/423 408/486/408
+f 402/489/402 417/495/417 416/492/416 401/487/401
+f 410/474/410 425/496/425 424/494/424 409/488/409
+f 403/476/403 418/497/418 417/495/417 402/489/402
+f 411/473/411 426/498/426 425/496/425 410/474/410
+f 404/475/404 419/499/419 418/497/418 403/476/403
+f 412/477/412 427/500/427 426/498/426 411/473/411
+f 405/478/405 420/501/420 419/499/419 404/475/404
+f 413/479/413 428/502/428 427/500/427 412/477/412
+f 406/480/406 421/503/421 420/501/420 405/478/405
+f 399/481/399 414/504/414 82/505/82
+f 308/506/308 428/502/428 413/479/413
+f 407/484/407 422/491/422 421/503/421 406/480/406
+f 400/485/400 415/493/415 414/504/414 399/481/399
+f 427/500/427 442/507/442 441/508/441 426/498/426
+f 420/501/420 435/509/435 434/510/434 419/499/419
+f 428/502/428 443/511/443 442/507/442 427/500/427
+f 421/503/421 436/512/436 435/509/435 420/501/420
+f 414/504/414 429/513/429 82/514/82
+f 308/515/308 443/511/443 428/502/428
+f 422/491/422 437/516/437 436/512/436 421/503/421
+f 415/493/415 430/517/430 429/513/429 414/504/414
+f 423/490/423 438/518/438 437/516/437 422/491/422
+f 416/492/416 431/519/431 430/517/430 415/493/415
+f 424/494/424 439/520/439 438/518/438 423/490/423
+f 417/495/417 432/521/432 431/519/431 416/492/416
+f 425/496/425 440/522/440 439/520/439 424/494/424
+f 418/497/418 433/523/433 432/521/432 417/495/417
+f 426/498/426 441/508/441 440/522/440 425/496/425
+f 419/499/419 434/510/434 433/523/433 418/497/418
+f 431/519/431 446/524/446 445/525/445 430/517/430
+f 439/520/439 454/526/454 453/527/453 438/518/438
+f 432/521/432 447/528/447 446/524/446 431/519/431
+f 440/522/440 455/529/455 454/526/454 439/520/439
+f 433/523/433 448/530/448 447/528/447 432/521/432
+f 441/508/441 456/531/456 455/529/455 440/522/440
+f 434/510/434 449/532/449 448/530/448 433/523/433
+f 442/507/442 457/533/457 456/531/456 441/508/441
+f 435/509/435 450/534/450 449/532/449 434/510/434
+f 443/511/443 458/535/458 457/533/457 442/507/442
+f 436/512/436 451/536/451 450/534/450 435/509/435
+f 429/513/429 444/537/444 82/538/82
+f 308/539/308 458/535/458 443/511/443
+f 437/516/437 452/540/452 451/536/451 436/512/436
+f 430/517/430 445/525/445 444/537/444 429/513/429
+f 438/518/438 453/527/453 452/540/452 437/516/437
+f 450/534/450 465/541/465 464/542/464 449/532/449
+f 458/535/458 473/543/473 472/544/472 457/533/457
+f 451/536/451 466/545/466 465/541/465 450/534/450
+f 444/537/444 459/546/459 82/547/82
+f 308/548/308 473/543/473 458/535/458
+f 452/540/452 467/549/467 466/545/466 451/536/451
+f 445/525/445 460/550/460 459/546/459 444/537/444
+f 453/527/453 468/551/468 467/549/467 452/540/452
+f 446/524/446 461/552/461 460/550/460 445/525/445
+f 454/526/454 469/553/469 468/551/468 453/527/453
+f 447/528/447 462/554/462 461/552/461 446/524/446
+f 455/529/455 470/555/470 469/553/469 454/526/454
+f 448/530/448 463/556/463 462/554/462 447/528/447
+f 456/531/456 471/557/471 470/555/470 455/529/455
+f 449/532/449 464/542/464 463/556/463 448/530/448
+f 457/533/457 472/544/472 471/557/471 456/531/456
+f 469/553/469 6/5/6 478/8/478 468/551/468
+f 462/554/462 476/9/476 1/1/1 461/552/461
+f 470/555/470 479/11/479 6/5/6 469/553/469
+f 463/556/463 2/13/2 476/9/476 462/554/462
+f 471/557/471 480/15/480 479/11/479 470/555/470
+f 464/542/464 477/17/477 2/13/2 463/556/463
+f 472/544/472 481/19/481 480/15/480 471/557/471
+f 465/541/465 3/21/3 477/17/477 464/542/464
+f 473/543/473 482/23/482 481/19/481 472/544/472
+f 466/545/466 4/25/4 3/21/3 465/541/465
+f 459/546/459 474/27/474 82/558/82
+f 308/559/308 482/23/482 473/543/473
+f 467/549/467 5/31/5 4/25/4 466/545/466
+f 460/550/460 475/4/475 474/27/474 459/546/459
+f 468/551/468 478/8/478 5/31/5 467/549/467
+f 461/552/461 1/1/1 475/4/475 460/550/460
diff --git a/paperscope/assets/aurora.jpg b/paperscope/assets/aurora.jpg
new file mode 100644
index 00000000..c8143491
Binary files /dev/null and b/paperscope/assets/aurora.jpg differ
diff --git a/paperscope/assets/button_animation_0001.png b/paperscope/assets/button_animation_0001.png
new file mode 100644
index 00000000..b0020920
Binary files /dev/null and b/paperscope/assets/button_animation_0001.png differ
diff --git a/paperscope/assets/button_animation_0002.png b/paperscope/assets/button_animation_0002.png
new file mode 100644
index 00000000..2fec4784
Binary files /dev/null and b/paperscope/assets/button_animation_0002.png differ
diff --git a/paperscope/assets/button_animation_0003.png b/paperscope/assets/button_animation_0003.png
new file mode 100644
index 00000000..22c344e5
Binary files /dev/null and b/paperscope/assets/button_animation_0003.png differ
diff --git a/paperscope/assets/button_animation_0004.png b/paperscope/assets/button_animation_0004.png
new file mode 100644
index 00000000..2f6c0413
Binary files /dev/null and b/paperscope/assets/button_animation_0004.png differ
diff --git a/paperscope/assets/button_animation_0005.png b/paperscope/assets/button_animation_0005.png
new file mode 100644
index 00000000..eb05dfc3
Binary files /dev/null and b/paperscope/assets/button_animation_0005.png differ
diff --git a/paperscope/assets/button_animation_0006.png b/paperscope/assets/button_animation_0006.png
new file mode 100644
index 00000000..462e99d9
Binary files /dev/null and b/paperscope/assets/button_animation_0006.png differ
diff --git a/paperscope/assets/button_animation_0007.png b/paperscope/assets/button_animation_0007.png
new file mode 100644
index 00000000..a9c49b9a
Binary files /dev/null and b/paperscope/assets/button_animation_0007.png differ
diff --git a/paperscope/assets/button_animation_0008.png b/paperscope/assets/button_animation_0008.png
new file mode 100644
index 00000000..121cf18d
Binary files /dev/null and b/paperscope/assets/button_animation_0008.png differ
diff --git a/paperscope/assets/button_animation_0009.png b/paperscope/assets/button_animation_0009.png
new file mode 100644
index 00000000..72bfbeb9
Binary files /dev/null and b/paperscope/assets/button_animation_0009.png differ
diff --git a/paperscope/assets/button_animation_0010.png b/paperscope/assets/button_animation_0010.png
new file mode 100644
index 00000000..bdc50622
Binary files /dev/null and b/paperscope/assets/button_animation_0010.png differ
diff --git a/paperscope/assets/button_animation_0011.png b/paperscope/assets/button_animation_0011.png
new file mode 100644
index 00000000..f55e6174
Binary files /dev/null and b/paperscope/assets/button_animation_0011.png differ
diff --git a/paperscope/assets/button_animation_0012.png b/paperscope/assets/button_animation_0012.png
new file mode 100644
index 00000000..81bbdd20
Binary files /dev/null and b/paperscope/assets/button_animation_0012.png differ
diff --git a/paperscope/assets/button_animation_0013.png b/paperscope/assets/button_animation_0013.png
new file mode 100644
index 00000000..1967a33d
Binary files /dev/null and b/paperscope/assets/button_animation_0013.png differ
diff --git a/paperscope/assets/button_animation_0014.png b/paperscope/assets/button_animation_0014.png
new file mode 100644
index 00000000..7372abdd
Binary files /dev/null and b/paperscope/assets/button_animation_0014.png differ
diff --git a/paperscope/assets/button_animation_0015.png b/paperscope/assets/button_animation_0015.png
new file mode 100644
index 00000000..df6db5b7
Binary files /dev/null and b/paperscope/assets/button_animation_0015.png differ
diff --git a/paperscope/assets/exhibit-bw.png b/paperscope/assets/exhibit-bw.png
new file mode 100644
index 00000000..1000b470
Binary files /dev/null and b/paperscope/assets/exhibit-bw.png differ
diff --git a/paperscope/assets/exhibit-color.png b/paperscope/assets/exhibit-color.png
new file mode 100644
index 00000000..6672e263
Binary files /dev/null and b/paperscope/assets/exhibit-color.png differ
diff --git a/paperscope/assets/home_mono.jpg b/paperscope/assets/home_mono.jpg
new file mode 100644
index 00000000..e645fd6e
Binary files /dev/null and b/paperscope/assets/home_mono.jpg differ
diff --git a/paperscope/assets/mymovies-bw.png b/paperscope/assets/mymovies-bw.png
new file mode 100644
index 00000000..0ba004ee
Binary files /dev/null and b/paperscope/assets/mymovies-bw.png differ
diff --git a/paperscope/assets/mymovies-color.png b/paperscope/assets/mymovies-color.png
new file mode 100644
index 00000000..819c9647
Binary files /dev/null and b/paperscope/assets/mymovies-color.png differ
diff --git a/paperscope/assets/photospheres-bw.png b/paperscope/assets/photospheres-bw.png
new file mode 100644
index 00000000..873a99d8
Binary files /dev/null and b/paperscope/assets/photospheres-bw.png differ
diff --git a/paperscope/assets/photospheres-color.png b/paperscope/assets/photospheres-color.png
new file mode 100644
index 00000000..2c712b6a
Binary files /dev/null and b/paperscope/assets/photospheres-color.png differ
diff --git a/paperscope/assets/tilt_animation_0000.png b/paperscope/assets/tilt_animation_0000.png
new file mode 100644
index 00000000..03829243
Binary files /dev/null and b/paperscope/assets/tilt_animation_0000.png differ
diff --git a/paperscope/assets/tilt_animation_0001.png b/paperscope/assets/tilt_animation_0001.png
new file mode 100644
index 00000000..de6db6a9
Binary files /dev/null and b/paperscope/assets/tilt_animation_0001.png differ
diff --git a/paperscope/assets/tilt_animation_0002.png b/paperscope/assets/tilt_animation_0002.png
new file mode 100644
index 00000000..58552797
Binary files /dev/null and b/paperscope/assets/tilt_animation_0002.png differ
diff --git a/paperscope/assets/tilt_animation_0003.png b/paperscope/assets/tilt_animation_0003.png
new file mode 100644
index 00000000..34b568c4
Binary files /dev/null and b/paperscope/assets/tilt_animation_0003.png differ
diff --git a/paperscope/assets/tilt_animation_0004.png b/paperscope/assets/tilt_animation_0004.png
new file mode 100644
index 00000000..c7126d9f
Binary files /dev/null and b/paperscope/assets/tilt_animation_0004.png differ
diff --git a/paperscope/assets/tilt_animation_0005.png b/paperscope/assets/tilt_animation_0005.png
new file mode 100644
index 00000000..e3f47298
Binary files /dev/null and b/paperscope/assets/tilt_animation_0005.png differ
diff --git a/paperscope/assets/tilt_animation_0006.png b/paperscope/assets/tilt_animation_0006.png
new file mode 100644
index 00000000..af203e9c
Binary files /dev/null and b/paperscope/assets/tilt_animation_0006.png differ
diff --git a/paperscope/assets/tilt_animation_0007.png b/paperscope/assets/tilt_animation_0007.png
new file mode 100644
index 00000000..0c7699ca
Binary files /dev/null and b/paperscope/assets/tilt_animation_0007.png differ
diff --git a/paperscope/assets/tilt_animation_0008.png b/paperscope/assets/tilt_animation_0008.png
new file mode 100644
index 00000000..c76a7dbf
Binary files /dev/null and b/paperscope/assets/tilt_animation_0008.png differ
diff --git a/paperscope/assets/tilt_animation_0009.png b/paperscope/assets/tilt_animation_0009.png
new file mode 100644
index 00000000..ee51cd40
Binary files /dev/null and b/paperscope/assets/tilt_animation_0009.png differ
diff --git a/paperscope/assets/tilt_animation_0010.png b/paperscope/assets/tilt_animation_0010.png
new file mode 100644
index 00000000..f6e895f7
Binary files /dev/null and b/paperscope/assets/tilt_animation_0010.png differ
diff --git a/paperscope/assets/tilt_animation_0011.png b/paperscope/assets/tilt_animation_0011.png
new file mode 100644
index 00000000..dd788e41
Binary files /dev/null and b/paperscope/assets/tilt_animation_0011.png differ
diff --git a/paperscope/assets/tilt_animation_0012.png b/paperscope/assets/tilt_animation_0012.png
new file mode 100644
index 00000000..bace3c4f
Binary files /dev/null and b/paperscope/assets/tilt_animation_0012.png differ
diff --git a/paperscope/assets/tilt_animation_0013.png b/paperscope/assets/tilt_animation_0013.png
new file mode 100644
index 00000000..02c6e044
Binary files /dev/null and b/paperscope/assets/tilt_animation_0013.png differ
diff --git a/paperscope/assets/tilt_animation_0014.png b/paperscope/assets/tilt_animation_0014.png
new file mode 100644
index 00000000..558ffd78
Binary files /dev/null and b/paperscope/assets/tilt_animation_0014.png differ
diff --git a/paperscope/assets/tutorial-bw.png b/paperscope/assets/tutorial-bw.png
new file mode 100644
index 00000000..88166fcd
Binary files /dev/null and b/paperscope/assets/tutorial-bw.png differ
diff --git a/paperscope/assets/tutorial-color.png b/paperscope/assets/tutorial-color.png
new file mode 100644
index 00000000..a04be4b7
Binary files /dev/null and b/paperscope/assets/tutorial-color.png differ
diff --git a/paperscope/build.gradle b/paperscope/build.gradle
new file mode 100644
index 00000000..7c9cf848
--- /dev/null
+++ b/paperscope/build.gradle
@@ -0,0 +1,104 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdk = 35
+ lintOptions {
+ abortOnError = false
+ }
+ defaultConfig {
+ applicationId "com.google.cardboard.paperscope"
+ // #gles3 #vulkan - You can reduce minSdkVersion down to 16 depending
+ // on rendering APIs supported:
+ //
+ // OpenGL ES 2.0 - Requires 16 or higher
+ // OpenGL ES 3.0 - Requires 18 or higher
+ // Vulkan - Requires 24 or higher
+ //
+ // See the release notes for details.
+ minSdkVersion = 26
+ targetSdkVersion = 35
+ versionCode 1
+ versionName "1.33.0"
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ ndk {
+ abiFilters 'armeabi-v7a', 'arm64-v8a'
+ }
+ externalNativeBuild.cmake {
+ arguments "-DANDROID_STL=c++_shared"
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ externalNativeBuild.cmake {
+ path "CMakeLists.txt"
+ }
+ namespace = 'com.google.vr.cardboard.paperscope.demo'
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ java.srcDirs = [
+ 'about',
+ 'common',
+ 'core',
+ 'exhibit',
+ 'magicwindow',
+ 'myvideos',
+ 'photosphere',
+ 'settings',
+ ]
+ res.srcDirs = ['res', 'about/res', 'common/res', 'myvideos/res', 'settings/res', 'magicwindow/res']
+ assets.srcDirs = ['assets', 'photosphere/assets', 'exhibit/assets']
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.7.1'
+ // Android Mobile Vision
+ // TODO(b/213613345) Migrate to ML Kit.
+ implementation 'com.google.android.gms:play-services-vision:20.1.3'
+ implementation 'com.google.android.material:material:1.13.0'
+ implementation 'com.google.protobuf:protobuf-javalite:4.34.1'
+ implementation "com.google.flogger:flogger:0.9"
+ implementation 'com.google.flogger:flogger-system-backend:0.9'
+ implementation 'com.google.guava:guava:33.5.0-android'
+ implementation 'com.adobe.xmp:xmpcore:5.1.3'
+ implementation 'androidx.palette:palette:1.0.0'
+ implementation project(":sdk")
+}
+
+// The dependencies for NDK builds live inside the .aar files so they need to
+// be extracted before NDK targets can link against.
+task extractNdk {
+ dependsOn ':sdk:assembleRelease'
+ doLast {
+ copy {
+ from zipTree("${project.rootDir}/sdk/build/outputs/aar/sdk-release.aar")
+ into "libraries/"
+ include "jni/**/libGfxPluginCardboard.so"
+ }
+ copy {
+ from "${project.rootDir}/sdk/include/cardboard.h"
+ into "libraries/"
+ }
+ }
+}
+
+task deleteNdk(type: Delete) {
+ delete "libraries/jni"
+ delete "libraries/cardboard.h"
+}
+
+preBuild.dependsOn(extractNdk)
+clean.dependsOn(deleteNdk)
diff --git a/paperscope/common/AndroidManifest.xml b/paperscope/common/AndroidManifest.xml
new file mode 100644
index 00000000..f8d52fa1
--- /dev/null
+++ b/paperscope/common/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/paperscope/common/DemoBaseActivity.java b/paperscope/common/DemoBaseActivity.java
new file mode 100644
index 00000000..c554e9de
--- /dev/null
+++ b/paperscope/common/DemoBaseActivity.java
@@ -0,0 +1,82 @@
+package com.google.vr.cardboard.paperscope.demo.common;
+
+import com.google.vr.cardboard.paperscope.demo.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.OrientationEventListener;
+import com.google.cardboard.sdk.CardboardView;
+import com.google.cardboard.sdk.CardboardView.Renderer;
+import com.google.common.flogger.FluentLogger;
+import com.google.vr.cardboard.paperscope.demo.settings.SettingsActivity;
+
+/** Base activity class for the demos. */
+public abstract class DemoBaseActivity extends AppCompatActivity {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ protected DemoBaseController extends Renderer> demoActivityController;
+ private OrientationEventListener orientationEventListener;
+ protected CardboardView cardboardView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.demo_base_hud);
+ cardboardView = findViewById(R.id.cardboard_view);
+
+ demoActivityController = createController();
+ initializeOrientationEventListener();
+
+ demoActivityController.setOnCloseButtonListener(this::finish);
+ demoActivityController.setOnSettingsButtonListener(this::showSettings);
+ demoActivityController.enableOnTriggerEventListener();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ demoActivityController.onResume();
+
+ if (orientationEventListener != null) {
+ orientationEventListener.enable();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ demoActivityController.onPause();
+
+ if (orientationEventListener != null) {
+ orientationEventListener.disable();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ demoActivityController.shutdown();
+ orientationEventListener = null;
+ }
+
+ protected abstract DemoBaseController extends Renderer> createController();
+
+ private void initializeOrientationEventListener() {
+ orientationEventListener =
+ new OrientationEventListener(this) {
+ @Override
+ public void onOrientationChanged(int orientation) {
+ if (demoActivityController != null
+ && demoActivityController.shouldFinishActivity(orientation)) {
+ logger.atInfo().log("Finishing activity due to orientation change.");
+ finish();
+ }
+ }
+ };
+ }
+
+ private void showSettings() {
+ Intent intent = new Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ }
+}
diff --git a/paperscope/common/DemoBaseController.java b/paperscope/common/DemoBaseController.java
new file mode 100644
index 00000000..bc4b64c9
--- /dev/null
+++ b/paperscope/common/DemoBaseController.java
@@ -0,0 +1,107 @@
+package com.google.vr.cardboard.paperscope.demo.common;
+
+import static java.lang.Math.min;
+
+import android.view.OrientationEventListener;
+import com.google.cardboard.sdk.CardboardView;
+import com.google.cardboard.sdk.CardboardView.Renderer;
+
+/** Controller for the demo activity. */
+public class DemoBaseController {
+ private static final String TAG = DemoBaseController.class.getSimpleName();
+ protected final CardboardView cardboardView;
+ protected T renderer;
+ private static final int TILT_DETECTION_TOLERANCE_IN_DEGREES = 30;
+ private static final int LANDSCAPE_LEFT_DEGREES = 270;
+ private static final int LANDSCAPE_RIGHT_DEGREES = 90;
+
+ public DemoBaseController(CardboardView cardboardView) {
+ this.cardboardView = cardboardView;
+ CardboardView.setUseCardboardGlSurfaceView(true);
+ this.cardboardView.setOnViewDetachedRunnable(this::shutdown);
+ this.cardboardView.setStereoRenderMode(true);
+ }
+
+ public void setOnCloseButtonListener(Runnable onCloseButtonListener) {
+ cardboardView.setOnBackButtonClick(onCloseButtonListener);
+ }
+
+ public void setOnSettingsButtonListener(Runnable onSettingsButtonListener) {
+ cardboardView.setOnSettingsButtonClick(onSettingsButtonListener);
+ }
+
+ public void enableOnTriggerEventListener() {}
+
+ public void onResume() {
+ cardboardView.onResume();
+ }
+
+ public void onPause() {
+ cardboardView.onPause();
+ }
+
+ public void shutdown() {
+ if (renderer != null) {
+ renderer.onRendererShutdown();
+ }
+ cardboardView.onDestroy();
+ }
+
+ /**
+ * Determines whether the activity should be finished based on the device's orientation.
+ *
+ * @param orientation The current orientation of the device in degrees.
+ * @return True if the activity should be finished, false otherwise.
+ */
+ public boolean shouldFinishActivity(int orientation) {
+ if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
+ return false;
+ }
+
+ return !isLandscape(orientation);
+ }
+
+ /**
+ * Checks if the device is in a landscape orientation (left or right).
+ *
+ * @param orientationDegrees The current orientation of the device in degrees.
+ * @return True if the device is in a landscape orientation, false otherwise.
+ */
+ private boolean isLandscape(int orientationDegrees) {
+ return isLandscapeLeft(orientationDegrees) || isLandscapeRight(orientationDegrees);
+ }
+
+ /**
+ * Checks if the device is in a left landscape orientation.
+ *
+ * @param orientationDegrees The current orientation of the device in degrees.
+ * @return True if the device is in a left landscape orientation, false otherwise.
+ */
+ private boolean isLandscapeLeft(int orientationDegrees) {
+ return getCircularDistance(orientationDegrees, LANDSCAPE_LEFT_DEGREES)
+ < TILT_DETECTION_TOLERANCE_IN_DEGREES;
+ }
+
+ /**
+ * Checks if the device is in a right landscape orientation.
+ *
+ * @param orientationDegrees The current orientation of the device in degrees.
+ * @return True if the device is in a right landscape orientation, false otherwise.
+ */
+ private boolean isLandscapeRight(int orientationDegrees) {
+ return getCircularDistance(orientationDegrees, LANDSCAPE_RIGHT_DEGREES)
+ < TILT_DETECTION_TOLERANCE_IN_DEGREES;
+ }
+
+ /**
+ * Returns the shortest distance between two angles on a circle.
+ *
+ * @param angle1 The first angle, in degrees.
+ * @param angle2 The second angle, in degrees.
+ * @return The shortest distance, in degrees.
+ */
+ private int getCircularDistance(int angle1, int angle2) {
+ int diff = Math.abs(angle1 - angle2);
+ return min(diff, 360 - diff);
+ }
+}
diff --git a/paperscope/common/res/layout/demo_base_hud.xml b/paperscope/common/res/layout/demo_base_hud.xml
new file mode 100644
index 00000000..85186011
--- /dev/null
+++ b/paperscope/common/res/layout/demo_base_hud.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/paperscope/core/AppGridView.java b/paperscope/core/AppGridView.java
new file mode 100644
index 00000000..c5d98489
--- /dev/null
+++ b/paperscope/core/AppGridView.java
@@ -0,0 +1,43 @@
+package com.google.vr.cardboard.paperscope.demo;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.GridView;
+
+/**
+ * This class holds the {@link CardView} elements that are displayed in the {@link MainActivity}.
+ *
+ * Customized {@link GridView} that correctly sets its height to {@link
+ * android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.
+ *
+ * @see Related discussion on StackOverflow.
+ */
+public class AppGridView extends GridView {
+ public AppGridView(Context context) {
+ super(context);
+ }
+
+ public AppGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AppGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int heightSpec;
+
+ if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
+ // The two leftmost bits in the height measure spec have
+ // a special meaning, hence we can't use them to describe height.
+ heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
+ } else {
+ // Any other height should be respected as is.
+ heightSpec = heightMeasureSpec;
+ }
+
+ super.onMeasure(widthMeasureSpec, heightSpec);
+ }
+}
diff --git a/paperscope/core/BasicAppInfo.java b/paperscope/core/BasicAppInfo.java
new file mode 100644
index 00000000..5b7f428b
--- /dev/null
+++ b/paperscope/core/BasicAppInfo.java
@@ -0,0 +1,57 @@
+package com.google.vr.cardboard.paperscope.demo;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import androidx.palette.graphics.Palette;
+
+/**
+ * This class holds the information necessary for constructing the supported apps list in the {@link
+ * GridView}.
+ */
+public class BasicAppInfo {
+ private static final int ICON_SIZE_PIXELS = 100;
+ private String packageName;
+ private String appName;
+ private Intent startIntent;
+ private Drawable icon;
+ private Palette palette;
+
+ public BasicAppInfo(String packageName, String appName, Intent startIntent, Drawable icon) {
+ this.packageName = packageName;
+ this.appName = appName;
+ this.startIntent = startIntent;
+ this.icon = icon;
+ this.palette =
+ Palette.from(convertToBitmap(icon, ICON_SIZE_PIXELS, ICON_SIZE_PIXELS)).generate();
+ }
+
+ private Bitmap convertToBitmap(Drawable drawable, int widthPixels, int heightPixels) {
+ Bitmap mutableBitmap = Bitmap.createBitmap(widthPixels, heightPixels, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(mutableBitmap);
+ drawable.setBounds(0, 0, widthPixels, heightPixels);
+ drawable.draw(canvas);
+ return mutableBitmap;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public String getAppName() {
+ return appName;
+ }
+
+ public Intent getStartIntent() {
+ return startIntent;
+ }
+
+ public Drawable getIcon() {
+ return icon;
+ }
+
+ public Palette getPalette() {
+ return palette;
+ }
+}
diff --git a/paperscope/core/CardboardLauncherActivity.java b/paperscope/core/CardboardLauncherActivity.java
new file mode 100644
index 00000000..06fcba9e
--- /dev/null
+++ b/paperscope/core/CardboardLauncherActivity.java
@@ -0,0 +1,256 @@
+package com.google.vr.cardboard.paperscope.demo;
+
+import com.google.vr.cardboard.paperscope.demo.R;
+
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.opengl.GLSurfaceView;
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import android.util.Log;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.PopupMenu;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
+import com.google.vr.cardboard.paperscope.demo.settings.SettingsActivity;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * A Google Cardboard demo application.
+ *
+ *
This is the main Activity for the demo application. It initializes a GLSurfaceView to allow
+ * rendering.
+ */
+public class CardboardLauncherActivity extends AppCompatActivity
+ implements PopupMenu.OnMenuItemClickListener {
+ static {
+ System.loadLibrary("cardboard_jni");
+ }
+
+ private static final String TAG = CardboardLauncherActivity.class.getSimpleName();
+
+ // Opaque native pointer to the native CardboardApp instance.
+ // This object is owned by the CardboardLauncherActivity instance
+ // and passed to the native methods.
+ private long nativeApp;
+ private TransitionView transitionView;
+ private TransitionView.Listener transitionViewListener;
+ private GLSurfaceView glView;
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public void onCreate(Bundle savedInstance) {
+ super.onCreate(savedInstance);
+
+ nativeApp = nativeOnCreate(getAssets());
+
+ setContentView(R.layout.activity_vr);
+ glView = findViewById(R.id.surface_view);
+ glView.setEGLContextClientVersion(2);
+ Renderer renderer = new Renderer();
+ glView.setRenderer(renderer);
+ glView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ glView.setOnTouchListener(
+ (v, event) -> {
+ if (nativeShouldLaunchDemo(nativeApp)) {
+ launchDemo(nativeDemoToLaunch(nativeApp));
+ }
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ // Signal a trigger event.
+ glView.queueEvent(
+ () -> {
+ nativeOnTriggerEvent(nativeApp);
+ });
+ return true;
+ }
+ return false;
+ });
+
+ // TODO(b/139010241): Avoid that action and status bar are displayed when pressing settings
+ // button.
+ setImmersiveSticky();
+
+ // Forces screen to max brightness.
+ WindowManager.LayoutParams layout = getWindow().getAttributes();
+ layout.screenBrightness = 1.f;
+ getWindow().setAttributes(layout);
+
+ // Prevents screen from dimming/locking.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ // Buttons listeners for close and settings.
+ ImageButton closeButton = findViewById(R.id.ui_back_button);
+ closeButton.setOnClickListener(
+ (view) -> {
+ Log.d(TAG, "Close button clicked");
+ closeSample();
+ });
+ ImageButton settingsButton = findViewById(R.id.ui_settings_button);
+ settingsButton.setOnClickListener(
+ (view) -> {
+ Log.d(TAG, "Settings button clicked");
+ showSettings(view);
+ });
+
+ // Create TransitionView Listener to pass to the Transition View
+ transitionViewListener =
+ new TransitionView.Listener() {
+ @Override
+ public void onSwitchButtonClicked() {
+ Intent intent = new Intent(CardboardLauncherActivity.this, SettingsActivity.class);
+ startActivity(intent);
+ }
+
+ @Override
+ public void onExitButtonClicked() {
+ closeSample();
+ }
+ };
+
+ // Create the full-screen custom view
+ transitionView = new TransitionView(this, transitionViewListener);
+
+ // Set layout params to fill the screen
+ FrameLayout.LayoutParams params =
+ new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ // Add the TransitionView custom view to the activity
+ addContentView(transitionView, params);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ nativeOnPause(nativeApp);
+ glView.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ glView.onResume();
+ nativeOnResume(nativeApp);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ nativeOnDestroy(nativeApp);
+ nativeApp = 0;
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus) {
+ setImmersiveSticky();
+ }
+ }
+
+ private class Renderer implements GLSurfaceView.Renderer {
+ @Override
+ public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
+ nativeOnSurfaceCreated(nativeApp);
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl10, int width, int height) {
+ nativeSetScreenParams(nativeApp, width, height);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl10) {
+ nativeOnDrawFrame(nativeApp);
+ }
+ }
+
+ /** Callback for when close button is pressed. */
+ public void closeSample() {
+ Log.d(TAG, "Leaving VR sample");
+ finish();
+ }
+
+ /** Callback for when settings_menu button is pressed. */
+ public void showSettings(View view) {
+ PopupMenu popup = new PopupMenu(this, view);
+ MenuInflater inflater = popup.getMenuInflater();
+ inflater.inflate(R.menu.settings_menu, popup.getMenu());
+ popup.setOnMenuItemClickListener(this);
+ popup.show();
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (item.getItemId() == R.id.switch_viewer) {
+ switchViewer();
+ return true;
+ }
+ return false;
+ }
+
+ private void setImmersiveSticky() {
+ WindowInsetsControllerCompat controller =
+ new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView());
+ controller.hide(WindowInsetsCompat.Type.systemBars());
+ controller.setSystemBarsBehavior(
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ }
+
+ private void launchDemo(String demoName) {
+ String className = "";
+ if (demoName.equals("exhibit")) {
+ className = "com.google.vr.cardboard.paperscope.demo.exhibit.ExhibitActivity";
+ } else if (demoName.equals("mymovies")) {
+ className = "com.google.vr.cardboard.paperscope.demo.myvideos.MyVideosActivity";
+ } else if (demoName.equals("photospheres")) {
+ className = "com.google.vr.cardboard.paperscope.demo.photosphere.PhotoSphereActivity";
+ }
+ try {
+ Class> activityClass = Class.forName(className);
+ Intent intent = new Intent(this, activityClass);
+ startActivity(intent);
+ nativeDemoLaunched(nativeApp);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void switchViewer() {
+ nativeSwitchViewer(nativeApp);
+ }
+
+ private native long nativeOnCreate(AssetManager assetManager);
+
+ private native void nativeOnDestroy(long nativeApp);
+
+ private native void nativeOnSurfaceCreated(long nativeApp);
+
+ private native void nativeOnDrawFrame(long nativeApp);
+
+ private native void nativeOnTriggerEvent(long nativeApp);
+
+ private native void nativeOnPause(long nativeApp);
+
+ private native void nativeOnResume(long nativeApp);
+
+ private native void nativeSetScreenParams(long nativeApp, int width, int height);
+
+ private native void nativeSwitchViewer(long nativeApp);
+
+ private native boolean nativeShouldLaunchDemo(long nativeApp);
+
+ private native String nativeDemoToLaunch(long nativeApp);
+
+ private native void nativeDemoLaunched(long nativeApp);
+}
diff --git a/paperscope/core/MainActivity.java b/paperscope/core/MainActivity.java
new file mode 100644
index 00000000..06126a50
--- /dev/null
+++ b/paperscope/core/MainActivity.java
@@ -0,0 +1,208 @@
+package com.google.vr.cardboard.paperscope.demo;
+
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.Toast;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+import androidx.viewpager2.widget.ViewPager2;
+import com.google.android.material.appbar.AppBarLayout;
+import com.google.android.material.appbar.AppBarLayout.Behavior;
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.tabs.TabLayoutMediator;
+import com.google.cardboard.sdk.CardboardViewApi;
+import com.google.vr.cardboard.paperscope.demo.about.AboutActivity;
+import com.google.vr.cardboard.paperscope.demo.magicwindow.MagicWindowActivity;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * This is the Activity where the user can see a list of Cardboard compatible apps to launch, along
+ * with the provided Cardboard Demo.
+ */
+public class MainActivity extends AppCompatActivity {
+ static {
+ System.loadLibrary("cardboard_jni");
+ }
+
+ private long nativeApp;
+
+ private static final String TAG = MainActivity.class.getSimpleName();
+
+ /** Index of MyLibrary fragment in the tab. */
+ private static final int TAB_INDEX_MY_LIBRARY = 0;
+
+ /** Number of fragments in the tab. */
+ private static final int NUM_FRAGMENTS = 1;
+
+ /** The URL for the Google Privacy Policy. */
+ private static final Uri GOOGLE_PRIVACY_URL =
+ Uri.parse("http://www.google.com/policies/privacy/");
+
+ /** The URL for the Google Cardboard Contact Us page. */
+ private static final Uri GOOGLE_CONTACT_URL =
+ Uri.parse(
+ "https://support.google.com/cardboard?hl=en&sjid=7565740170206509904-NC#topic=6295044");
+
+ private CoordinatorLayout coordinatorLayout;
+ private CoordinatorLayout.LayoutParams layoutParams;
+ private AppBarLayout appBarLayout;
+ private AppBarLayout.Behavior behavior;
+ private ViewPager2 pager;
+
+ private CardboardViewApi cardboardViewApi;
+
+ public static T checkNotNull(@Nullable T reference) {
+ if (reference == null) {
+ throw new NullPointerException();
+ }
+ return reference;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstance) {
+ super.onCreate(savedInstance);
+
+ nativeApp = nativeOnCreate(getAssets());
+
+ setContentView(R.layout.my_library);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ coordinatorLayout = checkNotNull((CoordinatorLayout) findViewById(R.id.coordinator));
+ appBarLayout = checkNotNull((AppBarLayout) findViewById(R.id.appbar));
+ layoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
+ layoutParams.setBehavior(new Behavior());
+ behavior = (AppBarLayout.Behavior) checkNotNull(layoutParams.getBehavior());
+
+ ViewCompat.setOnApplyWindowInsetsListener(
+ coordinatorLayout,
+ (v, windowInsets) -> {
+ Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
+
+ MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
+ mlp.leftMargin = insets.left;
+ mlp.bottomMargin = insets.bottom;
+ mlp.topMargin = insets.top;
+ mlp.rightMargin = insets.right;
+ v.setLayoutParams(mlp);
+
+ return WindowInsetsCompat.CONSUMED;
+ });
+
+ pager = checkNotNull((ViewPager2) findViewById(R.id.pager));
+ pager.setAdapter(new TabsAdapter(this));
+ pager.registerOnPageChangeCallback(
+ new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ // Reset toolbar's position when switching pages. This is done by simulating a quick
+ // fling.
+ behavior.onNestedFling(
+ /* coordinatorLayout= */ coordinatorLayout,
+ /* child= */ appBarLayout,
+ /* target= */ null,
+ /* velocityX= */ 0,
+ /* velocityY= */ -1000,
+ /* consumed= */ true);
+ }
+ });
+
+ TabLayout tabLayout = checkNotNull((TabLayout) findViewById(R.id.tabs));
+ new TabLayoutMediator(
+ tabLayout,
+ pager,
+ (tab, position) -> {
+ tab.setText(
+ switch (position) {
+ case TAB_INDEX_MY_LIBRARY -> getString(R.string.app_grid_view_name);
+ default ->
+ throw new IllegalArgumentException(
+ "Invalid fragment index. index=" + position);
+ });
+ })
+ .attach();
+
+ cardboardViewApi = new CardboardViewApi(this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (!cardboardViewApi.hasSavedDeviceParams()) {
+ Intent intent = new Intent(this, MagicWindowActivity.class);
+ startActivity(intent);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.my_library_settings_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == R.id.action_switch_viewer) {
+ nativeSwitchViewer(nativeApp);
+ return true;
+ } else if (id == R.id.action_about) {
+ startActivity(new Intent(this, AboutActivity.class));
+ return true;
+ } else if (id == R.id.action_privacy_policy) {
+ try {
+ startActivity(new Intent(Intent.ACTION_VIEW, GOOGLE_PRIVACY_URL));
+ } catch (android.content.ActivityNotFoundException e) {
+ Toast.makeText(this, R.string.view_web_page_failed, Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ } else if (id == R.id.contact_us) {
+ try {
+ startActivity(new Intent(Intent.ACTION_VIEW, GOOGLE_CONTACT_URL));
+ } catch (android.content.ActivityNotFoundException e) {
+ Toast.makeText(this, R.string.view_web_page_failed, Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private native long nativeOnCreate(AssetManager assetManager);
+
+ private native void nativeSwitchViewer(long nativeApp);
+
+ /** The adapter for the two fragments. */
+ private static class TabsAdapter extends FragmentStateAdapter {
+
+ TabsAdapter(AppCompatActivity activity) {
+ super(activity);
+ }
+
+ @Override
+ public Fragment createFragment(int index) {
+ return switch (index) {
+ case TAB_INDEX_MY_LIBRARY -> new MyLibraryFragment();
+ default -> throw new IllegalArgumentException("Invalid fragment index. index=" + index);
+ };
+ }
+
+ @Override
+ public int getItemCount() {
+ return NUM_FRAGMENTS;
+ }
+ }
+}
diff --git a/paperscope/core/MyLibraryFragment.java b/paperscope/core/MyLibraryFragment.java
new file mode 100644
index 00000000..d9f26a91
--- /dev/null
+++ b/paperscope/core/MyLibraryFragment.java
@@ -0,0 +1,134 @@
+package com.google.vr.cardboard.paperscope.demo;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.palette.graphics.Palette;
+import java.util.List;
+
+/** Fragment where the list of Cardboard supported apps is generated and displayed to the user. */
+public class MyLibraryFragment extends Fragment {
+ private static final String LOG_TAG = MyLibraryFragment.class.getName();
+ private static final int DEMO_ICON_PADDING_COLOR = 0xFFE0E0E0;
+ private PackageManager packageManager;
+ private GridView gridView;
+ private AppAdapter adapter;
+ private Activity activity;
+ private SupportedAppProvider supportedAppProvider;
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.my_library_fragment, container, false);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ try {
+ activity = (MainActivity) context;
+ } catch (ClassCastException e) {
+ ClassCastException exception =
+ new ClassCastException(context.toString() + " must be an instance of MainActivity");
+ exception.initCause(e);
+ throw exception;
+ }
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ packageManager = getContext().getPackageManager();
+ supportedAppProvider = SupportedAppProvider.getInstance();
+ adapter = new AppAdapter();
+ gridView = (GridView) view.findViewById(R.id.gridview);
+ gridView.setAdapter(adapter);
+ supportedAppProvider.populateSupportedAppsList(packageManager, activity, adapter);
+ }
+
+ /** The adapter for the grid view that provides the view of each of the grid items. */
+ private class AppAdapter extends BaseAdapter {
+
+ @Override
+ public int getCount() {
+ return supportedAppProvider.getInstalledApps().size();
+ }
+
+ @Override
+ @Nullable
+ public Object getItem(int position) {
+ return null;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ private void updateAppItemFromBasicAppInfo(View appItemView, BasicAppInfo appInfo) {
+ TextView appNameView = (TextView) appItemView.findViewById(R.id.app_name);
+ appNameView.setText(appInfo.getAppName());
+ ImageView iconView = (ImageView) appItemView.findViewById(R.id.app_icon);
+ iconView.setImageDrawable(appInfo.getIcon());
+
+ appItemView.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ try {
+ activity.startActivity(appInfo.getStartIntent());
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(activity, R.string.error_opening_app, Toast.LENGTH_SHORT).show();
+ Log.e(LOG_TAG, "Unable to launch activity " + appInfo.getPackageName() + ": " + e);
+ }
+ }
+ });
+ }
+
+ private int getIconPaddingColor(Palette palette, int defaultColor) {
+ return palette.getLightMutedColor(
+ palette.getMutedColor(
+ palette.getLightVibrantColor(palette.getVibrantColor(defaultColor))));
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = activity.getLayoutInflater().inflate(R.layout.app_grid_card, parent, false);
+ }
+ List installedApps = supportedAppProvider.getInstalledApps();
+ updateAppItemFromBasicAppInfo(convertView, installedApps.get(position));
+ ImageView iconView = (ImageView) convertView.findViewById(R.id.app_icon);
+ int color =
+ getIconPaddingColor(
+ installedApps.get(position).getPalette(),
+ ContextCompat.getColor(activity, R.color.transparent));
+
+ // Special case the "Cardboard Demos" icon padding color
+ if (installedApps.get(position).getPackageName().equals(activity.getPackageName())) {
+ // Light grey color surrounding the "Cardboard Demos" icon. Spec is here:
+ // https://folio.googleplex.com/ericamorse/Cardboard/November-release/spec#%2Fmy-library-spec.png
+ color = DEMO_ICON_PADDING_COLOR;
+ }
+
+ iconView.setBackgroundColor(color);
+ return convertView;
+ }
+ }
+}
diff --git a/paperscope/core/SupportedAppProvider.java b/paperscope/core/SupportedAppProvider.java
new file mode 100644
index 00000000..2e6d1b04
--- /dev/null
+++ b/paperscope/core/SupportedAppProvider.java
@@ -0,0 +1,161 @@
+package com.google.vr.cardboard.paperscope.demo;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.widget.BaseAdapter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class provides a list of supported apps to be displayed by the {@link MyLibraryFragment}
+ * fragment. Apps can advertise cardboard support by adding the following category section to a
+ * given intent filter in their AndroidManifest.xml:
+ */
+final class SupportedAppProvider {
+ private static SupportedAppProvider instance;
+ private static final String LOG_TAG = SupportedAppProvider.class.getName();
+ private static final String CARDBOARD_SUPPORT_CATEGORY_INTENT_TAG =
+ "com.google.intent.category.CARDBOARD";
+ private List installedApps;
+
+ private SupportedAppProvider() {
+ installedApps = new ArrayList();
+ }
+
+ public static SupportedAppProvider getInstance() {
+ if (instance == null) {
+ instance = new SupportedAppProvider();
+ }
+ return instance;
+ }
+
+ @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16.
+ private BasicAppInfo buildCardboardDemoAppInfo(Context context) {
+ Intent intent = new Intent(context, CardboardLauncherActivity.class);
+
+ String packageName = context.getPackageName();
+ BasicAppInfo appInfo =
+ new BasicAppInfo(
+ packageName,
+ context.getResources().getString(R.string.bundled_demos_label),
+ intent,
+ context.getResources().getDrawable(R.drawable.cardboard_demo));
+
+ return appInfo;
+ }
+
+ public List getInstalledApps() {
+ return installedApps;
+ }
+
+ private void sortAppListByName(List apps) {
+ Comparator comparator =
+ new Comparator() {
+ @Override
+ public int compare(BasicAppInfo first, BasicAppInfo second) {
+ return first.getAppName().compareTo(second.getAppName());
+ }
+ };
+ Collections.sort(apps, comparator);
+ }
+
+ private List getCardboardActivities(PackageManager packageManager) {
+ Intent cardboardIntent = new Intent(Intent.ACTION_MAIN);
+ cardboardIntent.addCategory(CARDBOARD_SUPPORT_CATEGORY_INTENT_TAG);
+ return packageManager.queryIntentActivities(cardboardIntent, 0);
+ }
+
+ private BasicAppInfo createBasicAppInfoFromResolveInfoAndIntent(
+ PackageManager pm, ResolveInfo resolveInfo, Intent intent) {
+ final String packageName = resolveInfo.activityInfo.packageName;
+ if (packageName == null) {
+ Log.w(LOG_TAG, "packageName not set in activityInfo");
+ return null;
+ }
+
+ CharSequence appName = resolveInfo.loadLabel(pm);
+ if (appName == null) {
+ Log.w(LOG_TAG, "application label not found for " + packageName);
+ return null;
+ }
+
+ Drawable icon = pm.getApplicationIcon(resolveInfo.activityInfo.applicationInfo);
+ if (icon == null) {
+ Log.w(LOG_TAG, "application icon not found for " + packageName);
+ return null;
+ }
+
+ return new BasicAppInfo(packageName, appName.toString(), intent, icon);
+ }
+
+ private BasicAppInfo createBasicAppInfoFromResolveInfo(
+ PackageManager pm, ResolveInfo resolveInfo) {
+ // Create an intent that will launch the input activity.
+ final ActivityInfo activityInfo = resolveInfo.activityInfo;
+ Intent intent = new Intent();
+ intent.setClassName(activityInfo.applicationInfo.packageName, activityInfo.name);
+
+ return createBasicAppInfoFromResolveInfoAndIntent(pm, resolveInfo, intent);
+ }
+
+ private void appendInstalledAppsWithCardboardSupportIntent(
+ PackageManager packageManager, Context context, Map supportedAppsMap) {
+ List cardboardApps = getCardboardActivities(packageManager);
+ for (ResolveInfo resolveInfo : cardboardApps) {
+
+ // Check that the package name is not already in the list, if it was most likely we already
+ // added the intent for an activity in the same package that matched better. Therefore we are
+ // skipping the next one.
+ // This also avoid somebody abusing this "API" by declaring multiple activities from the same
+ // app.
+ if (supportedAppsMap.containsKey(resolveInfo.activityInfo.applicationInfo.packageName)) {
+ continue;
+ }
+
+ // Check to make sure the activity is not in our own APK.
+ if (resolveInfo.activityInfo.applicationInfo.packageName.equalsIgnoreCase(
+ context.getPackageName())) {
+ continue;
+ }
+
+ // Bundle the app name with its icon and start intent.
+ BasicAppInfo basicAppInfo = createBasicAppInfoFromResolveInfo(packageManager, resolveInfo);
+
+ // Add the app info if valid to the list of apps.
+ if (basicAppInfo != null) {
+ supportedAppsMap.put(basicAppInfo.getPackageName(), basicAppInfo);
+ }
+ }
+ }
+
+ private List getSupportedAppList(PackageManager packageManager, Context context) {
+ // Map of package name and basicAppInfo that will be used to ensure uniqueness.
+ Map supportedAppsMap = new LinkedHashMap();
+ // Add apps that have cardboard intent.
+ appendInstalledAppsWithCardboardSupportIntent(packageManager, context, supportedAppsMap);
+
+ List supportedApps = new ArrayList(supportedAppsMap.values());
+ sortAppListByName(supportedApps);
+ return supportedApps;
+ }
+
+ public void populateSupportedAppsList(
+ PackageManager packageManager, Context context, BaseAdapter adapter) {
+ installedApps.clear();
+ installedApps.add(buildCardboardDemoAppInfo(context));
+ installedApps.addAll(getSupportedAppList(packageManager, context));
+
+ adapter.notifyDataSetChanged();
+ }
+}
diff --git a/paperscope/core/TransitionView.java b/paperscope/core/TransitionView.java
new file mode 100644
index 00000000..de006dec
--- /dev/null
+++ b/paperscope/core/TransitionView.java
@@ -0,0 +1,205 @@
+package com.google.vr.cardboard.paperscope.demo;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.drawable.ColorDrawable;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.OrientationEventListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+
+/** The transition view, which is displayed before entering VR mode. */
+public class TransitionView extends FrameLayout {
+
+ /** Interface for listening to events from the transition view. */
+ public interface Listener {
+ public void onSwitchButtonClicked();
+
+ public void onExitButtonClicked();
+ }
+
+ public static final int TRANSITION_BACKGROUND_COLOR = 0xFF455A64;
+
+ /**
+ * Duration (in milliseconds), of the animation used to dismiss the view after landspace left
+ * detected.
+ */
+ public static final int TRANSITION_ANIMATION_DURATION_MS = 500;
+
+ /**
+ * Amount of time (in milliseconds) to delay the dismissal of the transition view, if the device
+ * is initially in landscape left, so the user can still see the transition briefly before
+ * entering VR.
+ */
+ public static final int ALREADY_LANDSCAPE_LEFT_TRANSITION_DELAY_MS = 2000;
+
+ /**
+ * Tolerance (in degrees) for landscape device orientation detection. This should be a relatively
+ * low tolerance as it is used for auto-dismissal of this view when in landscape. If this is too
+ * high, then the transition view may dismiss before the phone is in the viewer.
+ */
+ private static final int LANDSCAPE_TOLERANCE_DEGREES = 5;
+
+ private final Context context;
+ private OrientationEventListener orientationEventListener;
+ private Listener transitionViewListener;
+
+ private static boolean isLandscapeLeft(int orientationDegrees) {
+ return Math.abs(orientationDegrees - 270) < LANDSCAPE_TOLERANCE_DEGREES;
+ }
+
+ public TransitionView(Context context, Listener listener) {
+ super(context);
+
+ this.context = context;
+ transitionViewListener = listener;
+ setBackground(new ColorDrawable(TRANSITION_BACKGROUND_COLOR));
+ inflateContentView(R.layout.transition_view);
+ // Rotate the view -90 degrees to simulate portrait
+ setRotation(-90);
+ }
+
+ private void inflateContentView(int layoutId) {
+ removeAllViews();
+
+ LayoutInflater.from(getContext()).inflate(layoutId, this, true);
+ View switchActionView = findViewById(R.id.transition_switch_action);
+ switchActionView.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // When the switch action is tapped, launched directly in to the change viewer
+ transitionViewListener.onSwitchButtonClicked();
+ }
+ });
+
+ ImageButton backButton = (ImageButton) findViewById(R.id.back_button);
+ backButton.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ transitionViewListener.onExitButtonClicked();
+ }
+ });
+
+ // Dismiss the view if the user clicks on the transition icon.
+ ImageView transitionIcon = (ImageView) findViewById(R.id.transition_icon);
+ transitionIcon.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ fadeOutAndRemove(/* delay= */ false);
+ }
+ });
+ }
+
+ @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16.
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // Get screen dimensions
+ Display display =
+ ((WindowManager) this.context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+ Point size = new Point();
+ display.getSize(size);
+ // Landscape width = real screen width, height = screen height
+ int width = size.x;
+ display.getRealSize(size);
+ int height = size.y;
+
+ // After rotation, we want the "portrait" layout:
+ ViewGroup.LayoutParams params = getLayoutParams();
+ if (params == null) {
+ params = new ViewGroup.LayoutParams(height, width);
+ } else {
+ params.width = height;
+ params.height = width;
+ }
+ setLayoutParams(params);
+
+ // Move it back into position (translate after rotation)
+ setTranslationX((width - height) / 2f);
+ setTranslationY((height - width) / 2f);
+
+ startOrientationMonitor();
+ }
+
+ private void fadeOutAndRemove(boolean delay) {
+ // Once we are ready to fade out, immediately stop and invalidate the orientation listener.
+ stopOrientationMonitor();
+
+ Animation existingFadeOut = getAnimation();
+ if (existingFadeOut != null) {
+ // Don't reschedule an animation if an existing one is running or will start before the new
+ // one.
+ if (delay || existingFadeOut.getStartOffset() == 0) {
+ return;
+ }
+ // The clearAnimation() call will trigger the listener's onAnimationEnd method, which
+ // will in turn detach the TransitionView. Avoid this by first clearing the listener.
+ existingFadeOut.setAnimationListener(null);
+ clearAnimation();
+ }
+
+ Animation newFadeOut = new AlphaAnimation(/* fromAlpha= */ 1, /* toAlpha= */ 0);
+ newFadeOut.setInterpolator(new AccelerateInterpolator());
+ newFadeOut.setRepeatCount(0);
+ newFadeOut.setDuration(TRANSITION_ANIMATION_DURATION_MS);
+ if (delay) {
+ newFadeOut.setStartOffset(ALREADY_LANDSCAPE_LEFT_TRANSITION_DELAY_MS);
+ }
+
+ newFadeOut.setAnimationListener(
+ new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ TransitionView.this.setVisibility(View.GONE);
+ ((ViewGroup) getParent()).removeView(TransitionView.this);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+
+ startAnimation(newFadeOut);
+ }
+
+ private void startOrientationMonitor() {
+ if (orientationEventListener != null) {
+ return;
+ }
+
+ orientationEventListener =
+ new OrientationEventListener(getContext()) {
+ @Override
+ public void onOrientationChanged(int orientation) {
+ if (isLandscapeLeft(orientation)) {
+ fadeOutAndRemove(/* delay= */ false);
+ }
+ }
+ };
+
+ orientationEventListener.enable();
+ }
+
+ private void stopOrientationMonitor() {
+ if (orientationEventListener == null) {
+ return;
+ }
+
+ orientationEventListener.disable();
+ orientationEventListener = null;
+ }
+}
diff --git a/paperscope/magicwindow/AndroidManifest.xml b/paperscope/magicwindow/AndroidManifest.xml
new file mode 100644
index 00000000..0f76e5f8
--- /dev/null
+++ b/paperscope/magicwindow/AndroidManifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/paperscope/magicwindow/MagicWindowActivity.java b/paperscope/magicwindow/MagicWindowActivity.java
new file mode 100644
index 00000000..686ecbdd
--- /dev/null
+++ b/paperscope/magicwindow/MagicWindowActivity.java
@@ -0,0 +1,257 @@
+package com.google.vr.cardboard.paperscope.demo.magicwindow;
+
+import com.google.vr.cardboard.paperscope.demo.R;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import android.opengl.GLSurfaceView;
+import android.os.Build;
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.TextView;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.snackbar.Snackbar;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * This is the main Activity for the demo application. It shows a monoscopic view of a VR world that
+ * is controlled by the user's head tracking.
+ */
+public class MagicWindowActivity extends AppCompatActivity {
+ static {
+ System.loadLibrary("cardboard_jni");
+ }
+
+ // Random request code for photos and videos.
+ private static final int PHOTOS_AND_VIDEOS_REQUEST_CODE = 234;
+ private static final String TAG = MagicWindowActivity.class.getSimpleName();
+
+ private static final Uri CARDBOARD_MANUFACTURERS_URL =
+ Uri.parse("https://developers.google.com/cardboard/manufacturers");
+
+ private long nativeApp;
+ private GLSurfaceView glView;
+ private Snackbar permissionRationalSnackbar;
+ private boolean alreadyAskedStoragePermission = false;
+
+ @Override
+ public void onCreate(Bundle savedInstance) {
+ super.onCreate(savedInstance);
+
+ nativeApp = nativeOnCreate(getAssets());
+
+ setContentView(R.layout.magic_window);
+
+ View container = findViewById(R.id.coordinator);
+ ViewCompat.setOnApplyWindowInsetsListener(
+ container,
+ (v, windowInsets) -> {
+ Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
+
+ MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
+ mlp.leftMargin = insets.left;
+ mlp.bottomMargin = insets.bottom;
+ mlp.rightMargin = insets.right;
+ v.setLayoutParams(mlp);
+
+ return WindowInsetsCompat.CONSUMED;
+ });
+
+ glView = findViewById(R.id.surface_view);
+ glView.setEGLContextClientVersion(2);
+ Renderer renderer = new Renderer();
+ glView.setRenderer(renderer);
+ glView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+
+ TextView getOneTv = findViewById(R.id.get_cardboard);
+ getOneTv.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, CARDBOARD_MANUFACTURERS_URL);
+ startActivity(intent);
+ }
+ });
+
+ FloatingActionButton nextFab = findViewById(R.id.next);
+ nextFab.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (nativeHasSavedDeviceParams(nativeApp)) {
+ finish();
+ } else {
+ nativeScanViewer(nativeApp);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ nativeOnPause(nativeApp);
+ glView.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (nativeHasSavedDeviceParams(nativeApp)) {
+ finish();
+ }
+
+ if (!hasMediaPermissions()) {
+ requestMediaPermissions();
+ }
+
+ glView.onResume();
+ nativeOnResume(nativeApp);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ nativeOnDestroy(nativeApp);
+ nativeApp = 0;
+ }
+
+ /**
+ * Gets whether UI should be shown with rationale before requesting a permission.
+ *
+ * @param permissions Permissions to request.
+ * @return True if UI with rationale should be shown before requesting a permission. Otherwise,
+ * return false.
+ */
+ private boolean shouldShowRequestPermissionRationale(List permissions) {
+ for (String permission : permissions) {
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the permissions required to access images and videos from the external storage.
+ *
+ * Based on the API level, the permission to be requested may be different. When the API level
+ * is below Android T READ_EXTERNAL_STORAGE have to be requested. From Android T on, due to
+ * granular media permissions, READ_MEDIA_IMAGES and READ_MEDIA_VIDEO have to be requested.
+ *
+ * @return string array with permissions to be requested.
+ */
+ private List getStoragePermissionsToRequest() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ return Collections.unmodifiableList(
+ Arrays.asList(
+ Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO));
+ }
+
+ return Collections.unmodifiableList(Arrays.asList(Manifest.permission.READ_EXTERNAL_STORAGE));
+ }
+
+ private void requestMediaPermissions() {
+ List permissions = getStoragePermissionsToRequest();
+ if (shouldShowRequestPermissionRationale(permissions)) {
+ if (permissionRationalSnackbar == null) {
+ final View rootView = ((ViewGroup) findViewById(android.R.id.content)).getChildAt(0);
+
+ permissionRationalSnackbar =
+ Snackbar.make(
+ rootView, R.string.read_media_permission_rationale, Snackbar.LENGTH_INDEFINITE)
+ .setAction(
+ android.R.string.ok,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ActivityCompat.requestPermissions(
+ MagicWindowActivity.this,
+ permissions.toArray(new String[0]),
+ PHOTOS_AND_VIDEOS_REQUEST_CODE);
+ // After click, the current Snackbar will be automatically removed from the
+ // SnackbarManager so next time calling show() on it won't show it. As a
+ // result,
+ // we explicitly set it to be null so next time we will first create a new
+ // Snackbar then call show() on the new Snackbar.
+ permissionRationalSnackbar = null;
+ }
+ });
+ }
+
+ // Only call show() if the current Snackbar is not shown to avoid the animation.
+ if (!permissionRationalSnackbar.isShown()) {
+ permissionRationalSnackbar.show();
+ }
+ } else if (!alreadyAskedStoragePermission) {
+ alreadyAskedStoragePermission = true;
+ // Storage permission has not been granted yet. Request it directly.
+ ActivityCompat.requestPermissions(
+ this, permissions.toArray(new String[0]), PHOTOS_AND_VIDEOS_REQUEST_CODE);
+ }
+ }
+
+ private boolean hasMediaPermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES)
+ == PackageManager.PERMISSION_GRANTED
+ && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_VIDEO)
+ == PackageManager.PERMISSION_GRANTED;
+ } else {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ }
+
+ private class Renderer implements GLSurfaceView.Renderer {
+ @Override
+ public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
+ nativeOnSurfaceCreated(nativeApp);
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl10, int width, int height) {
+ nativeSetScreenParams(nativeApp, width, height);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl10) {
+ nativeOnDrawFrame(nativeApp);
+ }
+ }
+
+ private native long nativeOnCreate(AssetManager assetManager);
+
+ private native void nativeOnDestroy(long nativeApp);
+
+ private native void nativeOnResume(long nativeApp);
+
+ private native void nativeOnPause(long nativeApp);
+
+ private native void nativeOnSurfaceCreated(long nativeApp);
+
+ private native void nativeSetScreenParams(long nativeApp, int width, int height);
+
+ private native void nativeOnDrawFrame(long nativeApp);
+
+ private native void nativeScanViewer(long nativeApp);
+
+ private native boolean nativeHasSavedDeviceParams(long nativeApp);
+}
diff --git a/paperscope/myvideos/AndroidManifest.xml b/paperscope/myvideos/AndroidManifest.xml
new file mode 100644
index 00000000..dcda03b7
--- /dev/null
+++ b/paperscope/myvideos/AndroidManifest.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/paperscope/myvideos/ImageFetcher.java b/paperscope/myvideos/ImageFetcher.java
new file mode 100644
index 00000000..f3ae82a1
--- /dev/null
+++ b/paperscope/myvideos/ImageFetcher.java
@@ -0,0 +1,136 @@
+package com.google.vr.cardboard.paperscope.demo.myvideos;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Pair;
+import android.util.Size;
+import com.google.cardboard.sdk.CardboardView;
+import com.google.common.flogger.FluentLogger;
+import com.google.vr.cardboard.paperscope.demo.myvideos.VideoFetcher.VideoInfo;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Simple image downloader in a background thread.
+ */
+public class ImageFetcher {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final int DISPLAY_DELAY_MILLIS = 50;
+ private static final int THUMBNAIL_WIDTH_SIZE = 512;
+ private static final int THUMBNAIL_HEIGHT_SIZE = 384;
+
+ private final CardboardView cardboardView;
+ private final MyVideosRenderer renderer;
+ private final Handler handler;
+ private int fetchCount;
+ private ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ private final ContentResolver contentResolver;
+
+ public ImageFetcher(
+ Context context, final CardboardView cardboardView, final MyVideosRenderer renderer) {
+ this.cardboardView = cardboardView;
+ this.renderer = renderer;
+ contentResolver = context.getContentResolver();
+ handler = new ImageHandler(this);
+ }
+
+ @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16.
+ private static class ImageHandler extends Handler {
+ private final WeakReference imageFetcherReference;
+
+ ImageHandler(ImageFetcher imageFetcher) {
+ imageFetcherReference = new WeakReference<>(imageFetcher);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ ImageFetcher imageFetcher = imageFetcherReference.get();
+ if (imageFetcher == null) {
+ return;
+ }
+
+ if (msg.obj instanceof Bitmap) {
+ final Bitmap bitmap = (Bitmap) msg.obj;
+ final int index = msg.what;
+ // Delay the loading of each thumbnail by a bit so that all the GL textures don't get
+ // created at the same time, causing stuttering.
+ // TODO(kvaalen): it's still possible for multiple thumbnails to be added at the same
+ // time
+ // since these times are relative to when the thumbnail loaded and are not
+ // absolute.
+ postDelayed(
+ () -> {
+ // Use queueEvent to have it run on the GL thread.
+ imageFetcher.cardboardView.queueEvent(
+ () -> imageFetcher.renderer.addThumbnail(bitmap, index));
+ },
+ imageFetcher.fetchCount * ((long) DISPLAY_DELAY_MILLIS));
+ imageFetcher.fetchCount++;
+ } else {
+ logger.atSevere().log("Object isn't a bitmap");
+ }
+ }
+ }
+
+ /**
+ * @param indexAndPaths Pairs of file path strings and their index in the list of videos loaded.
+ */
+ public void downloadImages(final List> indexAndVideosInfo) {
+ Runnable runnable =
+ () -> {
+ for (Pair indexAndVideoInfo : indexAndVideosInfo) {
+ if (Thread.currentThread().isInterrupted()) {
+ return;
+ }
+ doDownload(indexAndVideoInfo.first, indexAndVideoInfo.second);
+ }
+ };
+ executor.execute(runnable);
+ }
+
+ /**
+ * Creates the bitmap for the thumbnail for the video loaded. Once the bitmap is created, it is
+ * queued for rendering by sending it to the handler.
+ *
+ * Based on the API level, the bitmap is created from different sources. When the API level is
+ * below Android Q´s the bitmap is created using the absolute path of the video. From Android Q,
+ * the bitmap is created using the Uri of the video.
+ *
+ * @param index Index of the video file in the loaded videos list.
+ * @param videoInfo Structure containing the file path string and the URI of the loaded video.
+ */
+ private void doDownload(int index, VideoInfo videoInfo) {
+ Bitmap bitmap = null;
+ Uri videoUri = videoInfo.getVideoStreamUri();
+ Size thumbnailSize = new Size(THUMBNAIL_WIDTH_SIZE, THUMBNAIL_HEIGHT_SIZE);
+ try {
+ bitmap = contentResolver.loadThumbnail(videoUri, thumbnailSize, null);
+ } catch (IOException e) {
+ logger.atSevere().log(
+ "Error getting bitmap from Uri %s : %s", videoUri.getPath(), e.getMessage());
+ }
+
+ if (!Thread.currentThread().isInterrupted()) {
+ handler.obtainMessage(index, bitmap).sendToTarget();
+ }
+ }
+
+ public void reset() {
+ // Note: This also resets the thumbnail loading delay in the handler.
+ fetchCount = 0;
+ executor.shutdownNow();
+ executor = Executors.newSingleThreadExecutor();
+ // Cancel all previous thumbnail update messages in the handler, since they are now obsolete.
+ handler.removeCallbacksAndMessages(null);
+ }
+
+}
diff --git a/paperscope/myvideos/MyVideosActivity.java b/paperscope/myvideos/MyVideosActivity.java
new file mode 100644
index 00000000..34adeb9d
--- /dev/null
+++ b/paperscope/myvideos/MyVideosActivity.java
@@ -0,0 +1,12 @@
+package com.google.vr.cardboard.paperscope.demo.myvideos;
+
+import com.google.vr.cardboard.paperscope.demo.common.DemoBaseActivity;
+import com.google.vr.cardboard.paperscope.demo.common.DemoBaseController;
+
+/** Demo that allows the user to view videos stored on their device. */
+public class MyVideosActivity extends DemoBaseActivity {
+ @Override
+ protected DemoBaseController createController() {
+ return new MyVideosController(cardboardView);
+ }
+}
diff --git a/paperscope/myvideos/MyVideosController.java b/paperscope/myvideos/MyVideosController.java
new file mode 100644
index 00000000..80e58dea
--- /dev/null
+++ b/paperscope/myvideos/MyVideosController.java
@@ -0,0 +1,48 @@
+package com.google.vr.cardboard.paperscope.demo.myvideos;
+
+import com.google.cardboard.sdk.CardboardView;
+import com.google.vr.cardboard.paperscope.demo.common.DemoBaseController;
+
+/** Demo that allows the user to view My Videos. */
+public class MyVideosController extends DemoBaseController {
+ private final VideoSequencer sequencer;
+
+ public MyVideosController(CardboardView cardboardView) {
+ super(cardboardView);
+ renderer = new MyVideosRenderer(cardboardView.getContext());
+ sequencer = new VideoSequencer(cardboardView.getContext(), cardboardView, renderer);
+ this.cardboardView.setRenderer(renderer);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ sequencer.startPlayback();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ sequencer.stop();
+ }
+
+ @Override
+ public void shutdown() {
+ super.shutdown();
+ sequencer.release();
+ }
+
+ @Override
+ public void enableOnTriggerEventListener() {
+ cardboardView.setOnTriggerEvent(
+ () -> {
+ if (renderer != null && sequencer != null) {
+ if (renderer.isScreenOnlyTargeted()) {
+ sequencer.togglePause();
+ } else {
+ sequencer.playTargetedVideo();
+ }
+ }
+ });
+ }
+}
diff --git a/paperscope/myvideos/MyVideosPlayer.java b/paperscope/myvideos/MyVideosPlayer.java
new file mode 100644
index 00000000..13ff1b57
--- /dev/null
+++ b/paperscope/myvideos/MyVideosPlayer.java
@@ -0,0 +1,291 @@
+package com.google.vr.cardboard.paperscope.demo.myvideos;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Handler;
+import android.view.Surface;
+import com.google.common.flogger.FluentLogger;
+import com.google.vr.cardboard.paperscope.demo.myvideos.VideoFetcher.VideoInfo;
+
+/** Playback controller for local videos. */
+public class MyVideosPlayer implements MyVideosRenderer.SurfaceTextureListener {
+
+ /** Simple listener interface for video playback events. */
+ public interface Listener {
+ void onVideoEnded();
+ void onPlaybackError(int what, int extra);
+ void onPlaybackSucceeded();
+ }
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Context context;
+ private final Listener listener;
+ private final MyVideosRenderer renderer;
+ private final AudioManager audioManager;
+ private final ProgressTracker progressTracker;
+
+ private MediaPlayer mediaPlayer;
+ private SurfaceTexture surfaceTexture;
+ private boolean isSurfaceCreated;
+ private Uri pendingStreamUri;
+ private boolean isPaused;
+
+ public MyVideosPlayer(Context context, Listener listener, MyVideosRenderer renderer) {
+ this.context = context;
+ this.listener = listener;
+ this.renderer = renderer;
+ audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ progressTracker = new ProgressTracker();
+ }
+
+ @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16.
+ public void stop() {
+ progressTracker.stopTracking();
+ // Release the player instead of stopping so that an async prepare gets stopped.
+ releasePlayer();
+ audioManager.abandonAudioFocus(null);
+ }
+
+ public boolean isPaused() {
+ return isPaused;
+ }
+
+ public void togglePause() {
+ if (mediaPlayer != null) {
+ if (isPaused) {
+ mediaPlayer.start();
+ } else {
+ mediaPlayer.pause();
+ }
+ isPaused = !isPaused;
+ }
+ }
+
+ public void play() {
+ if (mediaPlayer != null && isPaused) {
+ togglePause();
+ }
+ }
+
+ public void pause() {
+ if (mediaPlayer != null && !isPaused) {
+ togglePause();
+ }
+ }
+
+ public void play(VideoInfo video) {
+ startPlaybackStream(video.getVideoStreamUri());
+ }
+
+ public void release() {
+ progressTracker.stopTracking();
+ releasePlayer();
+ }
+
+ public void releasePlayer() {
+ if (mediaPlayer != null) {
+ isPaused = false;
+ mediaPlayer.release();
+ mediaPlayer = null;
+ }
+ }
+
+ private void startPlaybackStream(Uri streamUri) {
+ renderer.setIsPlaybackStarting(true);
+ renderer.setIsBuffering(true);
+ if (!isSurfaceCreated) {
+ pendingStreamUri = streamUri;
+ return;
+ }
+
+ pendingStreamUri = null;
+
+ if (mediaPlayer != null) {
+ releasePlayer();
+ }
+ mediaPlayer = createMediaPlayer();
+
+ try {
+ mediaPlayer.setDataSource(context, streamUri);
+ } catch (Exception e) {
+ logger.atSevere().log(
+ "MediaPlayer setMediaDataSource exception with stream: %s %s", streamUri, e.getMessage());
+ return;
+ }
+
+ // Prepare and start the method asynchronously, otherwise it blocks the main thread.
+ mediaPlayer.setOnPreparedListener(
+ new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer mediaPlayer) {
+ if (mediaPlayer.getVideoHeight() == 0) {
+ logger.atSevere().log("Video looks invalid, its height is zero.");
+ return;
+ }
+
+ float videoAspectRatio =
+ ((float) mediaPlayer.getVideoWidth()) / mediaPlayer.getVideoHeight();
+ renderer.setVideoAspectRatio(videoAspectRatio);
+ mediaPlayer.start();
+ }
+ });
+
+ try {
+ mediaPlayer.prepareAsync();
+ progressTracker.startTracking();
+ } catch (IllegalStateException e) {
+ logger.atSevere().log("MediaPlayer prepare() failed %s", e.getMessage());
+ return;
+ }
+ }
+
+ @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16.
+ private MediaPlayer createMediaPlayer() {
+ MediaPlayer mediaPlayer = new MediaPlayer();
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mediaPlayer) {
+ listener.onVideoEnded();
+ }
+ });
+ mediaPlayer.setOnErrorListener(
+ new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
+ logger.atSevere().log("MediaPlayer error - what: %s, extra: %s", what, extra);
+ // TODO(kvaalen): Have more fine grained control over the listener events.
+ listener.onPlaybackError(what, extra);
+ return true;
+ }
+ });
+ mediaPlayer.setOnInfoListener(
+ new MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(MediaPlayer mediaPlayer, int what, int extra) {
+ switch (what) {
+ case MediaPlayer.MEDIA_INFO_BUFFERING_START -> renderer.setIsBuffering(true);
+ case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
+ renderer.setIsBuffering(false);
+ listener.onPlaybackSucceeded();
+ }
+ case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START -> {
+ renderer.setIsPlaybackStarting(false);
+ renderer.setIsBuffering(false);
+ }
+ }
+ return true;
+ }
+ });
+
+ Surface surface = new Surface(surfaceTexture);
+ mediaPlayer.setSurface(surface);
+ surface.release();
+
+ return mediaPlayer;
+ }
+
+ // MyVideosStereoRenderer.SurfaceTextureListener
+
+ @Override
+ public void onSurfaceTextureCreated(SurfaceTexture surfaceTexture) {
+ isSurfaceCreated = true;
+ this.surfaceTexture = surfaceTexture;
+ if (pendingStreamUri != null) {
+ startPlaybackStream(pendingStreamUri);
+ }
+ }
+
+ /**
+ * Sends pings to check the progress of the video every so often.
+ */
+ private class ProgressTracker {
+
+ private static final int DELAY_MILLIS = 100;
+
+ private final Handler handler;
+ private final Runnable ping;
+
+ private boolean lastIsBuffering;
+ private boolean lastIsPlaybackStarting;
+
+ @SuppressWarnings("deprecation") // This is ignored because our app minSdkVersion is 16.
+ public ProgressTracker() {
+ handler = new Handler();
+ ping = new Runnable() {
+ @Override
+ public void run() {
+ ping();
+ }
+ };
+ }
+
+ public void startTracking() {
+ handler.removeCallbacksAndMessages(null);
+ lastIsBuffering = true;
+ lastIsPlaybackStarting = true;
+ ping();
+ }
+
+ public void stopTracking() {
+ handler.removeCallbacksAndMessages(null);
+ }
+
+ private void ping() {
+ checkIsPlaybackStarted();
+ checkBufferingStatus();
+ float ratio;
+ if (mediaPlayer != null && mediaPlayer.isPlaying()) {
+ int pos = mediaPlayer.getCurrentPosition();
+ int duration = mediaPlayer.getDuration();
+ ratio = pos / (float) duration;
+ } else {
+ ratio = 0f;
+ }
+ renderer.setProgressRatio(ratio);
+ handler.postDelayed(ping, DELAY_MILLIS);
+ }
+
+ /**
+ * For some reason the Galaxy Note 2 doesn't get the onInfo events from the MediaPlayer.
+ * This check is a fallback to detect if a buffering spinner should be shown.
+ * TODO(kvaalen): Figure out why the Galaxy Note 2 doesn't get onInfo events and remove this
+ * when it's fixed.
+ */
+ private void checkBufferingStatus() {
+ boolean isBuffering = mediaPlayer == null || !mediaPlayer.isPlaying();
+ if (isBuffering == lastIsBuffering) {
+ return;
+ }
+
+ renderer.setIsBuffering(isBuffering && !isPaused);
+ lastIsBuffering = isBuffering;
+ }
+
+ /**
+ * For some reason the Galaxy Note 2 doesn't get the onInfo events from the MediaPlayer.
+ * This check is a fallback to detect if the splash screen should be shown.
+ * TODO(kvaalen): Figure out why the Galaxy Note 2 doesn't get onInfo events and remove this
+ * when it's fixed.
+ */
+ private void checkIsPlaybackStarted() {
+ boolean isPlaybackStarting =
+ !isPaused
+ && (mediaPlayer == null
+ || !mediaPlayer.isPlaying()
+ || mediaPlayer.getCurrentPosition() == 0);
+ if (isPlaybackStarting == lastIsPlaybackStarting) {
+ return;
+ }
+
+ renderer.setIsPlaybackStarting(isPlaybackStarting);
+ lastIsPlaybackStarting = isPlaybackStarting;
+ }
+
+ }
+
+}
diff --git a/paperscope/myvideos/MyVideosRenderer.java b/paperscope/myvideos/MyVideosRenderer.java
new file mode 100644
index 00000000..9c8cebe3
--- /dev/null
+++ b/paperscope/myvideos/MyVideosRenderer.java
@@ -0,0 +1,1246 @@
+package com.google.vr.cardboard.paperscope.demo.myvideos;
+
+import com.google.vr.cardboard.paperscope.demo.R;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.SurfaceTexture;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+import android.os.SystemClock;
+import com.google.cardboard.sdk.CardboardView;
+import com.google.cardboard.sdk.CardboardView.Eye;
+import com.google.cardboard.sdk.HeadTransform;
+import com.google.cardboard.sdk.Viewport;
+import com.google.common.base.VerifyException;
+import com.google.common.flogger.FluentLogger;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import javax.microedition.khronos.egl.EGLConfig;
+
+/** Renderer that displays a main video screen and a grid of video thumbnails around the viewer. */
+public class MyVideosRenderer
+ implements CardboardView.Renderer, SurfaceTexture.OnFrameAvailableListener {
+
+ /** Listener for surface texture created events . */
+ public interface SurfaceTextureListener {
+ void onSurfaceTextureCreated(SurfaceTexture surfaceTexture);
+ }
+
+ private volatile int activeThumbnail = -1;
+ private final float[] thumbnailDistances;
+ private final float[] thumbnailBrightness;
+ private final float[] thumbnailAlpha;
+ private volatile int nowPlayingIndex = -1;
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final int NUM_SPRITE_TEXTURES = 7;
+ private static final int NUM_THUMBNAIL_TEXTURES = 80;
+
+ private static final int FLOAT_SIZE_BYTES = 4;
+ private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES;
+ private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0;
+ private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3;
+
+ // All distances in meters unless otherwise specified.
+
+ private static final float SCENE_MULTIPLIER = 1;
+ private static final float CAMERA_DISTANCE = 5 * SCENE_MULTIPLIER;
+
+ private static final int FBO_WIDTH = 8;
+ private static final int FBO_HEIGHT = 8;
+ private boolean enableFramebuffer = false;
+ private ByteBuffer framePixels;
+
+ // Red, green and blue values for the glow.
+ private final float[] glowValues;
+
+ // Distance from camera to thumbnail, thumbnail sphere radius.
+ private static final float DEFAULT_THUMBNAIL_DISTANCE = 4 * CAMERA_DISTANCE;
+ private static final float ACTIVE_THUMBNAIL_DISTANCE = 2.5f * CAMERA_DISTANCE;
+ private static final float Z_NEAR = 1;
+ private static final float Z_FAR = DEFAULT_THUMBNAIL_DISTANCE * 2 * SCENE_MULTIPLIER;
+ private static final float HALF_VIDEO_WIDTH = 2.8f * SCENE_MULTIPLIER;
+ private static final float FLOOR_TILE_SIZE = 4f * SCENE_MULTIPLIER;
+ private static final float VIDEO_WIDTH = HALF_VIDEO_WIDTH * 2;
+
+ // The default video aspect ratio. This is used to display the video glow, which acts like a
+ // a TV frame.
+ private static final float DEFAULT_ASPECT_RATIO = (16 / 9f);
+ private static final float VIDEO_HEIGHT = VIDEO_WIDTH / DEFAULT_ASPECT_RATIO;
+ private static final float HALF_VIDEO_HEIGHT = VIDEO_HEIGHT / 2;
+ private static final float CAMERA_HEIGHT = 0;
+ // Angle between sphere slices/rows.
+ private static final float ROW_ANGLE = 18;
+ // Number of sphere rows. N translates to center row + N-1 rows on top and N-1 on bottom.
+ private static final int ROWS_COUNT = 3;
+ // Angle reserved for screen, thumbnails wouldn't be drawn within it.
+ private static final float SCREEN_ANGLE = 100f;
+ // Approximate screen angles used to tell if the user is targeting the screen when pulling the
+ // magnet. Don't target the side edges of the screen.
+ private static final float TARGET_SCREEN_YAW = SCREEN_ANGLE - 40f;
+ private static final float TARGET_SCREEN_PITCH_UP = 45f;
+ private static final float TARGET_SCREEN_PITCH_DOWN = -20f;
+ private static final float TARGET_SEARCH_YAW = SCREEN_ANGLE - 80f;
+ private static final float TARGET_SEARCH_PITCH_UP = -30f;
+ private static final float TARGET_SEARCH_PITCH_DOWN = -45f;
+ private static final float HALF_THUMBNAIL_WIDTH = 3.05f * SCENE_MULTIPLIER;
+ private static final float THUMBNAIL_WIDTH = HALF_THUMBNAIL_WIDTH * 2;
+ private static final float THUMBNAIL_HEIGHT = THUMBNAIL_WIDTH * (9 / 16f);
+ private static final float HALF_THUMBNAIL_HEIGHT = THUMBNAIL_HEIGHT / 2;
+ private static final float SEARCH_SIZE = HALF_THUMBNAIL_HEIGHT / 4f;
+
+ private static final float[] videoVerticesData = {
+ // X, Y, Z, U, V
+ -HALF_VIDEO_WIDTH, HALF_VIDEO_HEIGHT, 0, 1, 1, HALF_VIDEO_WIDTH, HALF_VIDEO_HEIGHT, 0, 0, 1,
+ -HALF_VIDEO_WIDTH, -HALF_VIDEO_HEIGHT, 0, 1, 0, HALF_VIDEO_WIDTH, -HALF_VIDEO_HEIGHT, 0, 0, 0,
+ };
+
+ private static final float[] thumbnailVerticesData = {
+ // X, Y, Z, U, V
+ -HALF_THUMBNAIL_WIDTH, HALF_THUMBNAIL_HEIGHT, 0, 1, 1, HALF_THUMBNAIL_WIDTH,
+ HALF_THUMBNAIL_HEIGHT, 0, 0, 1,
+ -HALF_THUMBNAIL_WIDTH, -HALF_THUMBNAIL_HEIGHT, 0, 1, 0, HALF_THUMBNAIL_WIDTH,
+ -HALF_THUMBNAIL_HEIGHT, 0, 0, 0,
+ };
+
+ private static final float[] floorVerticesData = {
+ // X, Y, Z, U, V
+ -FLOOR_TILE_SIZE, FLOOR_TILE_SIZE, 0, 1, 1, FLOOR_TILE_SIZE, FLOOR_TILE_SIZE, 0, 0, 1,
+ -FLOOR_TILE_SIZE, -FLOOR_TILE_SIZE, 0, 1, 0, FLOOR_TILE_SIZE, -FLOOR_TILE_SIZE, 0, 0, 0,
+ };
+
+ private static final float[] lightboxVerticesData = {
+ // X,Y,Z, U, V
+ 1, 1, 0, 1, 1, -1, 1, 0, 0, 1, 1, -1, 0, 1, 0, -1, -1, 0, 0, 0,
+ };
+
+ private static final float[] searchVerticesData = {
+ // X,Y,Z, U, V
+ SEARCH_SIZE,
+ SEARCH_SIZE,
+ 0,
+ 0,
+ 1,
+ -SEARCH_SIZE,
+ SEARCH_SIZE,
+ 0,
+ 1,
+ 1,
+ SEARCH_SIZE,
+ -SEARCH_SIZE,
+ 0,
+ 0,
+ 0,
+ -SEARCH_SIZE,
+ -SEARCH_SIZE,
+ 0,
+ 1,
+ 0,
+ };
+
+ // The seek bar is a thin bar just under the video. It will get scaled in the X axis to be the
+ // right width.
+ private static final float[] seekBarVerticesData = {
+ // X, Y, Z, U, V
+ 0,
+ -HALF_VIDEO_HEIGHT * 1.01f,
+ 0,
+ 0,
+ 1,
+ -VIDEO_WIDTH,
+ -HALF_VIDEO_HEIGHT * 1.01f,
+ 0,
+ 1,
+ 1,
+ 0,
+ -HALF_VIDEO_HEIGHT * 1.02f,
+ 0,
+ 0,
+ 0,
+ -VIDEO_WIDTH,
+ -HALF_VIDEO_HEIGHT * 1.02f,
+ 0,
+ 1,
+ 0,
+ };
+
+ private static final float DEFAULT_THUMBNAIL_BRIGHTNESS = 0.5f;
+ private static final float ACTIVE_THUMBNAIL_BRIGHTNESS = 1.1f;
+ private static final float NOW_PLAYING_THUMBNAIL_BRIGHTNESS = 0.4f;
+ private static final float MIN_VIDEO_GLOW_BRIGHTNESS = 0.2f;
+ // The red, green, and blue brightness values for the glow when no video is playing.
+ // It has a red tint to match the red background when there is no video.
+ private static final float[] startVideoGlowBrightness = {1f, .2f, .5f};
+
+ // Full screen quad shader for drawing of a texture using texture coordinates as screen space
+ // vertex coordinates.
+ private static final String FSQ_VERTEX_SHADER =
+ """
+ const vec2 xy_rescale = vec2(0.5, 0.5);
+ attribute vec4 aPosition;
+ varying vec2 vTextureCoord;
+ void main() {
+ vTextureCoord = xy_rescale + aPosition.xy * xy_rescale;
+ gl_Position = vec4(aPosition.xy, 0.0, 1.0);
+ }\
+ """;
+
+ private static final String FSQ_FRAGMENT_SHADER =
+ """
+ #extension GL_OES_EGL_image_external : require
+ precision mediump float;
+ varying vec2 vTextureCoord;
+ uniform samplerExternalOES imageTexture;
+ void main() {
+ vec4 color = texture2D(imageTexture, vTextureCoord);
+ gl_FragColor = color + vec4(vTextureCoord.x * 0.1, vTextureCoord.y * 0.1, 0.0, 1.0);
+ }
+ """;
+
+ private static final String VERTEX_SHADER =
+ """
+ uniform mat4 uMvpMatrix;
+ uniform mat4 uStMatrix;
+ attribute vec4 aPosition;
+ attribute vec4 aTextureCoord;
+ varying vec2 vTextureCoord;
+ void main() {
+ gl_Position = uMvpMatrix * aPosition;
+ vTextureCoord = (uStMatrix * aTextureCoord).xy;
+ }
+ """;
+
+ private static final String FRAGMENT_SHADER =
+ """
+ precision mediump float;
+ varying vec2 vTextureCoord;
+ uniform sampler2D imageTexture;
+ uniform vec4 uBrightness;
+ void main() {
+ gl_FragColor = texture2D(imageTexture, vTextureCoord) * uBrightness;
+ }
+ """;
+
+ // Fragment shader using the samplerExternalOES so that it can use video as a texture.
+ private static final String VIDEO_FRAGMENT_SHADER =
+ """
+ #extension GL_OES_EGL_image_external : require
+ precision mediump float;
+ varying vec2 vTextureCoord;
+ uniform samplerExternalOES sTexture;
+ void main() {
+ gl_FragColor = texture2D(sTexture, vTextureCoord);
+ }
+ """;
+
+ /** Struct holding a program and its handlers. */
+ private static class ProgramHolder {
+ int program;
+ int uMvpMatrix;
+ int uStMatrix;
+ int aPosition;
+ int aTexture;
+ int uBrightness;
+ }
+
+ private static class FsqProgramHolder {
+ int program;
+ int aPosition;
+ }
+
+ private final FloatBuffer videoVertices;
+ private final FloatBuffer thumbnailVertices;
+ private final FloatBuffer lightboxVertices;
+ private final FloatBuffer seekBarVertices;
+ private final FloatBuffer floorVertices;
+ private final FloatBuffer searchVertices;
+
+ private final float[] mvpMatrix = new float[16];
+ private final float[] tmpMatrix = new float[16];
+ private final float[] videoStMatrix = new float[16];
+ private final float[] spriteStMatrix = new float[16];
+
+ // Model matrix
+ private final float[] modelMatrix = new float[16];
+ // View matrix
+ private final float[] viewMatrix = new float[16];
+ private final float[] lookAtMatrix = new float[16];
+ private final float[] headViewMatrix = new float[16];
+
+ // We use a separate program for the video texture since it uses its own fragment shader.
+ private ProgramHolder videoProgramHolder;
+ // Program used for all image based textures.
+ private ProgramHolder spriteProgramHolder;
+ private FsqProgramHolder fsqProgramHolder;
+
+ private int[] textureIds;
+ private int[] thumbnailTextureIds;
+ private int transitionBackgroundTextureId;
+ private int noVideoBackgroundTextureId;
+ private int videoGlowTextureId;
+ private int nowPlayingTextureId;
+ private int seekBarTextureId;
+ private int videoTextureId;
+ private int floorTextureId;
+
+ private SurfaceTexture surfaceTexture;
+ private boolean surfaceNeedsUpdate = false;
+ private long initialTime;
+ private float animationTime;
+
+ private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
+
+ private final Context context;
+ private final Bitmap[] thumbnailBitmaps;
+
+ private SurfaceTextureListener surfaceTextureListener;
+
+ private boolean isPlaybackStarting;
+ private float seekBarRatio;
+
+ private final float[] eulerAngles = new float[3];
+ private float lastYaw = 0.0f;
+ private float lastPitch = 0.0f;
+ private float targetScreenYaw;
+ private float screenYaw;
+ private boolean isScreenTargeted;
+ private boolean isSearchTargeted;
+
+ private int[] fboTargetTextureIds;
+ private int fboTargetTextureId;
+ private int framebuffer;
+ private int frameCounter = 0;
+
+ private boolean isListening;
+
+ // Whether there is no videos to display, in which case we show a "no video" background
+ // explaining to the user that they can take videos to view them here.
+ private boolean noVideos = true;
+
+ // Video aspect ratio. Will be updated according to video played.
+ private float videoAspectRatio = DEFAULT_ASPECT_RATIO;
+
+ public MyVideosRenderer(Context context) {
+ this.context = context;
+
+ videoVertices =
+ ByteBuffer.allocateDirect(videoVerticesData.length * FLOAT_SIZE_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ videoVertices.put(videoVerticesData).position(0);
+
+ thumbnailVertices =
+ ByteBuffer.allocateDirect(thumbnailVerticesData.length * FLOAT_SIZE_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ thumbnailVertices.put(thumbnailVerticesData).position(0);
+
+ lightboxVertices =
+ ByteBuffer.allocateDirect(lightboxVerticesData.length * FLOAT_SIZE_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ lightboxVertices.put(lightboxVerticesData).position(0);
+
+ seekBarVertices =
+ ByteBuffer.allocateDirect(seekBarVerticesData.length * FLOAT_SIZE_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ seekBarVertices.put(seekBarVerticesData).position(0);
+
+ floorVertices =
+ ByteBuffer.allocateDirect(floorVerticesData.length * FLOAT_SIZE_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ floorVertices.put(floorVerticesData).position(0);
+
+ searchVertices =
+ ByteBuffer.allocateDirect(searchVerticesData.length * FLOAT_SIZE_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ searchVertices.put(searchVerticesData).position(0);
+
+ thumbnailBitmaps = new Bitmap[NUM_THUMBNAIL_TEXTURES];
+ thumbnailDistances = new float[NUM_THUMBNAIL_TEXTURES];
+ thumbnailBrightness = new float[NUM_THUMBNAIL_TEXTURES];
+ thumbnailAlpha = new float[NUM_THUMBNAIL_TEXTURES];
+ glowValues = new float[3];
+
+ isPlaybackStarting = true;
+ }
+
+ public synchronized void setVideoAspectRatio(float videoAspectRatio) {
+ // We don't want to go over DEFAULT_ASPECT_RATIO otherwise the video would go over the video
+ // frame.
+ this.videoAspectRatio = min(DEFAULT_ASPECT_RATIO, videoAspectRatio);
+ }
+
+ public synchronized float getVideoAspectRatio() {
+ return videoAspectRatio;
+ }
+
+ public void setSurfaceTextureListener(SurfaceTextureListener surfaceTextureListener) {
+ this.surfaceTextureListener = surfaceTextureListener;
+ }
+
+ public void setIsBuffering(boolean isBuffering) {}
+
+ public void setIsPlaybackStarting(boolean isPlaybackStarting) {
+ this.isPlaybackStarting = isPlaybackStarting;
+ setGlowValues(
+ startVideoGlowBrightness[0], startVideoGlowBrightness[1], startVideoGlowBrightness[2]);
+ }
+
+ private void setGlowValues(float r, float g, float b) {
+ glowValues[0] = r;
+ glowValues[1] = g;
+ glowValues[2] = b;
+ }
+
+ public void setCurrentVideoIndex(int videoIndex) {
+ this.nowPlayingIndex = videoIndex;
+ }
+
+ public void setProgressRatio(float seekBarRatio) {
+ this.seekBarRatio = seekBarRatio;
+ }
+
+ public int getMaxThumbnailCount() {
+ return NUM_THUMBNAIL_TEXTURES;
+ }
+
+ public synchronized int getActiveThumbnailId() {
+ return activeThumbnail;
+ }
+
+ public boolean isScreenOnlyTargeted() {
+ return isScreenTargeted && activeThumbnail == -1;
+ }
+
+ public boolean isSearchTargeted() {
+ return isSearchTargeted;
+ }
+
+ public void centerScreen() {
+ targetScreenYaw = lastYaw + screenYaw - 180f;
+ }
+
+ private synchronized void setNoVideos(boolean noVideos) {
+ this.noVideos = noVideos;
+ }
+
+ private synchronized boolean hasNoVideos() {
+ return noVideos;
+ }
+
+ public void addThumbnail(Bitmap bitmap, int index) {
+ if (index >= NUM_THUMBNAIL_TEXTURES) {
+ logger.atSevere().log("Adding thumbnail with an index too high");
+ return;
+ }
+
+ // All thumbnails at the quality we request have letterboxing. Crop it out.
+ int cropAmount = bitmap.getHeight() / 8;
+ Bitmap croppedBitmap =
+ Bitmap.createBitmap(
+ bitmap, 0, cropAmount, bitmap.getWidth(), bitmap.getHeight() - 2 * cropAmount);
+ // Textures sizes need to be a power of 2.
+ Bitmap scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, 256, 256, false);
+ thumbnailBitmaps[index] = scaledBitmap;
+ loadThumbnailTexture(index);
+
+ bitmap.recycle();
+ croppedBitmap.recycle();
+ }
+
+ public void onThumbnailSelected(int id) {
+ if (id < NUM_THUMBNAIL_TEXTURES && thumbnailBitmaps[id] != null && activeThumbnail != -1) {
+ // If the user selects a thumbnail, show that it is selected by making it pop out and zoom
+ // again.
+ thumbnailDistances[id] = DEFAULT_THUMBNAIL_DISTANCE;
+ }
+ }
+
+ @Override
+ public void onNewFrame(HeadTransform headTransform) {
+ headTransform.getHeadView(headViewMatrix, 0);
+ // Yaw must be remapped due to sphere being upside down.
+ headTransform.getEulerAngles(eulerAngles, 0);
+ lastYaw = 180f - (float) Math.toDegrees(-eulerAngles[1]) - screenYaw;
+ lastYaw = lastYaw % 360f;
+ if (lastYaw < 0f) {
+ lastYaw += 360f;
+ }
+ lastPitch = (float) Math.toDegrees(eulerAngles[0]);
+ isScreenTargeted =
+ lastYaw > 180f - (TARGET_SCREEN_YAW / 2)
+ && lastYaw < 180f + (TARGET_SCREEN_YAW / 2)
+ && lastPitch < TARGET_SCREEN_PITCH_UP
+ && lastPitch > TARGET_SCREEN_PITCH_DOWN;
+ isSearchTargeted =
+ lastYaw > 180f - (TARGET_SEARCH_YAW / 2)
+ && lastYaw < 180f + (TARGET_SEARCH_YAW / 2)
+ && lastPitch < TARGET_SEARCH_PITCH_UP
+ && lastPitch > TARGET_SEARCH_PITCH_DOWN;
+
+ synchronized (this) {
+ if (surfaceNeedsUpdate) {
+ surfaceTexture.updateTexImage();
+ surfaceTexture.getTransformMatrix(videoStMatrix);
+ surfaceNeedsUpdate = false;
+ }
+ }
+ animationTime = SystemClock.uptimeMillis() - initialTime;
+
+ if (enableFramebuffer) {
+ frameCounter++;
+ if (frameCounter == 5) {
+ frameCounter = 0;
+ getAverageFrameColor();
+ }
+ }
+ }
+
+ @Override
+ public void onDrawEye(Eye eye) {
+ eye.applyHeadView(headViewMatrix);
+
+ GLES20.glEnable(GLES20.GL_BLEND);
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+
+ drawFloor(eye);
+ drawVideoGlow(eye);
+ drawVideo(eye);
+ drawSeekBar(eye);
+ drawThumbnailSphere(eye, 46);
+ }
+
+ @Override
+ public void onFinishFrame(Viewport viewport) {}
+
+ private void drawThumbnail(
+ int id, boolean active, float thumbnailAngle, float rowAngle, Eye eye) {
+ if (thumbnailBitmaps[id] == null) {
+ // Not thumbnail for this id.
+ return;
+ }
+
+ int textureId = thumbnailTextureIds[id];
+ createGeometry(spriteProgramHolder, thumbnailVertices, textureId);
+
+ // Slow up-down movement animation.
+ float angleAnimationDiff = (float) Math.sin(animationTime / 4000f + (id * 10f)) * 1.5f;
+
+ setBaseModelMatrix();
+ Matrix.rotateM(modelMatrix, 0, thumbnailAngle, 0, 1, 0);
+ Matrix.rotateM(modelMatrix, 0, rowAngle + angleAnimationDiff, 1, 0, 0);
+
+ float thumbnailDistance = updateAndGetThumbnailDistance(id, active);
+ Matrix.translateM(modelMatrix, 0, 0, 0, thumbnailDistance);
+
+ // Flip the model for the background so that we can have it upright regardless of the stMatrix
+ Matrix.rotateM(modelMatrix, 0, 180, 1, 0, 0);
+
+ Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0);
+ Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0);
+ Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0);
+
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0);
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0);
+
+ float alpha = updateAndGetThumbnailAlpha(id);
+ float brightness = alpha * updateAndGetThumbnailBrightness(id, active);
+ GLES20.glUniform4f(spriteProgramHolder.uBrightness, brightness, brightness, brightness, 1f);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ checkGlError("glDrawArrays");
+
+ // TODO(kvaalen): merge this now playing gl drawing with the above thumbnail gl drawing.
+ // They do the same thing, except that the now playing is smaller and lower.
+ if (id == nowPlayingIndex) {
+
+ createGeometry(spriteProgramHolder, thumbnailVertices, nowPlayingTextureId);
+
+ // Slow up-down movement animation.
+ angleAnimationDiff = (float) Math.sin(animationTime / 4000f + (id * 10f)) * 1.5f;
+
+ setBaseModelMatrix();
+ Matrix.rotateM(modelMatrix, 0, thumbnailAngle, 0, 1, 0);
+ Matrix.rotateM(modelMatrix, 0, rowAngle + angleAnimationDiff, 1, 0, 0);
+
+ Matrix.translateM(modelMatrix, 0, 0, 0, thumbnailDistance * 0.95f);
+
+ // Flip the model for the background so that we can have it upright regardless of the stMatrix
+ Matrix.rotateM(modelMatrix, 0, 180, 1, 0, 0);
+
+ Matrix.scaleM(modelMatrix, 0, 0.6f, 0.6f, 0.6f);
+
+ Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0);
+ Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0);
+ Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0);
+
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0);
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0);
+ GLES20.glUniform4f(spriteProgramHolder.uBrightness, 1.2f, 1.2f, 1.2f, 1.2f);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ checkGlError("glDrawArrays");
+ }
+ }
+
+ private float updateAndGetThumbnailAlpha(int id) {
+ // TODO(kvaalen): Make these animations depend on absolute time, not on the frame rate.
+ thumbnailAlpha[id] = thumbnailAlpha[id] + ((1f - thumbnailAlpha[id]) / 30f);
+ return thumbnailAlpha[id];
+ }
+
+ private float updateAndGetThumbnailDistance(int id, boolean isActive) {
+ if (thumbnailDistances[id] == 0f) {
+ thumbnailDistances[id] = DEFAULT_THUMBNAIL_DISTANCE;
+ }
+
+ float targetThumbnailDistance =
+ isActive ? ACTIVE_THUMBNAIL_DISTANCE : DEFAULT_THUMBNAIL_DISTANCE;
+
+ thumbnailDistances[id] =
+ thumbnailDistances[id] + ((targetThumbnailDistance - thumbnailDistances[id]) / 10f);
+
+ return thumbnailDistances[id];
+ }
+
+ private float updateAndGetThumbnailBrightness(int id, boolean isActive) {
+ if (thumbnailBrightness[id] == 0f) {
+ thumbnailBrightness[id] = DEFAULT_THUMBNAIL_BRIGHTNESS;
+ }
+
+ float targetThumbnailBrightness =
+ isActive ? ACTIVE_THUMBNAIL_BRIGHTNESS : DEFAULT_THUMBNAIL_BRIGHTNESS;
+
+ targetThumbnailBrightness =
+ (nowPlayingIndex == id) ? NOW_PLAYING_THUMBNAIL_BRIGHTNESS : targetThumbnailBrightness;
+
+ thumbnailBrightness[id] =
+ thumbnailBrightness[id] + ((targetThumbnailBrightness - thumbnailBrightness[id]) / 10);
+
+ return thumbnailBrightness[id];
+ }
+
+ private void drawThumbnailSphere(Eye eye, int thumbnails) {
+ GLES20.glUseProgram(spriteProgramHolder.program);
+ checkGlError("glUseProgram");
+
+ int decreasePerRow = 2;
+ int unusedOnAllRows = 0;
+ for (int row = 0; row < ROWS_COUNT; row++) {
+ unusedOnAllRows += decreasePerRow * (row + 1);
+ }
+
+ int thumbnailPerRow = (thumbnails + unusedOnAllRows) / ((ROWS_COUNT - 1) * 2 + 1);
+
+ // Number of thumbnail on this row would be decreased by this value.
+ int unusedOnCurrentRow = 0;
+ int thumbnailId = 0;
+ float rowAngle = 0;
+ int newActiveThumbnail = -1;
+ for (int row = 0; row < ROWS_COUNT; row++) {
+ int tiles = thumbnailPerRow - unusedOnCurrentRow;
+
+ for (int x = 0; x < tiles; x++) {
+ float angleRegion = 360f - SCREEN_ANGLE;
+
+ float anglesPerTile = angleRegion / (tiles - 1);
+ float thumbnailAngle = (anglesPerTile * x) + (SCREEN_ANGLE / 2);
+ if (thumbnailAngle > 360f) {
+ thumbnailAngle -= 360f;
+ }
+
+ float minRowAngle = -rowAngle - (ROW_ANGLE / 2f);
+ float maxRowAngle = -rowAngle + (ROW_ANGLE / 2f);
+ boolean rowActive = (lastPitch >= minRowAngle) && (lastPitch < maxRowAngle);
+
+ float minAngle = thumbnailAngle - (anglesPerTile / 2f) - 180f;
+ float maxAngle = thumbnailAngle + (anglesPerTile / 2f) - 180f;
+
+ float adjustedMinAngle = minAngle < 0 ? minAngle + 360 : minAngle;
+ float adjustedMaxAngle = minAngle < 0 ? maxAngle + 360 : maxAngle;
+ boolean columnActive = (lastYaw >= adjustedMinAngle) && (lastYaw < adjustedMaxAngle);
+
+ drawThumbnail(thumbnailId, columnActive && rowActive, thumbnailAngle, rowAngle, eye);
+ if (columnActive && rowActive) {
+ newActiveThumbnail = thumbnailId;
+ }
+ thumbnailId += 1;
+
+ // Draw bottom of the sphere, but because view is upsidedown - this is actually top half.
+ if (row != 0) {
+ rowActive = false;
+ minRowAngle = rowAngle - (ROW_ANGLE / 2f);
+ maxRowAngle = rowAngle + (ROW_ANGLE / 2f);
+ rowActive = (lastPitch >= minRowAngle) && (lastPitch < maxRowAngle);
+ drawThumbnail(thumbnailId, columnActive && rowActive, thumbnailAngle, -rowAngle, eye);
+ if (columnActive && rowActive) {
+ newActiveThumbnail = thumbnailId;
+ }
+ thumbnailId += 1;
+ }
+ }
+ rowAngle += ROW_ANGLE;
+ unusedOnCurrentRow += 2;
+ }
+
+ synchronized (this) {
+ activeThumbnail = newActiveThumbnail;
+ }
+ }
+
+ /**
+ * Create the base model matrix and set it in MMAtrix. The base model matrix is the rotation
+ * matrix to place the screen in the location where it's thumbnail was.
+ */
+ private void setBaseModelMatrix() {
+ screenYaw = screenYaw + ((targetScreenYaw - screenYaw) / 800);
+ Matrix.setRotateM(modelMatrix, 0, screenYaw, 0, 1, 0);
+ }
+
+ public void setListenStatusActive(boolean active) {
+ isListening = active;
+ }
+
+ public boolean isListening() {
+ return isListening;
+ }
+
+ private void drawVideo(Eye eye) {
+ float[] stMatrix;
+ ProgramHolder programHolder;
+ int textureId;
+
+ // If the video hasn't yet started.
+ if (isPlaybackStarting) {
+ stMatrix = spriteStMatrix;
+ programHolder = spriteProgramHolder;
+
+ if (hasNoVideos()) {
+ textureId = noVideoBackgroundTextureId;
+ } else {
+ // There's at least one video, so if we're here it means we're starting playback of
+ // another video, so show a transition texture.
+ textureId = transitionBackgroundTextureId;
+ }
+ } else {
+ stMatrix = videoStMatrix;
+ programHolder = videoProgramHolder;
+ textureId = videoTextureId;
+ }
+
+ GLES20.glUseProgram(programHolder.program);
+ checkGlError("glUseProgram");
+ createGeometry(programHolder, videoVertices, textureId);
+
+ setBaseModelMatrix();
+
+ // Scale the video texture according to its aspect ratio.
+ float horizontalScaling = getVideoAspectRatio() / DEFAULT_ASPECT_RATIO;
+ Matrix.scaleM(modelMatrix, 0, horizontalScaling, 1.0f, 1.0f);
+
+ Matrix.translateM(modelMatrix, 0, 0, 0, CAMERA_DISTANCE);
+
+ if (isPlaybackStarting) {
+ // Flip the model for the background so that we can have it upright regardless of the stMatrix
+ Matrix.rotateM(modelMatrix, 0, 180, 1, 0, 0);
+ GLES20.glUniform4f(spriteProgramHolder.uBrightness, 1, 1, 1, 1);
+ }
+
+ Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0);
+ Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0);
+ Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0);
+
+ GLES20.glUniformMatrix4fv(programHolder.uMvpMatrix, 1, false, mvpMatrix, 0);
+ GLES20.glUniformMatrix4fv(programHolder.uStMatrix, 1, false, stMatrix, 0);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ checkGlError("glDrawArrays");
+ }
+
+ private void drawFloor(Eye eye) {
+ GLES20.glUseProgram(spriteProgramHolder.program);
+ checkGlError("glUseProgram");
+ createGeometry(spriteProgramHolder, floorVertices, floorTextureId);
+
+ setBaseModelMatrix();
+
+ Matrix.rotateM(modelMatrix, 0, 90, 1, 0, 0);
+ Matrix.translateM(modelMatrix, 0, 0, 0, CAMERA_DISTANCE + 1.0f);
+
+ GLES20.glUniform4f(spriteProgramHolder.uBrightness, 1, 1, 1, 1);
+
+ Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0);
+ Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0);
+ Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0);
+
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0);
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ checkGlError("glDrawArrays");
+ }
+
+ private void drawSeekBar(Eye eye) {
+ GLES20.glUseProgram(spriteProgramHolder.program);
+ checkGlError("glUseProgram");
+ createGeometry(spriteProgramHolder, seekBarVertices, seekBarTextureId);
+
+ setBaseModelMatrix();
+ Matrix.translateM(modelMatrix, 0, 0, 0, CAMERA_DISTANCE);
+ Matrix.translateM(modelMatrix, 0, HALF_VIDEO_WIDTH, 0, 0);
+ Matrix.scaleM(modelMatrix, 0, seekBarRatio, 1f, 1f);
+
+ Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0);
+ Matrix.multiplyMM(mvpMatrix, 0, viewMatrix, 0, modelMatrix, 0);
+ Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, mvpMatrix, 0);
+
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0);
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0);
+ GLES20.glUniform4f(spriteProgramHolder.uBrightness, 1, 1, 1, 1);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ checkGlError("glDrawArrays");
+ }
+
+ /** Draws the glowing border around the video. */
+ private void drawVideoGlow(Eye eye) {
+ GLES20.glUseProgram(spriteProgramHolder.program);
+ checkGlError("glUseProgram");
+ createGeometry(spriteProgramHolder, videoVertices, videoGlowTextureId);
+
+ setBaseModelMatrix();
+ Matrix.translateM(modelMatrix, 0, 0, 0, CAMERA_DISTANCE);
+ float glowTextureScale = 0.2f;
+ Matrix.scaleM(modelMatrix, 0, 1f + glowTextureScale, 1f + glowTextureScale * (16f / 9f), 1f);
+ Matrix.multiplyMM(viewMatrix, 0, eye.getEyeView(), 0, lookAtMatrix, 0);
+ Matrix.multiplyMM(tmpMatrix, 0, viewMatrix, 0, modelMatrix, 0);
+ Matrix.multiplyMM(mvpMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, tmpMatrix, 0);
+
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uMvpMatrix, 1, false, mvpMatrix, 0);
+ GLES20.glUniformMatrix4fv(spriteProgramHolder.uStMatrix, 1, false, spriteStMatrix, 0);
+
+ GLES20.glUniform4f(
+ spriteProgramHolder.uBrightness, glowValues[0], glowValues[1], glowValues[2], 1f);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ checkGlError("glDrawArrays");
+ }
+
+ private void createGeometry(ProgramHolder programHolder, FloatBuffer vertices, int textureId) {
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ int glTextureType =
+ textureId == videoTextureId ? GL_TEXTURE_EXTERNAL_OES : GLES20.GL_TEXTURE_2D;
+ GLES20.glBindTexture(glTextureType, textureId);
+
+ vertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET);
+ GLES20.glVertexAttribPointer(
+ programHolder.aPosition,
+ 3,
+ GLES20.GL_FLOAT,
+ false,
+ TRIANGLE_VERTICES_DATA_STRIDE_BYTES,
+ vertices);
+ checkGlError("glVertexAttribPointer aPosition");
+ GLES20.glEnableVertexAttribArray(programHolder.aPosition);
+ checkGlError("glEnableVertexAttribArray aPosition handle");
+
+ vertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET);
+ GLES20.glVertexAttribPointer(
+ programHolder.aTexture,
+ 3,
+ GLES20.GL_FLOAT,
+ false,
+ TRIANGLE_VERTICES_DATA_STRIDE_BYTES,
+ vertices);
+ checkGlError("glVertexAttribPointer aTexture handle");
+ GLES20.glEnableVertexAttribArray(programHolder.aTexture);
+ checkGlError("glEnableVertexAttribArray aTexture handle");
+ }
+
+ private void createFsqGeometry(
+ FsqProgramHolder programHolder, FloatBuffer vertices, int textureId) {
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ int glTextureType =
+ textureId == videoTextureId ? GL_TEXTURE_EXTERNAL_OES : GLES20.GL_TEXTURE_2D;
+ GLES20.glBindTexture(glTextureType, textureId);
+
+ vertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET);
+ GLES20.glVertexAttribPointer(
+ programHolder.aPosition,
+ 3,
+ GLES20.GL_FLOAT,
+ false,
+ TRIANGLE_VERTICES_DATA_STRIDE_BYTES,
+ vertices);
+ checkGlError("glVertexAttribPointer aPosition");
+ GLES20.glEnableVertexAttribArray(programHolder.aPosition);
+ checkGlError("glEnableVertexAttribArray aPosition handle");
+ }
+
+ private void getAverageFrameColor() {
+ if (isPlaybackStarting) {
+ return;
+ }
+
+ // Save the current framebuffer so we can restore it at the end of this method
+ // (necessary for this code to work with distortion correction enabled).
+ int[] params = new int[1];
+ GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, params, 0);
+ int currentFrameBuffer = params[0];
+
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer);
+ GLES20.glViewport(0, 0, FBO_WIDTH, FBO_HEIGHT);
+ GLES20.glScissor(0, 0, FBO_WIDTH, FBO_HEIGHT);
+
+ checkGlError("glBindFramebuffer");
+ GLES20.glClearColor(0f, 0f, 0f, 1f);
+ GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
+
+ GLES20.glUseProgram(fsqProgramHolder.program);
+ checkGlError("glUseProgram");
+ createFsqGeometry(fsqProgramHolder, lightboxVertices, videoTextureId);
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ checkGlError("glDrawArrays");
+
+ framePixels.clear();
+ GLES20.glReadPixels(
+ 0, 0, FBO_WIDTH, FBO_HEIGHT, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, framePixels);
+ checkGlError("glReadPixels");
+
+ setGlowValues(0, 0, 0);
+ while (framePixels.hasRemaining()) {
+ glowValues[0] += framePixels.get() & 0xFF;
+ glowValues[1] += framePixels.get() & 0xFF;
+ glowValues[2] += framePixels.get() & 0xFF;
+ framePixels.get(); // Read the alpha to advance the buffer.
+ }
+
+ for (int i = 0; i < 3; i++) {
+ // Clamp the value so there is always some glow.
+ glowValues[i] =
+ max(
+ MIN_VIDEO_GLOW_BRIGHTNESS,
+ glowValues[i] / 255f / (FBO_WIDTH * FBO_HEIGHT * 3.0f) * 2.0f);
+ }
+
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, currentFrameBuffer);
+ }
+
+ @Override
+ public void onSurfaceChanged(int width, int height) {
+ // Nothing to do. Handled by GvrView.
+ }
+
+ @Override
+ public void onSurfaceCreated(EGLConfig config) {
+ initialTime = SystemClock.uptimeMillis();
+ videoProgramHolder = createProgramHolder(VIDEO_FRAGMENT_SHADER, false);
+ spriteProgramHolder = createProgramHolder(FRAGMENT_SHADER, true);
+ fsqProgramHolder = createFsqProgramHolder(FSQ_VERTEX_SHADER, FSQ_FRAGMENT_SHADER);
+
+ Matrix.setLookAtM(
+ lookAtMatrix, 0, 0, CAMERA_HEIGHT, 0, 0, CAMERA_HEIGHT, CAMERA_DISTANCE, 0, 1, 0);
+ Matrix.setIdentityM(videoStMatrix, 0);
+ Matrix.setIdentityM(spriteStMatrix, 0);
+
+ textureIds = new int[NUM_SPRITE_TEXTURES];
+ GLES20.glGenTextures(NUM_SPRITE_TEXTURES, textureIds, 0);
+
+ // Create the texture used for transitions between videos.
+ transitionBackgroundTextureId = textureIds[0];
+ createResourceTexture(transitionBackgroundTextureId, R.raw.transition_bg);
+
+ // Create the texture used when there is no video.
+ noVideoBackgroundTextureId = textureIds[1];
+ createResourceTexture(noVideoBackgroundTextureId, R.raw.novideos_bg);
+
+ // Create the texture used by the video background logo.
+ videoGlowTextureId = textureIds[2];
+ createResourceTexture(videoGlowTextureId, R.raw.glow_bg);
+
+ // Create the texture used by the video background logo.
+ nowPlayingTextureId = textureIds[3];
+ createResourceTexture(nowPlayingTextureId, R.raw.playing);
+
+ // Create the texture used by the seekbar.
+ seekBarTextureId = textureIds[4];
+ createResourceTexture(seekBarTextureId, R.raw.seekbar);
+
+ // Create the texture used for video playback.
+ videoTextureId = textureIds[5];
+ GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, videoTextureId);
+
+ // Create the texture used for floor.
+ floorTextureId = textureIds[6];
+ createResourceTexture(floorTextureId, R.raw.floor);
+
+ checkGlError("glBindTexture mTextureID");
+
+ GLES20.glTexParameterf(
+ GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
+ GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+
+ surfaceTexture = new SurfaceTexture(videoTextureId);
+ surfaceTexture.setOnFrameAvailableListener(this);
+
+ surfaceTextureListener.onSurfaceTextureCreated(surfaceTexture);
+
+ synchronized (this) {
+ surfaceNeedsUpdate = false;
+ }
+
+ fboTargetTextureId = createFboTargetTexture(FBO_WIDTH, FBO_HEIGHT);
+ framebuffer = createFrameBuffer(fboTargetTextureId);
+ if (framebuffer != -1) {
+ framePixels = ByteBuffer.allocateDirect(FBO_WIDTH * FBO_HEIGHT * 4);
+ framePixels.order(ByteOrder.nativeOrder());
+ enableFramebuffer = true;
+ }
+
+ // Update the thumbnails that have been loaded so far.
+ thumbnailTextureIds = new int[NUM_THUMBNAIL_TEXTURES];
+ GLES20.glGenTextures(NUM_THUMBNAIL_TEXTURES, thumbnailTextureIds, 0);
+ for (int i = 0; i < NUM_THUMBNAIL_TEXTURES; i++) {
+ if (thumbnailBitmaps[i] != null) {
+ loadThumbnailTexture(i);
+ }
+ }
+ }
+
+ private int createFboTargetTexture(int width, int height) {
+ fboTargetTextureIds = new int[1];
+ GLES20.glGenTextures(1, fboTargetTextureIds, 0);
+ int texture = fboTargetTextureIds[0];
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
+ GLES20.glTexImage2D(
+ GLES20.GL_TEXTURE_2D,
+ 0,
+ GLES20.GL_RGBA,
+ width,
+ height,
+ 0,
+ GLES20.GL_RGBA,
+ GLES20.GL_UNSIGNED_BYTE,
+ null);
+
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ return texture;
+ }
+
+ private int createFrameBuffer(int fboTargetTextureId) {
+ // Save the current framebuffer so we can restore it at the end of this method
+ // (necessary for this code to work with distortion correction enabled).
+ int[] params = new int[1];
+ GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, params, 0);
+ int currentFrameBuffer = params[0];
+
+ int[] framebuffers = new int[1];
+ GLES20.glGenFramebuffers(1, framebuffers, 0);
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffers[0]);
+
+ int[] renderbuffers = new int[1];
+ GLES20.glGenRenderbuffers(1, renderbuffers, 0);
+
+ GLES20.glFramebufferTexture2D(
+ GLES20.GL_FRAMEBUFFER,
+ GLES20.GL_COLOR_ATTACHMENT0,
+ GLES20.GL_TEXTURE_2D,
+ fboTargetTextureId,
+ 0);
+ int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
+
+ if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
+ logger.atSevere().log("Framebuffer is not complete: %x", status);
+ return -1;
+ }
+
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, currentFrameBuffer);
+ return framebuffers[0];
+ }
+
+ private void loadThumbnailTexture(int index) {
+ if (thumbnailTextureIds == null) {
+ // The thumbnail textures will get loaded later once the thumbnailTextureIds are generated.
+ return;
+ }
+
+ int textureId = thumbnailTextureIds[index];
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
+
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
+
+ GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, thumbnailBitmaps[index], 0);
+ }
+
+ private void createResourceTexture(int textureId, int resourceId) {
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
+
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
+ GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
+
+ InputStream is = context.getResources().openRawResource(resourceId);
+ Bitmap bitmap;
+ try {
+ bitmap = BitmapFactory.decodeStream(is);
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+
+ GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
+ bitmap.recycle();
+ }
+
+ @Override
+ public synchronized void onFrameAvailable(SurfaceTexture surface) {
+ // We received a frame, so it means we have at least one valid video.
+ setNoVideos(false);
+ surfaceNeedsUpdate = true;
+ }
+
+ /** Creates a GL program and the associated handlers from a given fragment shader. */
+ private ProgramHolder createProgramHolder(String fragmentShader, boolean supportsBrightness) {
+ ProgramHolder holder = new ProgramHolder();
+ holder.program = createProgram(VERTEX_SHADER, fragmentShader);
+ if (holder.program == 0) {
+ throw new VerifyException("Could not create program");
+ }
+
+ holder.aPosition = GLES20.glGetAttribLocation(holder.program, "aPosition");
+ checkGlError("glGetAttribLocation aPosition");
+ if (holder.aPosition == -1) {
+ throw new VerifyException("Could not get attrib location for aPosition");
+ }
+ holder.aTexture = GLES20.glGetAttribLocation(holder.program, "aTextureCoord");
+ checkGlError("glGetAttribLocation aTextureCoord");
+ if (holder.aTexture == -1) {
+ throw new VerifyException("Could not get attrib location for aTextureCoord");
+ }
+
+ holder.uMvpMatrix = GLES20.glGetUniformLocation(holder.program, "uMvpMatrix");
+ checkGlError("glGetUniformLocation uMvpMatrix");
+ if (holder.uMvpMatrix == -1) {
+ throw new VerifyException("Could not get attrib location for uMvpMatrix");
+ }
+
+ holder.uStMatrix = GLES20.glGetUniformLocation(holder.program, "uStMatrix");
+ checkGlError("glGetUniformLocation uStMatrix");
+ if (holder.uStMatrix == -1) {
+ throw new VerifyException("Could not get attrib location for uStMatrix");
+ }
+
+ if (supportsBrightness) {
+ holder.uBrightness = GLES20.glGetUniformLocation(holder.program, "uBrightness");
+ checkGlError("glGetUniformLocation uBrightness");
+ if (holder.uBrightness == -1) {
+ throw new VerifyException("Could not get attrib location for uBrightness");
+ }
+ }
+
+ return holder;
+ }
+
+ private FsqProgramHolder createFsqProgramHolder(String vertexShader, String fragmentShader) {
+ FsqProgramHolder holder = new FsqProgramHolder();
+ holder.program = createProgram(vertexShader, fragmentShader);
+ if (holder.program == 0) {
+ throw new VerifyException("Could not create program");
+ }
+
+ holder.aPosition = GLES20.glGetAttribLocation(holder.program, "aPosition");
+ checkGlError("glGetAttribLocation aPosition");
+ if (holder.aPosition == -1) {
+ throw new VerifyException("Could not get attrib location for aPosition");
+ }
+ return holder;
+ }
+
+ private int loadShader(int shaderType, String source) {
+ int shader = GLES20.glCreateShader(shaderType);
+ if (shader != 0) {
+ GLES20.glShaderSource(shader, source);
+ GLES20.glCompileShader(shader);
+ int[] compiled = new int[1];
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
+ if (compiled[0] == 0) {
+ logger.atSevere().log(
+ "Could not compile shader %s %s", shaderType, GLES20.glGetShaderInfoLog(shader));
+ GLES20.glDeleteShader(shader);
+ shader = 0;
+ }
+ }
+ return shader;
+ }
+
+ private int createProgram(String vertexSource, String fragmentSource) {
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
+ if (vertexShader == 0) {
+ return 0;
+ }
+ int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
+ if (pixelShader == 0) {
+ return 0;
+ }
+
+ int program = GLES20.glCreateProgram();
+ if (program != 0) {
+ GLES20.glAttachShader(program, vertexShader);
+ checkGlError("glAttachShader");
+ GLES20.glAttachShader(program, pixelShader);
+ checkGlError("glAttachShader");
+ GLES20.glLinkProgram(program);
+ int[] linkStatus = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
+ if (linkStatus[0] != GLES20.GL_TRUE) {
+ logger.atSevere().log("Could not link program: %s", GLES20.glGetProgramInfoLog(program));
+ GLES20.glDeleteProgram(program);
+ program = 0;
+ }
+ }
+ return program;
+ }
+
+ private void checkGlError(String op) {
+ int error;
+ while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+ logger.atSevere().log("%s : glError: %s", op, error);
+ throw new VerifyException(op + ": glError " + error);
+ }
+ }
+
+ @Override
+ public void onRendererShutdown() {
+ GLES20.glDeleteTextures(NUM_SPRITE_TEXTURES, textureIds, 0);
+ GLES20.glDeleteTextures(NUM_THUMBNAIL_TEXTURES, thumbnailTextureIds, 0);
+ GLES20.glDeleteTextures(1, fboTargetTextureIds, 0);
+
+ GLES20.glDeleteProgram(videoProgramHolder.program);
+ GLES20.glDeleteProgram(spriteProgramHolder.program);
+ GLES20.glDeleteProgram(fsqProgramHolder.program);
+ }
+}
diff --git a/paperscope/myvideos/VideoFetcher.java b/paperscope/myvideos/VideoFetcher.java
new file mode 100644
index 00000000..823195f9
--- /dev/null
+++ b/paperscope/myvideos/VideoFetcher.java
@@ -0,0 +1,106 @@
+package com.google.vr.cardboard.paperscope.demo.myvideos;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import com.google.common.flogger.FluentLogger;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A basic video fetcher for videos stored on the phone.
+ * TODO(dcoz): test this class.
+ */
+public class VideoFetcher {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ /** Simple structure with information about a video stored on the phone file system. */
+ public static class VideoInfo {
+
+ /** Stream for the video. */
+ private Uri videoStreamUri;
+
+ /** The absolute file path to the video file. */
+ private String videoAbsolutePath;
+
+ public VideoInfo(Uri videoStreamUri, String videoAbsolutePath) {
+ this.videoStreamUri = videoStreamUri;
+ this.videoAbsolutePath = videoAbsolutePath;
+ }
+
+ public Uri getVideoStreamUri() {
+ return videoStreamUri;
+ }
+
+ public String getVideoAbsolutePath() {
+ return videoAbsolutePath;
+ }
+ }
+
+ /**
+ * Interface used to communicate video results back to requesters.
+ */
+ public interface Callback {
+ /**
+ * Called when a video list response was obtained successfully.
+ */
+ void onResponse(List response);
+
+ /**
+ * Called when an exception is caught while getting the response.
+ */
+ void onError(Exception exception);
+ }
+
+ private final Callback videoListCallback;
+ private Cursor videoCursor;
+ private Context context;
+
+ public VideoFetcher(Context context, Callback videoListCallback) {
+ this.context = context;
+ this.videoListCallback = videoListCallback;
+ }
+
+ /**
+ * Fetches the local videos stored on the phone. On success, the list of videos are sent to
+ * the video callback specified in the constructor.
+ */
+ public void getLocalVideos() {
+ // Prepare a query for local videos, sorted by date.
+ // TODO(dcoz): figure out what we should do for videos that don't have the expected 'landscape'
+ // aspect ratio.
+
+ String[] projection = {
+ MediaStore.Video.Media._ID, MediaStore.Video.Media.DATA,
+ };
+ videoCursor = context.getContentResolver().query(
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ null,
+ null,
+ MediaStore.MediaColumns.DATE_ADDED + " DESC");
+
+ if (videoCursor == null) {
+ logger.atWarning().log("Error querying local videos.");
+ videoListCallback.onError(new IOException("Error querying local videos."));
+ }
+
+ int idColumn = videoCursor.getColumnIndex(MediaStore.Video.Media._ID);
+ int dataColumn = videoCursor.getColumnIndex(MediaStore.Video.Media.DATA);
+
+ List videoInfoList = new ArrayList();
+ while (videoCursor.moveToNext()) {
+ Uri contentUri = Uri.parse(
+ android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ + "/" + videoCursor.getString(idColumn));
+
+ videoInfoList.add(new VideoInfo(contentUri, videoCursor.getString(dataColumn)));
+ }
+
+ videoListCallback.onResponse(videoInfoList);
+ }
+
+}
diff --git a/paperscope/myvideos/VideoSequencer.java b/paperscope/myvideos/VideoSequencer.java
new file mode 100644
index 00000000..3cb38e3e
--- /dev/null
+++ b/paperscope/myvideos/VideoSequencer.java
@@ -0,0 +1,204 @@
+package com.google.vr.cardboard.paperscope.demo.myvideos;
+
+import android.content.Context;
+import android.util.Pair;
+import com.google.cardboard.sdk.CardboardView;
+import com.google.common.flogger.FluentLogger;
+import com.google.vr.cardboard.paperscope.demo.myvideos.VideoFetcher.VideoInfo;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Sequencer for fetching and playing videos.
+ */
+public class VideoSequencer implements MyVideosPlayer.Listener {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ /**
+ * When an error happens, we skip to the next video. To ensure we don't hit the server too much
+ * if something goes wrong, we limit the number of skips.
+ */
+ private static final int MAX_CONSECUTIVE_ERRORS_ALLOWED = 20;
+
+ private final CardboardView cardboardView;
+ private final MyVideosRenderer renderer;
+ private final MyVideosPlayer player;
+ private final VideoFetcher videoFetcher;
+ private final ImageFetcher imageFetcher;
+
+ private List videoList;
+ private int currentVideoIndex;
+ private int numThumbnailsFetched;
+ private int consecutiveErrorCount;
+
+ public VideoSequencer(Context context, CardboardView cardboardView, MyVideosRenderer renderer) {
+ this.cardboardView = cardboardView;
+ this.renderer = renderer;
+
+ player = new MyVideosPlayer(context, this /* Listener */, renderer);
+ videoFetcher = new VideoFetcher(context, new VideoListCallback());
+ imageFetcher = new ImageFetcher(context, cardboardView, renderer);
+
+ renderer.setSurfaceTextureListener(player);
+ }
+
+ public void startPlayback() {
+ if (videoList != null && !videoList.isEmpty()) {
+ currentVideoIndex = currentVideoIndex < 0 ? 0 : currentVideoIndex;
+ player.play(videoList.get(currentVideoIndex));
+ } else {
+ imageFetcher.reset();
+ currentVideoIndex = -1;
+ maybeLoadMoreVideos();
+ }
+ }
+
+ public void stop() {
+ if (player != null) {
+ player.stop();
+ }
+ }
+
+ public void togglePause() {
+ if (player != null) {
+ player.togglePause();
+ }
+ }
+
+ public void play() {
+ if (player != null) {
+ player.play();
+ }
+ }
+
+ public void pause() {
+ if (player != null) {
+ player.pause();
+ }
+ }
+
+ public void release() {
+ player.release();
+ }
+
+ public void playTargetedVideo() {
+ // Might be null if the trigger is pulled before any video is loaded.
+ if (videoList == null) {
+ return;
+ }
+
+ final int videoIndex = renderer.getActiveThumbnailId();
+ if (videoIndex < 0 || videoIndex >= videoList.size()) {
+ logger.atSevere().log("Video index out of bounds");
+ return;
+ }
+
+ cardboardView.queueEvent(new Runnable() {
+ @Override
+ public void run() {
+ renderer.onThumbnailSelected(videoIndex);
+ }
+ });
+ playVideoByIndex(videoIndex);
+ }
+
+ /**
+ * Load local videos.
+ */
+ private void maybeLoadMoreVideos() {
+ if (videoList == null) {
+ videoFetcher.getLocalVideos();
+ }
+ }
+
+ private void playNext() {
+ if (videoList != null) {
+ playVideoByIndex((currentVideoIndex + 1) % videoList.size());
+ }
+ }
+
+ private void playVideoByIndex(int index) {
+ if (videoList == null) {
+ return;
+ }
+
+ if (index < 0 || index >= videoList.size()) {
+ logger.atSevere().log(
+ "Tried to play video at index: %d , but video list size is %d", index, videoList.size());
+ return;
+ }
+
+ currentVideoIndex = index;
+ player.play(videoList.get(index));
+ renderer.setCurrentVideoIndex(index);
+ }
+
+ private void fetchThumbnails(List videoList, int startIndex) {
+ int index = startIndex;
+ List> thumbnailUrls = new ArrayList<>(videoList.size());
+ for (VideoInfo videoInfo : videoList) {
+ if (numThumbnailsFetched++ >= renderer.getMaxThumbnailCount()) {
+ break;
+ }
+ thumbnailUrls.add(Pair.create(index++, videoInfo));
+ }
+ imageFetcher.downloadImages(thumbnailUrls);
+ }
+
+ // MyVideosPlayer.Listener
+
+ @Override
+ public void onVideoEnded() {
+ playNext();
+ }
+
+ @Override
+ public void onPlaybackError(int what, int extra) {
+ if (consecutiveErrorCount++ < MAX_CONSECUTIVE_ERRORS_ALLOWED) {
+ playNext();
+ } else {
+ // TODO(kvaalen): Show a visual error message.
+ player.stop();
+ }
+ }
+
+ @Override
+ public void onPlaybackSucceeded() {
+ consecutiveErrorCount = 0;
+ }
+
+ /**
+ * Callback listening to streams from a GData request and plays the appropriate one.
+ */
+ private class VideoListCallback implements VideoFetcher.Callback {
+
+ @Override
+ public void onResponse(List videoList) {
+ if (!videoList.isEmpty()) {
+ int thumbnailStartIndex;
+ if (VideoSequencer.this.videoList == null) {
+ thumbnailStartIndex = 0;
+ VideoSequencer.this.videoList = videoList;
+ playVideoByIndex(0);
+ } else {
+ thumbnailStartIndex = VideoSequencer.this.videoList.size();
+ // Append the results so that we can request several categories.
+ VideoSequencer.this.videoList.addAll(videoList);
+ }
+ fetchThumbnails(videoList, thumbnailStartIndex);
+ maybeLoadMoreVideos();
+ } else {
+ onError(new Exception("Video list is empty"));
+ }
+ }
+
+ @Override
+ public void onError(Exception exception) {
+ // TODO(kvaalen): Show some type of visual error.
+ logger.atSevere().withCause(exception).log("Error getting videos");
+ }
+
+ }
+
+}
diff --git a/paperscope/myvideos/res/raw/floor.png b/paperscope/myvideos/res/raw/floor.png
new file mode 100644
index 00000000..8bc0d1fa
Binary files /dev/null and b/paperscope/myvideos/res/raw/floor.png differ
diff --git a/paperscope/myvideos/res/raw/glow_bg.png b/paperscope/myvideos/res/raw/glow_bg.png
new file mode 100644
index 00000000..f67d4199
Binary files /dev/null and b/paperscope/myvideos/res/raw/glow_bg.png differ
diff --git a/paperscope/myvideos/res/raw/novideos_bg.png b/paperscope/myvideos/res/raw/novideos_bg.png
new file mode 100644
index 00000000..972f0fcd
Binary files /dev/null and b/paperscope/myvideos/res/raw/novideos_bg.png differ
diff --git a/paperscope/myvideos/res/raw/playing.png b/paperscope/myvideos/res/raw/playing.png
new file mode 100644
index 00000000..a51d57e8
Binary files /dev/null and b/paperscope/myvideos/res/raw/playing.png differ
diff --git a/paperscope/myvideos/res/raw/seekbar.png b/paperscope/myvideos/res/raw/seekbar.png
new file mode 100644
index 00000000..19ba9d9a
Binary files /dev/null and b/paperscope/myvideos/res/raw/seekbar.png differ
diff --git a/paperscope/myvideos/res/raw/transition_bg.png b/paperscope/myvideos/res/raw/transition_bg.png
new file mode 100644
index 00000000..99b603bc
Binary files /dev/null and b/paperscope/myvideos/res/raw/transition_bg.png differ
diff --git a/paperscope/paperscope_launcher/common/strings/localized_strings.cc b/paperscope/paperscope_launcher/common/strings/localized_strings.cc
new file mode 100644
index 00000000..e3b5e2e5
--- /dev/null
+++ b/paperscope/paperscope_launcher/common/strings/localized_strings.cc
@@ -0,0 +1,46 @@
+#include "../common/strings/localized_strings.h"
+
+#include
+#include
+#include
+#include
+
+namespace cardboard_demo {
+
+std::vector GetProbableLocalesInOrder(std::string locale) {
+ std::vector out;
+ // Convert BCP 47 codes (dash separators) to ISO 15897 codes (underscores),
+ // to match the canonical format used by Translation Console.
+ std::replace(locale.begin(), locale.end(), '-', '_');
+
+ // First, try using the ISO 15897 locale (e.g. "pt_BR").
+ out.push_back(locale);
+
+#if defined(ION_PLATFORM_IOS) || defined(ION_PLATFORM_ANDROID)
+ // Replace common iOS script IDs with canonical ISO 15897 codes.
+ if (locale == "zh_Hans") {
+ locale = "zh_CN";
+ } else if (locale == "zh_Hant") {
+ locale = "zh_TW";
+ }
+ out.push_back(locale);
+#endif // defined(ION_PLATFORM_IOS) || defined(ION_PLATFORM_ANDROID)
+
+ // Then look up the locale's ISO 639 alpha-2 code (2 letter code, e.g. "pt").
+ if (locale.size() > 2) {
+ locale.resize(2);
+ out.push_back(locale);
+ }
+
+ // Otherwise, fallback to ISO 639-1 English ("en") which must be present in
+ // any vr::LocaleMap.
+ out.push_back("en");
+ return out;
+}
+
+const char* GetLocalizedStringFromMap(std::string_view key,
+ const LocaleEntry& entry) {
+ auto iter = entry.find(key.data());
+ return (iter == entry.end()) ? nullptr : iter->second.c_str();
+}
+} // namespace cardboard_demo
diff --git a/paperscope/paperscope_launcher/common/strings/localized_strings.h b/paperscope/paperscope_launcher/common/strings/localized_strings.h
new file mode 100644
index 00000000..6de60d13
--- /dev/null
+++ b/paperscope/paperscope_launcher/common/strings/localized_strings.h
@@ -0,0 +1,30 @@
+// Used by the code generated by vr/apps/common/tools/genstrings.py.
+
+#ifndef PAPERSCOPE_LAUNCHER_COMMON_STRINGS_LOCALIZED_STRINGS_H_
+#define PAPERSCOPE_LAUNCHER_COMMON_STRINGS_LOCALIZED_STRINGS_H_
+
+#include
+#include
+#include
+#include
+
+namespace cardboard_demo {
+typedef std::unordered_map LocaleEntry;
+typedef std::unordered_map LocaleMap;
+
+// Returns the locale strings to try to try for a given locale in order of best
+// choice to worst.
+// This should be used for retrieving LocaleEntrys from a LocaleMap which is
+// autogenerated by //vr/apps/common/tools/localization/genstrings.py.
+// "en" is required in any LocaleMap and will thus always be included at the
+// end of the vector as a last resort.
+std::vector GetProbableLocalesInOrder(std::string locale);
+
+// Returns localized string for |key| in the |entry| if it exists, otherwise
+// returns nullptr.
+const char* GetLocalizedStringFromMap(std::string_view key,
+ const LocaleEntry& entry);
+} // namespace cardboard_demo
+
+
+#endif // PAPERSCOPE_LAUNCHER_COMMON_STRINGS_LOCALIZED_STRINGS_H_
diff --git a/paperscope/paperscope_launcher/common/strings/localized_strings_test.cc b/paperscope/paperscope_launcher/common/strings/localized_strings_test.cc
new file mode 100644
index 00000000..35cb35f9
--- /dev/null
+++ b/paperscope/paperscope_launcher/common/strings/localized_strings_test.cc
@@ -0,0 +1,110 @@
+#include "vr/apps/cardboard/demo/common/strings/localized_strings.h"
+
+#include
+#include
+
+#include "testing/base/public/gunit.h"
+
+using cardboard_demo::GetLocalizedStringFromMap;
+using cardboard_demo::GetProbableLocalesInOrder;
+using cardboard_demo::LocaleMap;
+
+namespace {
+const cardboard_demo::LocaleEntry& GetLocalizedStringsMap(
+ const std::string& target_locale,
+ cardboard_demo::LocaleMap& locale_map) {
+ const std::vector locales =
+ GetProbableLocalesInOrder(target_locale);
+ for (auto& locale : locales) {
+ if (locale_map.find(locale) != locale_map.end()) {
+ return locale_map.at(locale);
+ }
+ }
+ // Fallback to last locale option.
+ return locale_map.at(locales[locales.size() - 1]);
+}
+} // namespace
+
+TEST(LocalizedStrings, GetProbableLocalesInOrder) {
+ LocaleMap locale_map = {
+ {
+ "en",
+ {
+ {"1", "one"},
+ {"2", "two"},
+ },
+ },
+ {
+ "fr",
+ {
+ {"1", "une"},
+ {"2", "deux"},
+ },
+ },
+ };
+ std::vector locales = GetProbableLocalesInOrder("fr");
+ // Expecting ["fr", "en"].
+ EXPECT_EQ(locales.size(), 2);
+ EXPECT_EQ("fr", locales[0]);
+ EXPECT_EQ("en", locales[1]);
+ locales = GetProbableLocalesInOrder("en");
+ // Expecting "en" to be first (may be 2 "en" instances).
+ EXPECT_GE(locales.size(), 1);
+ EXPECT_EQ("en", locales[0]);
+}
+
+TEST(LocalizedStrings, GetLocalizedStringFromMap) {
+ LocaleMap locale_map = {
+ {
+ "en",
+ {
+ {"1", "one"},
+ {"2", "two"},
+ },
+ },
+ {
+ "fr",
+ {
+ {"1", "une"},
+ {"2", "deux"},
+ },
+ },
+ };
+ LocaleMap en_map = {
+ {
+ "en",
+ {
+ {"3", "three"},
+ {"4", "four"},
+ },
+ },
+ };
+ auto locale = GetLocalizedStringsMap("en", locale_map);
+ EXPECT_EQ("one", locale["1"]);
+ EXPECT_EQ("two", locale["2"]);
+ EXPECT_EQ(locale.end(), locale.find("3"));
+ EXPECT_STREQ("one", GetLocalizedStringFromMap("1", locale));
+ EXPECT_STREQ("two", GetLocalizedStringFromMap("2", locale));
+ EXPECT_EQ(nullptr, GetLocalizedStringFromMap("3", locale));
+
+ locale = GetLocalizedStringsMap("en", en_map);
+ EXPECT_EQ("three", locale["3"]);
+ EXPECT_EQ("four", locale["4"]);
+ EXPECT_EQ(locale.end(), locale.find("1"));
+
+ locale = GetLocalizedStringsMap("fr", locale_map);
+ EXPECT_EQ("une", locale["1"]);
+ EXPECT_EQ("deux", locale["2"]);
+ EXPECT_EQ(locale.end(), locale.find("3"));
+
+ locale = GetLocalizedStringsMap("fr_ext", locale_map);
+ EXPECT_EQ("une", locale["1"]);
+ EXPECT_EQ("deux", locale["2"]);
+ EXPECT_EQ(locale.end(), locale.find("3"));
+
+ // Test that we fall back to English.
+ locale = GetLocalizedStringsMap("sp", locale_map);
+ EXPECT_EQ("one", locale["1"]);
+ EXPECT_EQ("two", locale["2"]);
+ EXPECT_EQ(locale.end(), locale.find("3"));
+}
diff --git a/paperscope/paperscope_launcher/common/tools/genstrings.py b/paperscope/paperscope_launcher/common/tools/genstrings.py
new file mode 100755
index 00000000..6e102f49
--- /dev/null
+++ b/paperscope/paperscope_launcher/common/tools/genstrings.py
@@ -0,0 +1,487 @@
+#!/usr/bin/python
+"""A script which can be used to generate code to easily load strings.
+
+Run this script inside a custom build step with:
+ genstrings.py
+ --resource_file=resources/strings/en.lproj/MyStrings.strings
+ --namespace=mystrings
+ --out_dir=resources/strings/
+TODO(bwuest): Add unit test. b/24000998
+"""
+
+import optparse
+import os
+import re
+import sys
+from typing import Dict, List
+
+
+class Error(Exception):
+ """The base call of all exceptions in this module."""
+ pass
+
+
+class StringsParser(object):
+ """A class which can be used to parse xcode .strings files."""
+
+ def __init__(self, resource_file: str, namespace: str, out_dir: str,
+ check_substitution_tokens: bool, language: str) -> None:
+ """Initializes parser with the resource file to use.
+
+ Args:
+ resource_file: The name of the Localizeable.strings file.
+ namespace: The namespace used for the generated classes.
+ out_dir: The directory where the generated code should go.
+ check_substitution_tokens: Whether or not to validate that the
+ substitution tokens are valid.
+ language: Target language for code generation.
+ """
+ self._resource_file_name = resource_file
+ self._namespace = namespace
+ self._out_dir = out_dir
+ self._parsed_strings = {}
+ self._check_substitution_tokens = check_substitution_tokens
+ self._language = language
+
+ def Run(self):
+ """Parses the resource file, and generates the output files."""
+ self._ParseFile()
+ if self._language == 'cc':
+ self._GenerateCc()
+ elif self._language == 'java':
+ self._GenerateJava()
+ else:
+ raise Error('Invalid language specified')
+
+ def _GenerateEnumConstant(self, string_key):
+ """Generates a string constant from a string key.
+
+ Args:
+ string_key: The key for the string. This needs to be converted into
+ a camelcase string, prefixed by kStr.
+
+ Returns:
+ The enum constant for the given string key.
+ """
+ return 'kStr' + ''.join([part[0].upper() + part[1:]
+ for part in string_key.split('_')])
+
+ def _GenerateCc(self) -> None:
+ """Generates the contents of the C++ output files, from the parsed input."""
+
+ locale_map = self._GetFullLocaleMap()
+
+ locale_map_initialization_functions = []
+ locale_map_initialization = []
+ # Inserting locales one at a time to the map, to avoid having more than one
+ # on the stack at a given time, which would violate stack frame size checks.
+ for locale_id, string_map in locale_map.items():
+ string_map = [
+ ' this_language_map["%s"] = "%s";' % i
+ for i in string_map.items()
+ ]
+
+ locale_map_initialization_functions.append("""\
+ void Add%(locale_id)sToMap(cardboard_demo::LocaleMap& locale_map) {
+ auto& this_language_map = locale_map["%(locale_id)s"];
+%(string_map)s
+ }
+ """ % dict(locale_id=locale_id, string_map='\n'.join(string_map)))
+
+ locale_map_initialization.append("""\
+ if (locale == "%(locale_id)s") {
+ Add%(locale_id)sToMap(locale_map);
+ }""" % dict(locale_id=locale_id))
+
+ if not os.path.exists(self._out_dir):
+ os.makedirs(self._out_dir)
+
+ locale_helpers = """\
+std::string GetCurrentLocale(JNIEnv* env) {
+ if (!env) {
+ __android_log_print(ANDROID_LOG_ERROR,
+ "cardboard_demo",
+ "GetCurrentLocale: JNIEnv is null");
+ return "en";
+ }
+
+ const char* method = "toLanguageTag";
+ jclass locale_class = env->FindClass("java/util/Locale");
+ jmethodID get_default_method = env->GetStaticMethodID(
+ locale_class, "getDefault", "()Ljava/util/Locale;");
+ jobject default_locale =
+ env->CallStaticObjectMethod(locale_class, get_default_method);
+ jmethodID get_method =
+ env->GetMethodID(locale_class, method, "()Ljava/lang/String;");
+ if (get_method == nullptr) {
+ __android_log_print(ANDROID_LOG_ERROR,
+ "cardboard_demo",
+ "GetCurrentLocale: GetMethodID is null.");
+ return "en";
+ }
+
+ jobject get_str = env->CallObjectMethod(default_locale, get_method);
+ jstring get_str_jstring = static_cast(get_str);
+
+ const char* data = env->GetStringUTFChars(get_str_jstring, nullptr);
+ size_t size = env->GetStringUTFLength(get_str_jstring);
+ std::string cpp_string(data, size);
+ env->ReleaseStringUTFChars(get_str_jstring, data);
+
+ return cpp_string;
+}
+"""
+ locale_decls = """\
+const char* GetLocalizedString(StringId string_id, JNIEnv* env);
+const char* GetLocalizedString(const std::string& key, JNIEnv* env);\
+"""
+ locale_defs = """\
+const char* GetLocalizedString(StringId string_id, JNIEnv* env) {
+ return GetLocalizedStringForLocale(kLocalizedStringTable[string_id],
+ GetCurrentLocale(env), env);
+}
+
+const char* GetLocalizedString(const std::string& key, JNIEnv* env) {
+ return GetLocalizedStringForLocale(key, GetCurrentLocale(env), env);
+}
+"""
+
+ cc_header = open(
+ os.path.join(self._out_dir, ('%s_strings.h' % self._namespace)), 'w')
+ cc_header.write("""\
+// GENERATED CODE : DO NOT EDIT BY HAND
+#ifndef VR_APPS_CARDBOARD_DEMO_COMMON_TOOLS_%(namespace_upper)s_STRINGS_H_
+#define VR_APPS_CARDBOARD_DEMO_COMMON_TOOLS_%(namespace_upper)s_STRINGS_H_
+
+#include
+#include
+
+namespace %(namespace)s {
+
+typedef enum {
+%(enums)s
+} StringId;
+
+%(locale_decls)s
+const char* GetLocalizedStringForLocale(StringId string_id,
+ const std::string& locale,
+ JNIEnv* env);
+const char* GetLocalizedStringForLocale(const std::string& key,
+ const std::string& locale,
+ JNIEnv* env);
+
+} // namespace %(namespace)s
+
+#endif // VR_APPS_CARDBOARD_DEMO_COMMON_TOOLS_%(namespace_upper)s_STRINGS_H_
+""" % dict(
+ enums='\n'.join([' %s,' % self._GenerateEnumConstant(k)
+ for k in self._parsed_strings.keys()]),
+ namespace=self._namespace,
+ namespace_upper=self._namespace.upper(),
+ locale_decls=locale_decls))
+
+ cc_header.close()
+
+ # TODO(bwuest): Use a template library like cheetah or jinja2 to make this
+ # generated code easier to read.
+ cc_source = open(
+ os.path.join(self._out_dir, ('%s_strings.cc' % self._namespace)), 'w')
+ cc_source.write(
+ """\
+// GENERATED CODE : DO NOT EDIT BY HAND
+#include "%(namespace)s_strings.h"
+
+#include "../common/strings/localized_strings.h"
+
+#include
+#include
+
+namespace {
+
+const char* const kLocalizedStringTable[] = {
+%(parsed_strings)s
+};
+
+
+%(locale_map_initialization_functions)s;
+
+void AddLocaleToMap(const std::string& locale, cardboard_demo::LocaleMap& locale_map) {
+%(locale_map_initialization)s;
+}
+
+// Retrieves the LocaleMap, lazily loading in the provided locale if necessary.
+const cardboard_demo::LocaleMap& GetLocaleMapWithLocale(const std::string& locale) {
+ static cardboard_demo::LocaleMap s_locale_map;
+ // Lazily add the locale to the map if it's not already there.
+ if (s_locale_map.find(locale) == s_locale_map.end()) {
+ AddLocaleToMap(locale, s_locale_map);
+ }
+
+ return s_locale_map;
+}
+
+%(locale_helpers)s\
+
+} // namespace
+
+namespace %(namespace)s {
+
+const char* GetLocalizedStringForLocale(StringId string_id,
+ const std::string& locale,
+ JNIEnv* env) {
+ return GetLocalizedStringForLocale(kLocalizedStringTable[string_id], locale, env);
+}
+
+const char* GetLocalizedStringForLocale(const std::string& key,
+ const std::string& target_locale,
+ JNIEnv* env) {
+ if(!env)
+ {
+ __android_log_print(ANDROID_LOG_ERROR,
+ "cardboard_demo",
+ "GetLocalizedStringForLocale: JNIEnv is null");
+ return nullptr;
+ }
+
+ const std::vector locales =
+ cardboard_demo::GetProbableLocalesInOrder(target_locale);
+ for (auto& locale : locales) {
+ const cardboard_demo::LocaleMap& locale_map = GetLocaleMapWithLocale(locale);
+ auto locale_entry_iter = locale_map.find(locale);
+ if (locale_entry_iter != locale_map.end()) {
+ const char* out =
+ cardboard_demo::GetLocalizedStringFromMap(key, locale_entry_iter->second);
+ if (!out) {
+ continue;
+ }
+ return out;
+ }
+ }
+ return nullptr;
+}
+
+%(locale_defs)s\
+
+} // namespace %(namespace)s
+""" % dict(namespace=self._namespace,
+ num_parsed_strings=len(self._parsed_strings),
+ locale_defs=locale_defs,
+ locale_helpers=locale_helpers,
+ locale_map_initialization_functions='\n'.join(
+ locale_map_initialization_functions),
+ locale_map_initialization='\n'.join(locale_map_initialization),
+ parsed_strings='\n'.join([
+ ' "%s", // %s' % (k, v)
+ for k, v in self._parsed_strings.items()
+ ])))
+
+ cc_source.close()
+
+ def _GenerateJava(self) -> None:
+ """Generates the Java output files, from the parsed input."""
+
+ # TODO(b/29054912): Refactor this method to be consistent with GenerateCc.
+
+ if not os.path.exists(self._out_dir):
+ os.makedirs(self._out_dir)
+ strings_class = open(
+ os.path.join(self._out_dir, ('%s.java' % 'Strings')), 'w')
+ strings_class.write('package %s;\n\n' % self._namespace)
+
+ strings_class.write('import java.util.HashMap;\n')
+ strings_class.write('import java.util.Locale;\n')
+ strings_class.write('import java.util.Map;\n')
+ strings_class.write('\n')
+
+ strings_class.write('/** @hide */\n')
+ strings_class.write('public class %s {\n' % 'Strings')
+ # Generate constants for each string used as the 'stringId' which is passed
+ # to getStrings(String).
+ for key, value in self._parsed_strings.items():
+ strings_class.write('public static final String %s = "%s";\n' %
+ (key, key))
+
+ strings_class.write('private static final '
+ 'Map> LANGUAGE_MAP;\n')
+ strings_class.write('static {\n')
+ strings_class.write(' LANGUAGE_MAP = '
+ 'new HashMap>();\n')
+
+ locale_map = self._GetFullLocaleMap()
+ for locale_id in locale_map:
+ strings_class.write(' Map %s = '
+ 'new HashMap();\n' % locale_id)
+ string_map = locale_map[locale_id]
+ for key, value in string_map.items():
+ strings_class.write(' %s.put("%s", "%s");\n' % (locale_id, key, value))
+ strings_class.write(' LANGUAGE_MAP.put("%s", %s);\n\n' %
+ (locale_id, locale_id))
+ strings_class.write('}\n')
+ strings_class.write('\n')
+ strings_class.write('private static '
+ 'Map getLanguageMap(Locale locale) {\n')
+ strings_class.write(' String language = locale.getLanguage();\n')
+ strings_class.write(' if (LANGUAGE_MAP.containsKey(language)) {\n')
+ strings_class.write(' return LANGUAGE_MAP.get(language);\n')
+ strings_class.write(' } else {\n')
+ strings_class.write(' // If language is not found, default to english.'
+ '\n')
+ strings_class.write(' return LANGUAGE_MAP.get("en");\n')
+ strings_class.write(' }\n')
+ strings_class.write('}\n')
+ strings_class.write('\n')
+ strings_class.write('private static String getString('
+ 'String stringId, Locale locale) {\n')
+ strings_class.write(' Map stringMap = '
+ 'getLanguageMap(locale);\n')
+ strings_class.write(' if (stringMap.containsKey(stringId)) {\n')
+ strings_class.write(' return stringMap.get(stringId);\n')
+ strings_class.write(' } else {\n')
+ strings_class.write(' // If the string is not found for the specific '
+ 'language, fall back to the original string.\n')
+ strings_class.write(' return LANGUAGE_MAP.get("en").get(stringId);\n')
+ strings_class.write(' }\n')
+ strings_class.write('}\n')
+ strings_class.write('\n')
+ strings_class.write('public static String getString(String stringId) {\n')
+ strings_class.write(' return getString(stringId, Locale.getDefault());\n')
+ strings_class.write('}\n')
+
+ strings_class.write('}\n')
+
+ strings_class.close()
+
+ def _ParseFile(self) -> None:
+ """Parses the file at self._resource_file_name into self._parsed_strings.
+
+ The strings file contains the strings required by the application.
+ This file will consist of comment lines /* */ and strings lines:
+ "key" = "value";.
+ """
+ self._parsed_strings = self._ParseStringsFile(
+ self._resource_file_name, self._check_substitution_tokens)
+
+ def _ParseStringsFile(self, resource_file: str,
+ check_substitution_tokens: bool) -> Dict[str, str]:
+ """Parses the file at self._resource_file_name into self._parsed_strings.
+
+ The strings file contains the strings required by the application.
+ This file will consist of comment lines /* */ and strings lines:
+ "key" = "value";.
+
+ Args:
+ resource_file: The name of the Localizeable.strings file.
+ check_substitution_tokens: Whether or not to validate that the
+ substitution tokens are valid.
+
+ Returns:
+ Map of key/value pairs of localized strings.
+ """
+ parsed_strings = {}
+ strings_file = open(resource_file, 'r')
+ for line in strings_file:
+ match = re.match(r'"([a-zA-Z0-9_]*)"\s*=\s*"(.*)";$', line)
+ if match:
+ key = match.group(1)
+ value = match.group(2)
+ if check_substitution_tokens:
+ substitution_tokens = re.findall(r'(\%[0-9a-zA-Z@\.\$]+)', value)
+ if len(substitution_tokens) > 1:
+ for substitution_token in substitution_tokens:
+ # Match numbered substitutions ie. %1$d
+ if not re.match(r'\%[0-9]+\$', substitution_token):
+ sys.stderr.write(
+ 'Line: %s includes multiple substitution parameters, '
+ 'some of which are not numbered. This can cause issues '
+ 'in some languages. You are seeing this error because '
+ '--check_substitution_tokens is passed to genstrings.py. '
+ 'Remove this flag if you are not launched in languages '
+ 'where this is a problem.' % line)
+ exit(1)
+
+ parsed_strings[key] = value
+ strings_file.close()
+ return parsed_strings
+
+ def _GetFullLocaleMap(self):
+ """Parses all the string files for each locale.
+
+ The location of each locale is based on the location of
+ self._resource_file_name.
+ Returns:
+ Map of a map of key/value pairs of localized strings per locale.
+ """
+ locale_map = {}
+ # Get the directory that the base strings file is in.
+ base_dir = os.path.abspath(os.path.join(self._resource_file_name,
+ os.pardir))
+ # Get the root strings directory that contains all locales.
+ root_resource_dir = os.path.abspath(os.path.join(base_dir, os.pardir))
+ # Get the base name of the strings file as that should be the same in each
+ # locale directory.
+ strings_filename = os.path.basename(self._resource_file_name)
+ # Get the list of all locale directories.
+ locale_dirs = os.listdir(root_resource_dir)
+
+ # For each locale, parse the strings file and put that in a map.
+ for locale_dir in locale_dirs:
+ locale_file = os.path.abspath(os.path.join(root_resource_dir, locale_dir,
+ strings_filename))
+ parsed_strings = self._ParseStringsFile(locale_file,
+ self._check_substitution_tokens)
+ # Strip off the lproj suffix so we are only left with the locale id.
+ locale_id = re.sub(r'\.lproj$', '', locale_dir)
+ locale_map[locale_id] = parsed_strings
+ return locale_map
+
+
+def main(argv: List[str]) -> int:
+ """Main method."""
+ try:
+ parser = optparse.OptionParser()
+ parser.add_option('--out_dir',
+ help='The directory where code should be placed',
+ dest='out_dir',
+ action='store',
+ default='')
+ parser.add_option(
+ '--resource_file',
+ help='The english Localizeable.strings file to be used',
+ dest='resource_file',
+ action='store',
+ default='../Resources/Strings/en.lproj/Localizable.string')
+ parser.add_option('--namespace',
+ help='Namespace used in generated code',
+ dest='namespace',
+ action='store',
+ default='sassafras')
+ parser.add_option('--check_substitution_tokens',
+ help=('When set, each string will be parsed for '
+ 'substitution tokens %d, and will fail if there '
+ 'are multiple substitution tokens and any are '
+ 'not numbered.'),
+ dest='check_substitution_tokens',
+ action='store_true')
+ parser.add_option('--language',
+ help='Output language, either C++ (cc) or Java (java)',
+ dest='language',
+ action='store',
+ default='cc')
+ (options, args) = parser.parse_args(args=argv)
+ if args:
+ raise Error('No argument allowed.')
+ strings_parser = StringsParser(options.resource_file, options.namespace,
+ options.out_dir,
+ options.check_substitution_tokens,
+ options.language)
+ strings_parser.Run()
+
+ return 0
+ except Error as e:
+ sys.stderr.write('%s: Error: %s\n' % (os.path.basename(sys.argv[0]),
+ e.args[0]))
+ return 1
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/paperscope/paperscope_launcher/common/tools/localization_strings.bzl b/paperscope/paperscope_launcher/common/tools/localization_strings.bzl
new file mode 100644
index 00000000..d97a1a44
--- /dev/null
+++ b/paperscope/paperscope_launcher/common/tools/localization_strings.bzl
@@ -0,0 +1,80 @@
+load("//third_party/bazel_rules/rules_cc/cc:cc_library.bzl", "cc_library")
+
+def localization_strings_genrule(
+ name,
+ srcs,
+ visibility,
+ namespace,
+ resource_file):
+ """A genrule that creates C++ files for localized strings.
+
+ This is a helper function that creates a genrule to generate localization
+ string files from a set of resource files. It is used by the
+ `localization_strings` macro.
+
+ Args:
+ name: The name of the target.
+ srcs: A list of source files containing the localized strings.
+ visibility: The visibility of the generated files.
+ namespace: The namespace for the generated C++/Java/Objective-C code.
+ resource_file: The path to the primary resource file (e.g.,
+ en.lproj/cardboard.strings).
+ """
+
+ native.genrule(
+ name = "gen_" + name,
+ outs = [
+ namespace + "_strings.h",
+ namespace + "_strings.cc",
+ ],
+ srcs = srcs,
+ cmd = ("$(location //vr/apps/cardboard/demo/common/tools:genstrings)" +
+ " --resource_file=$(location %s)" % resource_file +
+ " --namespace=" + namespace +
+ " --out_dir=$(@D)" +
+ " --language=cc"),
+ tools = [
+ "//vr/apps/cardboard/demo/common/tools:genstrings",
+ ],
+ visibility = visibility,
+ )
+ native.filegroup(
+ name = "srcs_filegroup_" + name,
+ srcs = [
+ namespace + "_strings.cc",
+ ],
+ visibility = visibility,
+ )
+ native.filegroup(
+ name = "hdrs_filegroup_" + name,
+ srcs = [
+ namespace + "_strings.h",
+ ],
+ visibility = visibility,
+ )
+
+def localization_strings(
+ name,
+ srcs,
+ visibility,
+ namespace,
+ resource_file,
+ copts = None):
+ localization_strings_genrule(
+ name,
+ srcs,
+ visibility,
+ namespace,
+ resource_file,
+ )
+
+ cc_library(
+ name = name,
+ srcs = [":srcs_filegroup_" + name],
+ hdrs = [":hdrs_filegroup_" + name],
+ copts = copts,
+ visibility = visibility,
+ deps = [
+ "//vr/apps/cardboard/demo/common/strings:localized_strings",
+ ],
+ )
diff --git a/paperscope/paperscope_launcher/common/tools/objc_file_set.bzl b/paperscope/paperscope_launcher/common/tools/objc_file_set.bzl
new file mode 100644
index 00000000..44fff9bf
--- /dev/null
+++ b/paperscope/paperscope_launcher/common/tools/objc_file_set.bzl
@@ -0,0 +1,59 @@
+"""Support for zipping a Fileset, retaining its directory structure."""
+
+def _fileset_as_zip_impl(ctx):
+ """Implementation of the FilesetAsZip rule.
+
+ Args:
+ ctx: The Skylark context.
+ Returns:
+ A list with the `DefaultInfo` provider that propagates a zip file for
+ each `Fileset` in `srcs`.
+ """
+ srcs = ctx.files.srcs # Must be fileset rules.
+ outputs = []
+ counter = 1
+
+ for fileset in srcs:
+ zipname = ctx.label.name + "." + str(counter) + ".zip"
+ counter += 1
+
+ output_zip = ctx.actions.declare_file(zipname)
+ outputs.append(output_zip)
+
+ message = "Generating %s archive" % (output_zip)
+ sed_expr = "s|\\(%s/\\)\\(.*\\)|\\\\2=\\\\1\\\\2|g" % fileset.dirname
+
+ # There's a //tools/zip with weird-ass args, but at least it's platform neutral.
+ command_expr = '%s c %s $(find -L %s | sed "%s")' % (
+ ctx.executable._zip.path,
+ output_zip.path,
+ fileset.path,
+ sed_expr,
+ )
+ ctx.actions.run_shell(
+ inputs = [ctx.executable._zip, fileset],
+ outputs = [output_zip],
+ command = command_expr,
+ mnemonic = "EXPFilesetAsZipImpl",
+ progress_message = "%s %s" % (message, ctx.label),
+ )
+ return [DefaultInfo(files = depset(outputs))]
+
+fileset_as_zip = rule(
+ implementation = _fileset_as_zip_impl,
+ attrs = {
+ "srcs": attr.label_list(
+ allow_files = True,
+ mandatory = True,
+ ),
+ "_zip": attr.label(
+ default = Label("//tools/zip:zipper"),
+ cfg = "exec",
+ allow_single_file = True,
+ executable = True,
+ ),
+ },
+)
+
+def FilesetAsZip(name, srcs):
+ fileset_as_zip(name = name, srcs = srcs)
diff --git a/paperscope/paperscope_launcher/resources/strings/ar.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ar.lproj/cardboard.strings
new file mode 100644
index 00000000..b7815415
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/ar.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "برنامج تعليمي";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "فيديو الترحيب";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "المستكشف";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "معرض";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "تنزه في المناطق الحضرية";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "كليدوسكوب";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "رحلة المنطقة القطبية الشمالية";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "تعذر الاتصال بالخادم\nتحقق من الاتصال بالإنترنت";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "عذرًا، حدث شيئ ما خاطئ، يُرجى إعادة المحاولة لاحقًا";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "اضغط على الزر\nللنقر";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "الإمالة للوصول إلى\nالصفحة الرئيسية";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© راندي كوك";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© راندي كوك";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© راندي كوك";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© راندي كوك";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© راندي كوك";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "نموذج استكشافات\nالمتحف الأمريكي للتاريخ الطبيعي";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "متحف فرونتيرز أوف فلايت";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "متحف مكوك الفضاء انديفور";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "كويفا دل إنديو\nبورتوريكو";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "ينابيع جنهافر الساخنة\nأيسلندا";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "مسبار المريخ المتجوّل\nصورة من ناسا/مختبر الدفع النفاث";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(انقر للانتقال إلى %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "باریس";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "طوکیو";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "البندقية";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "نيويورك";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "روما";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "القدس";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "مونت كارلو";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "لندن";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "الحيد المرجاني العظيم";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "دليل الجولة";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "مقاطع الفيديو التابعة لي";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "الخروج";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "الرجوع إلى العروض";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "يمكنك تدوير هاتفك للعودة إلى القائمة";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "اسحب هاتفك من العارض للخروج من العروض التوضيحية.";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "التالي";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "اضغط على الزر للتحديد.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "يمكنك تدوير العارض للتنقل إلى الخلف.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "جرِّبه الآن.";
diff --git a/paperscope/paperscope_launcher/resources/strings/bg.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/bg.lproj/cardboard.strings
new file mode 100644
index 00000000..c20055dd
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/bg.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Урок";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Приветствен видеоклип";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Изследовател";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Изложба";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Градска разходка";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Калейдоскоп";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Пътешествие в Арктика";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Не може да се осъществи връзка със сървъра\nПроверете връзката си с интернет";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "За съжаление, нещо се обърка. Моля, опитайте отново по-късно.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Натиснете бутона\nза кликване";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Наклонете\nза връщане в началото";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Дрънкалка сокол\n© Ранди Кук";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Ватиканска маска\n© Ранди Кук";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Маска на вожд\n© Ранди Кук";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Прорицателят\n© Ранди Кук";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Бетховен\n© Ранди Кук";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Американски музей по естествена история\nПримерни експедиции";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Границите на полета";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Космическата совалка „Индевър“";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Куева дел Индио\nПуерто Рико";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Горещи извори в Гюнюхвер\nИсландия";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Марсоходът „Спирит“\nИзображение от NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Кликнете, за да преминете към %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Париж";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Токио";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Венеция";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Ню Йорк";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Рим";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Ерусалим";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Монте Карло";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Лондон";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Големият бариерен риф";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Екскурзовод";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Моите видеоклипове";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "ИЗХОД";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "КЪМ ДЕМОНСТРАЦИИТЕ";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Завъртете телефона си, за да се върнете в менюто";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Извадете телефона си от очилата, за да излезете от демонстрациите";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "НАПРЕД";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Натиснете бутона, за да изберете.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Завъртете очилата, за да навигирате назад.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Изпробвайте сега.";
diff --git a/paperscope/paperscope_launcher/resources/strings/ca.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ca.lproj/cardboard.strings
new file mode 100644
index 00000000..87c72ca3
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/ca.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Vídeo de benvinguda";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorador";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exposició";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Excursió per la ciutat";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Calidoscopi";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Viatge per l'Àrtic";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "No es pot connectar al servidor\nComprova la connexió a Internet";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "S'ha produït un error. Torna a provar-ho més tard.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Fes clic\nal botó";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Inclina el visor\nper tornar a l'inici";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Museu nord-americà d'història natural (American Museum of Natural History)\nMostra d'expedicions";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Museu aeroespacial Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Transbordador espacial Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Fonts termals de Gunnuhver\nIslàndia";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Spirit, el robot de Mart\nImatge de la NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Fes clic per anar a %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "París";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tòquio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venècia";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Nova York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Montecarlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londres";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Gran Barrera de Corall";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Visita guiada";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Els meus vídeos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "SURT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "TORNA A DEMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Gira el telèfon per tornar al menú";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Treu el telèfon del visor per sortir de les demostracions";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "SEGÜENT";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Prem el botó per fer una selecció.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Gira el visor per tornar enrere.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Prova-ho ara.";
diff --git a/paperscope/paperscope_launcher/resources/strings/cs.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/cs.lproj/cardboard.strings
new file mode 100644
index 00000000..3d4e8bf1
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/cs.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Výukový program";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Uvítací video";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arktická výprava";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Nelze se připojit k serveru,\nzkontrolujte připojení k internetu.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Omlouváme se, něco se pokazilo, zkuste to prosím znovu později.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Zmáčknutím tlačítka\nkliknete";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Nakloněním se vrátíte\ndomů";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Americké přírodovědné muzeum\nUkázka expedic";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Muzeum Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Raketoplán Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPortoriko";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Termální prameny Gunnuhver\nIsland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Vozítko Spirit na Marsu\nObrázek od NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Kliknutím přejdete na místo: %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paříž";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Benátky";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Řím";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jeruzalém";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londýn";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Velký bariérový útes";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tour Guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Moje videa";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "UKONČIT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "ZPĚT NA UKÁZKY";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Zpět do nabídky se vrátíte otočením telefonu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Pokud chcete ukončit ukázky, vyjměte telefon z brýlí";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "DALŠÍ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Výběr provedete stisknutím tlačítka.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Zpět se dostanete otočením brýlí.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Zkuste si to.";
diff --git a/paperscope/paperscope_launcher/resources/strings/da.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/da.lproj/cardboard.strings
new file mode 100644
index 00000000..4318f814
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/da.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Selvstudie";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Velkomstvideo";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Polarrejse";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Der kan ikke oprettes forbindelse til serveren\nKontrollér din internetforbindelse";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Der gik noget galt. Prøv igen senere";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Tryk på knappen\nfor at klikke";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Vip for at komme til\nstartsiden";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nSmagsprøve på Expeditions";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Rumfærgen Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Varme kilder ved Gunnuhver\nIsland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nBillede fra NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Klik for at gå til %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venedig";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rom";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tour Guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "My Videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "AFSLUT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "TILBAGE TIL DEMOER";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Roter telefonen for at komme tilbage til menuen";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Tag din telefon ud af brillen for at afslutte demoerne";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "NÆSTE";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Tryk på knappen for at vælge.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Drej din fremviser for at gå tilbage.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Prøv det nu.";
diff --git a/paperscope/paperscope_launcher/resources/strings/de.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/de.lproj/cardboard.strings
new file mode 100644
index 00000000..bd809eeb
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/de.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Anleitung";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Einführungsvideo";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Ausstellung";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Stadttour";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoskop";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Reise in die Arktis";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Keine Verbindung zum Server möglich\nBitte überprüfe deine Internetverbindung.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Ein Fehler ist aufgetreten. Bitte versuche es später noch einmal.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Zum Auslösen\nTaste drücken";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Kippen, um zur\nStartseite zu wechseln";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditionsbeispiel";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Flugpioniere";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Raumfähre \"Endeavour\"";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Heiße Quellen von Gunnuhver\nIsland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars-Rover \"Spirit\"\nBild von NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Klicken, um zu \"%2$s\" zu gehen)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venedig";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rom";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Google Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Reiseführer";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Meine Videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "BEENDEN";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "Zurück zu Demos";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Drehe dein Smartphone, um zum Menü zurückzukehren.";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Trenne das Smartphone vom Gerät, um die Demos zu beenden.";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "WEITER";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Um eine Schaltfläche zu betätigen, drücke die Taste an deinem Gerät.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Um zurückzunavigieren, drehe die Brille.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Probiere es jetzt aus.";
diff --git a/paperscope/paperscope_launcher/resources/strings/el.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/el.lproj/cardboard.strings
new file mode 100644
index 00000000..d4b05f20
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/el.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Οδηγός εκμάθησης";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Βίντεο καλωσορίσματος";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Εξερευνητής";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Έκθεμα";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Περίπατος στην πόλη";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Καλειδοσκόπιο";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Ταξίδι στην Αρκτική";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Αδύνατη η σύνδεση στο διακομιστή:\nελέγξτε τη σύνδεσή σας στο διαδίκτυο.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Παρουσιάστηκε κάποιο πρόβλημα. Δοκιμάστε ξανά αργότερα.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Πατήστε το κουμπί\nγια να κάνετε κλικ";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Δώστε κλίση για επιστροφή\nστην αρχή";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Αμερικανικό Μουσείο Φυσικής Ιστορίας\nΔείγμα από τις Αποστολές";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Τα σύνορα της πτήσης";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Διαστημικό λεωφορείο \"Endeavour\"";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nΠόρτο Ρίκο";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Θερμές πηγές Gunnuhver\nΙσλανδία";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nΕικόνα από NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Κλικ για μετάβαση σε %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Παρίσι";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Τόκιο";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Βενετία";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Νέα Υόρκη";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Ρώμη";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Ιερουσαλήμ";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Μόντε Κάρλο";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Λονδίνο";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Μεγάλος Κοραλλιογενής Ύφαλος";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Ταξιδιωτικός οδηγός";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Τα βίντεό μου";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "ΕΞΟΔΟΣ";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "ΕΠΙΣΤΡΟΦΗ ΣΤΑ DEMO";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Περιστρέψτε το τηλέφωνό σας για να επιστρέψετε στο μενού";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Αφαιρέστε το τηλέφωνό σας από το σύστημα προβολής για έξοδο από τις επιδείξεις";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "ΕΠΟΜΕΝΟ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Πατήστε το κουμπί για να επιλέξετε.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Περιστρέψτε το σύστημα προβολής που διαθέτετε, για να περιηγηθείτε προς τα πίσω.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Δοκιμάστε το τώρα.";
diff --git a/paperscope/paperscope_launcher/resources/strings/en.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/en.lproj/cardboard.strings
new file mode 100644
index 00000000..331a2ab2
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/en.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Welcome Video";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arctic Journey";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Cannot connect to server\ncheck your internet connection";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Sorry something went wrong, please try again later";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Press the button\nto click";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Tilt to get\nhome";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions Sample";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour Space Shuttle";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nImage from NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Click to go to %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venice";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rome";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tour Guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "My Videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "EXIT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "BACK TO DEMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Rotate your phone to get back to the menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Take your phone out of your viewer to exit the demos";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "NEXT";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Press the button to select.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Rotate your viewer in order to navigate back.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Try it out now.";
diff --git a/paperscope/paperscope_launcher/resources/strings/en_AU.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/en_AU.lproj/cardboard.strings
new file mode 100644
index 00000000..abdf277c
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/en_AU.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Welcome Video";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arctic Journey";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Cannot connect to server\nCheck your internet connection";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Sorry, something went wrong. Please try again later.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Press the button\nto click";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Tilt to get\nhome";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions sample";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour Space Shuttle";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nImage from NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Click to go to %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venice";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rome";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tour Guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "My videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "EXIT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "BACK TO DEMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Rotate your phone to get back to the menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Take your phone out of your viewer to exit the demos";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "NEXT";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Press the button to select.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Rotate your viewer in order to navigate back.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Try it out now.";
diff --git a/paperscope/paperscope_launcher/resources/strings/en_GB.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/en_GB.lproj/cardboard.strings
new file mode 100644
index 00000000..abdf277c
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/en_GB.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Welcome Video";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arctic Journey";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Cannot connect to server\nCheck your internet connection";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Sorry, something went wrong. Please try again later.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Press the button\nto click";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Tilt to get\nhome";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions sample";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour Space Shuttle";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nImage from NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Click to go to %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venice";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rome";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tour Guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "My videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "EXIT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "BACK TO DEMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Rotate your phone to get back to the menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Take your phone out of your viewer to exit the demos";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "NEXT";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Press the button to select.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Rotate your viewer in order to navigate back.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Try it out now.";
diff --git a/paperscope/paperscope_launcher/resources/strings/en_IN.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/en_IN.lproj/cardboard.strings
new file mode 100644
index 00000000..abdf277c
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/en_IN.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Welcome Video";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arctic Journey";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Cannot connect to server\nCheck your internet connection";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Sorry, something went wrong. Please try again later.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Press the button\nto click";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Tilt to get\nhome";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions sample";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour Space Shuttle";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nImage from NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Click to go to %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venice";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rome";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tour Guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "My videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "EXIT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "BACK TO DEMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Rotate your phone to get back to the menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Take your phone out of your viewer to exit the demos";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "NEXT";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Press the button to select.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Rotate your viewer in order to navigate back.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Try it out now.";
diff --git a/paperscope/paperscope_launcher/resources/strings/es.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/es.lproj/cardboard.strings
new file mode 100644
index 00000000..700e42ad
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/es.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Vídeo de bienvenida";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorador";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exposición";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Paseo por la ciudad";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Caleidoscopio";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Viaje ártico";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "No se puede conectar con el servidor\nComprueba tu conexión a Internet";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Se ha producido un error; vuelve a intentarlo dentro de un rato";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Pulsa el botón\npara hacer clic";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Inclínalo para ir\nal inicio";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Sonajero de halcón\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Máscara vaticana\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Máscara del jefe\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "El vidente\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Museo Americano de Ciencias Naturales (Nueva York)\nMuestra de Expediciones";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Fronteras del vuelo";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Lanzadera espacial \"Endeavour\"";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Manantiales de aguas termales de Gunnuhver\nIslandia";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Rover \"Spirit\" de Marte\nImagen de la NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Haz clic para ir a %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "París";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venecia";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Nueva York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalén";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Montecarlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londres";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "La Gran Barrera de Coral";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Visita guiada";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Mis vídeos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "SALIR";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "VOLVER A LAS DEMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Gira tu teléfono para volver al menú";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Saca tu teléfono del visor para salir de las demos";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "SIGUIENTE";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Pulsa el botón para seleccionar.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Para ir a la imagen anterior, gira el visor.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Pruébalo ahora.";
diff --git a/paperscope/paperscope_launcher/resources/strings/es_MX.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/es_MX.lproj/cardboard.strings
new file mode 100644
index 00000000..55db7d0f
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/es_MX.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Instructivo";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Video de bienvenida";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorador";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibición";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Caminata urbana";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Caleidoscopio";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Viaje al Ártico";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "No se puede establecer conexión con el servidor\nComprueba la conexión a Internet";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Se produjo un error. Vuelve a intentarlo más tarde.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Presiona el botón\npara hacer clic";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Inclina el visor\npara ir al inicio";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Museo de historia natural de los Estados Unidos\nMuestra de expediciones";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Transbordador espacial Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Fuente termal de Gunnuhver\nIslandia";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Spirit, robot explorador de Marte\nImagen de NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Haz clic para ir a %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "París";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venecia";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Nueva York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalén";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londres";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Gran Barrera de Coral";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Guía turística";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Mis videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "SALIR";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "VOLVER A LAS DEMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Gira el teléfono para volver al menú";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Quita el teléfono del visor para salir de las demostraciones";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "SIGUIENTE";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Presiona el botón para seleccionar.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Gira el visor para navegar hacia atrás.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Pruébalo ahora";
diff --git a/paperscope/paperscope_launcher/resources/strings/es_US.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/es_US.lproj/cardboard.strings
new file mode 100644
index 00000000..55db7d0f
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/es_US.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Instructivo";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Video de bienvenida";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorador";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibición";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Caminata urbana";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Caleidoscopio";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Viaje al Ártico";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "No se puede establecer conexión con el servidor\nComprueba la conexión a Internet";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Se produjo un error. Vuelve a intentarlo más tarde.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Presiona el botón\npara hacer clic";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Inclina el visor\npara ir al inicio";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Museo de historia natural de los Estados Unidos\nMuestra de expediciones";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Transbordador espacial Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Fuente termal de Gunnuhver\nIslandia";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Spirit, robot explorador de Marte\nImagen de NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Haz clic para ir a %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "París";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venecia";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Nueva York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalén";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londres";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Gran Barrera de Coral";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Guía turística";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Mis videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "SALIR";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "VOLVER A LAS DEMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Gira el teléfono para volver al menú";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Quita el teléfono del visor para salir de las demostraciones";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "SIGUIENTE";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Presiona el botón para seleccionar.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Gira el visor para navegar hacia atrás.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Pruébalo ahora";
diff --git a/paperscope/paperscope_launcher/resources/strings/fa.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/fa.lproj/cardboard.strings
new file mode 100644
index 00000000..58ace530
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/fa.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "آموزش گام به گام";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "ویدیوی خوش آمدگویی";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "کاوشگر";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "نمایش";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "راهپیمایی در شهر";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "زیبابین";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "سفر به قطب شمال";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "به سرور متصل نشدید\nاتصال اینترنت را بررسی کنید";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "متأسفم... مشکلی روی داد، لطفاً بعداً دوباره امتحان کنید";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "دکمه را\nبرای کلیک کردن فشار دهید";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "کج کنید\nتا به صفحه اصلی بروید";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "موزه تاریخ طبیعی آمریکا\nنمونه اکتشاف";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "مرزهای پرواز";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "شاتل فضایی اندور";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "کیووا دل ایندیو\nپورتو ریکو";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "بهار گرم گونوهور\nایسلند";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "مارس اسپیریت روور\nتصویر از NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(برای رفتن به %2$s کلیک کنید)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "پاریس";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "توکیو";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "ونیز";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "نیویورک";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "رم";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "بیت المقدس";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "مونته کارلو";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "لندن";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "صخره گریت بریر";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "راهنمای تور";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "ویدیوهای من";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "خروج";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "بازگشت به بخشهای نمایشی";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "تلفنتان را بچرخانید تا به منو برگردید";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "برای خروج از نسخههای نمایشی، تلفنتان را از نظارهگر بیرون ببرید";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "بعدی";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "دکمه را برای انتخاب فشار دهید.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "برای پیمایش به عقب، نظارهگر را بچرخانید.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "الان امتحان کنید.";
diff --git a/paperscope/paperscope_launcher/resources/strings/fi.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/fi.lproj/cardboard.strings
new file mode 100644
index 00000000..f3436d29
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/fi.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Ohje";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Tervehdysvideo";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Tutkimusmatkailija";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Näyttely";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Kaupunkikävely";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoskooppi";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arktinen matka";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Palvelimeen ei saada yhteyttä.\nTarkista internetyhteys.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Jokin meni pieleen. Yritä myöhemmin uudelleen.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Klikkaa painamalla\npainiketta.";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Siirry etusivulle\nkallistamalla.";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nTutkimusmatkailija-katkelma";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour-avaruusalus";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhverin kuumat lähteet\nIslanti";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars-mönkijä Spirit\nKuvalähde: NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Siirry sijaintiin %2$s klikkaamalla.)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Pariisi";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venetsia";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rooma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Lontoo";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Iso valliriutta";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Matkaopas";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Omat videot";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "POISTU";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "TAKAISIN DEMOIHIN";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Palaa valikkoon kääntämällä puhelinta.";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Poistu esittelystä ottamalla puhelin pois laseista.";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "SEURAAVA";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Valitse painamalla painiketta.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Siirry takaisin kääntämällä laseja.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Kokeile nyt.";
diff --git a/paperscope/paperscope_launcher/resources/strings/fil.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/fil.lproj/cardboard.strings
new file mode 100644
index 00000000..09a3b0fd
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/fil.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Welcome Video";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Paglalakbay sa Arctic";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Hindi makakonekta sa server\ntingnan ang iyong koneksyon sa internet";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Paumanhin nagkaroon ng problema, pakisubukang muli sa ibang pagkakataon";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Pindutin ang button\nupang i-click";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "I-tilt upang pumunta sa\nhome";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nSample ng Mga Expedition";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour Space Shuttle";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIceland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nImahe mula sa NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(I-click upang pumunta sa %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venice";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rome";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Gabay sa Paglilibot";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Aking Mga Video";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "MAG-EXIT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "BALIK SA MGA DEMO";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "I-rotate ang telepono mo upang bumalik sa menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Lumabas sa iyong viewer ng telepono mo para lumabas sa mga demo";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "SUSUNOD";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Pindutin ang button upang piliin.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "I-rotate ang iyong viewer upang mag-navigate pabalik.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Subukan ito ngayon.";
diff --git a/paperscope/paperscope_launcher/resources/strings/fr.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/fr.lproj/cardboard.strings
new file mode 100644
index 00000000..d8e0766c
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/fr.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Didacticiel";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Vidéo de bienvenue";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorateur";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exposition";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Randonnée urbaine";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaléidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Voyage en Arctique";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Impossible de se connecter au serveur.\nVérifiez votre connexion Internet.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Une erreur s'est produite. Veuillez réessayer plus tard.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Appuyez sur le bouton\npour sélectionner.";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Inclinez Cardboard pour revenir\nà la page d'accueil.";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Muséum américain d'histoire naturelle\nExemple d'utilisation d'Expéditions";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Musée Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Navette spatiale Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Grotte de l'Indien\nPorto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Sources thermales de Gunnuhver\nIslande";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nImage de la NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(cliquez pour accéder à %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venise";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rome";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jérusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londres";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Grande Barrière de corail";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Visite guidée";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Mes vidéos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "QUITTER";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "RETOUR AUX DÉMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Faites pivoter votre téléphone pour revenir au menu.";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Pour quitter les démos, sortez votre téléphone de la visionneuse.";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "SUIVANT";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Appuyez sur le bouton pour sélectionner.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Pivotez votre visionneuse pour revenir à la page précédente.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Essayez maintenant.";
diff --git a/paperscope/paperscope_launcher/resources/strings/he.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/he.lproj/cardboard.strings
new file mode 100644
index 00000000..97421100
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/he.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "מדריך";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "סרטון היכרות";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "סייר";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "הצגה";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "טיול רגלי בעיר";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "קליידוסקופ";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "מסע בקוטב הצפוני";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "לא ניתן להתחבר לשרת\nבדוק את החיבור לאינטרנט";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "מצטערים, משהו השתבש. נסה שוב מאוחר יותר";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "לחץ על הכפתור\nכדי להפעיל";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "הטה כדי לעבור\nלמסך הראשי";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "המוזיאון האמריקאי לתולדות הטבע\nדוגמה למסע";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "מוזיאון Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "מעבורת החלל אנדוור";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "מערת Cueva del Indio\nפוארטו ריקו";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "מעיינות חמים ב-Gunnuhver\nאיסלנד";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "רכב המאדים 'ספיריט'\nתמונה מאת NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(לחץ כדי לעבור אל %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "פריס";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "טוקיו";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "ונציה";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "ניו יורק";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "רומא";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "ירושלים";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "מונטה קרלו";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "לונדון";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "שונית המחסום הגדולה";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "מדריך טיולים";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "הסרטונים שלי";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "יציאה";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "חזרה להדגמות";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "סובב את הטלפון כדי לחזור לתפריט";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "כדי לצאת מתצוגת ההדגמה צריך להוציא את הטלפון ממכשיר הצפייה";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "הבא";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "לחץ על הלחצן כדי לבחור.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "סובב את מכשיר הצפייה כדי לנווט חזרה.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "נסה זאת עכשיו.";
diff --git a/paperscope/paperscope_launcher/resources/strings/hi.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/hi.lproj/cardboard.strings
new file mode 100644
index 00000000..02ec1b00
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/hi.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "ट्यूटोरियल";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "स्वागत वीडियो";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "अन्वेषक";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "प्रदर्शन";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "अर्बन हाइक";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "कलाइडोस्कोप";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "आर्कटिक की यात्रा";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "सर्वर से कनेक्ट नहीं हो सकता\nअपना इंटरनेट कनेक्शन जांचें";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "क्षमा करें कुछ गलत हुआ, कृपया बाद में फिर से प्रयास करें";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "क्लिक करने के लिए\nबटन दबाएं";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "होम पर जाने\nके लिए झुकाएं";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© रैंड कुक";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© रैंड कुक";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© रैंड कुक";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© रैंड कुक";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© रैंड कुक";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "अमेरिकन म्यूज़ियम ऑफ़ नेचुरल हिस्ट्री\nखोजयात्रा नमूना";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "फ़्रंटियर्स ऑफ़ फ़्लाइट";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "एंडेवर अंतरिक्ष शटल";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "क्यूवा डेल इंडियो\nपोर्टो रिको";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "गुनह्वेर हॉट स्प्रिंग्स\nआइसलैंड";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "मार्स स्पिरिट रोवर\nNASA/JPL से चित्र";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(%2$s पर जाने के लिए क्लिक करें)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "पेरिस";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "टोक्यो";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "वेनिस";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "न्यूयॉर्क";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "रोम";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "यरूशलम";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "मोंटे कार्लो";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "लंदन";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "ग्रेट बैरियर रीफ़";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "पृथ्वी";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "भ्रमण मार्गदर्शक";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "मेरे वीडियो";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "बाहर आएं";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "डेमो पर वापस जाएं";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "मेनू पर वापस जाने के लिए अपना फ़ोन घुमाएं";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "अगर आपको डेमो नहीं देखने हैं, तो अपने फ़ोन को व्यूअर से बाहर निकालें";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "अगला";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "चुनने के लिए बटन दबाएं.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "पीछे नेविगेट करने के लिए अपने व्यूअर को घुमाएं.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "इसे अभी आज़माएं.";
diff --git a/paperscope/paperscope_launcher/resources/strings/hr.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/hr.lproj/cardboard.strings
new file mode 100644
index 00000000..8e352b78
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/hr.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Vodič";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Videozapis dobrodošlice";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Istraživač";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Izložak";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urbano pješačenje";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoskop";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arktičko putovanje";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Povezivanje s poslužiteljem nije moguće\nprovjerite internetsku vezu";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Nešto nije u redu; pokušajte ponovno kasnije";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Da biste kliknuli, pritisnite tipku \n";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Nagnite da biste stigli\nkući";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatikanska maska\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Maska poglavice\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Vidjelica\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Američki prirodoslovni muzej\nPrimjer ekspedicija";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Muzej Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Svemirska letjelica Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPortoriko";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Vrelo Gunnuhver\nIsland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Rover Spirit na Marsu\nSliku omogućuju NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Kliknite da biste prešli na %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Pariz";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venecija";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rim";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jeruzalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Veliki koraljni greben";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Vodič kroz aplikaciju";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Moji videozapisi";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "IZLAZ";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "POVRATAK NA POKAZNE APLIKACIJE";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Okrenite telefon da biste se vratili na izbornik";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Izvadite telefon iz maske za izlazak iz pokaznih verzija";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "DALJE";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Pritisnite gumb za odabir.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Zakrenite svoju masku kako biste se mogli vratiti natrag.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Isprobajte odmah.";
diff --git a/paperscope/paperscope_launcher/resources/strings/hu.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/hu.lproj/cardboard.strings
new file mode 100644
index 00000000..568cb8bb
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/hu.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Útmutató";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Üdvözlő videó";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Kirándulás a sarkkörön túlra";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Nem sikerült csatlakozni a szerverre.\nEllenőrizze internetkapcsolatát.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Sajnos hiba történt, próbálkozzon újra később.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "A kattintáshoz\nnyomja meg a gombot";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "A keződképernyő\nmegjelenítéséhez döntse meg";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpedíciók";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight múzeum";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour űrsikló";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver hőforrásai\nIzland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Spirit marsjáró\nA NASA/JPL képe";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Kattintson a következőhöz induláshoz: %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Párizs";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokió";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Velence";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Róma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jeruzsálem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte-Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Nagy-korallzátony";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Túravezető";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Saját videóim";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "KILÉPÉS";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "VISSZA A BEMUTATÓHOZ";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "A menühöz való visszatéréshez fordítsa el telefonját";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "A demókból való kilépéshez vegye ki telefonját a szemüvegből";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "KÖVETKEZŐ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "A kiválasztáshoz nyomja meg a gombot.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "A visszalépéshez fordítsa el a szemüveget.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Próbálja ki most.";
diff --git a/paperscope/paperscope_launcher/resources/strings/id.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/id.lproj/cardboard.strings
new file mode 100644
index 00000000..60915d28
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/id.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Video Selamat Datang";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Penjelajah";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Pameran";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Jalan-jalan di Kota";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoskop";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Perjalanan Arktik";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Tidak dapat tersambung ke server\nperiksa sambungan internet Anda";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Terjadi kesalahan, coba lagi nanti";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Tekan tombol\nuntuk mengeklik";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Goyangkan untuk menuju\nberanda";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Topeng Vatikan\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Topeng Kepala Suku\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Museum Sejarah Alam Amerika\nContoh Expedisi";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Pesawat Ulang Alik Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Riko";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Sumber Air Panas Gunnuhver\nIslandia";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nGambar dari NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Klik untuk membuka %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venesia";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Yerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Karang Penghalang Besar";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Pemandu Tur";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Video Saya";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "KELUAR";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "KEMBALI KE DEMO";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Putar ponsel untuk kembali ke menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Keluarkan ponsel dari penampil untuk keluar dari demo";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "BERIKUTNYA";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Tekan tombol untuk memilih.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Putar penampil Anda untuk bernavigasi kembali.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Cobalah sekarang.";
diff --git a/paperscope/paperscope_launcher/resources/strings/it.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/it.lproj/cardboard.strings
new file mode 100644
index 00000000..f1809b78
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/it.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Video di benvenuto";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Esploratore";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Mostra";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Passeggiata in città";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Caleidoscopio";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Viaggio in Artide";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Impossibile connettersi al server.\nVerifica la connessione a Internet.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Si è verificato un errore. Riprova più tardi.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Premi il pulsante\nper fare clic";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Inclina per andare alla\nhome";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Museo americano di storia naturale\nEsempio di spedizione";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Museo Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Navetta spaziale Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPortorico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Sorgenti termali di Gunnuhver\nIslanda";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Veicolo spaziale Spirit su Marte\nImmagine di NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Fai clic per andare a %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Parigi";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venezia";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Gerusalemme";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londra";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Grande Barriera Corallina";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Guida turistica";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "I miei video";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "ESCI";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "TORNA ALLE DEMO";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Ruota il telefono per tornare al menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Togli lo smartphone dal visore per uscire dalle demo";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "AVANTI";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Premi il pulsante per selezionare.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Ruota il visore per tornare indietro.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Prova adesso.";
diff --git a/paperscope/paperscope_launcher/resources/strings/iw.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/iw.lproj/cardboard.strings
new file mode 100644
index 00000000..97421100
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/iw.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "מדריך";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "סרטון היכרות";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "סייר";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "הצגה";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "טיול רגלי בעיר";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "קליידוסקופ";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "מסע בקוטב הצפוני";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "לא ניתן להתחבר לשרת\nבדוק את החיבור לאינטרנט";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "מצטערים, משהו השתבש. נסה שוב מאוחר יותר";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "לחץ על הכפתור\nכדי להפעיל";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "הטה כדי לעבור\nלמסך הראשי";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "המוזיאון האמריקאי לתולדות הטבע\nדוגמה למסע";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "מוזיאון Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "מעבורת החלל אנדוור";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "מערת Cueva del Indio\nפוארטו ריקו";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "מעיינות חמים ב-Gunnuhver\nאיסלנד";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "רכב המאדים 'ספיריט'\nתמונה מאת NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(לחץ כדי לעבור אל %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "פריס";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "טוקיו";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "ונציה";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "ניו יורק";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "רומא";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "ירושלים";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "מונטה קרלו";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "לונדון";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "שונית המחסום הגדולה";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "מדריך טיולים";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "הסרטונים שלי";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "יציאה";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "חזרה להדגמות";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "סובב את הטלפון כדי לחזור לתפריט";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "כדי לצאת מתצוגת ההדגמה צריך להוציא את הטלפון ממכשיר הצפייה";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "הבא";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "לחץ על הלחצן כדי לבחור.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "סובב את מכשיר הצפייה כדי לנווט חזרה.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "נסה זאת עכשיו.";
diff --git a/paperscope/paperscope_launcher/resources/strings/ja.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ja.lproj/cardboard.strings
new file mode 100644
index 00000000..0675a8a9
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/ja.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "チュートリアル";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "ウェルカム動画";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "探検";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "鑑賞";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "街を歩く";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "万華鏡";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "北極の旅";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "サーバーに接続できません\nインターネット接続をご確認ください";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "エラーが発生しました。しばらくしてからもう一度お試しください";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "ボタンを押して\nクリックします";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "傾けて\nホーム画面に戻ります";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "アメリカ自然史博物館\n探索のサンプル";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "最先端航空博物館";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "スペースシャトル・エンデバー";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "インディアンの洞窟\nプエルトリコ";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "グンヌクヴェル温泉\nアイスランド";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "マーズ・スピリット・ローバー\n画像提供: NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(クリックして%2$sに移動)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "パリ";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "東京";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "ベネチア";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "ニューヨーク";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "ローマ";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "エルサレム";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "モンテカルロ";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "ロンドン";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "グレートバリアリーフ";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "ツアーガイド";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "マイ動画";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "退出";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "デモに戻る";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "メニューに戻るには、スマートフォンを傾けます";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "デモを終了するには、スマートフォンをビューアから取り外します";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "次へ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "選択するにはボタンを押します。";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "ビューアを回転させると、前に戻ります。";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "回転させてみてください。";
diff --git a/paperscope/paperscope_launcher/resources/strings/ko.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ko.lproj/cardboard.strings
new file mode 100644
index 00000000..39bb609f
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/ko.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "가이드";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "시작 동영상";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "탐험";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "전시";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "도시 하이킹";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "만화경";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "북극 여행";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "서버에 연결할 수 없습니다.\n인터넷 연결을 확인하세요.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "오류가 발생했습니다. 나중에 다시 시도해 주세요.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "클릭하려면\n버튼을 누르세요.";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "홈으로 이동하려면\n기울이세요.";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "미국 자연사 박물관\n탐험 샘플";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "비행기 박물관";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "엔데버 우주 왕복선";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "푸에르토리코\n꾸에바 델 인디오";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "아이슬란드\n군누베르 온천";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "화성 탐사선 스피릿\n이미지(출처: NASA/JPL)";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(클릭하여 %2$s(으)로 이동)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "파리";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "도쿄";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "베네치아";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "뉴욕";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "로마";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "예루살렘";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "몬테카를로";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "런던";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "그레이트 배리어 리프";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "어스";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "투어 가이드";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "내 동영상";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "종료";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "데모로 돌아가기";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "메뉴로 돌아가려면 스마트폰을 회전하세요.";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "데모를 종료하려면 스마트폰을 뷰어에서 꺼내세요.";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "다음";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "선택하려면 버튼을 누르세요.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "뒤로 이동하려면 뷰어를 회전하세요.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "지금 사용해 보세요.";
diff --git a/paperscope/paperscope_launcher/resources/strings/lt.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/lt.lproj/cardboard.strings
new file mode 100644
index 00000000..f0e6dff3
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/lt.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Mokymo programa";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Pasveikinimo vaizdo įrašas";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Tyrėjas";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Ekspozicija";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Žygis mieste";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoskopas";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arktinė kelionė";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Nepavyksta prisijungti prie serverio.\nPatikrinkite interneto ryšį";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Apgailestaujame, kažkas ne taip. Vėliau bandykite dar kartą";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Paspauskite mygtuką,\nkad spustelėtumėte";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Pakreipkite, kad patektumėte į\npagrindinį puslapį";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Amerikos natūralios istorijos muziejus\nEkspedicijų pavyzdys";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour Space Shuttle";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rikas";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIslandija";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nVaizdas iš NASA / JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Spustelėkite, kad patektumėte į %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paryžius";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokijas";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venecija";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Niujorkas";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jeruzalė";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Karlas";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londonas";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Didysis barjerinis rifas";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Kelionės vadovas";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Mano vaizdo įrašai";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "IŠEITI";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "ATGAL Į DEM. VERSIJĄ";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Pasukite telefoną, kad grįžtumėte į meniu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Išimkite telefoną iš žiūriklio, kad išeitumėte iš demonstracinės versijos";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "KITAS";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Paspauskite mygtuką, kad pasirinktumėte.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Pasukite žiūriklį, kad grįžtumėte atgal.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Išbandykite tai dabar.";
diff --git a/paperscope/paperscope_launcher/resources/strings/lv.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/lv.lproj/cardboard.strings
new file mode 100644
index 00000000..b2df864a
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/lv.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Pamācība";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Sveiciena videoklips";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Atklājējs";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Izstādes";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Pārgājiens pilsētā";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoskops";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arctic Journey";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Nevar izveidot savienojumu ar serveri.\nPārbaudiet interneta savienojumu.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Radās problēma. Lūdzu, vēlāk mēģiniet vēlreiz.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Nospiediet pogu,\nlai noklikšķinātu";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Nolieciet, lai pārietu\nuz sākumu";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Maraka “Vanags”\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Maska “Vatikāns”\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Virsaiša maska\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Sapņotājs\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Bēthovens\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "ASV Dabas vēstures muzejs\nEkspedīciju paraugs";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Muzejs “Frontiers of Flight”";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Kosmosa kuģis “Endeavour”";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Indiāņu ala\nPuertoriko";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver karstie avoti\nĪslande";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Marsa izpētes robots “Spirit”\nNASA/JPL attēls";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Noklikšķiniet, lai pārietu uz: %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Parīze";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokija";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venēcija";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Ņujorka";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jeruzaleme";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Montekarlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londona";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Lielais Barjerrifs";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Ceļvedis";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Mani videoklipi";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "IZIET";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "UZ DEMONSTRĀCIJĀM";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Lai atgrieztos izvēlnē, pagrieziet tālruni";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Lai izietu no demonstrācijas režīma, noņemiet tālruni no skatītāja";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "TĀLĀK";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Nospiediet pogu, lai veiktu atlasi.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Pagrieziet skatītāju, lai pārietu atpakaļ.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Izmēģiniet tūlīt!";
diff --git a/paperscope/paperscope_launcher/resources/strings/nb.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/nb.lproj/cardboard.strings
new file mode 100644
index 00000000..775d778f
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/nb.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Veiledning";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Velkomstvideo";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Utforsker";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Utstilling";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Gatevandring";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoskop";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Reise i Arktis";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Kan ikke koble til tjeneren\nsjekk internettforbindelsen din";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Beklager, noe gikk galt. Prøv igjen senere";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Trykk på knappen\nfor å klikke";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Vipp for å gå\ntilbake til startskjermen";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nsmakeprøve på Ekspedisjoner";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight-museet";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour-romfergen";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio-grotten\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Varme kilder i Gunnuhver\nIsland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nBilde fra NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Klikk for å gå til %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venezia";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Omvisning med guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Mine videoer";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "AVSLUTT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "TILBAKE TIL DEMOENE";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Rotér telefonen din for å gå tilbake til menyen";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Ta telefonen ut av brillene for å gå ut av demoene";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "NESTE";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Trykk på knappen for å velge.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Rotér brillene for å navigere tilbake.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Prøv det nå.";
diff --git a/paperscope/paperscope_launcher/resources/strings/nl.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/nl.lproj/cardboard.strings
new file mode 100644
index 00000000..1bbf6f07
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/nl.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Training";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Welkomstvideo";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Verkenner";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Tentoonstelling";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Stadswandeling";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscoop";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Poolreis";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Kan geen verbinding maken met de server.\nControleer je internetverbinding.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Er is iets misgegaan. Probeer het later opnieuw.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Druk op de knop\nom te klikken";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Kantel om naar\nde startpagina te gaan";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nVoorbeeld van expedities";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Spaceshuttle Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Thermale bronnen van Gunnuhver\nIJsland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nAfbeelding van NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Klik om naar %2$s te gaan)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Parijs";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venetië";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rome";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jeruzalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londen";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tourgids";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Mijn video's";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "SLUITEN";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "TERUG NAAR DEMO'S";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Draai je telefoon om terug te gaan naar het menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Haal je telefoon uit je bril om de demo's te verlaten";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "VOLGENDE";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Druk op de knop om op een knop te klikken.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Draai je bril om terug te navigeren.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Probeer het nu.";
diff --git a/paperscope/paperscope_launcher/resources/strings/pl.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/pl.lproj/cardboard.strings
new file mode 100644
index 00000000..2dac651f
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/pl.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Samouczek";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Wideo powitalne";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Odkrywca";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Eksponat";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Spacer po mieście";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kalejdoskop";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arktyczna podróż";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Nie można połączyć się z serwerem.\nSprawdź swoje połączenie internetowe.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Coś poszło nie tak. Spróbuj ponownie później.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Naciśnij przycisk,\nby rozpocząć";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Pochyl, aby przejść\nna stronę główną";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Amerykańskie Muzeum Historii Naturalnej\nPrzykłady wypraw";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Muzeum Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Prom kosmiczny Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPortoryko";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gorące źródła Gunnuhver\nIslandia";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Bezzałogowa misja na Marsa Spirit\nZdjęcie NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Kliknij, aby przejść do: %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paryż";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Wenecja";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Nowy Jork";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rzym";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerozolima";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londyn";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Wielka Rafa Koralowa";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Przewodnik po wycieczce";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Moje filmy";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "ZAKOŃCZ";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "POWRÓT DO DEMO";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Obróć telefon, aby wrócić do menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Wyjmij telefon z gogli, aby zamknąć wersje demonstracyjne";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "DALEJ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Naciśnij przycisk, by wybrać.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Obróć gogle, aby przejść wstecz.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Wypróbuj teraz.";
diff --git a/paperscope/paperscope_launcher/resources/strings/pt.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/pt.lproj/cardboard.strings
new file mode 100644
index 00000000..1c6e2a4b
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/pt.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Vídeo de boas-vindas";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorador";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exposição";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Caminhada urbana";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Caleidoscópio";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Jornada no Ártico";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Não é possível se conectar ao servidor\nVerifique sua conexão com a Internet";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Ocorreu um erro. Tente novamente mais tarde";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Pressione o botão\npara clicar";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Incline para acessar\na página inicial";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Máscara Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Máscara Vatican\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Máscara Chiefs\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Máscara The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Máscara Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Museu Americano de História Natural\nExemplo de \"Expedições\"";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Museu Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Ônibus espacial Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Caverna Cueva del Indio\nPorto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Águas termais em Gunnuhver\nIslândia";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Robô Spirit em Marte\nImagem de NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Clique para ir até %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tóquio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Veneza";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Nova York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalém";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londres";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Grande Barreira de Corais";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Google Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Guia de turismo";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Meus vídeos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "SAIR";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "VOLTAR DEMONSTRAÇÕES";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Gire seu smartphone para voltar ao menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Tire seu smartphone do visor para sair da demonstração";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "PRÓXIMA";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Pressione o botão para selecionar.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Gire seu visualizador para voltar na navegação.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Tente fazer isso agora.";
diff --git a/paperscope/paperscope_launcher/resources/strings/pt_BR.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/pt_BR.lproj/cardboard.strings
new file mode 100644
index 00000000..1c6e2a4b
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/pt_BR.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Vídeo de boas-vindas";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorador";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exposição";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Caminhada urbana";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Caleidoscópio";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Jornada no Ártico";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Não é possível se conectar ao servidor\nVerifique sua conexão com a Internet";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Ocorreu um erro. Tente novamente mais tarde";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Pressione o botão\npara clicar";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Incline para acessar\na página inicial";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Máscara Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Máscara Vatican\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Máscara Chiefs\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Máscara The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Máscara Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Museu Americano de História Natural\nExemplo de \"Expedições\"";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Museu Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Ônibus espacial Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Caverna Cueva del Indio\nPorto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Águas termais em Gunnuhver\nIslândia";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Robô Spirit em Marte\nImagem de NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Clique para ir até %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tóquio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Veneza";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Nova York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalém";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londres";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Grande Barreira de Corais";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Google Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Guia de turismo";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Meus vídeos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "SAIR";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "VOLTAR DEMONSTRAÇÕES";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Gire seu smartphone para voltar ao menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Tire seu smartphone do visor para sair da demonstração";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "PRÓXIMA";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Pressione o botão para selecionar.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Gire seu visualizador para voltar na navegação.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Tente fazer isso agora.";
diff --git a/paperscope/paperscope_launcher/resources/strings/pt_PT.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/pt_PT.lproj/cardboard.strings
new file mode 100644
index 00000000..0498f383
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/pt_PT.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Vídeo de boas-vindas";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Viagem ao Ártico";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Não é possível estabelecer a ligação ao servidor\nVerifique a ligação à Internet";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Passou-se qualquer coisa de errado. Tente novamente mais tarde.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Prima o botão\npara acionar";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Inclinar para ir para\na Página inicial";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExemplos de expedições";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour Space Shuttle";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPorto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver Hot Springs\nIslândia";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Spirit Rover\nImagem da NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Clique para ir para %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tóquio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Veneza";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Nova Iorque";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalém";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londres";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Grande Barreira de Coral";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tour Guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "My Videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "SAIR";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "VOLTAR ÀS DEMONSTR.";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Rode o seu telemóvel para voltar ao menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Retire o seu telemóvel do visualizador para sair das demonstrações";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "SEGUINTE";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Prima o botão para selecionar.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Rode o visualizador para navegar para trás.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Experimente agora.";
diff --git a/paperscope/paperscope_launcher/resources/strings/ro.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ro.lproj/cardboard.strings
new file mode 100644
index 00000000..ab6bb163
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/ro.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Tutorial";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Videoclip de bun venit";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Călătorie polară";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Conexiunea la server nu poate fi stabilită\nVerificați conexiunea la internet";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "A apărut o eroare. Încercați din nou";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Apăsați pe buton\npână face clic";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Înclinați pentru a accesa\necranul principal";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Masca Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Masca Vatican\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Masca Chief's\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Masca Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Masca Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Muzeul American de Istorie Naturală\nSecvență din Expediții";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Muzeul Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Nava spațială Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Izvoarele termale de la Gunnuhver\nIslanda";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Vehiculul spațial Spirit de pe Marte\nImagine de la NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Dați clic pentru a accesa %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Veneția";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Ierusalim";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londra";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Marea Barieră de Corali";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Ghid turistic";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Videoclipurile mele";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "IEȘI";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "ÎNAPOI LA DEMO";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Rotiți telefonul pentru a reveni la meniu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Scoate telefonul din vizualizator pentru a ieși din versiunile demonstrative";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "ÎNAINTE";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Apăsați pe buton pentru a selecta.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Rotiți vizualizatorul pentru a naviga înapoi.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Încercați acum.";
diff --git a/paperscope/paperscope_launcher/resources/strings/ru.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/ru.lproj/cardboard.strings
new file mode 100644
index 00000000..77392efa
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/ru.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Руководство";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Добро пожаловать!";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Экскурсии";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Выставка";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Путешествия";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Калейдоскоп";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Арктические пейзажи";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Не удается подключиться к серверу.\nПроверьте подключение к Интернету.";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Произошла ошибка. Повторите попытку позже.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Нажмите на кнопку,\nчтобы выбрать";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Наклоните, чтобы\nвернуться к началу";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Погремушка \"Ястреб\"\n© Ранде Кук";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Ватиканская маска\n© Ранде Кук";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Маска вождя\n© Ранде Кук";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Прорицатель\n© Ранде Кук";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Бетховен\n© Ранде Кук";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Американский музей естественной истории\nЭкспедиции";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Музей Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Космический челнок \"Индевор\"";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Пещера Куэва дель Индио\nПуэрто-Рико";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Горячие источники Гуннухвер\nИсландия";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Марсоход \"Спирит\"\nСнимок НАСА/ЛРД";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(нажмите, чтобы перейти далее: %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Париж";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Токио";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Венеция";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Нью-Йорк";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Рим";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Иерусалим";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Монте-Карло";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Лондон";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Большой Барьерный риф";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Планета Земля";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Экскурсия";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Мои видео";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "ВЫЙТИ";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "НАЗАД К РОЛИКАМ";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Чтобы вернуться в меню, поверните телефон";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Чтобы закрыть демо, выньте телефон из очков";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "ДАЛЕЕ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Чтобы что-то выбрать, нажмите кнопку на очках.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Поворачивайте очки, чтобы вернуться назад.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Попробуйте повернуть сейчас.";
diff --git a/paperscope/paperscope_launcher/resources/strings/sk.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/sk.lproj/cardboard.strings
new file mode 100644
index 00000000..6db06be2
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/sk.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Príručka";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Uvítacie video";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Arktická cesta";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Nepodarilo sa pripojiť k serveru,\nskontrolujte svoje internetové pripojenie";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Ľutujeme, vyskytol sa problém, skúste to znova";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Spustite\nstlačením tlačidla";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Naklonením prejdite\nna domovskú stránku";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Múzeum American Museum of Natural History\nUkážka expedícií";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Múzeum Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Raketopán Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Jaskyňa Cueva del Indio\nPortoriko";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Termálne pramene Gunnuhver\nIsland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Vozidlo Spirit expedície na Mars\nObrázok NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Kliknutím prejdite do umiestnenia %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paríž";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Benátky";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rím";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jeruzalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londýn";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Veľká koralová bariéra";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tour Guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "My Videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "UKONČIŤ";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "SPÄŤ NA UKÁŽKY";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Otočením telefónu sa vráťte na ponuku";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Ak chcete ukončiť demá, vyberte telefón zo zobrazovača";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "ĎALEJ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Výber uskutočníte stlačením tlačidla.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Ak chcete prejsť späť, otočte zobrazovačom.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Vyskúšajte to.";
diff --git a/paperscope/paperscope_launcher/resources/strings/sl.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/sl.lproj/cardboard.strings
new file mode 100644
index 00000000..bc9a4ca8
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/sl.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Vadnica";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Pozdravni video";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Raziskovalec";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Razstava";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Sprehod po mestu";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kalejdoskop";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Popotovanje po Arktiki";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "S strežnikom se ni mogoče povezati\npreverite internetno povezavo";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Prišlo je do napake. Poskusite znova pozneje.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Pritisnite gumb,\nda kliknete";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Nagnite za vrnitev\nna začetek";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Ameriški prirodoslovni muzej\nPrimer za storitev Expeditions";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Muzej Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Raketoplan Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPortoriko";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Geotermalno območje Gunnuhver\nIslandija";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Robot Spirit za raziskovanje Marsa\nSlika: NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Kliknite za pomik na lokacijo %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Pariz";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokio";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Benetke";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rim";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jeruzalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Veliki koralni greben";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Turistični vodnik";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Moji videoposnetki";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "IZHOD";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "PREDSTAVITVE";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Če se želite vrniti v meni, zasukajte telefon";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Za izhod iz predstavitvenih različic odstranite telefon iz pregledovalnika";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "NAPREJ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Za izbiro pritisnite gumb.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Če se želite pomakniti nazaj, zasukajte glavo (s pregledovalnikom).";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Poskusite zdaj.";
diff --git a/paperscope/paperscope_launcher/resources/strings/sr.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/sr.lproj/cardboard.strings
new file mode 100644
index 00000000..b79d192b
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/sr.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Водич";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Видео добродошлице";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Истраживач";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Изложба";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Пешачење кроз град";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Калеидоскоп";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Арктичко путовање";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Повезивање са сервером није успело\nПроверите интернет везу";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Жао нам је, нешто је пошло наопако. Покушајте поново касније.";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Притисните дугме\nда бисте кликнули";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Нагните да бисте отишли\nна почетак";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Звечка „Соко“\n© Ренд Кук";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Ватиканска маска\n© Ренд Кук";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Поглавичина маска\n© Ренд Кук";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Визионар\n© Ренд Кук";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Бетовен\n© Ренд Кук";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Амерички природњачки музеј\nПример експедиција";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Музеј Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Спејс-шатл Ендевор";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nПорторико";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Термални извори Гунухвер\nИсланд";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Ровер Спирит на Марсу\nСлика: НАСА/Лабораторија за млазни погон";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Кликните да бисте прешли на %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Париз";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Токио";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Венеција";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Њујорк";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Рим";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Јерусалим";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Монте Карло";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Лондон";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Велики корални гребен";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Туристички водич";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Моји видео снимци";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "ЗАТВОРИ";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "НАЗАД";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Окрените телефон да бисте се вратили у мени";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Извадите телефон из маске да бисте изашли из демо верзија";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "ДАЉЕ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Притисните дугме да бисте изабрали.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Ротирајте маску да бисте се вратили.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Испробајте одмах.";
diff --git a/paperscope/paperscope_launcher/resources/strings/sv.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/sv.lproj/cardboard.strings
new file mode 100644
index 00000000..06d45c90
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/sv.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Självstudier";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Välkomstvideo";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Explorer";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Exhibit";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Urban Hike";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleidoscope";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Res till Nordpolen";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Det går inte att ansluta till servern\nKontrollera internetanslutningen";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Något gick fel, försök igen senare";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Tryck på knappen\nför att klicka";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Luta för att återgå till\nstart";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "American Museum of Natural History\nExpeditions-exempel";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Rymdfärjan Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "De varma källorna i Gunnuhver\nIsland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Rovern Spirit på Mars\nBild från NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Klicka om du vill gå till %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venedig";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rom";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "London";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Stora Barriärrevet";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Reseledaren";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Mina videoklipp";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "AVSLUTA";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "TILLBAKA TILL DEMOS";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Om du roterar telefonen visas menyn igen";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Ta ut telefonen ur kikaren om du vill stänga demorna";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "NÄSTA";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Välj genom att trycka på knappen.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Rotera kikaren om du vill navigera bakåt.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Testa nu.";
diff --git a/paperscope/paperscope_launcher/resources/strings/th.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/th.lproj/cardboard.strings
new file mode 100644
index 00000000..896d5e6a
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/th.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "บทแนะนำ";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "วิดีโอต้อนรับ";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "นักสำรวจ";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "นิทรรศการ";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "เดินในเมือง";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "ภาพลานตา";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "เส้นทางอาร์กติก";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้\nโปรดตรวจดูการเชื่อมต่ออินเทอร์เน็ต";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "ขออภัย เกิดข้อผิดพลาดขึ้น โปรดลองอีกครั้งในภายหลัง";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "กดปุ่ม\nเพื่อคลิก";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "เอียงเพื่อไปยัง\nหน้าแรก";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "พิพิธภัณฑ์ประวัติศาสตร์ธรรมชาติอเมริกัน\nตัวอย่างการออกสำรวจ";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "พิพิธภัณฑ์ Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "กระสวยอวกาศเอนเดฟเวอร์";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "ถ้ำ Cueva del Indio\nประเทศเปอร์โตริโก";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "น้ำพุร้อน Gunnuhver\nประเทศไอซ์แลนด์";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "ยานสำรวจดาวอังคาร Spirit\nภาพจาก NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(คลิกเพื่อไปที่ %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "ปารีส";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "โตเกียว";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "เวนิส";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "นิวยอร์ก";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "โรม";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "เยรูซาเลม";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "มอนติคาร์โล";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "ลอนดอน";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "เกรตแบร์ริเออร์รีฟ";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "มัคคุเทศก์";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "วิดีโอของฉัน";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "ออก";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "กลับไปที่การสาธิต";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "หมุนโทรศัพท์เพื่อกลับไปที่เมนู";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "แกะโทรศัพท์ออกจากแว่นเพื่อออกจากการสาธิต";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "ถัดไป";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "กดปุ่มเพื่อเลือก";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "หมุนกล่องเพื่อย้อนกลับไป";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "ลองใช้ดู";
diff --git a/paperscope/paperscope_launcher/resources/strings/tr.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/tr.lproj/cardboard.strings
new file mode 100644
index 00000000..97003408
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/tr.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Eğitici";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Karşılama Videosu";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Gezgin";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Sergi";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Şehirde Yürüyüş";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kaleydoskop";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Kuzey Kutbu'na Yolculuk";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Sunucuya bağlanılamıyor,\nİnternet bağlantınızı kontrol edin";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Bir sorun oluştu, lütfen daha sonra tekrar deneyin";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Tıklamak için\ndüğmeye basın";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Başa dönmek için\neğin";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Amerikan Doğa Tarihi Müzesi\nSergi Örneği";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Endeavour Uzay Mekiği";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPorto Riko";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Gunnuhver Termal Kaplıcaları\nİzlanda";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Mars Keşif Aracı Spirit\nNASA/JPL'den gelen resim";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(%2$s konumuna gitmek için tıklayın)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venedik";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Roma";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Kudüs";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Londra";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Büyük Set Resifi";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Tour Guide";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "My Videos";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "ÇIK";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "DEMOLARA DÖN";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Menüye dönmek için telefonunuzu döndürün";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Demolardan çıkmak için telefonunuzu gözlükten çıkarın";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "SONRAKİ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Seçmek için düğmeye basın.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Geri gitmek için gözlüğünüzü döndürün.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Şimdi deneyin.";
diff --git a/paperscope/paperscope_launcher/resources/strings/uk.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/uk.lproj/cardboard.strings
new file mode 100644
index 00000000..292e29c4
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/uk.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Навчальний посібник";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Привітальне відео";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Дослідник";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Виставка";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Прогулянка містом";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Калейдоскоп";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Арктична подорож";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Не вдається з’єднатися із сервером\nперевірте з’єднання з Інтернетом";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Сталася помилка, повторіть спробу пізніше";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Натисніть кнопку\n";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Нахиліть окуляри,\nщоб повернутися на початок";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Брязкальце-яструб\n© Ренді Кук";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Ватиканська маска\n© Ренді Кук";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Маска вождя\n© Ренді Кук";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Провидець\n© Ренді Кук";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Бетховен\n© Ренді Кук";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Американський музей природознавства\nзразок експедиції";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Музей Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Космічний корабель \"Індевор\"";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Куева дель Індіо\nПуерто-Рико";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Термальні джерела Гуннухвер\nІсландія";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Марсохід \"Спіріт\"\nЗображення NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Натисніть, щоб перейти до об’єкта \"%2$s\")";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Париж";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Токіо";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Венеція";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "Нью-Йорк";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Рим";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Єрусалим";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Монте-Карло";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Лондон";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Великий бар’єрний риф";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Планета Земля";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Путівник";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Мої відео";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "ВИЙТИ";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "ДО ДЕМО-РОЛИКІВ";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Оберніть телефон, щоб повернутися до меню";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Вийміть телефон з окулярів, щоб вийти з режиму демонстрації";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "ДАЛІ";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Натисніть кнопку, щоб вибрати.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Оберніть окуляри, щоб повернутися назад.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Спробуйте зараз!";
diff --git a/paperscope/paperscope_launcher/resources/strings/vi.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/vi.lproj/cardboard.strings
new file mode 100644
index 00000000..5f35aacd
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/vi.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "Hướng dẫn";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "Video chào mừng";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "Khám phá";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "Triển lãm";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "Đi dạo trên phố";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "Kính vạn hoa";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "Chuyến phiêu lưu tới Bắc Cực";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "Không thể kết nối với máy chủ\nhãy kiểm tra kết nối internet của bạn";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "Rất tiếc vì đã xảy ra sự cố, vui lòng thử lại sau";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "Nhấn nút\nđể nhấp vào";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "Nghiêng để về\ntrình chạy chính";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Mặt nạ diều hâu\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Mặt nạ Vatican\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Mặt nạ thủ lĩnh\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "Mặt nạ ảo mộng\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Mặt nạ Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "Bảo tàng lịch sử tự nhiên Hoa Kỳ\nVí dụ cho Expeditions";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "Bảo tàng Frontiers of Flight";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "Tàu con thoi Endeavour";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "Cueva del Indio\nPuerto Rico";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "Suối nước nóng Gunnuhver\nIceland";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "Rô bốt tự hành thám hiểm sao Hỏa Spirit \nHình ảnh từ NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(Nhấp vào để chuyển tới %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "Paris";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "Tokyo";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "Venice";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "New York";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "Rome";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "Jerusalem";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "Monte Carlo";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "Luân Đôn";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "Great Barrier Reef";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Earth";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "Hướng dẫn chuyến đi";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "Video của tôi";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "THOÁT";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "VỀ LẠI B.TRÌNH DIỄN";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "Xoay điện thoại của bạn để quay lại menu";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "Lấy điện thoại ra khỏi thiết bị xem để thoát khỏi bản minh hoạ";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "TIẾP THEO";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "Nhấn nút để chọn.";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "Xoay thiết bị xem của bạn để điều hướng trở lại.";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "Hãy làm thử ngay bây giờ.";
diff --git a/paperscope/paperscope_launcher/resources/strings/zh_CN.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/zh_CN.lproj/cardboard.strings
new file mode 100644
index 00000000..e37855f6
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/zh_CN.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "教程";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "欢迎视频";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "探险者";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "展览";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "城市远足";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "万花筒";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "北极之旅";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "无法连接到服务器\n请检查您的互联网连接";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "抱歉,出了点问题。请重试";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "按下按钮\n即可点击";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "倾斜 Cardboard 以转到\n首页";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "美国自然历史博物馆\n实境教学样本";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "飞行先驱者";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "奋进号航天飞机";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "印第安洞穴\n波多黎各";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "格林达维克温泉\n冰岛";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "勇气号火星探测器\n图片来源:NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(点击即可转到%2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "巴黎";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "东京";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "威尼斯";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "纽约";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "罗马";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "耶路撒冷";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "蒙特卡洛";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "伦敦";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "大堡礁";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "Google 地球";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "导览";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "我的视频";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "退出";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "返回演示影片";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "旋转手机即可返回菜单";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "将手机从观看器中取出即可退出演示";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "下一步";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "按此按钮即可选择。";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "旋转眼镜即可返回。";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "立即尝试。";
diff --git a/paperscope/paperscope_launcher/resources/strings/zh_HK.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/zh_HK.lproj/cardboard.strings
new file mode 100644
index 00000000..11b9e904
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/zh_HK.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "教學課程";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "歡迎使用影片";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "探險家";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "展覽館";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "都市行腳";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "萬花筒";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "北極之旅";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "無法連線到伺服器\n請檢查您的網際網路連線";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "發生錯誤,請稍後再試";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "按下按鈕\n";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "將 Cardboard 傾斜即可\n回到啟動器主畫面";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "美國自然史博物館\n探險考察樣品";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "飛行里程碑博物館";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "奮進號太空梭";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "印第安岩洞\n波多黎各";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "格奴弗爾溫泉\n冰島";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "精神號火星探測車\n圖片來源:NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(按一下即可前往 %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "巴黎";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "東京";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "威尼斯";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "紐約";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "羅馬";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "耶路撒冷";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "蒙地卡羅";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "倫敦";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "大堡礁";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "地球";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "導覽";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "我的影片";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere 全景相片";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "離開";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "返回示範影片";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "旋轉您的手機以返回選單";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "將手機從觀影盒中取出,即可退出示範模式";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "下一步";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "按下按鈕以選取。";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "旋轉您的檢視器即可返回。";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "立即體驗。";
diff --git a/paperscope/paperscope_launcher/resources/strings/zh_TW.lproj/cardboard.strings b/paperscope/paperscope_launcher/resources/strings/zh_TW.lproj/cardboard.strings
new file mode 100644
index 00000000..9a523760
--- /dev/null
+++ b/paperscope/paperscope_launcher/resources/strings/zh_TW.lproj/cardboard.strings
@@ -0,0 +1,197 @@
+/*
+ * cardboard.strings
+ *
+ * Guidelines:
+ *
+ * 1/ Write keys as CamelCase. In the code, this will be prefixed with kStr: kStrThisIsAKey.
+ *
+ * 2/ For descriptions, use 100-column limit and align multiple lines on the first character.
+ * For strings, do not break lines.
+ *
+ * 3/ Write a meaningful description, so the translator can know what exactly you are talking about.
+ *
+ * 4/ Without context, a single word is hard to translate. For instance, the word "search" can be a
+ * noun or a verb and they have different translations in other languages. Add the grammar
+ * class, the verb tense or any other information to distinguish between homonyms.
+ *
+ * 5/ Supply the maximum number of characters for strings in the comment. For example:
+ * /# Button label which starts warp (i.e. faster than light travel) mode. (30 chars) #/
+ * "WarpModeButtonLabel" = "Warp Speed";
+ *
+ * 6/ For accessibility items, include "ACCESSIBILITY" in the key and comments. Generally
+ * accessibility strings do not have a limit, because they are spoken. Indicate this with (no
+ * limit).
+ */
+
+/* Title of tutorial demo. (30 chars) */
+"TutorialDemoTitle" = "教學課程";
+
+/* Title of welcome video that is shown to the user the first time they use Cardboard. (30 chars) */
+"WelcomeVideoTitle" = "歡迎使用影片";
+
+/* Title of explorer demo. (30 chars) */
+"ExplorerDemoTitle" = "探險家";
+
+/* Title of exhibit demo. (30 chars) */
+"ExhibitDemoTitle" = "展覽館";
+
+/* Title of urban hike demo. (30 chars) */
+"UrbanHikeDemoTitle" = "都市行腳";
+
+/* Title of exhibit demo. (30 chars) */
+"KaleidoscopeDemoTitle" = "萬花筒";
+
+/* Title of demo that is a collection of scenes set in an arctic landscape. (30 chars) */
+"ArcticJourneyDemoTitle" = "北極之旅";
+
+/* Message shown to the user if there is a problem connecting to the server. (80 chars) */
+"ConnectionErrorMessage" = "無法連線到伺服器\n請檢查您的網際網路連線";
+
+/* Message shown to the user if there was some generic error. (80 chars) */
+"GenericErrorMessage" = "發生錯誤,請稍後再試";
+
+/* Tutorial caption text asking the user to click the Cardboard button. (60 chars) */
+"TutorialClickCaption" = "按下按鈕\n";
+
+/* Tutorial caption text asking the user to tilt Cardboard to go home to the launcher.
+ (60 chars) */
+"TutorialTiltCaption" = "將 Cardboard 傾斜即可\n回到啟動器主畫面";
+
+/* Name of 'Hawk Rattle' mask, with copyright attribution to Rande Cook. (50 chars) */
+"HawkRattle" = "Hawk Rattle\n© Rande Cook";
+
+/* Name of 'Vatican' mask, with copyright attribution to Rande Cook. (50 chars) */
+"VaticanMask" = "Vatican Mask\n© Rande Cook";
+
+/* Name of 'Chiefs' mask, with copyright attribution to Rande Cook. (50 chars) */
+"ChiefsMask" = "Chief's Mask\n© Rande Cook";
+
+/* Name of 'Visionary' mask, with copyright attribution to Rande Cook. (50 chars) */
+"TheVisionary" = "The Visionary\n© Rande Cook";
+
+/* Name of 'Beethoven' mask, with copyright attribution to Rande Cook. (50 chars) */
+"Beethoven" = "Beethoven\n© Rande Cook";
+
+/*
+ * Title shown for sample panorama inside the American Museum of Natural History.
+ * (No limit)
+ */
+"AmericanMuseumNaturalHistory" = "美國自然史博物館\n探險考察樣品";
+
+/*
+ * Title shown for sample panorama inside the Frontiers of Flight museum in Dallas, TX.
+ * (No limit)
+ */
+"FrontiersOfFlight" = "飛行里程碑博物館";
+
+/* Title shown for sample panorama of the Endeavour Space Shuttle. (No limit) */
+"EndeavourSpaceShuttle" = "奮進號太空梭";
+
+/*
+ * Title shown for sample panorama inside 'Cueva del Indio,' a cave located in Puerto Rico.
+ * (No limit)
+ */
+"CuevaDelIndio" = "印第安岩洞\n波多黎各";
+
+/* Title shown for sample panorama of a hot springs located in Gunnuhver, Iceland. (No limit) */
+"GunnuhverHotSprings" = "格奴弗爾溫泉\n冰島";
+
+/* Title shown for sample panorama taken form the Mars rover 'Spirit'. (No limit) */
+"MarsSpiritRover" = "精神號火星探測車\n圖片來源:NASA/JPL";
+
+/*
+ * Label that reports the user's current location with an addtional prompt to go to the next
+ * location. The first parameter is the current location, the second parameter is the next
+ * location. (No limit)
+ */
+"UrbanHikeCurrentLocation" = "%1$s\n(按一下即可前往 %2$s)";
+
+/* Location name referencing Paris France in the Urban Hike demo. (No limit) */
+"UrbanHikeParis" = "巴黎";
+
+/* Location name referencing Tokyo Japan in the Urban Hike demo. (No limit) */
+"UrbanHikeTokyo" = "東京";
+
+/* Location name referencing Venice Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeVenice" = "威尼斯";
+
+/* Location name referencing New York, NY in the Urban Hike demo. (No limit) */
+"UrbanHikeNewYork" = "紐約";
+
+/* Location name referencing Rome Italy in the Urban Hike demo. (No limit) */
+"UrbanHikeRome" = "羅馬";
+
+/* Location name referencing Jerusalem Israel in the Urban Hike demo. (No limit) */
+"UrbanHikeJerusalem" = "耶路撒冷";
+
+/*
+ * Location name referencing the administrative area in the Principality of Monaco, Monte Carlo,
+ * in the Urban Hike demo. (No limit)
+*/
+"UrbanHikeMonteCarlo" = "蒙地卡羅";
+
+/* Location name referencing London England in the Urban Hike demo. (No limit) */
+"UrbanHikeLondon" = "倫敦";
+
+/*
+ * Location name referencing the Great Barrier Reef (the coral reef in Queensland, Australia)
+ * in the Urban Hike demo. (No limit)
+ */
+"UrbanHikeGreatBarrierReef" = "大堡礁";
+
+/*
+ * Title of 'Google Earth' demo inside Cardboard Android app. (30 chars)
+ * 'Earth' should be the translated the same way as the official 'Google Earth' product.
+ */
+"EarthAndroidDemoTitle" = "地球";
+
+ * Title of 'Tour Guide' demo inside Cardboard Android app. 'Tour Guide' acts as a virtual tour
+ * guide around Versailles. (30 chars)
+ */
+"TourGuideAndroidDemoTitle" = "導覽";
+
+/*
+ * Title of 'My Videos' demo inside Cardboard Android app. 'My Videos' allows the user to view all
+ * of their local videos in Cardboard as if viewing them on a large screen in front of them.
+ * (30 chars)
+ */
+"MyVideosAndroidDemoTitle" = "我的影片";
+
+/*
+ * Title of 'Photo Sphere' demo inside Cardboard Android app. 'Photo Sphere' shows all personal and
+ * locally stored photo spheres to the user. 'Photo Sphere' corresponds to the offical
+ * Google Photo Sphere product name. (30 chars)
+ */
+"PhotoSphereAndroidDemoTitle" = "Photo Sphere 全景相片";
+
+/*
+ * Title of 'Windy Day' demo inside Cardboard Android app. 'Windy Day' is a Google spotlight story
+ * where a hat is blown around by the wind. It should be considered a product name, and should
+ * be kept in English for languages that normally keep Google product names in English. (30 chars)
+ */
+"WindyDayAndroidDemoTitle" = "Windy Day";
+
+/* Title of button exit VR. (20 chars) */
+"Exit" = "結束";
+
+/* Title of button to go back to the Cardboard demos. (20 chars) */
+"BackToDemos" = "返回示範影片";
+
+/* Message telling the user that they can tilt their phone to go back.
+ (no limit) */
+"RotateToGoBack" = "旋轉您的手機以返回選單";
+
+/* Message telling the user to take their phone out of their viewer to exit the demos. (no limit) */
+"TakeYourPhoneOutToExitDemos" = "將手機從觀影盒中取出,即可退出示範模式";
+
+* Title of button for user to click to go to next step. (10 chars) */
+"Next" = "下一步";
+
+/* Message telling the user to press the button on the viewer to click a button. (no limit) */
+"PressTheButtonOnYourViewer" = "按下按鈕以選取。";
+
+/* Message telling the user to rotate the viewer to navigate back. (no limit) */
+"RotateYourViewer" = "旋轉您的檢視器即可返回。";
+
+/* Message telling the user to try rotating the view now. (no limit) */
+"TryItOutNow" = "立即體驗。";
diff --git a/paperscope/paperscope_launcher/src/animated_shape.cc b/paperscope/paperscope_launcher/src/animated_shape.cc
new file mode 100644
index 00000000..30b26418
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/animated_shape.cc
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "animated_shape.h"
+
+#include
+
+#include // NOLINT(build/c++11)
+#include "util.h"
+
+namespace cardboard_launcher {
+
+// Duration of each frame in the animation.
+const std::chrono::milliseconds kAnimationFrameDuration(67);
+
+void AnimatedShape::SetModelViewProjection(
+ const cardboard_demo::Matrix4x4& eye_view,
+ const cardboard_demo::Matrix4x4& projection_matrix) {
+ cardboard_demo::Matrix4x4 model_view_tutorial = eye_view * model_animation_;
+ model_view_projection_animation_ = projection_matrix * model_view_tutorial;
+}
+
+void AnimatedShape::LoadAnimations(GLuint obj_position_param_,
+ GLuint obj_uv_param_,
+ AAssetManager* asset_mgr_, JNIEnv* env,
+ jobject java_asset_mgr) {
+ env_ = env;
+ java_asset_mgr_ = java_asset_mgr;
+ for (int i = 0; i < static_cast(animation_frames_.size()); ++i) {
+ CARDBOARDDEMO_CHECK(animation_meshes_[i].Initialize(
+ obj_position_param_, obj_uv_param_, "IconMesh.obj", asset_mgr_));
+ CARDBOARDDEMO_CHECK(animation_textures_[i].Initialize(
+ env, java_asset_mgr_, animation_frames_[i]));
+ }
+
+ // Z- points forward, set the animation to be in front of the user,
+ // facing the user.
+ float yaw = 180.0f * M_PI / 180.0f; // rotate 180 degrees.
+ model_animation_ = cardboard_demo::GetTranslationMatrix({0, 2.2f, -1.5}) *
+ cardboard_demo::GetYawRotationMatrix(yaw);
+ cardboard_demo::CHECKGLERROR("LoadAnimations");
+}
+
+void AnimatedShape::Init() {
+ for (auto& texture : animation_textures_) {
+ texture.set_texture_id(0);
+ }
+}
+
+void AnimatedShape::DisplayFrame(
+ const std::chrono::steady_clock::time_point timestamp) {
+ if (!animation_textures_[animation_index_].get_texture_id() ||
+ !glIsTexture(animation_textures_[animation_index_].get_texture_id())) {
+ CARDBOARDDEMO_CHECK(animation_textures_[animation_index_].Initialize(
+ env_, java_asset_mgr_, animation_frames_[animation_index_]));
+ }
+ animation_textures_[animation_index_].Bind();
+ animation_meshes_[animation_index_].Draw();
+
+ if (!animation_frames_.empty()) {
+ if (timestamp - last_update_time_ >= kAnimationFrameDuration) {
+ animation_index_ = (animation_index_ + 1) % animation_frames_.size();
+ last_update_time_ = std::chrono::steady_clock::now();
+ }
+ }
+ cardboard_demo::CHECKGLERROR("DisplayFrame");
+}
+
+} // namespace cardboard_launcher
diff --git a/paperscope/paperscope_launcher/src/animated_shape.h b/paperscope/paperscope_launcher/src/animated_shape.h
new file mode 100644
index 00000000..a8b05ff4
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/animated_shape.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.
+ */
+#ifndef PAPERSCOPE_LAUNCHER_SRC_ANIMATED_SHAPE_H_
+#define PAPERSCOPE_LAUNCHER_SRC_ANIMATED_SHAPE_H_
+
+#include
+#include
+
+#include // NOLINT(build/c++11)
+#include
+
+#include
+#include "util.h"
+
+namespace cardboard_launcher {
+
+// An animated shape is a renderable that renders a series of images in order
+// to create frame-by-frame animations.
+class AnimatedShape {
+ public:
+ AnimatedShape() = default;
+
+ /**
+ * Creates an AnimatedShape from a vector of animation frames.
+ *
+ * @param animation_frames A vector of animation frames.
+ */
+ AnimatedShape(const std::vector& animation_frames)
+ : animation_frames_(animation_frames),
+ last_update_time_(std::chrono::steady_clock::now()),
+ animation_index_(0),
+ animation_meshes_(animation_frames.size()),
+ animation_textures_(animation_frames.size()) {}
+
+ /**
+ * Gets the model matrix for the animation.
+ *
+ * @return The model matrix for the animation.
+ */
+ cardboard_demo::Matrix4x4 GetModelAnimation() const {
+ return model_animation_;
+ }
+
+ /**
+ * Gets the modelview projection matrix for the animation.
+ *
+ * @return The modelview projection matrix for the animation.
+ */
+ cardboard_demo::Matrix4x4 GetModelViewProjectionAnimation() const {
+ return model_view_projection_animation_;
+ }
+ /**
+ * Sets the modelview projection matrix for the animation.
+ *
+ * @param eye_view The eye view matrix.
+ * @param projection_matrix The projection matrix.
+ */
+ void SetModelViewProjection(
+ const cardboard_demo::Matrix4x4& eye_view,
+ const cardboard_demo::Matrix4x4& projection_matrix);
+
+ /**
+ * Loads the animations from the given asset manager.
+ *
+ * @param obj_position_param The object position parameter.
+ * @param obj_uv_param The object uv parameter.
+ * @param asset_mgr The asset manager.
+ * @param env The JNI environment.
+ * @param java_asset_mgr The Java asset manager.
+ */
+ void LoadAnimations(GLuint obj_position_param_, GLuint obj_uv_param_,
+ AAssetManager* asset_mgr_, JNIEnv* env,
+ jobject java_asset_mgr_);
+
+ /**
+ * Prepares the animation frame for the current timestamp and updates the
+ * animation index if necessary.
+ *
+ * @param timestamp The current timestamp.
+ */
+ void DisplayFrame(std::chrono::steady_clock::time_point timestamp);
+
+ // Initializes the animated shape.
+ void Init();
+
+ private:
+ // List of animation frames.
+ std::vector animation_frames_;
+ // Last time the animation was updated.
+ std::chrono::steady_clock::time_point last_update_time_;
+ // Index of the current animation.
+ int animation_index_;
+
+ // Model and modelview projection matrices for the animation.
+ cardboard_demo::Matrix4x4 model_animation_;
+ cardboard_demo::Matrix4x4 model_view_projection_animation_;
+ std::vector animation_meshes_;
+ std::vector animation_textures_;
+
+ // JNI environment and Java asset manager for reloading textures.
+ JNIEnv* env_ = nullptr;
+ jobject java_asset_mgr_ = nullptr;
+};
+
+} // namespace cardboard_launcher
+
+#endif // PAPERSCOPE_LAUNCHER_SRC_ANIMATED_SHAPE_H_
diff --git a/paperscope/paperscope_launcher/src/cardboard_jni.cc b/paperscope/paperscope_launcher/src/cardboard_jni.cc
new file mode 100644
index 00000000..c723fb22
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/cardboard_jni.cc
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include
+#include
+
+#include
+#include
+
+#include "cardboard_launcher.h"
+#include "magic_window.h"
+#include "main.h"
+#include "settings.h"
+
+#define JNI_LAUNCHER_METHOD(return_type, method_name) \
+ JNIEXPORT return_type JNICALL \
+ Java_com_google_vr_cardboard_paperscope_demo_CardboardLauncherActivity_##method_name // NOLINT
+
+#define JNI_MAGIC_WINDOW_METHOD(return_type, method_name) \
+ JNIEXPORT return_type JNICALL \
+ Java_com_google_vr_cardboard_paperscope_demo_magicwindow_MagicWindowActivity_##method_name // NOLINT
+
+#define JNI_SETTINGS_METHOD(return_type, method_name) \
+ JNIEXPORT return_type JNICALL \
+ Java_com_google_vr_cardboard_paperscope_demo_settings_SettingsActivity_##method_name // NOLINT
+
+#define JNI_MAIN_METHOD(return_type, method_name) \
+ JNIEXPORT return_type JNICALL \
+ Java_com_google_vr_cardboard_paperscope_demo_MainActivity_##method_name // NOLINT
+
+namespace {
+
+inline jlong launcher_jptr(cardboard_launcher::CardboardLauncher* native_app) {
+ return reinterpret_cast(native_app);
+}
+
+inline cardboard_launcher::CardboardLauncher* launcher_native(jlong ptr) {
+ return reinterpret_cast(ptr);
+}
+
+inline jlong magic_window_jptr(magic_window::MagicWindow* native_app) {
+ return reinterpret_cast(native_app);
+}
+
+inline magic_window::MagicWindow* magic_window_native(jlong ptr) {
+ return reinterpret_cast(ptr);
+}
+
+inline jlong settings_jptr(settings::Settings* native_app) {
+ return reinterpret_cast(native_app);
+}
+
+inline settings::Settings* settings_native(jlong ptr) {
+ return reinterpret_cast(ptr);
+}
+
+inline jlong main_jptr(main::Main* native_app) {
+ return reinterpret_cast(native_app);
+}
+
+inline main::Main* main_native(jlong ptr) {
+ return reinterpret_cast(ptr);
+}
+
+JavaVM* javaVm;
+
+} // anonymous namespace
+
+extern "C" {
+
+JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
+ javaVm = vm;
+ return JNI_VERSION_1_6;
+}
+
+// Cardboard Launcher Functions
+JNI_LAUNCHER_METHOD(jlong, nativeOnCreate)
+(JNIEnv* /*env*/, jobject obj, jobject asset_mgr) {
+ return launcher_jptr(
+ new cardboard_launcher::CardboardLauncher(javaVm, obj, asset_mgr));
+}
+
+JNI_LAUNCHER_METHOD(void, nativeOnDestroy)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ delete launcher_native(native_app);
+}
+
+JNI_LAUNCHER_METHOD(void, nativeOnSurfaceCreated)
+(JNIEnv* env, jobject /*obj*/, jlong native_app) {
+ launcher_native(native_app)->OnSurfaceCreated(env);
+}
+
+JNI_LAUNCHER_METHOD(void, nativeOnDrawFrame)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ launcher_native(native_app)->OnDrawFrame();
+}
+
+JNI_LAUNCHER_METHOD(void, nativeOnTriggerEvent)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ launcher_native(native_app)->OnTriggerEvent();
+}
+
+JNI_LAUNCHER_METHOD(void, nativeOnPause)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ launcher_native(native_app)->OnPause();
+}
+
+JNI_LAUNCHER_METHOD(void, nativeOnResume)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ launcher_native(native_app)->OnResume();
+}
+
+JNI_LAUNCHER_METHOD(void, nativeSetScreenParams)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app, jint width, jint height) {
+ launcher_native(native_app)->SetScreenParams(width, height);
+}
+
+JNI_LAUNCHER_METHOD(void, nativeSwitchViewer)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ launcher_native(native_app)->SwitchViewer();
+}
+
+JNI_LAUNCHER_METHOD(bool, nativeShouldLaunchDemo)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ return launcher_native(native_app)->ShouldLaunchDemo();
+}
+
+JNI_LAUNCHER_METHOD(jstring, nativeDemoToLaunch)
+(JNIEnv* env, jobject /*obj*/, jlong native_app) {
+ return env->NewStringUTF(
+ (launcher_native(native_app)->DemoToLaunch()).c_str());
+}
+
+JNI_LAUNCHER_METHOD(void, nativeDemoLaunched)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ launcher_native(native_app)->DemoLaunched();
+}
+
+// Cardboard Magic Window Functions
+JNI_MAGIC_WINDOW_METHOD(jlong, nativeOnCreate)
+(JNIEnv* /*env*/, jobject obj, jobject asset_mgr) {
+ return magic_window_jptr(
+ new magic_window::MagicWindow(javaVm, obj, asset_mgr));
+}
+
+JNI_MAGIC_WINDOW_METHOD(void, nativeOnDestroy)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ delete magic_window_native(native_app);
+}
+
+JNI_MAGIC_WINDOW_METHOD(void, nativeOnSurfaceCreated)
+(JNIEnv* env, jobject /*obj*/, jlong native_app) {
+ magic_window_native(native_app)->OnSurfaceCreated(env);
+}
+
+JNI_MAGIC_WINDOW_METHOD(void, nativeOnDrawFrame)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ magic_window_native(native_app)->OnDrawFrame();
+}
+
+JNI_MAGIC_WINDOW_METHOD(void, nativeOnResume)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ magic_window_native(native_app)->OnResume();
+}
+
+JNI_MAGIC_WINDOW_METHOD(void, nativeOnPause)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ magic_window_native(native_app)->OnPause();
+}
+
+JNI_MAGIC_WINDOW_METHOD(void, nativeSetScreenParams)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app, jint width, jint height) {
+ magic_window_native(native_app)->SetScreenParams(width, height);
+}
+
+JNI_MAGIC_WINDOW_METHOD(void, nativeScanViewer)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ magic_window_native(native_app)->ScanViewer();
+}
+
+JNI_MAGIC_WINDOW_METHOD(bool, nativeHasSavedDeviceParams)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ return magic_window_native(native_app)->HasSavedDeviceParams();
+}
+
+// Cardboard Settings Functions
+JNI_SETTINGS_METHOD(jlong, nativeOnCreate)
+(JNIEnv* /*env*/, jobject obj, jobject asset_mgr) {
+ return settings_jptr(
+ new settings::Settings(javaVm, obj, asset_mgr));
+}
+
+JNI_SETTINGS_METHOD(void, nativeSwitchViewer)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ settings_native(native_app)->SwitchViewer();
+}
+
+// Cardboard Settings Functions
+JNI_MAIN_METHOD(jlong, nativeOnCreate)
+(JNIEnv* /*env*/, jobject obj, jobject asset_mgr) {
+ return main_jptr(
+ new main::Main(javaVm, obj, asset_mgr));
+}
+
+JNI_MAIN_METHOD(void, nativeSwitchViewer)
+(JNIEnv* /*env*/, jobject /*obj*/, jlong native_app) {
+ main_native(native_app)->SwitchViewer();
+}
+
+} // extern "C"
diff --git a/paperscope/paperscope_launcher/src/cardboard_launcher.cc b/paperscope/paperscope_launcher/src/cardboard_launcher.cc
new file mode 100644
index 00000000..5174310c
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/cardboard_launcher.cc
@@ -0,0 +1,658 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "cardboard_launcher.h"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "cardboard.h"
+#include "cardboard_strings.h"
+#include "demo_icon.h"
+#include "util.h"
+
+namespace cardboard_launcher {
+
+namespace {
+
+constexpr float kDefaultFloorHeight{-1.7f};
+
+// 6 Hz cutoff frequency for the velocity filter of the head tracker.
+constexpr int32_t kVelocityFilterCutoffFrequency{6};
+
+constexpr uint64_t kPredictionTimeWithoutVsyncNanos{50000000};
+
+// Angle threshold for determining whether the controller is pointing at the
+// demo icon.
+constexpr float kAngleLimit{0.1f};
+
+// Distance between the demos.
+constexpr float kDistanceBetweenDemos{0.7f};
+
+// Vertical offset for the black square.
+constexpr float kBlackSquareYOffset{2.2f};
+
+// Distance from the user for the black square.
+constexpr float kBlackSquareZOffset{-3.0f};
+
+// Scale for the background when showing demos.
+constexpr std::array kDemosBackgroundScale{3.1f, 1.1f, 1.0f};
+
+// Scale for the background during the tutorial.
+constexpr std::array kTutorialBackgroundScale{2.0f, 2.2f, 1.0f};
+
+// Simple shaders to render .obj files without any lighting.
+constexpr const char* kObjVertexShader =
+ R"glsl(
+ uniform mat4 u_MVP;
+ attribute vec4 a_Position;
+ attribute vec2 a_UV;
+ varying vec2 v_UV;
+
+ void main() {
+ v_UV = a_UV;
+ gl_Position = u_MVP * a_Position;
+ })glsl";
+
+constexpr const char* kObjFragmentShader =
+ R"glsl(
+ precision mediump float;
+
+ uniform sampler2D u_Texture;
+ varying vec2 v_UV;
+
+ void main() {
+ // The y coordinate of this sample's textures is reversed compared to
+ // what OpenGL expects, so we invert the y coordinate.
+ gl_FragColor = texture2D(u_Texture, vec2(v_UV.x, 1.0 - v_UV.y));
+ })glsl";
+
+constexpr const char* kVertexShader = R"glsl(
+ uniform mat4 u_MVP;
+ uniform mat4 u_Scale;
+ attribute vec4 a_Position;
+ void main () {
+ gl_Position = u_MVP * u_Scale * a_Position;
+ }
+)glsl";
+
+constexpr const char* kFragmentShader = R"glsl(
+ precision mediump float;
+ void main () {
+ gl_FragColor = vec4(0, 0, 0, 0.4);
+ }
+)glsl";
+
+// Button animation.
+const std::vector kTutorialButtonAssets{
+ "button_animation_0001.png", "button_animation_0002.png",
+ "button_animation_0003.png", "button_animation_0004.png",
+ "button_animation_0005.png", "button_animation_0006.png",
+ "button_animation_0007.png", "button_animation_0008.png",
+ "button_animation_0009.png", "button_animation_0010.png",
+ "button_animation_0011.png", "button_animation_0012.png",
+ "button_animation_0013.png", "button_animation_0014.png",
+ "button_animation_0015.png"};
+
+// Tilt animation.
+const std::vector kTutorialTiltAssets{
+ "tilt_animation_0000.png", "tilt_animation_0001.png",
+ "tilt_animation_0002.png", "tilt_animation_0003.png",
+ "tilt_animation_0004.png", "tilt_animation_0005.png",
+ "tilt_animation_0006.png", "tilt_animation_0007.png",
+ "tilt_animation_0008.png", "tilt_animation_0009.png",
+ "tilt_animation_0010.png", "tilt_animation_0011.png",
+ "tilt_animation_0012.png", "tilt_animation_0013.png",
+ "tilt_animation_0014.png"};
+
+} // anonymous namespace
+
+CardboardLauncher::CardboardLauncher(JavaVM* vm, jobject obj,
+ jobject asset_mgr_obj)
+ : head_tracker_{nullptr},
+ lens_distortion_{nullptr},
+ distortion_renderer_{nullptr},
+ screen_params_changed_{false},
+ device_params_changed_{false},
+ screen_width_{0},
+ screen_height_{0},
+ depthRenderBuffer_{0},
+ framebuffer_{0},
+ texture_{0},
+ obj_program_{0},
+ obj_position_param_{0},
+ obj_uv_param_{0},
+ obj_model_view_projection_param_{0},
+ should_launch_demo_{false},
+ state_{State::SHOW_DEMOS},
+ last_state_event_{StateEvent::NONE},
+ tutorial_animator_{kTutorialButtonAssets, kTutorialTiltAssets,
+ &text_renderer_},
+ black_background_mvp_param_{0},
+ black_background_scale_param_{0} {
+ JNIEnv* env;
+ vm->GetEnv((void**)&env, JNI_VERSION_1_6);
+ java_asset_mgr_ = env->NewGlobalRef(asset_mgr_obj);
+ asset_mgr_ = AAssetManager_fromJava(env, asset_mgr_obj);
+
+ Cardboard_initializeAndroid(vm, obj);
+ head_tracker_ = CardboardHeadTracker_create();
+ CardboardHeadTracker_setLowPassFilter(head_tracker_,
+ kVelocityFilterCutoffFrequency);
+}
+
+CardboardLauncher::~CardboardLauncher() {
+ CardboardHeadTracker_destroy(head_tracker_);
+ CardboardLensDistortion_destroy(lens_distortion_);
+ CardboardDistortionRenderer_destroy(distortion_renderer_);
+}
+
+void CardboardLauncher::OnSurfaceCreated(JNIEnv* env) {
+ const int obj_vertex_shader =
+ cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kObjVertexShader);
+ const int obj_fragment_shader =
+ cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kObjFragmentShader);
+
+ obj_program_ = glCreateProgram();
+ glAttachShader(obj_program_, obj_vertex_shader);
+ glAttachShader(obj_program_, obj_fragment_shader);
+ glLinkProgram(obj_program_);
+ glUseProgram(obj_program_);
+
+ cardboard_demo::CHECKGLERROR("Obj program");
+
+ obj_position_param_ = glGetAttribLocation(obj_program_, "a_Position");
+ obj_uv_param_ = glGetAttribLocation(obj_program_, "a_UV");
+ obj_model_view_projection_param_ =
+ glGetUniformLocation(obj_program_, "u_MVP");
+
+ cardboard_demo::CHECKGLERROR("Obj program params");
+
+ CARDBOARDDEMO_CHECK(background_.Initialize(obj_position_param_, obj_uv_param_,
+ "World.obj", asset_mgr_));
+ CARDBOARDDEMO_CHECK(
+ background_tex_.Initialize(env, java_asset_mgr_, "home_mono.jpg"));
+
+ text_renderer_.Init();
+ text_renderer_.LoadFont(asset_mgr_, "Roboto-Regular.ttf");
+
+ const std::array demo_icon_data{
+ DemoIconData{"tutorial", cardboard::GetLocalizedString(
+ cardboard::kStrTutorialDemoTitle, env)},
+ // BEGIN-INTERNAL-EXHIBIT
+ // Hide the exhibit demo from being open sourced. Do not remove the
+ // guards.
+ DemoIconData{"exhibit", cardboard::GetLocalizedString(
+ cardboard::kStrExhibitDemoTitle, env)},
+ // END-INTERNAL-EXHIBIT
+ DemoIconData{"mymovies",
+ cardboard::GetLocalizedString(
+ cardboard::kStrMyVideosAndroidDemoTitle, env)},
+ DemoIconData{"photospheres",
+ cardboard::GetLocalizedString(
+ cardboard::kStrPhotoSphereAndroidDemoTitle, env)}};
+
+ demos_.clear();
+ demos_.reserve(demo_icon_data.size());
+
+ // Create demos.
+ for (size_t i = 0; i < demo_icon_data.size(); ++i) {
+ // Demo meshes appear directly in front of user.
+ const float pos{((demo_icon_data.size() - 1) / 2.0f - i) *
+ kDistanceBetweenDemos};
+ demos_.emplace_back(demo_icon_data.at(i), pos, &text_renderer_);
+ }
+
+ tutorial_animator_.SetJNIEnv(env);
+
+ LoadBlackBackground();
+ LoadDemos(env);
+ LoadTutorial(env);
+
+ cardboard_demo::CHECKGLERROR("OnSurfaceCreated");
+}
+
+void CardboardLauncher::LoadBlackBackground() {
+ const float vertices[] {
+ 0.5f, 0.5f, 0.0f, // top right
+ 0.5f, -0.5f, 0.0f, // bottom right
+ -0.5f, -0.5f, 0.0f, // bottom left
+ -0.5f, 0.5f, 0.0f // top left
+ };
+
+ const uint32_t indices[]{
+ // note that we start from 0!
+ 0, 1, 3, // first triangle
+ 1, 2, 3 // second triangle
+ };
+
+ glGenBuffers(1, &black_background_vbo_);
+ glGenBuffers(1, &black_background_ebo_);
+
+ glBindBuffer(GL_ARRAY_BUFFER, black_background_vbo_);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, black_background_ebo_);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
+ GL_STATIC_DRAW);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+
+ const int32_t vertex_shader =
+ cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kVertexShader);
+ const int32_t fragment_shader =
+ cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kFragmentShader);
+
+ black_background_program_id_ = glCreateProgram();
+ glAttachShader(black_background_program_id_, vertex_shader);
+ glAttachShader(black_background_program_id_, fragment_shader);
+ cardboard_demo::CHECKGLERROR("LoadBlackBackground attach shaders");
+
+ glLinkProgram(black_background_program_id_);
+ cardboard_demo::CHECKGLERROR("LoadBlackBackground link program");
+
+ black_background_mvp_param_ =
+ glGetUniformLocation(black_background_program_id_, "u_MVP");
+ black_background_scale_param_ =
+ glGetUniformLocation(black_background_program_id_, "u_Scale");
+}
+
+void CardboardLauncher::LoadDemos(JNIEnv* env) {
+ for (DemoIcon& demo : demos_) {
+ demo.LoadDemo(obj_position_param_, obj_uv_param_, asset_mgr_, env,
+ java_asset_mgr_);
+ }
+}
+
+void CardboardLauncher::LoadTutorial(JNIEnv* env) {
+ tutorial_animator_.LoadAnimations(obj_position_param_, obj_uv_param_,
+ asset_mgr_, env, java_asset_mgr_);
+}
+
+void CardboardLauncher::SetScreenParams(int width, int height) {
+ screen_width_ = width;
+ screen_height_ = height;
+ screen_params_changed_ = true;
+}
+
+void CardboardLauncher::OnDrawFrame() {
+ if (!UpdateDeviceParams()) {
+ return;
+ }
+
+ // Update Head Pose.
+ head_view_ = GetPose();
+
+ // Incorporate the height from the floor into the head_view.
+ head_view_ = head_view_ * cardboard_demo::GetTranslationMatrix(
+ {0.0f, kDefaultFloorHeight, 0.0f});
+
+ // Bind buffer.
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
+
+ glEnable(GL_DEPTH_TEST);
+ glEnable(GL_CULL_FACE);
+ glDisable(GL_SCISSOR_TEST);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ // Draw eyes views.
+ for (int eye = 0; eye < 2; ++eye) {
+ glViewport(eye == kLeft ? 0 : screen_width_ / 2, 0, screen_width_ / 2,
+ screen_height_);
+
+ const cardboard_demo::Matrix4x4 eye_matrix =
+ cardboard_demo::GetMatrixFromGlArray(eye_matrices_[eye]);
+ const cardboard_demo::Matrix4x4 eye_view = eye_matrix * head_view_;
+
+ const cardboard_demo::Matrix4x4 projection_matrix =
+ cardboard_demo::GetMatrixFromGlArray(projection_matrices_[eye]);
+
+ for (DemoIcon& demo : demos_) {
+ const cardboard_demo::Matrix4x4 model_view_demo =
+ eye_view * demo.GetModel();
+ demo.SetModelViewProjection(projection_matrix * model_view_demo);
+ }
+
+ if (state_ == State::TILT_TUTORIAL || state_ == State::BUTTON_TUTORIAL) {
+ tutorial_animator_.SetScreenPosition(eye_view, projection_matrix);
+ }
+
+ model_view_projection_background_ = projection_matrix * eye_view;
+ const cardboard_demo::Matrix4x4 model_black_square =
+ cardboard_demo::GetTranslationMatrix({0.0f, kBlackSquareYOffset,
+ kBlackSquareZOffset});
+ model_view_projection_black_square_ =
+ projection_matrix * eye_view * model_black_square;
+
+ // Draw background and demos.
+ DrawWorld();
+
+ // Update State Machine.
+ UpdateState();
+ }
+
+ // Render.
+ CardboardDistortionRenderer_renderEyeToDisplay(
+ distortion_renderer_, /* target_display = */ 0, /* x = */ 0, /* y = */ 0,
+ screen_width_, screen_height_, &left_eye_texture_description_,
+ &right_eye_texture_description_);
+
+ cardboard_demo::CHECKGLERROR("onDrawFrame");
+}
+
+void CardboardLauncher::OnTriggerEvent() {
+ // Check if the pointed demo represents the built-in tutorial.
+ if (IsPointingAtDemo(demos_.at(0).GetModel())) {
+ LOGD("Launch Tutorial");
+ last_state_event_ = StateEvent::TUTORIAL_SELECTED;
+ } else {
+ for (size_t i = 1; i < demos_.size(); ++i) {
+ if (IsPointingAtDemo(demos_.at(i).GetModel()) &&
+ state_ == State::SHOW_DEMOS) {
+ LOGD("Launching Demo %s", demos_.at(i).GetDemoName().c_str());
+ should_launch_demo_ = true;
+ demo_to_launch_ = demos_.at(i).GetDemoName();
+ }
+ }
+ }
+
+ if (tutorial_animator_.IsNextClicked(head_view_, kAngleLimit)) {
+ last_state_event_ = StateEvent::TUTORIAL_NEXT_SELECTED;
+ }
+}
+
+void CardboardLauncher::OnPause() { CardboardHeadTracker_pause(head_tracker_); }
+
+void CardboardLauncher::OnResume() {
+ CardboardHeadTracker_resume(head_tracker_);
+
+ // Parameters may have changed.
+ device_params_changed_ = true;
+
+ // Check for device parameters existence in external storage. If they're
+ // missing, we must scan a Cardboard QR code and save the obtained parameters.
+ uint8_t* buffer;
+ int size;
+ CardboardQrCode_getSavedDeviceParams(&buffer, &size);
+ if (size == 0) {
+ SwitchViewer();
+ }
+ CardboardQrCode_destroy(buffer);
+}
+
+void CardboardLauncher::SwitchViewer() {
+ CardboardQrCode_scanQrCodeAndSaveDeviceParams();
+}
+
+bool CardboardLauncher::UpdateDeviceParams() {
+ // Checks if screen or device parameters changed.
+ if (!screen_params_changed_ && !device_params_changed_) {
+ return true;
+ }
+
+ // Get saved device parameters.
+ uint8_t* buffer;
+ int size;
+ CardboardQrCode_getSavedDeviceParams(&buffer, &size);
+
+ // If there are no parameters saved yet, returns false.
+ if (size == 0) {
+ return false;
+ }
+
+ CardboardLensDistortion_destroy(lens_distortion_);
+ lens_distortion_ = CardboardLensDistortion_create(buffer, size, screen_width_,
+ screen_height_);
+
+ CardboardQrCode_destroy(buffer);
+
+ GlSetup();
+
+ CardboardDistortionRenderer_destroy(distortion_renderer_);
+ const CardboardOpenGlEsDistortionRendererConfig config{kGlTexture2D};
+ distortion_renderer_ = CardboardOpenGlEs2DistortionRenderer_create(&config);
+
+ CardboardMesh left_mesh;
+ CardboardMesh right_mesh;
+ CardboardLensDistortion_getDistortionMesh(lens_distortion_, kLeft,
+ &left_mesh);
+ CardboardLensDistortion_getDistortionMesh(lens_distortion_, kRight,
+ &right_mesh);
+
+ CardboardDistortionRenderer_setMesh(distortion_renderer_, &left_mesh, kLeft);
+ CardboardDistortionRenderer_setMesh(distortion_renderer_, &right_mesh,
+ kRight);
+
+ // Get eye matrices.
+ CardboardLensDistortion_getEyeFromHeadMatrix(lens_distortion_, kLeft,
+ eye_matrices_[0]);
+ CardboardLensDistortion_getEyeFromHeadMatrix(lens_distortion_, kRight,
+ eye_matrices_[1]);
+ CardboardLensDistortion_getProjectionMatrix(lens_distortion_, kLeft, kZNear,
+ kZFar, projection_matrices_[0]);
+ CardboardLensDistortion_getProjectionMatrix(lens_distortion_, kRight, kZNear,
+ kZFar, projection_matrices_[1]);
+
+ screen_params_changed_ = false;
+ device_params_changed_ = false;
+
+ cardboard_demo::CHECKGLERROR("UpdateDeviceParams");
+
+ return true;
+}
+
+void CardboardLauncher::GlSetup() {
+ LOGD("GL SETUP");
+
+ if (framebuffer_ != 0) {
+ GlTeardown();
+ }
+
+ // Create render texture.
+ glGenTextures(1, &texture_);
+ glBindTexture(GL_TEXTURE_2D, texture_);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, screen_width_, screen_height_, 0,
+ GL_RGB, GL_UNSIGNED_BYTE, 0);
+
+ left_eye_texture_description_.texture = texture_;
+ left_eye_texture_description_.left_u = 0;
+ left_eye_texture_description_.right_u = 0.5;
+ left_eye_texture_description_.top_v = 1;
+ left_eye_texture_description_.bottom_v = 0;
+
+ right_eye_texture_description_.texture = texture_;
+ right_eye_texture_description_.left_u = 0.5;
+ right_eye_texture_description_.right_u = 1;
+ right_eye_texture_description_.top_v = 1;
+ right_eye_texture_description_.bottom_v = 0;
+
+ // Generate depth buffer to perform depth test.
+ glGenRenderbuffers(1, &depthRenderBuffer_);
+ glBindRenderbuffer(GL_RENDERBUFFER, depthRenderBuffer_);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, screen_width_,
+ screen_height_);
+ cardboard_demo::CHECKGLERROR("Create Render buffer");
+
+ // Create render demo icon.
+ glGenFramebuffers(1, &framebuffer_);
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+ texture_, 0);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+ GL_RENDERBUFFER, depthRenderBuffer_);
+
+ cardboard_demo::CHECKGLERROR("GlSetup");
+}
+
+void CardboardLauncher::GlTeardown() {
+ if (framebuffer_ == 0) {
+ return;
+ }
+ glDeleteRenderbuffers(1, &depthRenderBuffer_);
+ depthRenderBuffer_ = 0;
+ glDeleteFramebuffers(1, &framebuffer_);
+ framebuffer_ = 0;
+ glDeleteTextures(1, &texture_);
+ texture_ = 0;
+
+ cardboard_demo::CHECKGLERROR("GlTeardown");
+}
+
+cardboard_demo::Matrix4x4 CardboardLauncher::GetPose() const {
+ std::array out_orientation;
+ std::array out_position;
+ CardboardHeadTracker_getPose(
+ head_tracker_,
+ cardboard_demo::GetBootTimeNano() + kPredictionTimeWithoutVsyncNanos,
+ kLandscapeLeft, &out_position.at(0), &out_orientation.at(0));
+ return cardboard_demo::GetTranslationMatrix(out_position) *
+ cardboard_demo::Quatf::FromXYZW(&out_orientation.at(0)).ToMatrix();
+}
+
+void CardboardLauncher::DrawWorld() { DrawBackground(); }
+
+void CardboardLauncher::UpdateState() {
+ switch (state_) {
+ case State::SHOW_DEMOS:
+ DrawBlackBackgroundGL(kDemosBackgroundScale);
+ DrawDemos();
+ // Check transition to button tutorial state.
+ if (last_state_event_ == StateEvent::TUTORIAL_SELECTED) {
+ state_ = State::BUTTON_TUTORIAL;
+ }
+ break;
+ case State::BUTTON_TUTORIAL:
+ DrawBlackBackgroundGL(kTutorialBackgroundScale);
+ tutorial_animator_.DisplayButtonAnimation(
+ obj_program_, obj_model_view_projection_param_);
+ // Check transition to tilt tutorial state.
+ if (last_state_event_ == StateEvent::TUTORIAL_NEXT_SELECTED) {
+ state_ = State::TILT_TUTORIAL;
+ }
+ break;
+ case State::TILT_TUTORIAL:
+ DrawBlackBackgroundGL(kTutorialBackgroundScale);
+ const float roll_angle_ =
+ atan2(head_view_.m[1][0], head_view_.m[0][0]) * 180.0 / M_PI;
+ if (roll_angle_ < -80.0f && roll_angle_ > -100.0f) {
+ last_state_event_ = StateEvent::DEVICE_IN_PORTRAIT;
+ }
+ tutorial_animator_.DisplayTiltAnimation(obj_program_,
+ obj_model_view_projection_param_);
+ // Check transition to show demos state.
+ if (last_state_event_ == StateEvent::DEVICE_IN_PORTRAIT) {
+ state_ = State::SHOW_DEMOS;
+ }
+ break;
+ }
+}
+
+void CardboardLauncher::DrawDemos() {
+ glUseProgram(obj_program_);
+
+ for (DemoIcon& demo : demos_) {
+ const std::array target_array {
+ demo.GetModelViewProjection().ToGlArray()};
+ glUniformMatrix4fv(obj_model_view_projection_param_, 1, GL_FALSE,
+ target_array.data());
+
+ demo.Draw();
+ demo.SetPointingAtDemo(IsPointingAtDemo(demo.GetModel()));
+ }
+ cardboard_demo::CHECKGLERROR("DrawDemos");
+}
+
+void CardboardLauncher::DrawBackground() {
+ glUseProgram(obj_program_);
+
+ std::array background_array =
+ model_view_projection_background_.ToGlArray();
+ glUniformMatrix4fv(obj_model_view_projection_param_, 1, GL_FALSE,
+ background_array.data());
+
+ background_tex_.Bind();
+ background_.Draw();
+
+ cardboard_demo::CHECKGLERROR("DrawBackground");
+}
+
+void CardboardLauncher::DrawBlackBackgroundGL(
+ const std::array& scale) {
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_CULL_FACE);
+
+ glUseProgram(black_background_program_id_);
+
+ const std::array target_array{
+ model_view_projection_black_square_.ToGlArray()};
+ glUniformMatrix4fv(black_background_mvp_param_, 1, GL_FALSE,
+ target_array.data());
+
+ cardboard_demo::Matrix4x4 scale_matrix =
+ cardboard_demo::GetScaleMatrix(scale);
+ const std::array scale_array{scale_matrix.ToGlArray()};
+ glUniformMatrix4fv(black_background_scale_param_, 1, GL_FALSE,
+ scale_array.data());
+
+ glBindBuffer(GL_ARRAY_BUFFER, black_background_vbo_);
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
+ glEnableVertexAttribArray(0);
+
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, black_background_ebo_);
+
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
+
+ glDisableVertexAttribArray(0);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glEnable(GL_CULL_FACE);
+ glEnable(GL_DEPTH_TEST);
+}
+
+bool CardboardLauncher::IsPointingAtDemo(
+ const cardboard_demo::Matrix4x4& model_demo) {
+ // Compute vectors pointing towards the reticle and towards the demo icon in
+ // head space.
+ cardboard_demo::Matrix4x4 head_from_demo = head_view_ * model_demo;
+ const std::array unit_quaternion {0.f, 0.f, 0.f, 1.f};
+ const std::array point_vector {0.f, 0.f, -1.f, 0.f};
+ const std::array demo_vector { head_from_demo * unit_quaternion };
+
+ const float angle {
+ cardboard_demo::AngleBetweenVectors(point_vector, demo_vector)};
+ return angle < kAngleLimit;
+}
+
+bool CardboardLauncher::ShouldLaunchDemo() { return should_launch_demo_; }
+
+std::string CardboardLauncher::DemoToLaunch() { return demo_to_launch_; }
+
+void CardboardLauncher::DemoLaunched() { should_launch_demo_ = false; }
+
+} // namespace cardboard_launcher
diff --git a/paperscope/paperscope_launcher/src/cardboard_launcher.h b/paperscope/paperscope_launcher/src/cardboard_launcher.h
new file mode 100644
index 00000000..4c51d975
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/cardboard_launcher.h
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.
+ */
+#ifndef PAPERSCOPE_LAUNCHER_SRC_CARDBOARD_LAUNCHER_H_
+#define PAPERSCOPE_LAUNCHER_SRC_CARDBOARD_LAUNCHER_H_
+
+#include
+#include
+
+#include
+
+#include
+#include "cardboard.h"
+#include "demo_icon.h"
+#include "text_renderer.h"
+#include "tutorial_animator.h"
+#include "util.h"
+
+namespace cardboard_launcher {
+
+/**
+ * This is a sample app for the Cardboard SDK. It loads a simple environment and
+ * objects that you can click on.
+ */
+class CardboardLauncher {
+ public:
+ /**
+ * Creates a DemoCardboardDemo.
+ *
+ * @param vm JavaVM pointer.
+ * @param obj Android activity object.
+ * @param asset_mgr_obj The asset manager object.
+ */
+ CardboardLauncher(JavaVM* vm, jobject obj, jobject asset_mgr_obj);
+
+ ~CardboardLauncher();
+
+ /**
+ * Initializes any GL-related objects. This should be called on the rendering
+ * thread with a valid GL context.
+ *
+ * @param env The JNI environment.
+ */
+ void OnSurfaceCreated(JNIEnv* env);
+
+ /**
+ * Sets screen parameters.
+ *
+ * @param width Screen width
+ * @param height Screen height
+ */
+ void SetScreenParams(int width, int height);
+
+ /**
+ * Draws the scene. This should be called on the rendering thread.
+ */
+ void OnDrawFrame();
+
+ /**
+ * Hides the target object if it's being targeted.
+ */
+ void OnTriggerEvent();
+
+ /**
+ * Pauses head tracking.
+ */
+ void OnPause();
+
+ /**
+ * Resumes head tracking.
+ */
+ void OnResume();
+
+ /**
+ * Allows user to switch viewer.
+ */
+ void SwitchViewer();
+
+ /**
+ * Checks if the demo should be launched.
+ */
+ bool ShouldLaunchDemo();
+
+ /**
+ * Forwards the demo name to launch.
+ */
+ std::string DemoToLaunch();
+
+ /**
+ * Resets the should_launch_demo flag after the demo is launched.
+ */
+ void DemoLaunched();
+
+ private:
+ /**
+ * Current state of the launcher.
+ */
+ enum class State { SHOW_DEMOS, BUTTON_TUTORIAL, TILT_TUTORIAL };
+
+ /**
+ * State events of the launcher.
+ */
+ enum class StateEvent {
+ TUTORIAL_SELECTED,
+ TUTORIAL_NEXT_SELECTED,
+ DEVICE_IN_PORTRAIT,
+ NONE
+ };
+
+ /**
+ * Default near clip plane z-axis coordinate.
+ */
+ static constexpr float kZNear = 0.1f;
+
+ /**
+ * Default far clip plane z-axis coordinate.
+ */
+ static constexpr float kZFar = 100.f;
+
+ /**
+ * Updates device parameters, if necessary.
+ *
+ * @return true if device parameters were successfully updated.
+ */
+ bool UpdateDeviceParams();
+
+ /**
+ * Initializes GL environment.
+ */
+ void GlSetup();
+
+ /**
+ * Deletes GL environment.
+ */
+ void GlTeardown();
+
+ /**
+ * Gets head's pose as a 4x4 matrix.
+ *
+ * @return matrix containing head's pose.
+ */
+ cardboard_demo::Matrix4x4 GetPose() const;
+
+ /**
+ * Draws all world-space objects for the given eye.
+ */
+ void DrawWorld();
+
+ /**
+ * Draws the background.
+ */
+ void DrawBackground();
+
+ /**
+ * Draws once demo.
+ */
+ void DrawDemos();
+
+ /**
+ * Checks if user is pointing or looking at the demo mesh by calculating
+ * whether the angle between the user's gaze and the vector pointing towards
+ * the mesh is lower than some threshold.
+ *
+ * @return true if the user is pointing at the demo mesh.
+ */
+ bool IsPointingAtDemo(const cardboard_demo::Matrix4x4& model_demo);
+
+ /**
+ * Checks if user is pointing or looking at the next button by calculating
+ * whether the angle between the user's gaze and the vector pointing towards
+ * the button is lower than some threshold.
+ *
+ * @return true if the user is pointing at the next button.
+ */
+ bool IsPointingAtNextButton();
+
+ /**
+ * Loads the demo meshes.
+ *
+ * @param env The JNI environment.
+ */
+ void LoadDemos(JNIEnv* env);
+
+ /**
+ * Loads the tutorial animations.
+ *
+ * @param env The JNI environment.
+ */
+ void LoadTutorial(JNIEnv* env);
+
+ /**
+ * Shows the button tutorial.
+ */
+ void ShowButtonTutorial();
+
+ /**
+ * Updates the state of the launcher state machine.
+ */
+ void UpdateState();
+
+ /**
+ * Loads the black background and its parameters.
+ */
+ void LoadBlackBackground();
+
+ /**
+ * Draws the black background with the given scale.
+ *
+ * @param scale The scale of the black background.
+ */
+ void DrawBlackBackgroundGL(const std::array& scale);
+
+ jobject java_asset_mgr_;
+ AAssetManager* asset_mgr_;
+
+ CardboardHeadTracker* head_tracker_;
+ CardboardLensDistortion* lens_distortion_;
+ CardboardDistortionRenderer* distortion_renderer_;
+
+ CardboardEyeTextureDescription left_eye_texture_description_;
+ CardboardEyeTextureDescription right_eye_texture_description_;
+
+ bool screen_params_changed_;
+ bool device_params_changed_;
+ int screen_width_;
+ int screen_height_;
+
+ float projection_matrices_[2][16];
+ float eye_matrices_[2][16];
+
+ GLuint depthRenderBuffer_; // depth buffer
+ GLuint framebuffer_; // framebuffer object
+ GLuint texture_; // distortion texture
+
+ GLuint obj_program_;
+ GLuint obj_position_param_;
+ GLuint obj_uv_param_;
+ GLuint obj_model_view_projection_param_;
+
+ cardboard_demo::Matrix4x4 head_view_;
+
+ std::vector demos_;
+
+ cardboard_demo::Matrix4x4 model_view_projection_background_;
+
+ cardboard_demo::TexturedMesh background_;
+ cardboard_demo::Texture background_tex_;
+
+ // Indicates whether to launch the demo.
+ bool should_launch_demo_;
+
+ // Indicates the demo to launch.
+ std::string demo_to_launch_;
+
+ // Indicates the current state of the launcher.
+ State state_;
+ // Indicates the last state event of the launcher.
+ StateEvent last_state_event_;
+
+ // Tutorial animations.
+ TutorialAnimator tutorial_animator_;
+
+ GLuint black_background_vbo_;
+ GLuint black_background_ebo_;
+ GLuint black_background_program_id_;
+ GLuint black_background_mvp_param_;
+ GLuint black_background_scale_param_;
+
+ cardboard_demo::Matrix4x4 model_view_projection_black_square_;
+ cardboard_demo::TextRenderer text_renderer_;
+};
+
+} // namespace cardboard_launcher
+
+#endif // PAPERSCOPE_LAUNCHER_SRC_CARDBOARD_LAUNCHER_H_
diff --git a/paperscope/paperscope_launcher/src/demo_icon.cc b/paperscope/paperscope_launcher/src/demo_icon.cc
new file mode 100644
index 00000000..1a8a0d74
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/demo_icon.cc
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "demo_icon.h"
+
+#include
+
+#include "util.h"
+
+namespace cardboard_launcher {
+
+namespace {
+constexpr float kTextScale{0.002f};
+constexpr std::array kTextTutorialScale{-kTextScale,
+ kTextScale,
+ 1.0f};
+const cardboard_demo::Matrix4x4 kTextScaleMatrix{
+ cardboard_demo::GetScaleMatrix(kTextTutorialScale)};
+constexpr float kTextYOffset{-0.3f};
+} // namespace
+
+void DemoIcon::LoadDemo(GLuint obj_position_param, GLuint obj_uv_param,
+ AAssetManager* asset_mgr, JNIEnv* env,
+ jobject java_asset_mgr) {
+ CARDBOARDDEMO_CHECK(demo_mesh_.Initialize(obj_position_param, obj_uv_param,
+ "IconMesh.obj", asset_mgr));
+ CARDBOARDDEMO_CHECK(demo_not_selected_texture_.Initialize(
+ env, java_asset_mgr, demo_icon_data_.name + "-bw.png"));
+ CARDBOARDDEMO_CHECK(demo_selected_texture_.Initialize(
+ env, java_asset_mgr, demo_icon_data_.name + "-color.png"));
+}
+
+void DemoIcon::Draw() const {
+ if (is_pointing_at_demo_) {
+ demo_selected_texture_.Bind();
+ } else {
+ demo_not_selected_texture_.Bind();
+ }
+ demo_mesh_.Draw();
+
+ if (is_pointing_at_demo_) {
+ DrawDemoName();
+ }
+
+ cardboard_demo::CHECKGLERROR("DrawDemoExhibit");
+}
+
+void DemoIcon::DrawDemoName() const {
+ const float x_offset{
+ text_renderer_->GetTextWidth(demo_icon_data_.display_name) * 0.5f};
+
+ cardboard_demo::Matrix4x4 translation_matrix{
+ cardboard_demo::GetTranslationMatrix({x_offset * kTextScale,
+ kTextYOffset,
+ 0.0f})};
+
+ const cardboard_demo::Matrix4x4 model_view_projection =
+ GetModelViewProjection() * translation_matrix * kTextScaleMatrix;
+ text_renderer_->RenderText(demo_icon_data_.display_name,
+ model_view_projection);
+}
+} // namespace cardboard_launcher
diff --git a/paperscope/paperscope_launcher/src/demo_icon.h b/paperscope/paperscope_launcher/src/demo_icon.h
new file mode 100644
index 00000000..a67a6c8b
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/demo_icon.h
@@ -0,0 +1,125 @@
+
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.
+ */
+#ifndef PAPERSCOPE_LAUNCHER_SRC_DEMO_ICON_H_
+#define PAPERSCOPE_LAUNCHER_SRC_DEMO_ICON_H_
+
+#include
+
+#include
+#include
+
+#include "text_renderer.h"
+#include "util.h"
+
+namespace cardboard_launcher {
+
+struct DemoIconData {
+ std::string name;
+ std::string display_name;
+};
+
+class DemoIcon {
+ public:
+ /**
+ * Constructor for a demo icon.
+ *
+ * @param demo_name The name of the demo.
+ * @param pos The position of the demo.
+ */
+ DemoIcon(const cardboard_launcher::DemoIconData& demo_icon_data,
+ const float pos, cardboard_demo::TextRenderer* const text_renderer)
+ : demo_icon_data_{demo_icon_data}, text_renderer_{text_renderer} {
+ // Rotate the demo icon and place it in front of the user.
+ const float yaw{180.0f * M_PI / 180.0f};
+ model_demo_ = cardboard_demo::GetTranslationMatrix({-pos, 2.2f, -3.0f})
+ * cardboard_demo::GetYawRotationMatrix(yaw);
+ }
+
+ /**
+ * Returns the name of the demo.
+ */
+ std::string GetDemoName() const { return demo_icon_data_.name; }
+
+ /**
+ * Returns the model of the demo.
+ */
+ cardboard_demo::Matrix4x4 GetModel() const { return model_demo_; }
+
+ /**
+ * Returns the model view projection of the demo.
+ */
+ cardboard_demo::Matrix4x4 GetModelViewProjection() const {
+ return modelview_projection_demo_;
+ }
+
+ /**
+ * Sets the model of the demo.
+ *
+ * @param model The model of the demo.
+ */
+ void SetModel(const cardboard_demo::Matrix4x4& model) { model_demo_ = model; }
+
+ /**
+ * Sets the model view projection of the demo.
+ *
+ * @param modelview_projection The model view projection of the demo.
+ */
+ void SetModelViewProjection(
+ const cardboard_demo::Matrix4x4& modelview_projection) {
+ modelview_projection_demo_ = modelview_projection;
+ }
+
+ /**
+ * Sets whether the user is pointing at the demo.
+ *
+ * @param is_pointing_at_demo Whether the user is pointing at the demo.
+ */
+ void SetPointingAtDemo(bool is_pointing_at_demo) {
+ is_pointing_at_demo_ = is_pointing_at_demo;
+ }
+
+ /**
+ * Loads the demo mesh.
+ */
+ void LoadDemo(GLuint obj_position_param_, GLuint obj_uv_param_,
+ AAssetManager* asset_mgr_, JNIEnv* env,
+ jobject java_asset_mgr_);
+
+ /**
+ * Draws the demo mesh.
+ */
+ void Draw() const;
+
+ void DrawDemoName() const;
+
+ private:
+ cardboard_launcher::DemoIconData demo_icon_data_;
+ cardboard_demo::Matrix4x4 model_demo_;
+ cardboard_demo::Matrix4x4 modelview_projection_demo_;
+
+ cardboard_demo::TexturedMesh demo_mesh_;
+ cardboard_demo::Texture demo_not_selected_texture_;
+ cardboard_demo::Texture demo_selected_texture_;
+
+ cardboard_demo::TextRenderer* const text_renderer_;
+
+ bool is_pointing_at_demo_;
+};
+
+} // namespace cardboard_launcher
+
+#endif // PAPERSCOPE_LAUNCHER_SRC_DEMO_ICON_H_
diff --git a/paperscope/paperscope_launcher/src/magic_window.cc b/paperscope/paperscope_launcher/src/magic_window.cc
new file mode 100644
index 00000000..dfa3610d
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/magic_window.cc
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "magic_window.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "cardboard.h"
+#include "util/matrix_4x4.h"
+#include "util.h"
+
+namespace magic_window {
+
+namespace {
+
+constexpr float kDefaultFloorHeight = -1.7f;
+
+// 6 Hz cutoff frequency for the velocity filter of the head tracker.
+constexpr int kVelocityFilterCutoffFrequency = 6;
+
+constexpr uint64_t kPredictionTimeWithoutVsyncNanos = 50000000;
+
+// Simple shaders to render .obj files without any lighting.
+constexpr const char* kObjVertexShader =
+ R"glsl(
+ uniform mat4 u_MVP;
+ attribute vec4 a_Position;
+ attribute vec2 a_UV;
+ varying vec2 v_UV;
+
+ void main() {
+ v_UV = a_UV;
+ gl_Position = u_MVP * a_Position;
+ })glsl";
+
+constexpr const char* kObjFragmentShader =
+ R"glsl(
+ precision mediump float;
+
+ uniform sampler2D u_Texture;
+ varying vec2 v_UV;
+
+ void main() {
+ // The y coordinate of this sample's textures is reversed compared to
+ // what OpenGL expects, so we invert the y coordinate.
+ gl_FragColor = texture2D(u_Texture, vec2(v_UV.x, 1.0 - v_UV.y));
+ })glsl";
+} // anonymous namespace
+
+MagicWindow::MagicWindow(JavaVM* vm, jobject obj, jobject asset_mgr_obj)
+ : head_tracker_(nullptr),
+ screen_width_(0),
+ screen_height_(0),
+ obj_program_(0),
+ obj_model_view_projection_param_(0) {
+ JNIEnv* env;
+ vm->GetEnv((void**)&env, JNI_VERSION_1_6);
+ java_asset_mgr_ = env->NewGlobalRef(asset_mgr_obj);
+ asset_mgr_ = AAssetManager_fromJava(env, asset_mgr_obj);
+
+ Cardboard_initializeAndroid(vm, obj);
+ head_tracker_ = CardboardHeadTracker_create();
+ CardboardHeadTracker_setLowPassFilter(head_tracker_,
+ kVelocityFilterCutoffFrequency);
+}
+
+MagicWindow::~MagicWindow() { CardboardHeadTracker_destroy(head_tracker_); }
+
+void MagicWindow::OnSurfaceCreated(JNIEnv* env) {
+ const int obj_vertex_shader =
+ cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kObjVertexShader);
+ const int obj_fragment_shader =
+ cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kObjFragmentShader);
+
+ obj_program_ = glCreateProgram();
+ glAttachShader(obj_program_, obj_vertex_shader);
+ glAttachShader(obj_program_, obj_fragment_shader);
+ glLinkProgram(obj_program_);
+ glUseProgram(obj_program_);
+
+ GLuint obj_position_param = glGetAttribLocation(obj_program_, "a_Position");
+ GLuint obj_uv_param = glGetAttribLocation(obj_program_, "a_UV");
+ obj_model_view_projection_param_ =
+ glGetUniformLocation(obj_program_, "u_MVP");
+
+ CARDBOARDDEMO_CHECK(background_.Initialize(obj_position_param, obj_uv_param,
+ "World.obj", asset_mgr_));
+ CARDBOARDDEMO_CHECK(
+ background_tex_.Initialize(env, java_asset_mgr_, "home_mono.jpg"));
+}
+
+void MagicWindow::SetScreenParams(int width, int height) {
+ screen_width_ = width;
+ screen_height_ = height;
+ SetFov();
+ glViewport(0, 0, screen_width_, screen_height_);
+ glEnable(GL_CULL_FACE);
+ glCullFace(GL_BACK);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+}
+
+void MagicWindow::OnDrawFrame() {
+ // Update Head Pose.
+ cardboard_demo::Matrix4x4 head_view = GetPose();
+
+ // Incorporate the height from the floor into the head_view.
+ head_view = head_view * cardboard_demo::GetTranslationMatrix(
+ {0.0f, kDefaultFloorHeight, 0.0f});
+
+ const cardboard_demo::Matrix4x4 projection_matrix =
+ cardboard_demo::GetMatrixFromGlArray(magic_window_projection_matrix_);
+
+ model_view_projection_background_ = projection_matrix * head_view;
+
+ // Draw background.
+ DrawBackground();
+}
+
+void MagicWindow::OnPause() { CardboardHeadTracker_pause(head_tracker_); }
+
+void MagicWindow::ScanViewer() {
+ CardboardQrCode_scanQrCodeAndSaveDeviceParams();
+}
+
+bool MagicWindow::HasSavedDeviceParams() {
+ // Get saved device parameters.
+ uint8_t* buffer;
+ int size;
+ CardboardQrCode_getSavedDeviceParams(&buffer, &size);
+
+ return size > 0;
+}
+
+void MagicWindow::OnResume() { CardboardHeadTracker_resume(head_tracker_); }
+
+void MagicWindow::SetFov() {
+ float ar = (float)screen_width_ / screen_height_;
+ float fov_h = 2 * atan(tan(kFov_v / 2) * ar);
+ std::array magic_window_fov = {fov_h, fov_h, kFov_v, kFov_v};
+ cardboard::Matrix4x4::Perspective(magic_window_fov, kZNear, kZFar)
+ .ToArray(magic_window_projection_matrix_);
+}
+
+cardboard_demo::Matrix4x4 MagicWindow::GetPose() {
+ std::array out_orientation;
+ std::array out_position;
+ CardboardHeadTracker_getPose(
+ head_tracker_,
+ cardboard_demo::GetBootTimeNano() + kPredictionTimeWithoutVsyncNanos,
+ kPortrait, &out_position[0], &out_orientation[0]);
+ return cardboard_demo::GetTranslationMatrix(out_position) *
+ cardboard_demo::Quatf::FromXYZW(&out_orientation[0]).ToMatrix();
+}
+
+void MagicWindow::DrawBackground() {
+ glClearColor(.0f, .0f, .0f, 1.0f);
+ glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
+
+ std::array background_array =
+ model_view_projection_background_.ToGlArray();
+ glUniformMatrix4fv(obj_model_view_projection_param_, 1, GL_FALSE,
+ background_array.data());
+
+ background_tex_.Bind();
+ background_.Draw();
+}
+
+} // namespace magic_window
diff --git a/paperscope/paperscope_launcher/src/magic_window.h b/paperscope/paperscope_launcher/src/magic_window.h
new file mode 100644
index 00000000..1cc86e7e
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/magic_window.h
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.
+ */
+#ifndef PAPERSCOPE_LAUNCHER_SRC_MAGIC_WINDOW_H_
+#define PAPERSCOPE_LAUNCHER_SRC_MAGIC_WINDOW_H_
+
+#include
+#include
+
+#include
+#include "cardboard.h"
+#include "util.h"
+
+namespace magic_window {
+
+/**
+ * This is a magic window of the sample app for the Cardboard SDK. It shows a
+ * full-screen monoscopic view of a sample VR world.
+ */
+class MagicWindow {
+ public:
+ /**
+ * Creates the Magic Window of the sample app.
+ *
+ * @param vm JavaVM pointer.
+ * @param obj Android activity object.
+ * @param asset_mgr_obj The asset manager object.
+ */
+ MagicWindow(JavaVM* vm, jobject obj, jobject asset_mgr_obj);
+
+ ~MagicWindow();
+
+ /**
+ * Initializes any GL-related objects. This should be called on the rendering
+ * thread with a valid GL context.
+ *
+ * @param env The JNI environment.
+ */
+ void OnSurfaceCreated(JNIEnv* env);
+
+ /**
+ * Sets screen parameters.
+ *
+ * @param width Screen width
+ * @param height Screen height
+ */
+ void SetScreenParams(int width, int height);
+
+ /**
+ * Draws the scene. This should be called on the rendering thread.
+ */
+ void OnDrawFrame();
+
+ /**
+ * Resumes head tracking.
+ */
+ void OnResume();
+
+ /**
+ * Pauses head tracking.
+ */
+ void OnPause();
+
+ /**
+ * Allows user to scan a Cardboard viewer.
+ */
+ void ScanViewer();
+
+ /**
+ * Checks if a Cardboard viewer has previously been scanned and its parameters
+ * saved.
+ */
+ bool HasSavedDeviceParams();
+
+ private:
+ /**
+ * Default near clip plane z-axis coordinate.
+ */
+ static constexpr float kZNear = 0.1f;
+
+ /**
+ * Default far clip plane z-axis coordinate.
+ */
+ static constexpr float kZFar = 100.f;
+
+ /**
+ * Default vertical FOV set to 45 degrees.
+ */
+ static constexpr float kFov_v = 0.785398f;
+
+ /**
+ * Gets head's pose as a 4x4 matrix.
+ *
+ * @return matrix containing head's pose.
+ */
+ cardboard_demo::Matrix4x4 GetPose();
+
+ /**
+ * Initializes the FOV settings for the Magic Window.
+ */
+ void SetFov();
+
+ /**
+ * Draws the background.
+ */
+ void DrawBackground();
+
+ jobject java_asset_mgr_;
+ AAssetManager* asset_mgr_;
+
+ CardboardHeadTracker* head_tracker_;
+
+ int screen_width_;
+ int screen_height_;
+
+ float magic_window_projection_matrix_[16];
+
+ GLuint obj_program_;
+ GLuint obj_model_view_projection_param_;
+
+ cardboard_demo::Matrix4x4 model_view_projection_background_;
+ cardboard_demo::TexturedMesh background_;
+ cardboard_demo::Texture background_tex_;
+};
+
+} // namespace magic_window
+
+#endif // PAPERSCOPE_LAUNCHER_SRC_MAGIC_WINDOW_H_
diff --git a/paperscope/paperscope_launcher/src/main.cc b/paperscope/paperscope_launcher/src/main.cc
new file mode 100644
index 00000000..0935a70f
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/main.cc
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "main.h"
+
+#include
+#include
+#include
+#include
+
+
+#include "cardboard.h"
+
+namespace main {
+
+Main::Main(JavaVM* vm, jobject obj, jobject asset_mgr_obj) {
+ JNIEnv* env;
+ vm->GetEnv((void**)&env, JNI_VERSION_1_6);
+ java_asset_mgr_ = env->NewGlobalRef(asset_mgr_obj);
+ asset_mgr_ = AAssetManager_fromJava(env, asset_mgr_obj);
+
+ Cardboard_initializeAndroid(vm, obj);
+}
+
+Main::~Main() {}
+
+void Main::SwitchViewer() {
+ CardboardQrCode_scanQrCodeAndSaveDeviceParams();
+}
+
+} // namespace main
diff --git a/paperscope/paperscope_launcher/src/main.h b/paperscope/paperscope_launcher/src/main.h
new file mode 100644
index 00000000..c1ca1cc6
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/main.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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.
+ */
+#ifndef PAPERSCOPE_LAUNCHER_SRC_MAIN_H_
+#define PAPERSCOPE_LAUNCHER_SRC_MAIN_H_
+
+#include
+#include
+
+#include "cardboard.h"
+#include "util.h"
+
+
+namespace main {
+
+/**
+ * Handles the native interface calls for the Main Activity of the Cardboard
+ * sample app.
+ */
+class Main {
+ public:
+ /**
+ * Creates the Native interface for the Main Activity of the sample app.
+ *
+ * @param vm JavaVM pointer.
+ * @param obj Android activity object.
+ * @param asset_mgr_obj The asset manager object.
+ */
+ Main(JavaVM* vm, jobject obj, jobject asset_mgr_obj);
+
+ ~Main();
+
+ /**
+ * Allows user to scan a Cardboard viewer.
+ */
+ void SwitchViewer();
+
+ private:
+ jobject java_asset_mgr_;
+ AAssetManager* asset_mgr_;
+};
+
+} // namespace main
+
+#endif // PAPERSCOPE_LAUNCHER_SRC_MAIN_H_
diff --git a/paperscope/paperscope_launcher/src/settings.cc b/paperscope/paperscope_launcher/src/settings.cc
new file mode 100644
index 00000000..d9f8fc8e
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/settings.cc
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "settings.h"
+
+#include
+#include
+#include
+#include
+
+
+#include "cardboard.h"
+
+namespace settings {
+
+Settings::Settings(JavaVM* vm, jobject obj, jobject asset_mgr_obj) {
+ JNIEnv* env;
+ vm->GetEnv((void**)&env, JNI_VERSION_1_6);
+ java_asset_mgr_ = env->NewGlobalRef(asset_mgr_obj);
+ asset_mgr_ = AAssetManager_fromJava(env, asset_mgr_obj);
+
+ Cardboard_initializeAndroid(vm, obj);
+}
+
+Settings::~Settings() {}
+
+void Settings::SwitchViewer() {
+ CardboardQrCode_scanQrCodeAndSaveDeviceParams();
+}
+
+} // namespace settings
diff --git a/paperscope/paperscope_launcher/src/settings.h b/paperscope/paperscope_launcher/src/settings.h
new file mode 100644
index 00000000..55e8d03d
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/settings.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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.
+ */
+#ifndef PAPERSCOPE_LAUNCHER_SRC_SETTINGS_H_
+#define PAPERSCOPE_LAUNCHER_SRC_SETTINGS_H_
+
+#include
+#include
+
+#include "cardboard.h"
+#include "util.h"
+
+
+namespace settings {
+
+/**
+ * Handles the native interface calls for the Settings Activity of the Cardboard
+ * sample app.
+ */
+class Settings {
+ public:
+ /**
+ * Creates the Native interface for the Settings Activity of the sample app.
+ *
+ * @param vm JavaVM pointer.
+ * @param obj Android activity object.
+ * @param asset_mgr_obj The asset manager object.
+ */
+ Settings(JavaVM* vm, jobject obj, jobject asset_mgr_obj);
+
+ ~Settings();
+
+ /**
+ * Allows user to scan a Cardboard viewer.
+ */
+ void SwitchViewer();
+
+ private:
+ jobject java_asset_mgr_;
+ AAssetManager* asset_mgr_;
+};
+
+} // namespace settings
+
+#endif // PAPERSCOPE_LAUNCHER_SRC_SETTINGS_H_
diff --git a/paperscope/paperscope_launcher/src/text_renderer.cc b/paperscope/paperscope_launcher/src/text_renderer.cc
new file mode 100644
index 00000000..179fbd60
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/text_renderer.cc
@@ -0,0 +1,372 @@
+#include "text_renderer.h"
+
+#include
+#include
+
+#include "util.h"
+
+namespace cardboard_demo {
+namespace {
+
+constexpr const char* kTextVertexShader = R"glsl(
+ uniform mat4 u_MVP;
+ attribute vec4 a_Vertex; // vec4(pos.x, pos.y, tex.x, tex.y)
+ varying vec2 v_TexCoords;
+
+ void main() {
+ gl_Position = u_MVP * vec4(a_Vertex.xy, 0.0, 1.0);
+ v_TexCoords = a_Vertex.zw;
+ })glsl";
+
+constexpr const char* kTextFragmentShader = R"glsl(
+ precision mediump float;
+ varying vec2 v_TexCoords;
+ uniform sampler2D u_Texture;
+ uniform vec3 u_TextColor;
+
+ void main() {
+ // Use the red channel of the texture (luminance) as alpha.
+ float alpha = texture2D(u_Texture, v_TexCoords).r;
+ gl_FragColor = vec4(u_TextColor, alpha);
+ })glsl";
+
+// Converts a UTF-8 encoded string to a UTF-32 encoded string.
+std::u32string Utf8ToUtf32(const std::string& s) {
+ std::u32string result;
+ if (s.empty()) {
+ return result;
+ }
+ result.reserve(s.length());
+
+ for (size_t i = 0; i < s.length();) {
+ char32_t code_point;
+ const unsigned char c = s[i];
+
+ if (c < 0x80) { // 1-byte sequence
+ code_point = c;
+ i += 1;
+ } else if ((c & 0xE0) == 0xC0) { // 2-byte sequence
+ if (i + 1 >= s.length()) break;
+ code_point = ((c & 0x1F) << 6) | (s[i + 1] & 0x3F);
+ i += 2;
+ } else if ((c & 0xF0) == 0xE0) { // 3-byte sequence
+ if (i + 2 >= s.length()) break;
+ code_point =
+ ((c & 0x0F) << 12) | ((s[i + 1] & 0x3F) << 6) | (s[i + 2] & 0x3F);
+ i += 3;
+ } else if ((c & 0xF8) == 0xF0) { // 4-byte sequence
+ if (i + 3 >= s.length()) break;
+ code_point = ((c & 0x07) << 18) | ((s[i + 1] & 0x3F) << 12) |
+ ((s[i + 2] & 0x3F) << 6) | (s[i + 3] & 0x3F);
+ i += 4;
+ } else {
+ // Invalid UTF-8 sequence, skip this byte.
+ i += 1;
+ continue;
+ }
+ result.push_back(code_point);
+ }
+ return result;
+}
+} // namespace
+
+TextRenderer::TextRenderer()
+ : ft_library_{nullptr},
+ ft_face_{nullptr},
+ vbo_{0},
+ shader_program_{0},
+ mvp_param_{-1},
+ text_color_param_{-1},
+ vertex_param_{-1} {
+ if (FT_Init_FreeType(&ft_library_)) {
+ __android_log_print(ANDROID_LOG_ERROR, "TextRenderer",
+ "Could not initialize FreeType library");
+ }
+}
+
+TextRenderer::~TextRenderer() {
+ if (ft_face_) {
+ FT_Done_Face(ft_face_);
+ }
+ if (ft_library_) {
+ FT_Done_FreeType(ft_library_);
+ }
+
+ for (auto const& [codepoint, character] : characters_) {
+ glDeleteTextures(1, &character.texture_id);
+ }
+}
+
+void TextRenderer::Init() {
+ const GLuint vertex_shader{
+ cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kTextVertexShader)};
+ const GLuint fragment_shader{
+ cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kTextFragmentShader)};
+
+ shader_program_ = glCreateProgram();
+ glAttachShader(shader_program_, vertex_shader);
+ glAttachShader(shader_program_, fragment_shader);
+ glLinkProgram(shader_program_);
+
+ mvp_param_ = glGetUniformLocation(shader_program_, "u_MVP");
+ text_color_param_ = glGetUniformLocation(shader_program_, "u_TextColor");
+ vertex_param_ = glGetAttribLocation(shader_program_, "a_Vertex");
+
+ glGenBuffers(1, &vbo_);
+ glBindBuffer(GL_ARRAY_BUFFER, vbo_);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr,
+ GL_DYNAMIC_DRAW);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+
+ // Invalidate all textures in case of GL context loss.
+ for (auto& character : characters_) {
+ character.second.texture_id = 0;
+ }
+ cardboard_demo::CHECKGLERROR("TextRenderer::Init");
+}
+
+bool TextRenderer::LoadFont(AAssetManager* asset_mgr,
+ const std::string& font_path) {
+ if (asset_mgr == nullptr) {
+ __android_log_print(ANDROID_LOG_ERROR, "TextRenderer",
+ "Asset manager is null");
+ return false;
+ }
+ if (!ft_library_) {
+ __android_log_print(ANDROID_LOG_ERROR, "TextRenderer",
+ "FreeType library is not initialized");
+ return false;
+ }
+
+ AAsset* asset{
+ AAssetManager_open(asset_mgr, font_path.c_str(), AASSET_MODE_BUFFER)};
+ if (!asset) {
+ __android_log_print(ANDROID_LOG_ERROR, "TextRenderer",
+ "Could not open font asset");
+ return false;
+ }
+
+ const void* font_buffer{AAsset_getBuffer(asset)};
+ const auto font_buffer_size{static_cast(AAsset_getLength(asset))};
+
+ font_data_.assign(
+ static_cast(font_buffer),
+ static_cast(font_buffer) + font_buffer_size);
+
+ AAsset_close(asset);
+
+ if (FT_New_Memory_Face(ft_library_, font_data_.data(), font_data_.size(), 0,
+ &ft_face_)) {
+ __android_log_print(ANDROID_LOG_ERROR, "TextRenderer",
+ "Could not create FreeType face");
+ return false;
+ }
+
+ FT_Set_Pixel_Sizes(ft_face_, 0, 48);
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+
+ if (!characters_.empty()) {
+ ReloadCharacters();
+ }
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+ cardboard_demo::CHECKGLERROR("TextRenderer::LoadFont");
+
+ return true;
+}
+
+void TextRenderer::RenderText(const std::string& text,
+ cardboard_demo::Matrix4x4 mvp, float spacing,
+ float x_offset, float y_offset) {
+ const std::u32string utf32_text {Utf8ToUtf32(text)};
+ LoadGlypsForString(utf32_text);
+
+ // Save the current GL state that will be changed.
+ SaveGLState();
+
+ glUseProgram(shader_program_);
+ glUniformMatrix4fv(mvp_param_, 1, GL_FALSE, mvp.ToGlArray().data());
+ glUniform3f(text_color_param_,
+ text_color_[0],
+ text_color_[1],
+ text_color_[2]);
+
+ glActiveTexture(GL_TEXTURE0);
+
+ // Set GL state for text rendering.
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_CULL_FACE);
+
+ glBindBuffer(GL_ARRAY_BUFFER, vbo_);
+ glEnableVertexAttribArray(vertex_param_);
+ glVertexAttribPointer(vertex_param_, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
+ nullptr);
+
+ float x{x_offset};
+ float y{y_offset * (ft_face_->size->metrics.height >> 6)};
+ for (uint32_t c : utf32_text) {
+ if (c == '\n') {
+ x = x_offset;
+ y -= (ft_face_->size->metrics.height >> 6);
+ continue;
+ }
+
+ const auto it = characters_.find(c);
+ if (it == characters_.end()) {
+ __android_log_print(ANDROID_LOG_ERROR,
+ "TextRenderer",
+ "Glyph %d not found", c);
+ continue;
+ }
+ const Character& ch = it->second;
+
+ const float xpos{x + ch.bearing_x};
+ const float ypos{y - (ch.height - ch.bearing_y)};
+ const auto w{static_cast(ch.width)};
+ const auto h{static_cast(ch.height)};
+
+ const float vertices[6][4] = { // First triangle
+ {xpos, ypos + h, 0.0, 0.0}, // top left
+ {xpos, ypos, 0.0, 1.0}, // bottom left
+ {xpos + w, ypos, 1.0, 1.0}, // bottom right
+
+ // Second triangle
+ {xpos, ypos + h, 0.0, 0.0}, // top right
+ {xpos + w, ypos, 1.0, 1.0}, // bottom right
+ {xpos + w, ypos + h, 1.0, 0.0}}; // top right
+
+ glBindTexture(GL_TEXTURE_2D, ch.texture_id);
+ glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
+ glDrawArrays(GL_TRIANGLES, 0, 6);
+
+ x += (ch.advance >> 6) * spacing;
+ }
+
+ glDisableVertexAttribArray(vertex_param_);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ RestoreGLState();
+}
+
+void TextRenderer::SaveGLState() {
+ glGetIntegerv(GL_CURRENT_PROGRAM, &last_program_);
+ glGetBooleanv(GL_BLEND, &last_blend_enabled_);
+ glGetIntegerv(GL_BLEND_SRC_ALPHA, &last_blend_src_);
+ glGetIntegerv(GL_BLEND_DST_ALPHA, &last_blend_dst_);
+ glGetBooleanv(GL_CULL_FACE, &last_cull_face_enabled_);
+ glGetBooleanv(GL_DEPTH_TEST, &last_depth_test_enabled_);
+}
+
+void TextRenderer::RestoreGLState() const {
+ glUseProgram(last_program_);
+ if (last_blend_enabled_) {
+ glEnable(GL_BLEND);
+ glBlendFunc(last_blend_src_, last_blend_dst_);
+ } else {
+ glDisable(GL_BLEND);
+ }
+ if (last_cull_face_enabled_) {
+ glEnable(GL_CULL_FACE);
+ } else {
+ glDisable(GL_CULL_FACE);
+ }
+ if (last_depth_test_enabled_) {
+ glEnable(GL_DEPTH_TEST);
+ } else {
+ glDisable(GL_DEPTH_TEST);
+ }
+}
+
+float TextRenderer::GetTextWidth(const std::string& text, float spacing) {
+ float width {0.0f};
+
+ const std::u32string utf32_text = Utf8ToUtf32(text);
+ LoadGlypsForString(utf32_text);
+ for (uint32_t codepoint : utf32_text) {
+ const auto it = characters_.find(codepoint);
+ if (it == characters_.end()) {
+ __android_log_print(ANDROID_LOG_ERROR,
+ "TextRenderer",
+ "Glyph %d not found", codepoint);
+ continue;
+ }
+
+ const Character& ch {it->second};
+ width += (ch.advance >> 6) * spacing;
+ }
+ return width;
+}
+
+void TextRenderer::SetTextColor(float r, float g, float b) {
+ text_color_ = {r, g, b};
+}
+
+void TextRenderer::LoadGlypsForString(const std::u32string& utf32_text) {
+ for (auto codepoint : utf32_text) {
+ LoadGlyph(codepoint);
+ }
+}
+
+void TextRenderer::LoadGlyph(uint32_t codepoint) {
+ const auto it = characters_.find(codepoint);
+ if (it != characters_.end() && it->second.texture_id != 0 &&
+ glIsTexture(it->second.texture_id)) {
+ return;
+ }
+
+ GLuint old_texture_id = 0;
+ if (it != characters_.end()) {
+ old_texture_id = it->second.texture_id;
+ }
+
+ // Otherwise load from FreeType
+ if (FT_Load_Char(ft_face_, codepoint, FT_LOAD_RENDER)) {
+ __android_log_print(ANDROID_LOG_ERROR, "TextRenderer",
+ "Failed to load glyph %d",
+ codepoint);
+ return;
+ }
+
+ const GLuint texture {CreateTextureForGlyph()};
+
+ if (old_texture_id != 0 && glIsTexture(old_texture_id)) {
+ glDeleteTextures(1, &old_texture_id);
+ }
+
+ characters_[codepoint] = {texture,
+ ft_face_->glyph->bitmap.width,
+ ft_face_->glyph->bitmap.rows,
+ ft_face_->glyph->bitmap_left,
+ ft_face_->glyph->bitmap_top,
+ ft_face_->glyph->advance.x};
+}
+
+void TextRenderer::ReloadCharacters() {
+ for (auto &[codepoint, character] : characters_) {
+ if (FT_Load_Char(ft_face_, codepoint, FT_LOAD_RENDER)) {
+ continue;
+ }
+ character.texture_id = CreateTextureForGlyph();
+ }
+}
+
+GLuint TextRenderer::CreateTextureForGlyph() const {
+ GLuint texture;
+ glGenTextures(1, &texture);
+ glBindTexture(GL_TEXTURE_2D, texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, ft_face_->glyph->bitmap.width,
+ ft_face_->glyph->bitmap.rows, 0, GL_LUMINANCE,
+ GL_UNSIGNED_BYTE, ft_face_->glyph->bitmap.buffer);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ return texture;
+}
+
+} // namespace cardboard_demo
diff --git a/paperscope/paperscope_launcher/src/text_renderer.h b/paperscope/paperscope_launcher/src/text_renderer.h
new file mode 100644
index 00000000..70ffeb5d
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/text_renderer.h
@@ -0,0 +1,122 @@
+#ifndef PAPERSCOPE_LAUNCHER_SRC_TEXT_RENDERER_H_
+#define PAPERSCOPE_LAUNCHER_SRC_TEXT_RENDERER_H_
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include "util.h"
+
+namespace cardboard_demo {
+
+// Holds state information for a single character glyph.
+struct Character {
+ GLuint texture_id{}; // ID handle of the glyph texture
+ uint32_t width{};
+ uint32_t height{};
+ int32_t bearing_x{};
+ int32_t bearing_y{};
+ int64_t advance{};
+};
+
+// Renders text using OpenGL and FreeType.
+class TextRenderer {
+ public:
+ TextRenderer();
+ ~TextRenderer();
+
+ // Initializes shaders and OpenGL buffers. Must be called on the GL thread.
+ void Init();
+
+ // Loads a font from the given asset manager.
+ // The font data is copied and managed by this class.
+ bool LoadFont(AAssetManager* asset_mgr, const std::string& font_path);
+
+ // Renders a line of text.
+ void RenderText(const std::string& text, cardboard_demo::Matrix4x4 mvp,
+ float spacing = 1.0f, float x_offset = 0.0f,
+ float y_offset = 0.0f);
+
+ /**
+ * Returns the width of the text in pixels, given the text and spacing.
+ *
+ * @param text The text to measure.
+ * @param spacing The spacing between characters.
+ * @return The width of the text in pixels.
+ */
+ float GetTextWidth(const std::string& text, float spacing = 1.0f);
+
+ /**
+ * Sets the text color.
+ *
+ * @param r The red component of the color from 0.0f to 1.0f.
+ * @param g The green component of the color from 0.0f to 1.0f.
+ * @param b The blue component of the color from 0.0f to 1.0f.
+ */
+ void SetTextColor(float r, float g, float b);
+
+ private:
+ // Saves the current OpenGL state.
+ void SaveGLState();
+ // Restores the OpenGL state to its previous state.
+ void RestoreGLState() const;
+
+ /**
+ * Loads a glyph for the given codepoint.
+ *
+ * @param codepoint The codepoint of the glyph to load.
+ * @return True if the glyph was loaded successfully, false otherwise.
+ */
+ void LoadGlyph(uint32_t codepoint);
+
+ /**
+ * Loads glyphs for the given string.
+ *
+ * @param utf32_text The UTF-32 string to load glyphs for.
+ */
+ void LoadGlypsForString(const std::u32string& utf32_text);
+
+ /**
+ * Reloads characters. Needed after surface is created.
+ * This happens when blocking the phone or when the user sends the app to the background.
+ */
+ void ReloadCharacters();
+
+ /**
+ * Creates a texture for the given glyph.
+ *
+ * @return The texture ID of the glyph.
+ */
+ GLuint CreateTextureForGlyph() const;
+
+ FT_Library ft_library_;
+ FT_Face ft_face_;
+ std::vector font_data_; // To keep font data in memory.
+ std::unordered_map characters_;
+
+ GLuint vbo_{};
+ GLuint shader_program_{};
+ GLint mvp_param_{};
+ GLint text_color_param_{};
+ GLint vertex_param_{};
+
+ GLint last_program_{};
+ GLboolean last_blend_enabled_{};
+ GLint last_blend_src_{};
+ GLint last_blend_dst_{};
+ GLboolean last_cull_face_enabled_{};
+ GLboolean last_depth_test_enabled_{};
+
+ std::array text_color_ {1.0f, 1.0f, 1.0f};
+};
+
+} // namespace cardboard_demo
+
+#endif // PAPERSCOPE_LAUNCHER_SRC_TEXT_RENDERER_H_
diff --git a/paperscope/paperscope_launcher/src/tutorial_animator.cc b/paperscope/paperscope_launcher/src/tutorial_animator.cc
new file mode 100644
index 00000000..398fc0f5
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/tutorial_animator.cc
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "tutorial_animator.h"
+
+#include
+
+#include
+#include // NOLINT(build/c++11)
+
+#include "cardboard_strings.h"
+#include "util.h"
+
+namespace cardboard_launcher {
+
+namespace {
+constexpr const char* kVertexShader = R"glsl(
+ uniform mat4 u_MVP;
+ attribute vec4 a_Position;
+ void main () {
+ gl_Position = u_MVP * a_Position;
+ }
+)glsl";
+
+constexpr const char* kFragmentShader = R"glsl(
+ precision mediump float;
+ void main () {
+ gl_FragColor = vec4(1.0, 0.431, 0.251, 1.0); // Orange button
+ }
+)glsl";
+
+constexpr std::array kTextTutorialScale{-0.0013f, 0.0013f, 1.0f};
+constexpr std::array kTextTutorialModelMatrix{0.0f, -0.25f, 0.0f};
+
+constexpr std::array kTextInstructionsModelMatrix{0.0f, -0.25f,
+ 0.0f};
+
+constexpr std::array kNextButtonModelMatrix{0.0f, -0.5f, 0.0f};
+constexpr float kNextButtonTextXYScale{0.0012f};
+constexpr std::array kNextButtonTextScale{-kNextButtonTextXYScale,
+ kNextButtonTextXYScale,
+ 1.0f};
+} // namespace
+
+TutorialAnimator::TutorialAnimator(
+ const std::vector& button_animation_files,
+ const std::vector& tilt_animation_files,
+ cardboard_demo::TextRenderer* const text_renderer)
+ : button_animation_(button_animation_files),
+ tilt_animation_(tilt_animation_files),
+ text_renderer_(text_renderer) {
+ model_next_button_ =
+ cardboard_demo::GetTranslationMatrix(kNextButtonModelMatrix);
+}
+
+void TutorialAnimator::LoadAnimations(GLuint obj_position_param,
+ GLuint obj_uv_param,
+ AAssetManager* asset_mgr, JNIEnv* env,
+ jobject java_asset_mgr) {
+ button_animation_.LoadAnimations(obj_position_param, obj_uv_param, asset_mgr,
+ env, java_asset_mgr);
+ tilt_animation_.LoadAnimations(obj_position_param, obj_uv_param, asset_mgr,
+ env, java_asset_mgr);
+ LoadNextButton();
+}
+
+void TutorialAnimator::SetScreenPosition(
+ const cardboard_demo::Matrix4x4& eye_view,
+ const cardboard_demo::Matrix4x4& projection_matrix) {
+ tilt_animation_.SetModelViewProjection(eye_view, projection_matrix);
+ button_animation_.SetModelViewProjection(eye_view, projection_matrix);
+
+ // Set the model view projection of the next button.
+ model_view_projection_next_button_ = projection_matrix * eye_view *
+ button_animation_.GetModelAnimation() *
+ model_next_button_ * bg_scale_matrix_;
+
+ // Set the model view projection of the next button text.
+ model_view_projection_next_button_text_ =
+ projection_matrix * eye_view * button_animation_.GetModelAnimation() *
+ button_text_model_matrix_ * button_text_scale_matrix_;
+
+ // Set the model view projection of the text
+ const cardboard_demo::Matrix4x4 text_scale_matrix =
+ cardboard_demo::GetScaleMatrix(kTextTutorialScale);
+ const cardboard_demo::Matrix4x4 text_model_matrix =
+ cardboard_demo::GetTranslationMatrix(kTextTutorialModelMatrix);
+ model_view_projection_text_model_matrix_ =
+ projection_matrix * eye_view * button_animation_.GetModelAnimation() *
+ text_model_matrix * text_scale_matrix;
+
+ // Set the model view projection of the tilt instructions text.
+ const cardboard_demo::Matrix4x4 instructions_text_model_matrix =
+ cardboard_demo::GetTranslationMatrix(kTextInstructionsModelMatrix);
+ model_view_projection_instructions_text_matrix_ =
+ projection_matrix * eye_view * tilt_animation_.GetModelAnimation() *
+ instructions_text_model_matrix * text_scale_matrix;
+}
+
+void TutorialAnimator::DisplayTutorialText() const {
+ std::string press_button_string =
+ cardboard::GetLocalizedString(cardboard::kStrPressTheButtonOnYourViewer,
+ env_);
+ // Break the text into 2 lines.
+ size_t line_index {0};
+ for (size_t i = press_button_string.length() / 2;
+ i < press_button_string.length();
+ i++) {
+ if (press_button_string[i] == ' ') {
+ line_index = i;
+ press_button_string[i] = '\n';
+ break;
+ }
+ }
+
+ const float x_offset {
+ text_renderer_->GetTextWidth(press_button_string.substr(0, line_index)) *
+ 0.5f};
+
+ text_renderer_->RenderText(press_button_string,
+ model_view_projection_text_model_matrix_,
+ 1.0f, // spacing
+ -x_offset);
+}
+
+void TutorialAnimator::DisplayTiltInstructionsText() const {
+ std::string rotate_viewer_string =
+ cardboard::GetLocalizedString(cardboard::kStrRotateYourViewer,
+ env_);
+ // Break the text into 2 lines.
+ size_t line_index {0};
+ for (size_t i = rotate_viewer_string.length() / 2;
+ i < rotate_viewer_string.length();
+ i++) {
+ if (rotate_viewer_string[i] == ' ') {
+ line_index = i;
+ rotate_viewer_string[i] = '\n';
+ break;
+ }
+ }
+
+ float x_offset {
+ text_renderer_->GetTextWidth(rotate_viewer_string.substr(0, line_index)) *
+ 0.5f};
+
+ text_renderer_->RenderText(
+ rotate_viewer_string,
+ model_view_projection_instructions_text_matrix_, 1.0f, -x_offset);
+
+ const std::string try_it_out_now_string {
+ cardboard::GetLocalizedString(cardboard::kStrTryItOutNow, env_)};
+ x_offset = text_renderer_->GetTextWidth(
+ try_it_out_now_string) * 0.5f;
+
+ text_renderer_->RenderText(
+ try_it_out_now_string,
+ model_view_projection_instructions_text_matrix_,
+ 1.0f, // spacing
+ -x_offset, // x_offset
+ -3.0f); // y_offset
+}
+
+void TutorialAnimator::DisplayButtonAnimation(GLuint program,
+ GLuint obj_mvp_param) {
+ glUseProgram(program);
+ const std::array target_array =
+ button_animation_.GetModelViewProjectionAnimation().ToGlArray();
+ glUniformMatrix4fv(obj_mvp_param, 1, GL_FALSE, target_array.data());
+ button_animation_.DisplayFrame(std::chrono::steady_clock::now());
+ DisplayTutorialText();
+ DrawNextButton();
+ DrawNextButtonText();
+}
+
+void TutorialAnimator::DisplayTiltAnimation(GLuint program,
+ GLuint obj_mvp_param) {
+ glUseProgram(program);
+ const std::array target_array =
+ tilt_animation_.GetModelViewProjectionAnimation().ToGlArray();
+ glUniformMatrix4fv(obj_mvp_param, 1, GL_FALSE, target_array.data());
+
+ tilt_animation_.DisplayFrame(std::chrono::steady_clock::now());
+ DisplayTiltInstructionsText();
+}
+
+void TutorialAnimator::LoadNextButton() {
+ LoadNextButtonGLObjects();
+ CalculateNextButtonMatrices();
+}
+
+void TutorialAnimator::LoadNextButtonGLObjects() {
+ const float vertices[]{
+ 0.5f, 0.5f, 0.0f, // top right
+ 0.5f, -0.5f, 0.0f, // bottom right
+ -0.5f, -0.5f, 0.0f, // bottom left
+ -0.5f, 0.5f, 0.0f // top left
+ };
+
+ const uint32_t indices[]{
+ // note that we start from 0!
+ 0, 1, 3, // first triangle
+ 1, 2, 3 // second triangle
+ };
+
+ glGenBuffers(1, &next_button_background_vbo_);
+ glGenBuffers(1, &next_button_background_ebo_);
+
+ glBindBuffer(GL_ARRAY_BUFFER, next_button_background_vbo_);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+ cardboard_demo::CHECKGLERROR("LoadNextButton bind next button VBO");
+
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, next_button_background_ebo_);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
+ GL_STATIC_DRAW);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+
+ const GLuint vertex_shader{
+ cardboard_demo::LoadGLShader(GL_VERTEX_SHADER, kVertexShader)};
+ const GLuint fragment_shader{
+ cardboard_demo::LoadGLShader(GL_FRAGMENT_SHADER, kFragmentShader)};
+
+ next_button_background_program_id_ = glCreateProgram();
+ glAttachShader(next_button_background_program_id_, vertex_shader);
+ glAttachShader(next_button_background_program_id_, fragment_shader);
+ cardboard_demo::CHECKGLERROR("LoadBlackBackground attach shaders");
+
+ glLinkProgram(next_button_background_program_id_);
+ cardboard_demo::CHECKGLERROR("LoadBlackBackground link program");
+
+ next_button_background_mvp_param_ =
+ glGetUniformLocation(next_button_background_program_id_, "u_MVP");
+}
+
+void TutorialAnimator::CalculateNextButtonMatrices() {
+ next_button_text_width_ =
+ text_renderer_->GetTextWidth(
+ cardboard::GetLocalizedString(cardboard::kStrNext, env_), 0.9f) *
+ kNextButtonTextXYScale;
+ next_button_text_x_offset_ = next_button_text_width_ / 2.0f;
+
+ const float bg_width{1.0f * next_button_text_width_ * 1.3f};
+ bg_scale_matrix_ = cardboard_demo::GetScaleMatrix({bg_width, 0.10f, 1.0f});
+
+ button_text_scale_matrix_ =
+ cardboard_demo::GetScaleMatrix(kNextButtonTextScale);
+ button_text_model_matrix_ =
+ cardboard_demo::GetTranslationMatrix({next_button_text_x_offset_,
+ -0.52f, // y_offset
+ 0.0f});
+}
+
+void TutorialAnimator::DrawNextButton() {
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_CULL_FACE);
+
+ glUseProgram(next_button_background_program_id_);
+
+ const std::array target_array{
+ model_view_projection_next_button_.ToGlArray()};
+ glUniformMatrix4fv(next_button_background_mvp_param_, 1, GL_FALSE,
+ target_array.data());
+
+ glBindBuffer(GL_ARRAY_BUFFER, next_button_background_vbo_);
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
+ glEnableVertexAttribArray(0);
+
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, next_button_background_ebo_);
+
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
+
+ cardboard_demo::CHECKGLERROR("DrawNextButton draw elements");
+
+ glDisableVertexAttribArray(0);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glEnable(GL_CULL_FACE);
+ glEnable(GL_DEPTH_TEST);
+}
+
+void TutorialAnimator::DrawNextButtonText() const {
+ text_renderer_->RenderText(
+ cardboard::GetLocalizedString(cardboard::kStrNext, env_),
+ model_view_projection_next_button_text_,
+ 0.9f);
+}
+
+bool TutorialAnimator::IsNextClicked(
+ const cardboard_demo::Matrix4x4& device_pose, float angle_limit) const {
+ // Compute vectors pointing towards the reticle and towards the next button
+ // in head space.
+ cardboard_demo::Matrix4x4 head_from_demo =
+ device_pose * button_animation_.GetModelAnimation() * model_next_button_;
+ const std::array unit_quaternion {0.f, 0.f, 0.f, 1.f};
+ const std::array point_vector {0.f, 0.f, -1.f, 0.f};
+ const std::array target_vector = head_from_demo * unit_quaternion;
+
+ const float angle{
+ cardboard_demo::AngleBetweenVectors(point_vector, target_vector)};
+ return angle < angle_limit;
+}
+} // namespace cardboard_launcher
diff --git a/paperscope/paperscope_launcher/src/tutorial_animator.h b/paperscope/paperscope_launcher/src/tutorial_animator.h
new file mode 100644
index 00000000..20073b08
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/tutorial_animator.h
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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.
+ */
+#ifndef PAPERSCOPE_LAUNCHER_SRC_TUTORIAL_ANIMATOR_H_
+#define PAPERSCOPE_LAUNCHER_SRC_TUTORIAL_ANIMATOR_H_
+
+#include
+#include
+
+#include
+#include
+
+#include
+#include "animated_shape.h"
+#include "text_renderer.h"
+#include "util.h"
+
+namespace cardboard_launcher {
+
+// Handles all of the logic related to the Cardboard Demo App Tutorial.
+class TutorialAnimator {
+ public:
+ /**
+ * Creates a TutorialAnimator from vectors of button animation frames and
+ * tilt animation frames.
+ *
+ * @param button_animation_files A vector of button animation frames.
+ * @param tilt_animation_files A vector of tilt animation frames.
+ * @param device_pose pointer to the device pose. Used to determine when the
+ * device is in Portrait orientation.
+ */
+ TutorialAnimator(const std::vector& button_animation_files,
+ const std::vector& tilt_animation_files,
+ cardboard_demo::TextRenderer* const text_renderer);
+
+ /**
+ * Loads the animation files for both the button and tilt animations.
+ *
+ * @param obj_position_param The object position parameter.
+ * @param obj_uv_param The object uv parameter.
+ * @param asset_mgr The asset manager.
+ * @param env The JNI environment.
+ * @param java_asset_mgr The Java asset manager.
+ */
+ void LoadAnimations(GLuint obj_position_param, GLuint obj_uv_param,
+ AAssetManager* asset_mgr, JNIEnv* env,
+ jobject java_asset_mgr);
+
+ /**
+ * Sets the position of the animation on screen.
+ *
+ * @param eye_view The eye view matrix.
+ * @param projection_matrix The projection matrix.
+ */
+ void SetScreenPosition(const cardboard_demo::Matrix4x4& eye_view,
+ const cardboard_demo::Matrix4x4& projection_matrix);
+
+ /**
+ * Draw the current frame of the animation to be drawn on screen.
+ *
+ * @param program The OpenGL program.
+ * @param obj_mvp_param The OpenGL MVP matrix.
+ */
+ void DisplayButtonAnimation(GLuint program, GLuint obj_mvp_param);
+
+ /**
+ * Draw the current frame of the animation to be drawn on screen.
+ *
+ * @param program The OpenGL program.
+ * @param obj_mvp_param The OpenGL MVP matrix.
+ */
+ void DisplayTiltAnimation(GLuint program, GLuint obj_mvp_param);
+
+ /**
+ * Displays the tutorial text on screen.
+ */
+ void DisplayTutorialText() const;
+
+ /**
+ * Displays the tilt instructions on screen.
+ */
+ void DisplayTiltInstructionsText() const;
+
+ /**
+ * Loads the assets for the "Next" button.
+ */
+ void LoadNextButton();
+
+ /**
+ * Loads the OpenGL objects for the "Next" button background.
+ */
+ void LoadNextButtonGLObjects();
+
+ /**
+ * Calculates the matrices needed for the "Next" button background
+ * and text to be drawn on screen.
+ */
+ void CalculateNextButtonMatrices();
+
+ /**
+ * Draws the "Next" button background.
+ */
+ void DrawNextButton();
+
+ /**
+ * Draws the text for the "Next" button.
+ */
+ void DrawNextButtonText() const;
+
+ /**
+ * Indicates whether the Next button has been clicked.
+ * @param device_pose The device pose.
+ * @param angle_limit Angle threshold for determining whether the controller
+ * is pointing at the next button.
+ */
+ bool IsNextClicked(const cardboard_demo::Matrix4x4& device_pose,
+ float angle_limit) const;
+
+ /**
+ * Sets the JNI environment.
+ * @param env The JNI environment.
+ */
+ void SetJNIEnv(JNIEnv* env) { env_ = env; }
+
+ private:
+ AnimatedShape button_animation_;
+ AnimatedShape tilt_animation_;
+ cardboard_demo::TextRenderer* const text_renderer_;
+ JNIEnv* env_;
+
+ cardboard_demo::Matrix4x4 model_next_button_;
+ cardboard_demo::Matrix4x4 model_view_projection_next_button_;
+ cardboard_demo::Matrix4x4 model_view_projection_next_button_text_;
+ cardboard_demo::Matrix4x4 model_view_projection_text_model_matrix_;
+ cardboard_demo::Matrix4x4 model_view_projection_instructions_text_matrix_;
+
+ cardboard_demo::Matrix4x4 bg_scale_matrix_;
+
+ float next_button_text_width_{};
+ float next_button_text_x_offset_{};
+ cardboard_demo::Matrix4x4 button_text_scale_matrix_;
+ cardboard_demo::Matrix4x4 button_text_model_matrix_;
+
+ GLuint next_button_background_vbo_{};
+ GLuint next_button_background_ebo_{};
+ GLuint next_button_background_program_id_{};
+ GLuint next_button_background_mvp_param_{};
+};
+
+} // namespace cardboard_launcher
+#endif // PAPERSCOPE_LAUNCHER_SRC_TUTORIAL_ANIMATOR_H_
diff --git a/paperscope/paperscope_launcher/src/util.cc b/paperscope/paperscope_launcher/src/util.cc
new file mode 100644
index 00000000..18d92ade
--- /dev/null
+++ b/paperscope/paperscope_launcher/src/util.cc
@@ -0,0 +1,578 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "util.h"
+
+#include
+#include // Needed for strtok_r and strstr
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include