This is a simple example that shows how to host and resolve anchors using ARCore Cloud Anchors
+ * API calls. This app only has at most one anchor at a time, to focus more on the cloud aspect of
+ * anchors.
+ */
+public class CloudAnchorActivity extends AppCompatActivity implements GLSurfaceView.Renderer {
+ private static final String TAG = CloudAnchorActivity.class.getSimpleName();
+ private static final float[] OBJECT_COLOR = new float[] {139.0f, 195.0f, 74.0f, 255.0f};
+
+ private enum HostResolveMode {
+ NONE,
+ HOSTING,
+ RESOLVING,
+ }
+
+ // Rendering. The Renderers are created here, and initialized when the GL surface is created.
+ private GLSurfaceView surfaceView;
+ private final BackgroundRenderer backgroundRenderer = new BackgroundRenderer();
+ private final ObjectRenderer virtualObject = new ObjectRenderer();
+ private final ObjectRenderer virtualObjectShadow = new ObjectRenderer();
+ private final PlaneRenderer planeRenderer = new PlaneRenderer();
+ private final PointCloudRenderer pointCloudRenderer = new PointCloudRenderer();
+
+ private boolean installRequested;
+
+ // Temporary matrices allocated here to reduce number of allocations for each frame.
+ private final float[] anchorMatrix = new float[16];
+ private final float[] viewMatrix = new float[16];
+ private final float[] projectionMatrix = new float[16];
+
+ // Locks needed for synchronization
+ private final Object singleTapLock = new Object();
+ private final Object anchorLock = new Object();
+
+ // Tap handling and UI.
+ private GestureDetector gestureDetector;
+ private final SnackbarHelper snackbarHelper = new SnackbarHelper();
+ private DisplayRotationHelper displayRotationHelper;
+ private Button hostButton;
+ private Button resolveButton;
+ private TextView roomCodeText;
+
+ @GuardedBy("singleTapLock")
+ private MotionEvent queuedSingleTap;
+
+ private Session session;
+
+ @GuardedBy("anchorLock")
+ private Anchor anchor;
+
+ // Cloud Anchor Components.
+ private FirebaseManager firebaseManager;
+ private final CloudAnchorManager cloudManager = new CloudAnchorManager();
+ private HostResolveMode currentMode;
+ private RoomCodeAndCloudAnchorIdListener hostListener;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ surfaceView = findViewById(R.id.surfaceview);
+ displayRotationHelper = new DisplayRotationHelper(this);
+
+ // Set up tap listener.
+ gestureDetector =
+ new GestureDetector(
+ this,
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ synchronized (singleTapLock) {
+ if (currentMode == HostResolveMode.HOSTING) {
+ queuedSingleTap = e;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+ });
+ surfaceView.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
+
+ // Set up renderer.
+ surfaceView.setPreserveEGLContextOnPause(true);
+ surfaceView.setEGLContextClientVersion(2);
+ surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending.
+ surfaceView.setRenderer(this);
+ surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ surfaceView.setWillNotDraw(false);
+ installRequested = false;
+
+ // Initialize UI components.
+ hostButton = findViewById(R.id.host_button);
+ hostButton.setOnClickListener((view) -> onHostButtonPress());
+ resolveButton = findViewById(R.id.resolve_button);
+ resolveButton.setOnClickListener((view) -> onResolveButtonPress());
+ roomCodeText = findViewById(R.id.room_code_text);
+
+ // Initialize Cloud Anchor variables.
+ firebaseManager = new FirebaseManager(this);
+ currentMode = HostResolveMode.NONE;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (session == null) {
+ Exception exception = null;
+ int messageId = -1;
+ try {
+ switch (ArCoreApk.getInstance().requestInstall(this, !installRequested)) {
+ case INSTALL_REQUESTED:
+ installRequested = true;
+ return;
+ case INSTALLED:
+ break;
+ }
+
+ // ARCore requires camera permissions to operate. If we did not yet obtain runtime
+ // permission on Android M and above, now is a good time to ask the user for it.
+ if (!CameraPermissionHelper.hasCameraPermission(this)) {
+ CameraPermissionHelper.requestCameraPermission(this);
+ return;
+ }
+ session = new Session(this);
+ } catch (UnavailableArcoreNotInstalledException e) {
+ messageId = R.string.snackbar_arcore_unavailable;
+ exception = e;
+ } catch (UnavailableApkTooOldException e) {
+ messageId = R.string.snackbar_arcore_too_old;
+ exception = e;
+ } catch (UnavailableSdkTooOldException e) {
+ messageId = R.string.snackbar_arcore_sdk_too_old;
+ exception = e;
+ } catch (Exception e) {
+ messageId = R.string.snackbar_arcore_exception;
+ exception = e;
+ }
+
+ if (exception != null) {
+ snackbarHelper.showError(this, getString(messageId));
+ Log.e(TAG, "Exception creating session", exception);
+ return;
+ }
+
+ // Create default config and check if supported.
+ Config config = new Config(session);
+ config.setCloudAnchorMode(CloudAnchorMode.ENABLED);
+ session.configure(config);
+
+ // Setting the session in the HostManager.
+ cloudManager.setSession(session);
+ // Show the inital message only in the first resume.
+ snackbarHelper.showMessage(this, getString(R.string.snackbar_initial_message));
+ }
+
+ // Note that order matters - see the note in onPause(), the reverse applies here.
+ try {
+ session.resume();
+ } catch (CameraNotAvailableException e) {
+ // In some cases (such as another camera app launching) the camera may be given to
+ // a different app instead. Handle this properly by showing a message and recreate the
+ // session at the next iteration.
+ snackbarHelper.showError(this, getString(R.string.snackbar_camera_unavailable));
+ session = null;
+ return;
+ }
+ surfaceView.onResume();
+ displayRotationHelper.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (session != null) {
+ // Note that the order matters - GLSurfaceView is paused first so that it does not try
+ // to query the session. If Session is paused before GLSurfaceView, GLSurfaceView may
+ // still call session.update() and get a SessionPausedException.
+ displayRotationHelper.onPause();
+ surfaceView.onPause();
+ session.pause();
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
+ if (!CameraPermissionHelper.hasCameraPermission(this)) {
+ Toast.makeText(this, "Camera permission is needed to run this application", Toast.LENGTH_LONG)
+ .show();
+ if (!CameraPermissionHelper.shouldShowRequestPermissionRationale(this)) {
+ // Permission denied with checking "Do not ask again".
+ CameraPermissionHelper.launchPermissionSettings(this);
+ }
+ finish();
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ FullScreenHelper.setFullScreenOnWindowFocusChanged(this, hasFocus);
+ }
+
+ /**
+ * Handles the most recent user tap.
+ *
+ *
We only ever handle one tap at a time, since this app only allows for a single anchor.
+ *
+ * @param frame the current AR frame
+ * @param cameraTrackingState the current camera tracking state
+ */
+ private void handleTap(Frame frame, TrackingState cameraTrackingState) {
+ // Handle taps. Handling only one tap per frame, as taps are usually low frequency
+ // compared to frame rate.
+ synchronized (singleTapLock) {
+ synchronized (anchorLock) {
+ // Only handle a tap if the anchor is currently null, the queued tap is non-null and the
+ // camera is currently tracking.
+ if (anchor == null
+ && queuedSingleTap != null
+ && cameraTrackingState == TrackingState.TRACKING) {
+ Preconditions.checkState(
+ currentMode == HostResolveMode.HOSTING,
+ "We should only be creating an anchor in hosting mode.");
+ for (HitResult hit : frame.hitTest(queuedSingleTap)) {
+ if (shouldCreateAnchorWithHit(hit)) {
+ Anchor newAnchor = hit.createAnchor();
+ Preconditions.checkNotNull(hostListener, "The host listener cannot be null.");
+ cloudManager.hostCloudAnchor(newAnchor, hostListener);
+ setNewAnchor(newAnchor);
+ snackbarHelper.showMessage(this, getString(R.string.snackbar_anchor_placed));
+ break; // Only handle the first valid hit.
+ }
+ }
+ }
+ }
+ queuedSingleTap = null;
+ }
+ }
+
+ /** Returns {@code true} if and only if the hit can be used to create an Anchor reliably. */
+ private static boolean shouldCreateAnchorWithHit(HitResult hit) {
+ Trackable trackable = hit.getTrackable();
+ if (trackable instanceof Plane) {
+ // Check if the hit was within the plane's polygon.
+ return ((Plane) trackable).isPoseInPolygon(hit.getHitPose());
+ } else if (trackable instanceof Point) {
+ // Check if the hit was against an oriented point.
+ return ((Point) trackable).getOrientationMode() == OrientationMode.ESTIMATED_SURFACE_NORMAL;
+ }
+ return false;
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+ GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
+
+ // Prepare the rendering objects. This involves reading shaders, so may throw an IOException.
+ try {
+ // Create the texture and pass it to ARCore session to be filled during update().
+ backgroundRenderer.createOnGlThread(this);
+ planeRenderer.createOnGlThread(this, "models/trigrid.png");
+ pointCloudRenderer.createOnGlThread(this);
+
+ virtualObject.createOnGlThread(this, "models/andy.obj", "models/andy.png");
+ virtualObject.setMaterialProperties(0.0f, 2.0f, 0.5f, 6.0f);
+
+ virtualObjectShadow.createOnGlThread(
+ this, "models/andy_shadow.obj", "models/andy_shadow.png");
+ virtualObjectShadow.setBlendMode(BlendMode.Shadow);
+ virtualObjectShadow.setMaterialProperties(1.0f, 0.0f, 0.0f, 1.0f);
+ } catch (IOException ex) {
+ Log.e(TAG, "Failed to read an asset file", ex);
+ }
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl, int width, int height) {
+ displayRotationHelper.onSurfaceChanged(width, height);
+ GLES20.glViewport(0, 0, width, height);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ // Clear screen to notify driver it should not load any pixels from previous frame.
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
+
+ if (session == null) {
+ return;
+ }
+ // Notify ARCore session that the view size changed so that the perspective matrix and
+ // the video background can be properly adjusted.
+ displayRotationHelper.updateSessionIfNeeded(session);
+
+ try {
+ session.setCameraTextureName(backgroundRenderer.getTextureId());
+
+ // Obtain the current frame from ARSession. When the configuration is set to
+ // UpdateMode.BLOCKING (it is by default), this will throttle the rendering to the
+ // camera framerate.
+ Frame frame = session.update();
+ Camera camera = frame.getCamera();
+ TrackingState cameraTrackingState = camera.getTrackingState();
+
+ // Notify the cloudManager of all the updates.
+ cloudManager.onUpdate();
+
+ // Handle user input.
+ handleTap(frame, cameraTrackingState);
+
+ // If frame is ready, render camera preview image to the GL surface.
+ backgroundRenderer.draw(frame);
+
+ // If not tracking, don't draw 3d objects.
+ if (cameraTrackingState == TrackingState.PAUSED) {
+ return;
+ }
+
+ // Get camera and projection matrices.
+ camera.getViewMatrix(viewMatrix, 0);
+ camera.getProjectionMatrix(projectionMatrix, 0, 0.1f, 100.0f);
+
+ // Visualize tracked points.
+ // Use try-with-resources to automatically release the point cloud.
+ try (PointCloud pointCloud = frame.acquirePointCloud()) {
+ pointCloudRenderer.update(pointCloud);
+ pointCloudRenderer.draw(viewMatrix, projectionMatrix);
+ }
+
+ // Visualize planes.
+ planeRenderer.drawPlanes(
+ session.getAllTrackables(Plane.class), camera.getDisplayOrientedPose(), projectionMatrix);
+
+ // Check if the anchor can be visualized or not, and get its pose if it can be.
+ boolean shouldDrawAnchor = false;
+ synchronized (anchorLock) {
+ if (anchor != null && anchor.getTrackingState() == TrackingState.TRACKING) {
+ // Get the current pose of an Anchor in world space. The Anchor pose is updated
+ // during calls to session.update() as ARCore refines its estimate of the world.
+ anchor.getPose().toMatrix(anchorMatrix, 0);
+ shouldDrawAnchor = true;
+ }
+ }
+
+ // Visualize anchor.
+ if (shouldDrawAnchor) {
+ float[] colorCorrectionRgba = new float[4];
+ frame.getLightEstimate().getColorCorrection(colorCorrectionRgba, 0);
+
+ // Update and draw the model and its shadow.
+ float scaleFactor = 1.0f;
+ virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
+ virtualObjectShadow.updateModelMatrix(anchorMatrix, scaleFactor);
+ virtualObject.draw(viewMatrix, projectionMatrix, colorCorrectionRgba, OBJECT_COLOR);
+ virtualObjectShadow.draw(viewMatrix, projectionMatrix, colorCorrectionRgba, OBJECT_COLOR);
+ }
+ } catch (Throwable t) {
+ // Avoid crashing the application due to unhandled exceptions.
+ Log.e(TAG, "Exception on the OpenGL thread", t);
+ }
+ }
+
+ /** Sets the new value of the current anchor. Detaches the old anchor, if it was non-null. */
+ private void setNewAnchor(Anchor newAnchor) {
+ synchronized (anchorLock) {
+ if (anchor != null) {
+ anchor.detach();
+ }
+ anchor = newAnchor;
+ }
+ }
+
+ /** Callback function invoked when the Host Button is pressed. */
+ private void onHostButtonPress() {
+ if (currentMode == HostResolveMode.HOSTING) {
+ resetMode();
+ return;
+ }
+
+ if (hostListener != null) {
+ return;
+ }
+ resolveButton.setEnabled(false);
+ hostButton.setText(R.string.cancel);
+ snackbarHelper.showMessageWithDismiss(this, getString(R.string.snackbar_on_host));
+
+ hostListener = new RoomCodeAndCloudAnchorIdListener();
+ firebaseManager.getNewRoomCode(hostListener);
+ }
+
+ /** Callback function invoked when the Resolve Button is pressed. */
+ private void onResolveButtonPress() {
+ if (currentMode == HostResolveMode.RESOLVING) {
+ resetMode();
+ return;
+ }
+ ResolveDialogFragment dialogFragment = new ResolveDialogFragment();
+ dialogFragment.setOkListener(this::onRoomCodeEntered);
+ dialogFragment.show(getSupportFragmentManager(), "ResolveDialog");
+ }
+
+ /** Resets the mode of the app to its initial state and removes the anchors. */
+ private void resetMode() {
+ hostButton.setText(R.string.host_button_text);
+ hostButton.setEnabled(true);
+ resolveButton.setText(R.string.resolve_button_text);
+ resolveButton.setEnabled(true);
+ roomCodeText.setText(R.string.initial_room_code);
+ currentMode = HostResolveMode.NONE;
+ firebaseManager.clearRoomListener();
+ hostListener = null;
+ setNewAnchor(null);
+ snackbarHelper.hide(this);
+ cloudManager.clearListeners();
+ }
+
+ /** Callback function invoked when the user presses the OK button in the Resolve Dialog. */
+ private void onRoomCodeEntered(Long roomCode) {
+ currentMode = HostResolveMode.RESOLVING;
+ hostButton.setEnabled(false);
+ resolveButton.setText(R.string.cancel);
+ roomCodeText.setText(String.valueOf(roomCode));
+ snackbarHelper.showMessageWithDismiss(this, getString(R.string.snackbar_on_resolve));
+
+ // Register a new listener for the given room.
+ firebaseManager.registerNewListenerForRoom(
+ roomCode,
+ (cloudAnchorId) -> {
+ // When the cloud anchor ID is available from Firebase.
+ cloudManager.resolveCloudAnchor(
+ cloudAnchorId,
+ (anchor) -> {
+ // When the anchor has been resolved, or had a final error state.
+ CloudAnchorState cloudState = anchor.getCloudAnchorState();
+ if (cloudState.isError()) {
+ Log.w(
+ TAG,
+ "The anchor in room "
+ + roomCode
+ + " could not be resolved. The error state was "
+ + cloudState);
+ snackbarHelper.showMessageWithDismiss(
+ CloudAnchorActivity.this,
+ getString(R.string.snackbar_resolve_error, cloudState));
+ return;
+ }
+ snackbarHelper.showMessageWithDismiss(
+ CloudAnchorActivity.this, getString(R.string.snackbar_resolve_success));
+ setNewAnchor(anchor);
+ });
+ });
+ }
+
+ /**
+ * Listens for both a new room code and an anchor ID, and shares the anchor ID in Firebase with
+ * the room code when both are available.
+ */
+ private final class RoomCodeAndCloudAnchorIdListener
+ implements CloudAnchorManager.CloudAnchorListener, FirebaseManager.RoomCodeListener {
+
+ private Long roomCode;
+ private String cloudAnchorId;
+
+ @Override
+ public void onNewRoomCode(Long newRoomCode) {
+ Preconditions.checkState(roomCode == null, "The room code cannot have been set before.");
+ roomCode = newRoomCode;
+ roomCodeText.setText(String.valueOf(roomCode));
+ snackbarHelper.showMessageWithDismiss(
+ CloudAnchorActivity.this, getString(R.string.snackbar_room_code_available));
+ checkAndMaybeShare();
+ synchronized (singleTapLock) {
+ // Change currentMode to HOSTING after receiving the room code (not when the 'Host' button
+ // is tapped), to prevent an anchor being placed before we know the room code and able to
+ // share the anchor ID.
+ currentMode = HostResolveMode.HOSTING;
+ }
+ }
+
+ @Override
+ public void onError(DatabaseError error) {
+ Log.w(TAG, "A Firebase database error happened.", error.toException());
+ snackbarHelper.showError(
+ CloudAnchorActivity.this, getString(R.string.snackbar_firebase_error));
+ }
+
+ @Override
+ public void onCloudTaskComplete(Anchor anchor) {
+ CloudAnchorState cloudState = anchor.getCloudAnchorState();
+ if (cloudState.isError()) {
+ Log.e(TAG, "Error hosting a cloud anchor, state " + cloudState);
+ snackbarHelper.showMessageWithDismiss(
+ CloudAnchorActivity.this, getString(R.string.snackbar_host_error, cloudState));
+ return;
+ }
+ Preconditions.checkState(
+ cloudAnchorId == null, "The cloud anchor ID cannot have been set before.");
+ cloudAnchorId = anchor.getCloudAnchorId();
+ setNewAnchor(anchor);
+ checkAndMaybeShare();
+ }
+
+ private void checkAndMaybeShare() {
+ if (roomCode == null || cloudAnchorId == null) {
+ return;
+ }
+ firebaseManager.storeAnchorIdInRoom(roomCode, cloudAnchorId);
+ snackbarHelper.showMessageWithDismiss(
+ CloudAnchorActivity.this, getString(R.string.snackbar_cloud_id_shared));
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/cloudanchor/CloudAnchorManager.java b/app/src/main/java/com/google/ar/core/examples/java/cloudanchor/CloudAnchorManager.java
new file mode 100644
index 0000000..890e5f8
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/cloudanchor/CloudAnchorManager.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * 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.ar.core.examples.java.cloudanchor;
+
+import android.support.annotation.Nullable;
+import com.google.ar.core.Anchor;
+import com.google.ar.core.Anchor.CloudAnchorState;
+import com.google.ar.core.Session;
+import com.google.common.base.Preconditions;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A helper class to handle all the Cloud Anchors logic, and add a callback-like mechanism on top of
+ * the existing ARCore API.
+ */
+class CloudAnchorManager {
+ private static final String TAG =
+ CloudAnchorActivity.class.getSimpleName() + "." + CloudAnchorManager.class.getSimpleName();
+
+ /** Listener for the results of a host or resolve operation. */
+ interface CloudAnchorListener {
+
+ /** This method is invoked when the results of a Cloud Anchor operation are available. */
+ void onCloudTaskComplete(Anchor anchor);
+ }
+
+ @Nullable private Session session = null;
+ private final HashMap pendingAnchors = new HashMap<>();
+
+ /**
+ * This method is used to set the session, since it might not be available when this object is
+ * created.
+ */
+ synchronized void setSession(Session session) {
+ this.session = session;
+ }
+
+ /**
+ * This method hosts an anchor. The {@code listener} will be invoked when the results are
+ * available.
+ */
+ synchronized void hostCloudAnchor(Anchor anchor, CloudAnchorListener listener) {
+ Preconditions.checkNotNull(session, "The session cannot be null.");
+ Anchor newAnchor = session.hostCloudAnchor(anchor);
+ pendingAnchors.put(newAnchor, listener);
+ }
+
+ /**
+ * This method resolves an anchor. The {@code listener} will be invoked when the results are
+ * available.
+ */
+ synchronized void resolveCloudAnchor(String anchorId, CloudAnchorListener listener) {
+ Preconditions.checkNotNull(session, "The session cannot be null.");
+ Anchor newAnchor = session.resolveCloudAnchor(anchorId);
+ pendingAnchors.put(newAnchor, listener);
+ }
+
+ /** Should be called after a {@link Session#update()} call. */
+ synchronized void onUpdate() {
+ Preconditions.checkNotNull(session, "The session cannot be null.");
+ Iterator> iter = pendingAnchors.entrySet().iterator();
+ while (iter.hasNext()) {
+ Map.Entry entry = iter.next();
+ Anchor anchor = entry.getKey();
+ if (isReturnableState(anchor.getCloudAnchorState())) {
+ CloudAnchorListener listener = entry.getValue();
+ listener.onCloudTaskComplete(anchor);
+ iter.remove();
+ }
+ }
+ }
+
+ /** Used to clear any currently registered listeners, so they wont be called again. */
+ synchronized void clearListeners() {
+ pendingAnchors.clear();
+ }
+
+ private static boolean isReturnableState(CloudAnchorState cloudState) {
+ switch (cloudState) {
+ case NONE:
+ case TASK_IN_PROGRESS:
+ return false;
+ default:
+ return true;
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/cloudanchor/FirebaseManager.java b/app/src/main/java/com/google/ar/core/examples/java/cloudanchor/FirebaseManager.java
new file mode 100644
index 0000000..22be4f1
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/cloudanchor/FirebaseManager.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * 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.ar.core.examples.java.cloudanchor;
+
+import android.content.Context;
+import android.util.Log;
+import com.google.common.base.Preconditions;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.database.DataSnapshot;
+import com.google.firebase.database.DatabaseError;
+import com.google.firebase.database.DatabaseReference;
+import com.google.firebase.database.FirebaseDatabase;
+import com.google.firebase.database.MutableData;
+import com.google.firebase.database.Transaction;
+import com.google.firebase.database.ValueEventListener;
+
+/** A helper class to manage all communications with Firebase. */
+class FirebaseManager {
+ private static final String TAG =
+ CloudAnchorActivity.class.getSimpleName() + "." + FirebaseManager.class.getSimpleName();
+
+ /** Listener for a new room code. */
+ interface RoomCodeListener {
+
+ /** Invoked when a new room code is available from Firebase. */
+ void onNewRoomCode(Long newRoomCode);
+
+ /** Invoked if a Firebase Database Error happened while fetching the room code. */
+ void onError(DatabaseError error);
+ }
+
+ /** Listener for a new cloud anchor ID. */
+ interface CloudAnchorIdListener {
+
+ /** Invoked when a new cloud anchor ID is available. */
+ void onNewCloudAnchorId(String cloudAnchorId);
+ }
+
+ // Names of the nodes used in the Firebase Database
+ private static final String ROOT_FIREBASE_HOTSPOTS = "hotspot_list";
+ private static final String ROOT_LAST_ROOM_CODE = "last_room_code";
+
+ // Some common keys and values used when writing to the Firebase Database.
+ private static final String KEY_DISPLAY_NAME = "display_name";
+ private static final String KEY_ANCHOR_ID = "hosted_anchor_id";
+ private static final String KEY_TIMESTAMP = "updated_at_timestamp";
+ private static final String DISPLAY_NAME_VALUE = "Android EAP Sample";
+
+ private final FirebaseApp app;
+ private final DatabaseReference hotspotListRef;
+ private final DatabaseReference roomCodeRef;
+ private DatabaseReference currentRoomRef = null;
+ private ValueEventListener currentRoomListener = null;
+
+ /**
+ * Default constructor for the FirebaseManager.
+ *
+ * @param context The application context.
+ */
+ FirebaseManager(Context context) {
+ app = FirebaseApp.initializeApp(context);
+ if (app != null) {
+ DatabaseReference rootRef = FirebaseDatabase.getInstance(app).getReference();
+ hotspotListRef = rootRef.child(ROOT_FIREBASE_HOTSPOTS);
+ roomCodeRef = rootRef.child(ROOT_LAST_ROOM_CODE);
+
+ DatabaseReference.goOnline();
+ } else {
+ Log.d(TAG, "Could not connect to Firebase Database!");
+ hotspotListRef = null;
+ roomCodeRef = null;
+ }
+ }
+
+ /**
+ * Gets a new room code from the Firebase Database. Invokes the listener method when a new room
+ * code is available.
+ */
+ void getNewRoomCode(RoomCodeListener listener) {
+ Preconditions.checkNotNull(app, "Firebase App was null");
+ roomCodeRef.runTransaction(
+ new Transaction.Handler() {
+ @Override
+ public Transaction.Result doTransaction(MutableData currentData) {
+ Long nextCode = Long.valueOf(1);
+ Object currVal = currentData.getValue();
+ if (currVal != null) {
+ Long lastCode = Long.valueOf(currVal.toString());
+ nextCode = lastCode + 1;
+ }
+ currentData.setValue(nextCode);
+ return Transaction.success(currentData);
+ }
+
+ @Override
+ public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
+ if (!committed) {
+ listener.onError(error);
+ return;
+ }
+ Long roomCode = currentData.getValue(Long.class);
+ listener.onNewRoomCode(roomCode);
+ }
+ });
+ }
+
+ /** Stores the given anchor ID in the given room code. */
+ void storeAnchorIdInRoom(Long roomCode, String cloudAnchorId) {
+ Preconditions.checkNotNull(app, "Firebase App was null");
+ DatabaseReference roomRef = hotspotListRef.child(String.valueOf(roomCode));
+ roomRef.child(KEY_DISPLAY_NAME).setValue(DISPLAY_NAME_VALUE);
+ roomRef.child(KEY_ANCHOR_ID).setValue(cloudAnchorId);
+ roomRef.child(KEY_TIMESTAMP).setValue(System.currentTimeMillis());
+ }
+
+ /**
+ * Registers a new listener for the given room code. The listener is invoked whenever the data for
+ * the room code is changed.
+ */
+ void registerNewListenerForRoom(Long roomCode, CloudAnchorIdListener listener) {
+ Preconditions.checkNotNull(app, "Firebase App was null");
+ clearRoomListener();
+ currentRoomRef = hotspotListRef.child(String.valueOf(roomCode));
+ currentRoomListener =
+ new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot dataSnapshot) {
+ Object valObj = dataSnapshot.child(KEY_ANCHOR_ID).getValue();
+ if (valObj != null) {
+ String anchorId = String.valueOf(valObj);
+ if (!anchorId.isEmpty()) {
+ listener.onNewCloudAnchorId(anchorId);
+ }
+ }
+ }
+
+ @Override
+ public void onCancelled(DatabaseError databaseError) {
+ Log.w(TAG, "The Firebase operation was cancelled.", databaseError.toException());
+ }
+ };
+ currentRoomRef.addValueEventListener(currentRoomListener);
+ }
+
+ /**
+ * Resets the current room listener registered using {@link #registerNewListenerForRoom(Long,
+ * CloudAnchorIdListener)}.
+ */
+ void clearRoomListener() {
+ if (currentRoomListener != null && currentRoomRef != null) {
+ currentRoomRef.removeEventListener(currentRoomListener);
+ currentRoomListener = null;
+ currentRoomRef = null;
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/cloudanchor/ResolveDialogFragment.java b/app/src/main/java/com/google/ar/core/examples/java/cloudanchor/ResolveDialogFragment.java
new file mode 100644
index 0000000..bdaa95a
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/cloudanchor/ResolveDialogFragment.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * 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.ar.core.examples.java.cloudanchor;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import android.text.Editable;
+import android.view.View;
+import android.widget.EditText;
+import com.google.common.base.Preconditions;
+
+/** A DialogFragment for the Resolve Dialog Box. */
+public class ResolveDialogFragment extends DialogFragment {
+
+ interface OkListener {
+ /**
+ * This method is called by the dialog box when its OK button is pressed.
+ *
+ * @param dialogValue the long value from the dialog box
+ */
+ void onOkPressed(Long dialogValue);
+ }
+
+ private EditText roomCodeField;
+ private OkListener okListener;
+
+ public void setOkListener(OkListener okListener) {
+ this.okListener = okListener;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ FragmentActivity activity =
+ Preconditions.checkNotNull(getActivity(), "The activity cannot be null.");
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+
+ // Passing null as the root is fine, because the view is for a dialog.
+ View dialogView = activity.getLayoutInflater().inflate(R.layout.resolve_dialog, null);
+ roomCodeField = dialogView.findViewById(R.id.room_code_input);
+ builder
+ .setView(dialogView)
+ .setTitle(R.string.resolve_dialog_title)
+ .setPositiveButton(
+ R.string.resolve_dialog_ok,
+ (dialog, which) -> {
+ Editable roomCodeText = roomCodeField.getText();
+ if (okListener != null && roomCodeText != null && roomCodeText.length() > 0) {
+ Long longVal = Long.valueOf(roomCodeText.toString());
+ okListener.onOkPressed(longVal);
+ }
+ })
+ .setNegativeButton(R.string.cancel, (dialog, which) -> {});
+ return builder.create();
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/common/helpers/CameraPermissionHelper.java b/app/src/main/java/com/google/ar/core/examples/java/common/helpers/CameraPermissionHelper.java
new file mode 100644
index 0000000..7617e36
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/common/helpers/CameraPermissionHelper.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * 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.ar.core.examples.java.common.helpers;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.provider.Settings;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+
+/** Helper to ask camera permission. */
+public final class CameraPermissionHelper {
+ private static final int CAMERA_PERMISSION_CODE = 0;
+ private static final String CAMERA_PERMISSION = Manifest.permission.CAMERA;
+
+ /** Check to see we have the necessary permissions for this app. */
+ public static boolean hasCameraPermission(Activity activity) {
+ return ContextCompat.checkSelfPermission(activity, CAMERA_PERMISSION)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /** Check to see we have the necessary permissions for this app, and ask for them if we don't. */
+ public static void requestCameraPermission(Activity activity) {
+ ActivityCompat.requestPermissions(
+ activity, new String[] {CAMERA_PERMISSION}, CAMERA_PERMISSION_CODE);
+ }
+
+ /** Check to see if we need to show the rationale for this permission. */
+ public static boolean shouldShowRequestPermissionRationale(Activity activity) {
+ return ActivityCompat.shouldShowRequestPermissionRationale(activity, CAMERA_PERMISSION);
+ }
+
+ /** Launch Application Setting to grant permission. */
+ public static void launchPermissionSettings(Activity activity) {
+ Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.fromParts("package", activity.getPackageName(), null));
+ activity.startActivity(intent);
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/common/helpers/DisplayRotationHelper.java b/app/src/main/java/com/google/ar/core/examples/java/common/helpers/DisplayRotationHelper.java
new file mode 100644
index 0000000..e79434b
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/common/helpers/DisplayRotationHelper.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * 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.ar.core.examples.java.common.helpers;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+import com.google.ar.core.Session;
+
+/**
+ * Helper to track the display rotations. In particular, the 180 degree rotations are not notified
+ * by the onSurfaceChanged() callback, and thus they require listening to the android display
+ * events.
+ */
+public final class DisplayRotationHelper implements DisplayListener {
+ private boolean viewportChanged;
+ private int viewportWidth;
+ private int viewportHeight;
+ private final Display display;
+ private final DisplayManager displayManager;
+ private final CameraManager cameraManager;
+
+ /**
+ * Constructs the DisplayRotationHelper but does not register the listener yet.
+ *
+ * @param context the Android {@link Context}.
+ */
+ public DisplayRotationHelper(Context context) {
+ displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ display = windowManager.getDefaultDisplay();
+ }
+
+ /** Registers the display listener. Should be called from {@link Activity#onResume()}. */
+ public void onResume() {
+ displayManager.registerDisplayListener(this, null);
+ }
+
+ /** Unregisters the display listener. Should be called from {@link Activity#onPause()}. */
+ public void onPause() {
+ displayManager.unregisterDisplayListener(this);
+ }
+
+ /**
+ * Records a change in surface dimensions. This will be later used by {@link
+ * #updateSessionIfNeeded(Session)}. Should be called from {@link
+ * android.opengl.GLSurfaceView.Renderer
+ * #onSurfaceChanged(javax.microedition.khronos.opengles.GL10, int, int)}.
+ *
+ * @param width the updated width of the surface.
+ * @param height the updated height of the surface.
+ */
+ public void onSurfaceChanged(int width, int height) {
+ viewportWidth = width;
+ viewportHeight = height;
+ viewportChanged = true;
+ }
+
+ /**
+ * Updates the session display geometry if a change was posted either by {@link
+ * #onSurfaceChanged(int, int)} call or by {@link #onDisplayChanged(int)} system callback. This
+ * function should be called explicitly before each call to {@link Session#update()}. This
+ * function will also clear the 'pending update' (viewportChanged) flag.
+ *
+ * @param session the {@link Session} object to update if display geometry changed.
+ */
+ public void updateSessionIfNeeded(Session session) {
+ if (viewportChanged) {
+ int displayRotation = display.getRotation();
+ session.setDisplayGeometry(displayRotation, viewportWidth, viewportHeight);
+ viewportChanged = false;
+ }
+ }
+
+ /**
+ * Returns the aspect ratio of the GL surface viewport while accounting for the display rotation
+ * relative to the device camera sensor orientation.
+ */
+ public float getCameraSensorRelativeViewportAspectRatio(String cameraId) {
+ float aspectRatio;
+ int cameraSensorToDisplayRotation = getCameraSensorToDisplayRotation(cameraId);
+ switch (cameraSensorToDisplayRotation) {
+ case 90:
+ case 270:
+ aspectRatio = (float) viewportHeight / (float) viewportWidth;
+ break;
+ case 0:
+ case 180:
+ aspectRatio = (float) viewportWidth / (float) viewportHeight;
+ break;
+ default:
+ throw new RuntimeException("Unhandled rotation: " + cameraSensorToDisplayRotation);
+ }
+ return aspectRatio;
+ }
+
+ /**
+ * Returns the rotation of the back-facing camera with respect to the display. The value is one of
+ * 0, 90, 180, 270.
+ */
+ public int getCameraSensorToDisplayRotation(String cameraId) {
+ CameraCharacteristics characteristics;
+ try {
+ characteristics = cameraManager.getCameraCharacteristics(cameraId);
+ } catch (CameraAccessException e) {
+ throw new RuntimeException("Unable to determine display orientation", e);
+ }
+
+ // Camera sensor orientation.
+ int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+
+ // Current display orientation.
+ int displayOrientation = toDegrees(display.getRotation());
+
+ // Make sure we return 0, 90, 180, or 270 degrees.
+ return (sensorOrientation - displayOrientation + 360) % 360;
+ }
+
+ private int toDegrees(int rotation) {
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ return 0;
+ case Surface.ROTATION_90:
+ return 90;
+ case Surface.ROTATION_180:
+ return 180;
+ case Surface.ROTATION_270:
+ return 270;
+ default:
+ throw new RuntimeException("Unknown rotation " + rotation);
+ }
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(int displayId) {}
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ viewportChanged = true;
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/common/helpers/FullScreenHelper.java b/app/src/main/java/com/google/ar/core/examples/java/common/helpers/FullScreenHelper.java
new file mode 100644
index 0000000..7aaa1c7
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/common/helpers/FullScreenHelper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * 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.ar.core.examples.java.common.helpers;
+
+import android.app.Activity;
+import android.view.View;
+import android.view.WindowManager;
+
+/** Helper to set up the Android full screen mode. */
+public final class FullScreenHelper {
+ /**
+ * Sets the Android fullscreen flags. Expected to be called from {@link
+ * Activity#onWindowFocusChanged(boolean hasFocus)}.
+ *
+ * @param activity the Activity on which the full screen mode will be set.
+ * @param hasFocus the hasFocus flag passed from the {@link Activity#onWindowFocusChanged(boolean
+ * hasFocus)} callback.
+ */
+ public static void setFullScreenOnWindowFocusChanged(Activity activity, boolean hasFocus) {
+ if (hasFocus) {
+ // https://developer.android.com/training/system-ui/immersive.html#sticky
+ activity
+ .getWindow()
+ .getDecorView()
+ .setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/common/helpers/SnackbarHelper.java b/app/src/main/java/com/google/ar/core/examples/java/common/helpers/SnackbarHelper.java
new file mode 100644
index 0000000..e518c49
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/common/helpers/SnackbarHelper.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * 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.ar.core.examples.java.common.helpers;
+
+import android.app.Activity;
+import android.support.design.widget.BaseTransientBottomBar;
+import android.support.design.widget.Snackbar;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Helper to manage the sample snackbar. Hides the Android boilerplate code, and exposes simpler
+ * methods.
+ */
+public final class SnackbarHelper {
+ private static final int BACKGROUND_COLOR = 0xbf323232;
+ private Snackbar messageSnackbar;
+ private enum DismissBehavior { HIDE, SHOW, FINISH };
+ private int maxLines = 2;
+ private String lastMessage = "";
+
+ public boolean isShowing() {
+ return messageSnackbar != null;
+ }
+
+ /** Shows a snackbar with a given message. */
+ public void showMessage(Activity activity, String message) {
+ if (!message.isEmpty() && (!isShowing() || !lastMessage.equals(message))) {
+ lastMessage = message;
+ show(activity, message, DismissBehavior.HIDE);
+ }
+ }
+
+ /** Shows a snackbar with a given message, and a dismiss button. */
+ public void showMessageWithDismiss(Activity activity, String message) {
+ show(activity, message, DismissBehavior.SHOW);
+ }
+
+ /**
+ * Shows a snackbar with a given error message. When dismissed, will finish the activity. Useful
+ * for notifying errors, where no further interaction with the activity is possible.
+ */
+ public void showError(Activity activity, String errorMessage) {
+ show(activity, errorMessage, DismissBehavior.FINISH);
+ }
+
+ /**
+ * Hides the currently showing snackbar, if there is one. Safe to call from any thread. Safe to
+ * call even if snackbar is not shown.
+ */
+ public void hide(Activity activity) {
+ if (!isShowing()) {
+ return;
+ }
+ lastMessage = "";
+ Snackbar messageSnackbarToHide = messageSnackbar;
+ messageSnackbar = null;
+ activity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ messageSnackbarToHide.dismiss();
+ }
+ });
+ }
+
+ public void setMaxLines(int lines) {
+ maxLines = lines;
+ }
+
+ private void show(
+ final Activity activity, final String message, final DismissBehavior dismissBehavior) {
+ activity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ messageSnackbar =
+ Snackbar.make(
+ activity.findViewById(android.R.id.content),
+ message,
+ Snackbar.LENGTH_INDEFINITE);
+ messageSnackbar.getView().setBackgroundColor(BACKGROUND_COLOR);
+ if (dismissBehavior != DismissBehavior.HIDE) {
+ messageSnackbar.setAction(
+ "Dismiss",
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ messageSnackbar.dismiss();
+ }
+ });
+ if (dismissBehavior == DismissBehavior.FINISH) {
+ messageSnackbar.addCallback(
+ new BaseTransientBottomBar.BaseCallback() {
+ @Override
+ public void onDismissed(Snackbar transientBottomBar, int event) {
+ super.onDismissed(transientBottomBar, event);
+ activity.finish();
+ }
+ });
+ }
+ }
+ ((TextView)
+ messageSnackbar
+ .getView()
+ .findViewById(android.support.design.R.id.snackbar_text))
+ .setMaxLines(maxLines);
+ messageSnackbar.show();
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/common/rendering/BackgroundRenderer.java b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/BackgroundRenderer.java
new file mode 100644
index 0000000..f41b774
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/BackgroundRenderer.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * 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.ar.core.examples.java.common.rendering;
+
+import android.content.Context;
+import android.opengl.GLES11Ext;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+import android.support.annotation.NonNull;
+import com.google.ar.core.Coordinates2d;
+import com.google.ar.core.Frame;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+/**
+ * This class renders the AR background from camera feed. It creates and hosts the texture given to
+ * ARCore to be filled with the camera image.
+ */
+public class BackgroundRenderer {
+ private static final String TAG = BackgroundRenderer.class.getSimpleName();
+
+ // Shader names.
+ private static final String VERTEX_SHADER_NAME = "shaders/screenquad.vert";
+ private static final String FRAGMENT_SHADER_NAME = "shaders/screenquad.frag";
+
+ private static final int COORDS_PER_VERTEX = 2;
+ private static final int TEXCOORDS_PER_VERTEX = 2;
+ private static final int FLOAT_SIZE = 4;
+
+ private FloatBuffer quadCoords;
+ private FloatBuffer quadTexCoords;
+
+ private int quadProgram;
+
+ private int quadPositionParam;
+ private int quadTexCoordParam;
+ private int textureId = -1;
+
+ public int getTextureId() {
+ return textureId;
+ }
+
+ /**
+ * Allocates and initializes OpenGL resources needed by the background renderer. Must be called on
+ * the OpenGL thread, typically in {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10,
+ * EGLConfig)}.
+ *
+ * @param context Needed to access shader source.
+ */
+ public void createOnGlThread(Context context) throws IOException {
+ // Generate the background texture.
+ int[] textures = new int[1];
+ GLES20.glGenTextures(1, textures, 0);
+ textureId = textures[0];
+ int textureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
+ GLES20.glBindTexture(textureTarget, textureId);
+ GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+
+ int numVertices = 4;
+ if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) {
+ throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer.");
+ }
+
+ ByteBuffer bbCoords = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE);
+ bbCoords.order(ByteOrder.nativeOrder());
+ quadCoords = bbCoords.asFloatBuffer();
+ quadCoords.put(QUAD_COORDS);
+ quadCoords.position(0);
+
+ ByteBuffer bbTexCoordsTransformed =
+ ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
+ bbTexCoordsTransformed.order(ByteOrder.nativeOrder());
+ quadTexCoords = bbTexCoordsTransformed.asFloatBuffer();
+
+ int vertexShader =
+ ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_NAME);
+ int fragmentShader =
+ ShaderUtil.loadGLShader(TAG, context, GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_NAME);
+
+ quadProgram = GLES20.glCreateProgram();
+ GLES20.glAttachShader(quadProgram, vertexShader);
+ GLES20.glAttachShader(quadProgram, fragmentShader);
+ GLES20.glLinkProgram(quadProgram);
+ GLES20.glUseProgram(quadProgram);
+
+ ShaderUtil.checkGLError(TAG, "Program creation");
+
+ quadPositionParam = GLES20.glGetAttribLocation(quadProgram, "a_Position");
+ quadTexCoordParam = GLES20.glGetAttribLocation(quadProgram, "a_TexCoord");
+
+ ShaderUtil.checkGLError(TAG, "Program parameters");
+ }
+
+ /**
+ * Draws the AR background image. The image will be drawn such that virtual content rendered with
+ * the matrices provided by {@link com.google.ar.core.Camera#getViewMatrix(float[], int)} and
+ * {@link com.google.ar.core.Camera#getProjectionMatrix(float[], int, float, float)} will
+ * accurately follow static physical objects. This must be called before drawing virtual
+ * content.
+ *
+ * @param frame The current {@code Frame} as returned by {@link Session#update()}.
+ */
+ public void draw(@NonNull Frame frame) {
+ // If display rotation changed (also includes view size change), we need to re-query the uv
+ // coordinates for the screen rect, as they may have changed as well.
+ if (frame.hasDisplayGeometryChanged()) {
+ frame.transformCoordinates2d(
+ Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
+ quadCoords,
+ Coordinates2d.TEXTURE_NORMALIZED,
+ quadTexCoords);
+ }
+
+ if (frame.getTimestamp() == 0) {
+ // Suppress rendering if the camera did not produce the first frame yet. This is to avoid
+ // drawing possible leftover data from previous sessions if the texture is reused.
+ return;
+ }
+
+ draw();
+ }
+
+ /**
+ * Draws the camera image using the currently configured {@link BackgroundRenderer#quadTexCoords}
+ * image texture coordinates.
+ *
+ *
The image will be center cropped if the camera sensor aspect ratio does not match the screen
+ * aspect ratio, which matches the cropping behavior of {@link
+ * Frame#transformCoordinates2d(Coordinates2d, float[], Coordinates2d, float[])}.
+ */
+ public void draw(
+ int imageWidth, int imageHeight, float screenAspectRatio, int cameraToDisplayRotation) {
+ // Crop the camera image to fit the screen aspect ratio.
+ float imageAspectRatio = (float) imageWidth / imageHeight;
+ float croppedWidth;
+ float croppedHeight;
+ if (screenAspectRatio < imageAspectRatio) {
+ croppedWidth = imageHeight * screenAspectRatio;
+ croppedHeight = imageHeight;
+ } else {
+ croppedWidth = imageWidth;
+ croppedHeight = imageWidth / screenAspectRatio;
+ }
+
+ float u = (imageWidth - croppedWidth) / imageWidth * 0.5f;
+ float v = (imageHeight - croppedHeight) / imageHeight * 0.5f;
+
+ float[] texCoordTransformed;
+ switch (cameraToDisplayRotation) {
+ case 90:
+ texCoordTransformed = new float[] {1 - u, 1 - v, u, 1 - v, 1 - u, v, u, v};
+ break;
+ case 180:
+ texCoordTransformed = new float[] {1 - u, v, 1 - u, 1 - v, u, v, u, 1 - v};
+ break;
+ case 270:
+ texCoordTransformed = new float[] {u, v, 1 - u, v, u, 1 - v, 1 - u, 1 - v};
+ break;
+ case 0:
+ texCoordTransformed = new float[] {u, 1 - v, u, v, 1 - u, 1 - v, 1 - u, v};
+ break;
+ default:
+ throw new IllegalArgumentException("Unhandled rotation: " + cameraToDisplayRotation);
+ }
+
+ // Write image texture coordinates.
+ quadTexCoords.position(0);
+ quadTexCoords.put(texCoordTransformed);
+
+ draw();
+ }
+
+ /**
+ * Draws the camera background image using the currently configured {@link
+ * BackgroundRenderer#quadTexCoords} image texture coordinates.
+ */
+ private void draw() {
+ // Ensure position is rewound before use.
+ quadTexCoords.position(0);
+
+ // No need to test or write depth, the screen quad has arbitrary depth, and is expected
+ // to be drawn first.
+ GLES20.glDisable(GLES20.GL_DEPTH_TEST);
+ GLES20.glDepthMask(false);
+
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
+
+ GLES20.glUseProgram(quadProgram);
+
+ // Set the vertex positions.
+ GLES20.glVertexAttribPointer(
+ quadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadCoords);
+
+ // Set the texture coordinates.
+ GLES20.glVertexAttribPointer(
+ quadTexCoordParam, TEXCOORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadTexCoords);
+
+ // Enable vertex arrays
+ GLES20.glEnableVertexAttribArray(quadPositionParam);
+ GLES20.glEnableVertexAttribArray(quadTexCoordParam);
+
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+
+ // Disable vertex arrays
+ GLES20.glDisableVertexAttribArray(quadPositionParam);
+ GLES20.glDisableVertexAttribArray(quadTexCoordParam);
+
+ // Restore the depth state for further drawing.
+ GLES20.glDepthMask(true);
+ GLES20.glEnable(GLES20.GL_DEPTH_TEST);
+
+ ShaderUtil.checkGLError(TAG, "BackgroundRendererDraw");
+ }
+
+ private static final float[] QUAD_COORDS =
+ new float[] {
+ -1.0f, -1.0f, -1.0f, +1.0f, +1.0f, -1.0f, +1.0f, +1.0f,
+ };
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/common/rendering/ObjectRenderer.java b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/ObjectRenderer.java
new file mode 100644
index 0000000..9dfc0df
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/ObjectRenderer.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * 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.ar.core.examples.java.common.rendering;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+import de.javagl.obj.Obj;
+import de.javagl.obj.ObjData;
+import de.javagl.obj.ObjReader;
+import de.javagl.obj.ObjUtils;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+
+/** Renders an object loaded from an OBJ file in OpenGL. */
+public class ObjectRenderer {
+ private static final String TAG = ObjectRenderer.class.getSimpleName();
+
+ /**
+ * Blend mode.
+ *
+ * @see #setBlendMode(BlendMode)
+ */
+ public enum BlendMode {
+ /** Multiplies the destination color by the source alpha. */
+ Shadow,
+ /** Normal alpha blending. */
+ Grid
+ }
+
+ // Shader names.
+ private static final String VERTEX_SHADER_NAME = "shaders/object.vert";
+ private static final String FRAGMENT_SHADER_NAME = "shaders/object.frag";
+
+ private static final int COORDS_PER_VERTEX = 3;
+ private static final float[] DEFAULT_COLOR = new float[] {0f, 0f, 0f, 0f};
+
+ // Note: the last component must be zero to avoid applying the translational part of the matrix.
+ private static final float[] LIGHT_DIRECTION = new float[] {0.250f, 0.866f, 0.433f, 0.0f};
+ private final float[] viewLightDirection = new float[4];
+
+ // Object vertex buffer variables.
+ private int vertexBufferId;
+ private int verticesBaseAddress;
+ private int texCoordsBaseAddress;
+ private int normalsBaseAddress;
+ private int indexBufferId;
+ private int indexCount;
+
+ private int program;
+ private final int[] textures = new int[1];
+
+ // Shader location: model view projection matrix.
+ private int modelViewUniform;
+ private int modelViewProjectionUniform;
+
+ // Shader location: object attributes.
+ private int positionAttribute;
+ private int normalAttribute;
+ private int texCoordAttribute;
+
+ // Shader location: texture sampler.
+ private int textureUniform;
+
+ // Shader location: environment properties.
+ private int lightingParametersUniform;
+
+ // Shader location: material properties.
+ private int materialParametersUniform;
+
+ // Shader location: color correction property
+ private int colorCorrectionParameterUniform;
+
+ // Shader location: object color property (to change the primary color of the object).
+ private int colorUniform;
+
+ private BlendMode blendMode = null;
+
+ // Temporary matrices allocated here to reduce number of allocations for each frame.
+ private final float[] modelMatrix = new float[16];
+ private final float[] modelViewMatrix = new float[16];
+ private final float[] modelViewProjectionMatrix = new float[16];
+
+ // Set some default material properties to use for lighting.
+ private float ambient = 0.3f;
+ private float diffuse = 1.0f;
+ private float specular = 1.0f;
+ private float specularPower = 6.0f;
+
+ public ObjectRenderer() {}
+
+ /**
+ * Creates and initializes OpenGL resources needed for rendering the model.
+ *
+ * @param context Context for loading the shader and below-named model and texture assets.
+ * @param objAssetName Name of the OBJ file containing the model geometry.
+ * @param diffuseTextureAssetName Name of the PNG file containing the diffuse texture map.
+ */
+ public void createOnGlThread(Context context, String objAssetName, String diffuseTextureAssetName)
+ throws IOException {
+ final int vertexShader =
+ ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_NAME);
+ final int fragmentShader =
+ ShaderUtil.loadGLShader(TAG, context, GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_NAME);
+
+ program = GLES20.glCreateProgram();
+ GLES20.glAttachShader(program, vertexShader);
+ GLES20.glAttachShader(program, fragmentShader);
+ GLES20.glLinkProgram(program);
+ GLES20.glUseProgram(program);
+
+ ShaderUtil.checkGLError(TAG, "Program creation");
+
+ modelViewUniform = GLES20.glGetUniformLocation(program, "u_ModelView");
+ modelViewProjectionUniform = GLES20.glGetUniformLocation(program, "u_ModelViewProjection");
+
+ positionAttribute = GLES20.glGetAttribLocation(program, "a_Position");
+ normalAttribute = GLES20.glGetAttribLocation(program, "a_Normal");
+ texCoordAttribute = GLES20.glGetAttribLocation(program, "a_TexCoord");
+
+ textureUniform = GLES20.glGetUniformLocation(program, "u_Texture");
+
+ lightingParametersUniform = GLES20.glGetUniformLocation(program, "u_LightingParameters");
+ materialParametersUniform = GLES20.glGetUniformLocation(program, "u_MaterialParameters");
+ colorCorrectionParameterUniform =
+ GLES20.glGetUniformLocation(program, "u_ColorCorrectionParameters");
+ colorUniform = GLES20.glGetUniformLocation(program, "u_ObjColor");
+
+ ShaderUtil.checkGLError(TAG, "Program parameters");
+
+ // Read the texture.
+ Bitmap textureBitmap =
+ BitmapFactory.decodeStream(context.getAssets().open(diffuseTextureAssetName));
+
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glGenTextures(textures.length, textures, 0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
+
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, textureBitmap, 0);
+ GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+
+ textureBitmap.recycle();
+
+ ShaderUtil.checkGLError(TAG, "Texture loading");
+
+ // Read the obj file.
+ InputStream objInputStream = context.getAssets().open(objAssetName);
+ Obj obj = ObjReader.read(objInputStream);
+
+ // Prepare the Obj so that its structure is suitable for
+ // rendering with OpenGL:
+ // 1. Triangulate it
+ // 2. Make sure that texture coordinates are not ambiguous
+ // 3. Make sure that normals are not ambiguous
+ // 4. Convert it to single-indexed data
+ obj = ObjUtils.convertToRenderable(obj);
+
+ // OpenGL does not use Java arrays. ByteBuffers are used instead to provide data in a format
+ // that OpenGL understands.
+
+ // Obtain the data from the OBJ, as direct buffers:
+ IntBuffer wideIndices = ObjData.getFaceVertexIndices(obj, 3);
+ FloatBuffer vertices = ObjData.getVertices(obj);
+ FloatBuffer texCoords = ObjData.getTexCoords(obj, 2);
+ FloatBuffer normals = ObjData.getNormals(obj);
+
+ // Convert int indices to shorts for GL ES 2.0 compatibility
+ ShortBuffer indices =
+ ByteBuffer.allocateDirect(2 * wideIndices.limit())
+ .order(ByteOrder.nativeOrder())
+ .asShortBuffer();
+ while (wideIndices.hasRemaining()) {
+ indices.put((short) wideIndices.get());
+ }
+ indices.rewind();
+
+ int[] buffers = new int[2];
+ GLES20.glGenBuffers(2, buffers, 0);
+ vertexBufferId = buffers[0];
+ indexBufferId = buffers[1];
+
+ // Load vertex buffer
+ verticesBaseAddress = 0;
+ texCoordsBaseAddress = verticesBaseAddress + 4 * vertices.limit();
+ normalsBaseAddress = texCoordsBaseAddress + 4 * texCoords.limit();
+ final int totalBytes = normalsBaseAddress + 4 * normals.limit();
+
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBufferId);
+ GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, totalBytes, null, GLES20.GL_STATIC_DRAW);
+ GLES20.glBufferSubData(
+ GLES20.GL_ARRAY_BUFFER, verticesBaseAddress, 4 * vertices.limit(), vertices);
+ GLES20.glBufferSubData(
+ GLES20.GL_ARRAY_BUFFER, texCoordsBaseAddress, 4 * texCoords.limit(), texCoords);
+ GLES20.glBufferSubData(
+ GLES20.GL_ARRAY_BUFFER, normalsBaseAddress, 4 * normals.limit(), normals);
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ // Load index buffer
+ GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBufferId);
+ indexCount = indices.limit();
+ GLES20.glBufferData(
+ GLES20.GL_ELEMENT_ARRAY_BUFFER, 2 * indexCount, indices, GLES20.GL_STATIC_DRAW);
+ GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
+
+ ShaderUtil.checkGLError(TAG, "OBJ buffer load");
+
+ Matrix.setIdentityM(modelMatrix, 0);
+ }
+
+ /**
+ * Selects the blending mode for rendering.
+ *
+ * @param blendMode The blending mode. Null indicates no blending (opaque rendering).
+ */
+ public void setBlendMode(BlendMode blendMode) {
+ this.blendMode = blendMode;
+ }
+
+ /**
+ * Updates the object model matrix and applies scaling.
+ *
+ * @param modelMatrix A 4x4 model-to-world transformation matrix, stored in column-major order.
+ * @param scaleFactor A separate scaling factor to apply before the {@code modelMatrix}.
+ * @see android.opengl.Matrix
+ */
+ public void updateModelMatrix(float[] modelMatrix, float scaleFactor) {
+ float[] scaleMatrix = new float[16];
+ Matrix.setIdentityM(scaleMatrix, 0);
+ scaleMatrix[0] = scaleFactor;
+ scaleMatrix[5] = scaleFactor;
+ scaleMatrix[10] = scaleFactor;
+ Matrix.multiplyMM(this.modelMatrix, 0, modelMatrix, 0, scaleMatrix, 0);
+ }
+
+ /**
+ * Sets the surface characteristics of the rendered model.
+ *
+ * @param ambient Intensity of non-directional surface illumination.
+ * @param diffuse Diffuse (matte) surface reflectivity.
+ * @param specular Specular (shiny) surface reflectivity.
+ * @param specularPower Surface shininess. Larger values result in a smaller, sharper specular
+ * highlight.
+ */
+ public void setMaterialProperties(
+ float ambient, float diffuse, float specular, float specularPower) {
+ this.ambient = ambient;
+ this.diffuse = diffuse;
+ this.specular = specular;
+ this.specularPower = specularPower;
+ }
+
+ /**
+ * Draws the model.
+ *
+ * @param cameraView A 4x4 view matrix, in column-major order.
+ * @param cameraPerspective A 4x4 projection matrix, in column-major order.
+ * @param lightIntensity Illumination intensity. Combined with diffuse and specular material
+ * properties.
+ * @see #setBlendMode(BlendMode)
+ * @see #updateModelMatrix(float[], float)
+ * @see #setMaterialProperties(float, float, float, float)
+ * @see android.opengl.Matrix
+ */
+ public void draw(float[] cameraView, float[] cameraPerspective, float[] colorCorrectionRgba) {
+ draw(cameraView, cameraPerspective, colorCorrectionRgba, DEFAULT_COLOR);
+ }
+
+ public void draw(
+ float[] cameraView,
+ float[] cameraPerspective,
+ float[] colorCorrectionRgba,
+ float[] objColor) {
+
+ ShaderUtil.checkGLError(TAG, "Before draw");
+
+ // Build the ModelView and ModelViewProjection matrices
+ // for calculating object position and light.
+ Matrix.multiplyMM(modelViewMatrix, 0, cameraView, 0, modelMatrix, 0);
+ Matrix.multiplyMM(modelViewProjectionMatrix, 0, cameraPerspective, 0, modelViewMatrix, 0);
+
+ GLES20.glUseProgram(program);
+
+ // Set the lighting environment properties.
+ Matrix.multiplyMV(viewLightDirection, 0, modelViewMatrix, 0, LIGHT_DIRECTION, 0);
+ normalizeVec3(viewLightDirection);
+ GLES20.glUniform4f(
+ lightingParametersUniform,
+ viewLightDirection[0],
+ viewLightDirection[1],
+ viewLightDirection[2],
+ 1.f);
+ GLES20.glUniform4fv(colorCorrectionParameterUniform, 1, colorCorrectionRgba, 0);
+
+ // Set the object color property.
+ GLES20.glUniform4fv(colorUniform, 1, objColor, 0);
+
+ // Set the object material properties.
+ GLES20.glUniform4f(materialParametersUniform, ambient, diffuse, specular, specularPower);
+
+ // Attach the object texture.
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
+ GLES20.glUniform1i(textureUniform, 0);
+
+ // Set the vertex attributes.
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBufferId);
+
+ GLES20.glVertexAttribPointer(
+ positionAttribute, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, verticesBaseAddress);
+ GLES20.glVertexAttribPointer(normalAttribute, 3, GLES20.GL_FLOAT, false, 0, normalsBaseAddress);
+ GLES20.glVertexAttribPointer(
+ texCoordAttribute, 2, GLES20.GL_FLOAT, false, 0, texCoordsBaseAddress);
+
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ // Set the ModelViewProjection matrix in the shader.
+ GLES20.glUniformMatrix4fv(modelViewUniform, 1, false, modelViewMatrix, 0);
+ GLES20.glUniformMatrix4fv(modelViewProjectionUniform, 1, false, modelViewProjectionMatrix, 0);
+
+ // Enable vertex arrays
+ GLES20.glEnableVertexAttribArray(positionAttribute);
+ GLES20.glEnableVertexAttribArray(normalAttribute);
+ GLES20.glEnableVertexAttribArray(texCoordAttribute);
+
+ if (blendMode != null) {
+ GLES20.glDepthMask(false);
+ GLES20.glEnable(GLES20.GL_BLEND);
+ switch (blendMode) {
+ case Shadow:
+ // Multiplicative blending function for Shadow.
+ GLES20.glBlendFunc(GLES20.GL_ZERO, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+ break;
+ case Grid:
+ // Grid, additive blending function.
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+ break;
+ }
+ }
+
+ GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBufferId);
+ GLES20.glDrawElements(GLES20.GL_TRIANGLES, indexCount, GLES20.GL_UNSIGNED_SHORT, 0);
+ GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
+
+ if (blendMode != null) {
+ GLES20.glDisable(GLES20.GL_BLEND);
+ GLES20.glDepthMask(true);
+ }
+
+ // Disable vertex arrays
+ GLES20.glDisableVertexAttribArray(positionAttribute);
+ GLES20.glDisableVertexAttribArray(normalAttribute);
+ GLES20.glDisableVertexAttribArray(texCoordAttribute);
+
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+
+ ShaderUtil.checkGLError(TAG, "After draw");
+ }
+
+ private static void normalizeVec3(float[] v) {
+ float reciprocalLength = 1.0f / (float) Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
+ v[0] *= reciprocalLength;
+ v[1] *= reciprocalLength;
+ v[2] *= reciprocalLength;
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/common/rendering/PlaneRenderer.java b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/PlaneRenderer.java
new file mode 100644
index 0000000..c57c541
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/PlaneRenderer.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * 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.ar.core.examples.java.common.rendering;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+import com.google.ar.core.Camera;
+import com.google.ar.core.Plane;
+import com.google.ar.core.Pose;
+import com.google.ar.core.TrackingState;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Renders the detected AR planes. */
+public class PlaneRenderer {
+ private static final String TAG = PlaneRenderer.class.getSimpleName();
+
+ // Shader names.
+ private static final String VERTEX_SHADER_NAME = "shaders/plane.vert";
+ private static final String FRAGMENT_SHADER_NAME = "shaders/plane.frag";
+
+ private static final int BYTES_PER_FLOAT = Float.SIZE / 8;
+ private static final int BYTES_PER_SHORT = Short.SIZE / 8;
+ private static final int COORDS_PER_VERTEX = 3; // x, z, alpha
+
+ private static final int VERTS_PER_BOUNDARY_VERT = 2;
+ private static final int INDICES_PER_BOUNDARY_VERT = 3;
+ private static final int INITIAL_BUFFER_BOUNDARY_VERTS = 64;
+
+ private static final int INITIAL_VERTEX_BUFFER_SIZE_BYTES =
+ BYTES_PER_FLOAT * COORDS_PER_VERTEX * VERTS_PER_BOUNDARY_VERT * INITIAL_BUFFER_BOUNDARY_VERTS;
+
+ private static final int INITIAL_INDEX_BUFFER_SIZE_BYTES =
+ BYTES_PER_SHORT
+ * INDICES_PER_BOUNDARY_VERT
+ * INDICES_PER_BOUNDARY_VERT
+ * INITIAL_BUFFER_BOUNDARY_VERTS;
+
+ private static final float FADE_RADIUS_M = 0.25f;
+ private static final float DOTS_PER_METER = 10.0f;
+ private static final float EQUILATERAL_TRIANGLE_SCALE = (float) (1 / Math.sqrt(3));
+
+ // Using the "signed distance field" approach to render sharp lines and circles.
+ // {dotThreshold, lineThreshold, lineFadeSpeed, occlusionScale}
+ // dotThreshold/lineThreshold: red/green intensity above which dots/lines are present
+ // lineFadeShrink: lines will fade in between alpha = 1-(1/lineFadeShrink) and 1.0
+ // occlusionShrink: occluded planes will fade out between alpha = 0 and 1/occlusionShrink
+ private static final float[] GRID_CONTROL = {0.2f, 0.4f, 2.0f, 1.5f};
+
+ private int planeProgram;
+ private final int[] textures = new int[1];
+
+ private int planeXZPositionAlphaAttribute;
+
+ private int planeModelUniform;
+ private int planeNormalUniform;
+ private int planeModelViewProjectionUniform;
+ private int textureUniform;
+ private int lineColorUniform;
+ private int dotColorUniform;
+ private int gridControlUniform;
+ private int planeUvMatrixUniform;
+
+ private FloatBuffer vertexBuffer =
+ ByteBuffer.allocateDirect(INITIAL_VERTEX_BUFFER_SIZE_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ private ShortBuffer indexBuffer =
+ ByteBuffer.allocateDirect(INITIAL_INDEX_BUFFER_SIZE_BYTES)
+ .order(ByteOrder.nativeOrder())
+ .asShortBuffer();
+
+ // Temporary lists/matrices allocated here to reduce number of allocations for each frame.
+ private final float[] modelMatrix = new float[16];
+ private final float[] modelViewMatrix = new float[16];
+ private final float[] modelViewProjectionMatrix = new float[16];
+ private final float[] planeColor = new float[4];
+ private final float[] planeAngleUvMatrix =
+ new float[4]; // 2x2 rotation matrix applied to uv coords.
+
+ private final Map planeIndexMap = new HashMap<>();
+
+ public PlaneRenderer() {}
+
+ /**
+ * Allocates and initializes OpenGL resources needed by the plane renderer. Must be called on the
+ * OpenGL thread, typically in {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)}.
+ *
+ * @param context Needed to access shader source and texture PNG.
+ * @param gridDistanceTextureName Name of the PNG file containing the grid texture.
+ */
+ public void createOnGlThread(Context context, String gridDistanceTextureName) throws IOException {
+ int vertexShader =
+ ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_NAME);
+ int passthroughShader =
+ ShaderUtil.loadGLShader(TAG, context, GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_NAME);
+
+ planeProgram = GLES20.glCreateProgram();
+ GLES20.glAttachShader(planeProgram, vertexShader);
+ GLES20.glAttachShader(planeProgram, passthroughShader);
+ GLES20.glLinkProgram(planeProgram);
+ GLES20.glUseProgram(planeProgram);
+
+ ShaderUtil.checkGLError(TAG, "Program creation");
+
+ // Read the texture.
+ Bitmap textureBitmap =
+ BitmapFactory.decodeStream(context.getAssets().open(gridDistanceTextureName));
+
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glGenTextures(textures.length, textures, 0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
+
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, textureBitmap, 0);
+ GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+
+ ShaderUtil.checkGLError(TAG, "Texture loading");
+
+ planeXZPositionAlphaAttribute = GLES20.glGetAttribLocation(planeProgram, "a_XZPositionAlpha");
+
+ planeModelUniform = GLES20.glGetUniformLocation(planeProgram, "u_Model");
+ planeNormalUniform = GLES20.glGetUniformLocation(planeProgram, "u_Normal");
+ planeModelViewProjectionUniform =
+ GLES20.glGetUniformLocation(planeProgram, "u_ModelViewProjection");
+ textureUniform = GLES20.glGetUniformLocation(planeProgram, "u_Texture");
+ lineColorUniform = GLES20.glGetUniformLocation(planeProgram, "u_lineColor");
+ dotColorUniform = GLES20.glGetUniformLocation(planeProgram, "u_dotColor");
+ gridControlUniform = GLES20.glGetUniformLocation(planeProgram, "u_gridControl");
+ planeUvMatrixUniform = GLES20.glGetUniformLocation(planeProgram, "u_PlaneUvMatrix");
+
+ ShaderUtil.checkGLError(TAG, "Program parameters");
+ }
+
+ /** Updates the plane model transform matrix and extents. */
+ private void updatePlaneParameters(
+ float[] planeMatrix, float extentX, float extentZ, FloatBuffer boundary) {
+ System.arraycopy(planeMatrix, 0, modelMatrix, 0, 16);
+ if (boundary == null) {
+ vertexBuffer.limit(0);
+ indexBuffer.limit(0);
+ return;
+ }
+
+ // Generate a new set of vertices and a corresponding triangle strip index set so that
+ // the plane boundary polygon has a fading edge. This is done by making a copy of the
+ // boundary polygon vertices and scaling it down around center to push it inwards. Then
+ // the index buffer is setup accordingly.
+ boundary.rewind();
+ int boundaryVertices = boundary.limit() / 2;
+ int numVertices;
+ int numIndices;
+
+ numVertices = boundaryVertices * VERTS_PER_BOUNDARY_VERT;
+ // drawn as GL_TRIANGLE_STRIP with 3n-2 triangles (n-2 for fill, 2n for perimeter).
+ numIndices = boundaryVertices * INDICES_PER_BOUNDARY_VERT;
+
+ if (vertexBuffer.capacity() < numVertices * COORDS_PER_VERTEX) {
+ int size = vertexBuffer.capacity();
+ while (size < numVertices * COORDS_PER_VERTEX) {
+ size *= 2;
+ }
+ vertexBuffer =
+ ByteBuffer.allocateDirect(BYTES_PER_FLOAT * size)
+ .order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ }
+ vertexBuffer.rewind();
+ vertexBuffer.limit(numVertices * COORDS_PER_VERTEX);
+
+ if (indexBuffer.capacity() < numIndices) {
+ int size = indexBuffer.capacity();
+ while (size < numIndices) {
+ size *= 2;
+ }
+ indexBuffer =
+ ByteBuffer.allocateDirect(BYTES_PER_SHORT * size)
+ .order(ByteOrder.nativeOrder())
+ .asShortBuffer();
+ }
+ indexBuffer.rewind();
+ indexBuffer.limit(numIndices);
+
+ // Note: when either dimension of the bounding box is smaller than 2*FADE_RADIUS_M we
+ // generate a bunch of 0-area triangles. These don't get rendered though so it works
+ // out ok.
+ float xScale = Math.max((extentX - 2 * FADE_RADIUS_M) / extentX, 0.0f);
+ float zScale = Math.max((extentZ - 2 * FADE_RADIUS_M) / extentZ, 0.0f);
+
+ while (boundary.hasRemaining()) {
+ float x = boundary.get();
+ float z = boundary.get();
+ vertexBuffer.put(x);
+ vertexBuffer.put(z);
+ vertexBuffer.put(0.0f);
+ vertexBuffer.put(x * xScale);
+ vertexBuffer.put(z * zScale);
+ vertexBuffer.put(1.0f);
+ }
+
+ // step 1, perimeter
+ indexBuffer.put((short) ((boundaryVertices - 1) * 2));
+ for (int i = 0; i < boundaryVertices; ++i) {
+ indexBuffer.put((short) (i * 2));
+ indexBuffer.put((short) (i * 2 + 1));
+ }
+ indexBuffer.put((short) 1);
+ // This leaves us on the interior edge of the perimeter between the inset vertices
+ // for boundary verts n-1 and 0.
+
+ // step 2, interior:
+ for (int i = 1; i < boundaryVertices / 2; ++i) {
+ indexBuffer.put((short) ((boundaryVertices - 1 - i) * 2 + 1));
+ indexBuffer.put((short) (i * 2 + 1));
+ }
+ if (boundaryVertices % 2 != 0) {
+ indexBuffer.put((short) ((boundaryVertices / 2) * 2 + 1));
+ }
+ }
+
+ private void draw(float[] cameraView, float[] cameraPerspective, float[] planeNormal) {
+ // Build the ModelView and ModelViewProjection matrices
+ // for calculating cube position and light.
+ Matrix.multiplyMM(modelViewMatrix, 0, cameraView, 0, modelMatrix, 0);
+ Matrix.multiplyMM(modelViewProjectionMatrix, 0, cameraPerspective, 0, modelViewMatrix, 0);
+
+ // Set the position of the plane
+ vertexBuffer.rewind();
+ GLES20.glVertexAttribPointer(
+ planeXZPositionAlphaAttribute,
+ COORDS_PER_VERTEX,
+ GLES20.GL_FLOAT,
+ false,
+ BYTES_PER_FLOAT * COORDS_PER_VERTEX,
+ vertexBuffer);
+
+ // Set the Model and ModelViewProjection matrices in the shader.
+ GLES20.glUniformMatrix4fv(planeModelUniform, 1, false, modelMatrix, 0);
+ GLES20.glUniform3f(planeNormalUniform, planeNormal[0], planeNormal[1], planeNormal[2]);
+ GLES20.glUniformMatrix4fv(
+ planeModelViewProjectionUniform, 1, false, modelViewProjectionMatrix, 0);
+
+ indexBuffer.rewind();
+ GLES20.glDrawElements(
+ GLES20.GL_TRIANGLE_STRIP, indexBuffer.limit(), GLES20.GL_UNSIGNED_SHORT, indexBuffer);
+ ShaderUtil.checkGLError(TAG, "Drawing plane");
+ }
+
+ static class SortablePlane {
+ final float distance;
+ final Plane plane;
+
+ SortablePlane(float distance, Plane plane) {
+ this.distance = distance;
+ this.plane = plane;
+ }
+ }
+
+ /**
+ * Draws the collection of tracked planes, with closer planes hiding more distant ones.
+ *
+ * @param allPlanes The collection of planes to draw.
+ * @param cameraPose The pose of the camera, as returned by {@link Camera#getPose()}
+ * @param cameraPerspective The projection matrix, as returned by {@link
+ * Camera#getProjectionMatrix(float[], int, float, float)}
+ */
+ public void drawPlanes(Collection allPlanes, Pose cameraPose, float[] cameraPerspective) {
+ // Planes must be sorted by distance from camera so that we draw closer planes first, and
+ // they occlude the farther planes.
+ List sortedPlanes = new ArrayList<>();
+
+ for (Plane plane : allPlanes) {
+ if (plane.getTrackingState() != TrackingState.TRACKING || plane.getSubsumedBy() != null) {
+ continue;
+ }
+
+ float distance = calculateDistanceToPlane(plane.getCenterPose(), cameraPose);
+ if (distance < 0) { // Plane is back-facing.
+ continue;
+ }
+ sortedPlanes.add(new SortablePlane(distance, plane));
+ }
+ Collections.sort(
+ sortedPlanes,
+ new Comparator() {
+ @Override
+ public int compare(SortablePlane a, SortablePlane b) {
+ return Float.compare(a.distance, b.distance);
+ }
+ });
+
+ float[] cameraView = new float[16];
+ cameraPose.inverse().toMatrix(cameraView, 0);
+
+ // Planes are drawn with additive blending, masked by the alpha channel for occlusion.
+
+ // Start by clearing the alpha channel of the color buffer to 1.0.
+ GLES20.glClearColor(1, 1, 1, 1);
+ GLES20.glColorMask(false, false, false, true);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ GLES20.glColorMask(true, true, true, true);
+
+ // Disable depth write.
+ GLES20.glDepthMask(false);
+
+ // Additive blending, masked by alpha channel, clearing alpha channel.
+ GLES20.glEnable(GLES20.GL_BLEND);
+ GLES20.glBlendFuncSeparate(
+ GLES20.GL_DST_ALPHA, GLES20.GL_ONE, // RGB (src, dest)
+ GLES20.GL_ZERO, GLES20.GL_ONE_MINUS_SRC_ALPHA); // ALPHA (src, dest)
+
+ // Set up the shader.
+ GLES20.glUseProgram(planeProgram);
+
+ // Attach the texture.
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
+ GLES20.glUniform1i(textureUniform, 0);
+
+ // Shared fragment uniforms.
+ GLES20.glUniform4fv(gridControlUniform, 1, GRID_CONTROL, 0);
+
+ // Enable vertex arrays
+ GLES20.glEnableVertexAttribArray(planeXZPositionAlphaAttribute);
+
+ ShaderUtil.checkGLError(TAG, "Setting up to draw planes");
+
+ for (SortablePlane sortedPlane : sortedPlanes) {
+ Plane plane = sortedPlane.plane;
+ float[] planeMatrix = new float[16];
+ plane.getCenterPose().toMatrix(planeMatrix, 0);
+
+ float[] normal = new float[3];
+ // Get transformed Y axis of plane's coordinate system.
+ plane.getCenterPose().getTransformedAxis(1, 1.0f, normal, 0);
+
+ updatePlaneParameters(
+ planeMatrix, plane.getExtentX(), plane.getExtentZ(), plane.getPolygon());
+
+ // Get plane index. Keep a map to assign same indices to same planes.
+ Integer planeIndex = planeIndexMap.get(plane);
+ if (planeIndex == null) {
+ planeIndex = planeIndexMap.size();
+ planeIndexMap.put(plane, planeIndex);
+ }
+
+ // Set plane color. Computed deterministically from the Plane index.
+ int colorIndex = planeIndex % PLANE_COLORS_RGBA.length;
+ colorRgbaToFloat(planeColor, PLANE_COLORS_RGBA[colorIndex]);
+ GLES20.glUniform4fv(lineColorUniform, 1, planeColor, 0);
+ GLES20.glUniform4fv(dotColorUniform, 1, planeColor, 0);
+
+ // Each plane will have its own angle offset from others, to make them easier to
+ // distinguish. Compute a 2x2 rotation matrix from the angle.
+ float angleRadians = planeIndex * 0.144f;
+ float uScale = DOTS_PER_METER;
+ float vScale = DOTS_PER_METER * EQUILATERAL_TRIANGLE_SCALE;
+ planeAngleUvMatrix[0] = +(float) Math.cos(angleRadians) * uScale;
+ planeAngleUvMatrix[1] = -(float) Math.sin(angleRadians) * vScale;
+ planeAngleUvMatrix[2] = +(float) Math.sin(angleRadians) * uScale;
+ planeAngleUvMatrix[3] = +(float) Math.cos(angleRadians) * vScale;
+ GLES20.glUniformMatrix2fv(planeUvMatrixUniform, 1, false, planeAngleUvMatrix, 0);
+
+ draw(cameraView, cameraPerspective, normal);
+ }
+
+ // Clean up the state we set
+ GLES20.glDisableVertexAttribArray(planeXZPositionAlphaAttribute);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+ GLES20.glDisable(GLES20.GL_BLEND);
+ GLES20.glDepthMask(true);
+ GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
+
+ ShaderUtil.checkGLError(TAG, "Cleaning up after drawing planes");
+ }
+
+ // Calculate the normal distance to plane from cameraPose, the given planePose should have y axis
+ // parallel to plane's normal, for example plane's center pose or hit test pose.
+ public static float calculateDistanceToPlane(Pose planePose, Pose cameraPose) {
+ float[] normal = new float[3];
+ float cameraX = cameraPose.tx();
+ float cameraY = cameraPose.ty();
+ float cameraZ = cameraPose.tz();
+ // Get transformed Y axis of plane's coordinate system.
+ planePose.getTransformedAxis(1, 1.0f, normal, 0);
+ // Compute dot product of plane's normal with vector from camera to plane center.
+ return (cameraX - planePose.tx()) * normal[0]
+ + (cameraY - planePose.ty()) * normal[1]
+ + (cameraZ - planePose.tz()) * normal[2];
+ }
+
+ private static void colorRgbaToFloat(float[] planeColor, int colorRgba) {
+ planeColor[0] = ((float) ((colorRgba >> 24) & 0xff)) / 255.0f;
+ planeColor[1] = ((float) ((colorRgba >> 16) & 0xff)) / 255.0f;
+ planeColor[2] = ((float) ((colorRgba >> 8) & 0xff)) / 255.0f;
+ planeColor[3] = ((float) ((colorRgba >> 0) & 0xff)) / 255.0f;
+ }
+
+ private static final int[] PLANE_COLORS_RGBA = {
+ 0xFFFFFFFF,
+ 0xF44336FF,
+ 0xE91E63FF,
+ 0x9C27B0FF,
+ 0x673AB7FF,
+ 0x3F51B5FF,
+ 0x2196F3FF,
+ 0x03A9F4FF,
+ 0x00BCD4FF,
+ 0x009688FF,
+ 0x4CAF50FF,
+ 0x8BC34AFF,
+ 0xCDDC39FF,
+ 0xFFEB3BFF,
+ 0xFFC107FF,
+ 0xFF9800FF,
+ };
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/common/rendering/PointCloudRenderer.java b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/PointCloudRenderer.java
new file mode 100644
index 0000000..fb02149
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/PointCloudRenderer.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * 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.ar.core.examples.java.common.rendering;
+
+import android.content.Context;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+import android.opengl.Matrix;
+import com.google.ar.core.PointCloud;
+import java.io.IOException;
+
+/** Renders a point cloud. */
+public class PointCloudRenderer {
+ private static final String TAG = PointCloud.class.getSimpleName();
+
+ // Shader names.
+ private static final String VERTEX_SHADER_NAME = "shaders/point_cloud.vert";
+ private static final String FRAGMENT_SHADER_NAME = "shaders/point_cloud.frag";
+
+ private static final int BYTES_PER_FLOAT = Float.SIZE / 8;
+ private static final int FLOATS_PER_POINT = 4; // X,Y,Z,confidence.
+ private static final int BYTES_PER_POINT = BYTES_PER_FLOAT * FLOATS_PER_POINT;
+ private static final int INITIAL_BUFFER_POINTS = 1000;
+
+ private int vbo;
+ private int vboSize;
+
+ private int programName;
+ private int positionAttribute;
+ private int modelViewProjectionUniform;
+ private int colorUniform;
+ private int pointSizeUniform;
+
+ private int numPoints = 0;
+
+ // Keep track of the last point cloud rendered to avoid updating the VBO if point cloud
+ // was not changed. Do this using the timestamp since we can't compare PointCloud objects.
+ private long lastTimestamp = 0;
+
+ public PointCloudRenderer() {}
+
+ /**
+ * Allocates and initializes OpenGL resources needed by the plane renderer. Must be called on the
+ * OpenGL thread, typically in {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)}.
+ *
+ * @param context Needed to access shader source.
+ */
+ public void createOnGlThread(Context context) throws IOException {
+ ShaderUtil.checkGLError(TAG, "before create");
+
+ int[] buffers = new int[1];
+ GLES20.glGenBuffers(1, buffers, 0);
+ vbo = buffers[0];
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo);
+
+ vboSize = INITIAL_BUFFER_POINTS * BYTES_PER_POINT;
+ GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vboSize, null, GLES20.GL_DYNAMIC_DRAW);
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ ShaderUtil.checkGLError(TAG, "buffer alloc");
+
+ int vertexShader =
+ ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_NAME);
+ int passthroughShader =
+ ShaderUtil.loadGLShader(TAG, context, GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_NAME);
+
+ programName = GLES20.glCreateProgram();
+ GLES20.glAttachShader(programName, vertexShader);
+ GLES20.glAttachShader(programName, passthroughShader);
+ GLES20.glLinkProgram(programName);
+ GLES20.glUseProgram(programName);
+
+ ShaderUtil.checkGLError(TAG, "program");
+
+ positionAttribute = GLES20.glGetAttribLocation(programName, "a_Position");
+ colorUniform = GLES20.glGetUniformLocation(programName, "u_Color");
+ modelViewProjectionUniform = GLES20.glGetUniformLocation(programName, "u_ModelViewProjection");
+ pointSizeUniform = GLES20.glGetUniformLocation(programName, "u_PointSize");
+
+ ShaderUtil.checkGLError(TAG, "program params");
+ }
+
+ /**
+ * Updates the OpenGL buffer contents to the provided point. Repeated calls with the same point
+ * cloud will be ignored.
+ */
+ public void update(PointCloud cloud) {
+ if (cloud.getTimestamp() == lastTimestamp) {
+ // Redundant call.
+ return;
+ }
+ ShaderUtil.checkGLError(TAG, "before update");
+
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo);
+ lastTimestamp = cloud.getTimestamp();
+
+ // If the VBO is not large enough to fit the new point cloud, resize it.
+ numPoints = cloud.getPoints().remaining() / FLOATS_PER_POINT;
+ if (numPoints * BYTES_PER_POINT > vboSize) {
+ while (numPoints * BYTES_PER_POINT > vboSize) {
+ vboSize *= 2;
+ }
+ GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vboSize, null, GLES20.GL_DYNAMIC_DRAW);
+ }
+ GLES20.glBufferSubData(
+ GLES20.GL_ARRAY_BUFFER, 0, numPoints * BYTES_PER_POINT, cloud.getPoints());
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ ShaderUtil.checkGLError(TAG, "after update");
+ }
+
+ /**
+ * Renders the point cloud. ARCore point cloud is given in world space.
+ *
+ * @param cameraView the camera view matrix for this frame, typically from {@link
+ * com.google.ar.core.Camera#getViewMatrix(float[], int)}.
+ * @param cameraPerspective the camera projection matrix for this frame, typically from {@link
+ * com.google.ar.core.Camera#getProjectionMatrix(float[], int, float, float)}.
+ */
+ public void draw(float[] cameraView, float[] cameraPerspective) {
+ float[] modelViewProjection = new float[16];
+ Matrix.multiplyMM(modelViewProjection, 0, cameraPerspective, 0, cameraView, 0);
+
+ ShaderUtil.checkGLError(TAG, "Before draw");
+
+ GLES20.glUseProgram(programName);
+ GLES20.glEnableVertexAttribArray(positionAttribute);
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo);
+ GLES20.glVertexAttribPointer(positionAttribute, 4, GLES20.GL_FLOAT, false, BYTES_PER_POINT, 0);
+ GLES20.glUniform4f(colorUniform, 31.0f / 255.0f, 188.0f / 255.0f, 210.0f / 255.0f, 1.0f);
+ GLES20.glUniformMatrix4fv(modelViewProjectionUniform, 1, false, modelViewProjection, 0);
+ GLES20.glUniform1f(pointSizeUniform, 5.0f);
+
+ GLES20.glDrawArrays(GLES20.GL_POINTS, 0, numPoints);
+ GLES20.glDisableVertexAttribArray(positionAttribute);
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+ ShaderUtil.checkGLError(TAG, "Draw");
+ }
+}
diff --git a/app/src/main/java/com/google/ar/core/examples/java/common/rendering/ShaderUtil.java b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/ShaderUtil.java
new file mode 100644
index 0000000..340e977
--- /dev/null
+++ b/app/src/main/java/com/google/ar/core/examples/java/common/rendering/ShaderUtil.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ * 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.ar.core.examples.java.common.rendering;
+
+import android.content.Context;
+import android.opengl.GLES20;
+import android.util.Log;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/** Shader helper functions. */
+public class ShaderUtil {
+ /**
+ * Converts a raw text file, saved as a resource, into an OpenGL ES shader.
+ *
+ * @param type The type of shader we will be creating.
+ * @param filename The filename of the asset file about to be turned into a shader.
+ * @return The shader object handler.
+ */
+ public static int loadGLShader(String tag, Context context, int type, String filename)
+ throws IOException {
+ String code = readRawTextFileFromAssets(context, filename);
+ int shader = GLES20.glCreateShader(type);
+ GLES20.glShaderSource(shader, code);
+ GLES20.glCompileShader(shader);
+
+ // Get the compilation status.
+ final int[] compileStatus = new int[1];
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
+
+ // If the compilation failed, delete the shader.
+ if (compileStatus[0] == 0) {
+ Log.e(tag, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shader));
+ GLES20.glDeleteShader(shader);
+ shader = 0;
+ }
+
+ if (shader == 0) {
+ throw new RuntimeException("Error creating shader.");
+ }
+
+ return shader;
+ }
+
+ /**
+ * Checks if we've had an error inside of OpenGL ES, and if so what that error is.
+ *
+ * @param label Label to report in case of error.
+ * @throws RuntimeException If an OpenGL error is detected.
+ */
+ public static void checkGLError(String tag, String label) {
+ int lastError = GLES20.GL_NO_ERROR;
+ // Drain the queue of all errors.
+ int error;
+ while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+ Log.e(tag, label + ": glError " + error);
+ lastError = error;
+ }
+ if (lastError != GLES20.GL_NO_ERROR) {
+ throw new RuntimeException(label + ": glError " + lastError);
+ }
+ }
+
+ /**
+ * Converts a raw text file into a string.
+ *
+ * @param filename The filename of the asset file about to be turned into a shader.
+ * @return The context of the text file, or null in case of error.
+ */
+ private static String readRawTextFileFromAssets(Context context, String filename)
+ throws IOException {
+ try (InputStream inputStream = context.getAssets().open(filename);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append("\n");
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..3ecb8ae
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..bde73c5
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/resolve_dialog.xml b/app/src/main/res/layout/resolve_dialog.xml
new file mode 100644
index 0000000..c3a5fa6
--- /dev/null
+++ b/app/src/main/res/layout/resolve_dialog.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..9dfc6fa
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,49 @@
+
+
+
+
+ Cloud Anchor Example
+ Cancel
+ Host
+ Host an Anchor
+ Host
+ 0000
+ Resolve
+
+
+ Resolve
+ Resolve an Anchor
+ Enter Room Code
+
+
+ Now hosting anchor...
+ Please install ARCore.
+ Please update ARCore.
+ Please update the app with a newer version of the ARCore SDK.
+ This device does not support ARCore.
+ Camera unavailable. Please restart the app.
+ The anchor ID was shared via Firebase.
+ Dismiss
+ There was a Firebase Error. Please check Logcat for more details.
+ Hosting Error: %1$s
+ Please select Host or Resolve to continue.
+ Now in Hosting Mode. Press Cancel to Exit.
+ Now in Resolving Mode. Press Cancel to Exit.
+ Resolving Error: %1$s
+ The anchor was successfully resolved.
+ The room code is now available. Please place an anchor to host.
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d9fc77a
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..d9fe056
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,26 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.3.2'
+ classpath 'com.google.gms:google-services:3.2.0'
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ mavenLocal()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..aac7c9b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..87b738c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..5ae2618
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Nov 20 10:27:45 PST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..af6708f
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..6d57edc
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'