getCurrentSavedDeviceParams();
+void scanQrCodeAndSaveDeviceParams();
+void saveDeviceParams(const uint8_t* uri, int size);
+int getDeviceParamsChangedCount();
+} // namespace cardboard::qrcode
+
+#endif // CARDBOARD_SDK_QR_CODE_H_
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/AndroidManifest.xml b/mode/libraries/vr/libs/sdk/qrcode/android/AndroidManifest.xml
new file mode 100644
index 00000000..a9e2ab60
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/AndroidManifest.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/HeadsetDetectionActivity.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/HeadsetDetectionActivity.java
new file mode 100644
index 00000000..809afb57
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/HeadsetDetectionActivity.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import android.widget.Toast;
+import com.google.cardboard.sdk.qrcode.CardboardParamsUtils;
+
+/**
+ * A very light-weight activity with no layout, whose sole purpose is to react to external intents
+ * containing cardboard V1 NFC tag.
+ */
+public class HeadsetDetectionActivity extends AppCompatActivity {
+
+ /** Legacy URI scheme used in original Cardboard NFC tag. */
+ private static final String URI_SCHEME_LEGACY_CARDBOARD = "cardboard";
+
+ /** Legacy URI host used in original Cardboard NFC tag. */
+ private static final String URI_HOST_LEGACY_CARDBOARD = "v1.0.0";
+
+ /** URI of original cardboard NFC. */
+ private static final Uri URI_ORIGINAL_CARDBOARD_NFC =
+ new Uri.Builder()
+ .scheme(URI_SCHEME_LEGACY_CARDBOARD)
+ .authority(URI_HOST_LEGACY_CARDBOARD)
+ .build();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent() != null) {
+ processStartupIntent(getIntent());
+ }
+ finish();
+ }
+
+ // Checks whether the startup intent contains cardboard v1 NFC tag.
+ // If that's the case, updates the parameters.
+ private void processStartupIntent(Intent startupIntent) {
+ if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(startupIntent.getAction())
+ && startupIntent.getData() != null) {
+ // Saves V1 Cardboard params.
+ Uri uri = startupIntent.getData();
+ if (URI_ORIGINAL_CARDBOARD_NFC.equals(uri)) {
+ CardboardParamsUtils.saveCardboardV1DeviceParams(getApplicationContext());
+ }
+
+ Toast.makeText(this, R.string.viewer_detected, Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/QrCodeCaptureActivity.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/QrCodeCaptureActivity.java
new file mode 100755
index 00000000..10c4107b
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/QrCodeCaptureActivity.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk;
+
+import android.Manifest;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.vision.MultiProcessor;
+import com.google.android.gms.vision.barcode.Barcode;
+import com.google.android.gms.vision.barcode.BarcodeDetector;
+import com.google.cardboard.sdk.qrcode.CardboardParamsUtils;
+import com.google.cardboard.sdk.qrcode.QrCodeContentProcessor;
+import com.google.cardboard.sdk.qrcode.QrCodeTracker;
+import com.google.cardboard.sdk.qrcode.QrCodeTrackerFactory;
+import com.google.cardboard.sdk.qrcode.camera.CameraSource;
+import com.google.cardboard.sdk.qrcode.camera.CameraSourcePreview;
+import java.io.IOException;
+
+/**
+ * Manages the QR code capture activity. It scans permanently with the camera until it finds a valid
+ * QR code.
+ */
+public class QrCodeCaptureActivity extends AppCompatActivity
+ implements QrCodeTracker.Listener, QrCodeContentProcessor.Listener {
+ private static final String TAG = QrCodeCaptureActivity.class.getSimpleName();
+
+ // Intent request code to handle updating play services if needed.
+ private static final int RC_HANDLE_GMS = 9001;
+
+ // Permission request codes
+ private static final int PERMISSIONS_REQUEST_CODE = 2;
+
+ // Min sdk version required for google play services.
+ private static final int MIN_SDK_VERSION = 23;
+
+ private CameraSource cameraSource;
+ private CameraSourcePreview cameraSourcePreview;
+
+ // Flag used to avoid saving the device parameters more than once.
+ private static boolean qrCodeSaved = false;
+
+ /** Initializes the UI and creates the detector pipeline. */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.qr_code_capture);
+
+ cameraSourcePreview = findViewById(R.id.preview);
+ }
+
+ /**
+ * Checks for CAMERA permission.
+ *
+ * @return whether CAMERA permission is already granted.
+ */
+ private boolean isCameraEnabled() {
+ return ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * Checks for WRITE_EXTERNAL_STORAGE permission.
+ *
+ * @return whether WRITE_EXTERNAL_STORAGE permission is already granted.
+ */
+ private boolean isWriteExternalStoragePermissionsEnabled() {
+ return ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /** Handles the requests for activity permissions. */
+ private void requestPermissions() {
+ final String[] permissions =
+ VERSION.SDK_INT < VERSION_CODES.Q
+ ? new String[] {Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}
+ : new String[] {Manifest.permission.CAMERA};
+ ActivityCompat.requestPermissions(this, permissions, PERMISSIONS_REQUEST_CODE);
+ }
+
+ /**
+ * Callback for the result from requesting permissions.
+ *
+ * When Android SDK version is less than Q, both WRITE_EXTERNAL_STORAGE and CAMERA permissions
+ * are requested. Otherwise, only CAMERA permission is requested.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (VERSION.SDK_INT < VERSION_CODES.Q) {
+ if (!(isCameraEnabled() && isWriteExternalStoragePermissionsEnabled())) {
+ Log.i(TAG, getString(R.string.no_permissions));
+ Toast.makeText(this, R.string.no_permissions, Toast.LENGTH_LONG).show();
+ if (!ActivityCompat.shouldShowRequestPermissionRationale(
+ this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ || !ActivityCompat.shouldShowRequestPermissionRationale(
+ this, Manifest.permission.CAMERA)) {
+ // Permission denied with checking "Do not ask again".
+ Log.i(TAG, "Permission denied with checking \"Do not ask again\".");
+ launchPermissionsSettings();
+ }
+ finish();
+ }
+ } else {
+ if (!isCameraEnabled()) {
+ Log.i(TAG, getString(R.string.no_camera_permission));
+ Toast.makeText(this, R.string.no_camera_permission, Toast.LENGTH_LONG).show();
+ if (!ActivityCompat.shouldShowRequestPermissionRationale(
+ this, Manifest.permission.CAMERA)) {
+ // Permission denied with checking "Do not ask again". Note that in Android R "Do not ask
+ // again" is not available anymore.
+ Log.i(TAG, "Permission denied with checking \"Do not ask again\".");
+ launchPermissionsSettings();
+ }
+ finish();
+ }
+ }
+ }
+
+ private void launchPermissionsSettings() {
+ Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.fromParts("package", getPackageName(), null));
+ startActivity(intent);
+ }
+
+ /** Creates and starts the camera. */
+ private void createCameraSource() {
+ Context context = getApplicationContext();
+
+ BarcodeDetector qrCodeDetector =
+ new BarcodeDetector.Builder(context).setBarcodeFormats(Barcode.QR_CODE).build();
+
+ QrCodeTrackerFactory qrCodeFactory = new QrCodeTrackerFactory(this);
+
+ qrCodeDetector.setProcessor(new MultiProcessor.Builder<>(qrCodeFactory).build());
+
+ // Check that native dependencies are downloaded.
+ if (!qrCodeDetector.isOperational()) {
+ Toast.makeText(this, R.string.missing_dependencies, Toast.LENGTH_LONG).show();
+ Log.w(
+ TAG,
+ "QR Code detector is not operational. Try connecting to WiFi and updating Google Play"
+ + " Services or checking that the device storage isn't low.");
+ }
+
+ // Creates and starts the camera.
+ cameraSource = new CameraSource(getApplicationContext(), qrCodeDetector);
+ }
+
+ /** Restarts the camera. */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Checks for CAMERA permission and WRITE_EXTERNAL_STORAGE permission when running on Android P
+ // or below. If needed permissions are not granted, requests them.
+ if (!(isCameraEnabled()
+ && (VERSION.SDK_INT >= VERSION_CODES.Q || isWriteExternalStoragePermissionsEnabled()))) {
+ requestPermissions();
+ return;
+ }
+
+ createCameraSource();
+ qrCodeSaved = false;
+ startCameraSource();
+ }
+
+ /** Stops the camera. */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (cameraSourcePreview != null) {
+ cameraSourcePreview.stop();
+ cameraSourcePreview.release();
+ }
+ }
+
+ /** Starts or restarts the camera source, if it exists. */
+ private void startCameraSource() {
+ // Check that the device has play services available.
+ int code =
+ GoogleApiAvailability.getInstance()
+ .isGooglePlayServicesAvailable(getApplicationContext(), MIN_SDK_VERSION);
+ if (code != ConnectionResult.SUCCESS) {
+ Log.i(TAG, "isGooglePlayServicesAvailable() returned: " + new ConnectionResult(code));
+ Dialog dlg = GoogleApiAvailability.getInstance().getErrorDialog(this, code, RC_HANDLE_GMS);
+ dlg.show();
+ }
+
+ if (cameraSource != null) {
+ try {
+ cameraSourcePreview.start(cameraSource);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to start camera source.", e);
+ cameraSource.release();
+ cameraSource = null;
+ } catch (SecurityException e) {
+ Log.e(TAG, "Security exception: ", e);
+ }
+ Log.i(TAG, "cameraSourcePreview successfully started.");
+ }
+ }
+
+ /** Callback for when "SKIP" is touched */
+ public void skipQrCodeCapture(View view) {
+ Log.d(TAG, "QR code capture skipped");
+
+ // Check if there are already saved parameters, if not save Cardboard V1 ones.
+ final Context context = getApplicationContext();
+ byte[] deviceParams = CardboardParamsUtils.readDeviceParams(context);
+ if (deviceParams == null) {
+ CardboardParamsUtils.saveCardboardV1DeviceParams(context);
+ }
+ finish();
+ }
+
+ /**
+ * Callback for when a QR code is detected.
+ *
+ * @param qrCode Detected QR code.
+ */
+ @Override
+ public void onQrCodeDetected(Barcode qrCode) {
+ if (qrCode != null && !qrCodeSaved) {
+ qrCodeSaved = true;
+ QrCodeContentProcessor qrCodeContentProcessor = new QrCodeContentProcessor(this);
+ qrCodeContentProcessor.processAndSaveQrCode(qrCode, this);
+ }
+ }
+
+ /**
+ * Callback for when a QR code is processed and the parameters are saved in external storage.
+ *
+ * @param status Whether the parameters were successfully processed and saved.
+ */
+ @Override
+ public void onQrCodeSaved(boolean status) {
+ if (status) {
+ Log.d(TAG, "Device parameters saved in external storage.");
+ cameraSourcePreview.stop();
+ nativeIncrementDeviceParamsChangedCount();
+ finish();
+ } else {
+ Log.e(TAG, "Device parameters not saved in external storage.");
+ }
+ qrCodeSaved = false;
+ }
+
+ private native void nativeIncrementDeviceParamsChangedCount();
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/AsyncTask.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/AsyncTask.java
new file mode 100644
index 00000000..ec328e1d
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/AsyncTask.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode;
+
+import android.os.Handler;
+import android.os.Looper;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * AsyncTask is a helper class around Executor and Handler APIs. An asynchronous task runs on a
+ * background thread and publishes its results on the UI thread.
+ */
+public abstract class AsyncTask {
+ private final ExecutorService executor;
+ private final Handler handler;
+
+ public AsyncTask() {
+ executor = Executors.newSingleThreadExecutor();
+ handler = new Handler(Looper.getMainLooper());
+ }
+
+ public void execute(PARAM param) {
+ executor.execute(
+ () -> {
+ RESULT result = doInBackground(param);
+ handler.post(() -> onPostExecute(result));
+ });
+ executor.shutdown();
+ }
+
+ protected abstract RESULT doInBackground(PARAM param);
+
+ protected abstract void onPostExecute(RESULT result);
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/CardboardParamsUtils.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/CardboardParamsUtils.java
new file mode 100644
index 00000000..53d322e7
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/CardboardParamsUtils.java
@@ -0,0 +1,597 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Base64;
+import android.util.Log;
+import androidx.annotation.ChecksSdkIntAtLeast;
+import androidx.annotation.Nullable;
+import com.google.cardboard.sdk.UsedByNative;
+import com.google.cardboard.sdk.deviceparams.CardboardV1DeviceParams;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+
+/** Utility methods for managing configuration parameters. */
+public abstract class CardboardParamsUtils {
+ private static final String TAG = CardboardParamsUtils.class.getSimpleName();
+
+ /** URL key used to encode Cardboard device parameters. */
+ private static final String URI_KEY_PARAMS = "p";
+
+ /** Name of the folder where Cardboard configuration files are stored. */
+ private static final String CARDBOARD_CONFIG_FOLDER = "Cardboard";
+
+ /** Name of the file containing device parameters of the currently paired Cardboard device. */
+ private static final String CARDBOARD_DEVICE_PARAMS_FILE = "current_device_params";
+
+ /** Sentinel value for including device params in a stream. */
+ private static final int CARDBOARD_DEVICE_PARAMS_STREAM_SENTINEL = 0x35587a2b;
+
+ private static final String HTTPS_SCHEME = "https";
+ private static final String HTTP_SCHEME = "http";
+
+ /** URI short host of Google. */
+ private static final String URI_HOST_GOOGLE_SHORT = "g.co";
+
+ /** URI host of Google. */
+ private static final String URI_HOST_GOOGLE = "google.com";
+
+ /** URI path of Cardboard home page. */
+ private static final String URI_PATH_CARDBOARD_HOME = "cardboard";
+
+ /** URI path used in viewer param NFC and QR codes. */
+ private static final String URI_PATH_CARDBOARD_CONFIG = "cardboard/cfg";
+
+ /** Flags to encode and decode in Base64 device parameters in the Uri. */
+ private static final int URI_CODING_PARAMS = Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING;
+
+ /** URI of original cardboard QR code. */
+ private static final Uri URI_ORIGINAL_CARDBOARD_QR_CODE =
+ new Uri.Builder()
+ .scheme(HTTPS_SCHEME)
+ .authority(URI_HOST_GOOGLE_SHORT)
+ .appendEncodedPath(URI_PATH_CARDBOARD_HOME)
+ .build();
+
+ private static final int MAX_REDIRECTS = 5;
+ private static final String HTTP_SCHEME_PREFIX = "http://";
+ private static final String HTTPS_SCHEME_PREFIX = "https://";
+ private static final int HTTPS_TIMEOUT_MS = 5 * 1000;
+
+ /** Enum to determine which storage source to use. */
+ private enum StorageSource {
+ SCOPED_STORAGE,
+ EXTERNAL_STORAGE
+ };
+
+ /** Holds status for conversion from a URI to Cardboard device params. */
+ public static class UriToParamsStatus {
+ public static final int STATUS_OK = 0;
+ public static final int STATUS_UNEXPECTED_FORMAT = 1;
+ public static final int STATUS_CONNECTION_ERROR = 2;
+
+ public final int statusCode;
+ /** Only not null when statusCode is STATUS_OK. */
+ @Nullable public final byte[] params;
+
+ public static UriToParamsStatus success(byte[] params) {
+ return new UriToParamsStatus(STATUS_OK, params);
+ }
+
+ public static UriToParamsStatus error(int statusCode) {
+ return new UriToParamsStatus(statusCode, null);
+ }
+
+ private UriToParamsStatus(int statusCode, @Nullable byte[] params) {
+ this.statusCode = statusCode;
+ this.params = params;
+ }
+ }
+
+ /**
+ * Obtains the Cardboard device parameters from a Uri string and saves them.
+ *
+ *
Obtains the Cardboard device parameters from a Uri string (passed as a bytes array) and
+ * saves them into a predefined storage location.
+ *
+ * @param uriAsBytes URI string (as a bytes array) used to get the device parameters.
+ * @param context The current Context. It is or wraps an Activity or an Application instance.
+ */
+ @UsedByNative
+ public static void saveParamsFromUri(byte[] uriAsBytes, Context context) {
+ String uriAsString = new String(uriAsBytes);
+ UriToParamsStatus uriToParamsStatus = getParamsFromUriString(uriAsString, new UrlFactory());
+ if (uriToParamsStatus.statusCode != UriToParamsStatus.STATUS_OK) {
+ Log.e(TAG, "Error when trying to get the Cardboard device params from URI: " + uriAsString);
+ return;
+ }
+
+ boolean status = writeDeviceParams(uriToParamsStatus.params, context);
+ Log.d(TAG, "Could " + (!status ? "not " : "") + "save Cardboard device parameters.");
+ }
+
+ /**
+ * Saves the Cardboard V1 device parameters into a predefined storage location.
+ *
+ * @param context The current Context. It is or wraps an Activity or an Application instance.
+ */
+ public static void saveCardboardV1DeviceParams(Context context) {
+ byte[] deviceParams = CardboardV1DeviceParams.build().toByteArray();
+ boolean status = writeDeviceParams(deviceParams, context);
+ Log.d(TAG, "Could " + (!status ? "not " : "") + "save Cardboard V1 device parameters.");
+ }
+
+ /**
+ * Obtains the Cardboard device parameters from a URI string.
+ *
+ *
Analyses the URI obtained from a string in order to get the device parameters. If the
+ * obtained string matches a Cardboard V1 string format, the parameters are taken directly from
+ * the code. If the obtained string matches a Cardboard V2 string format, the parameters are taken
+ * from the URI query string (up to 5 redirections supported). This function only supports HTTPS
+ * connections. In case a URI containing an HTTP scheme is provided, it will be replaced by an
+ * HTTPS one.
+ *
+ * @param uriAsString URI string used to get the device parameters.
+ * @param urlFactory Factory for creating URL instance for HTTPS connection.
+ * @return A UriToParamsStatus instance containing the obtained result.
+ */
+ public static UriToParamsStatus getParamsFromUriString(
+ String uriAsString, UrlFactory urlFactory) {
+ Uri uri = Uri.parse(uriAsString);
+ if (uri == null) {
+ Log.e(TAG, "Error when parsing URI: " + uri);
+ return UriToParamsStatus.error(UriToParamsStatus.STATUS_UNEXPECTED_FORMAT);
+ }
+
+ // If needed, prefix free text results with a https prefix.
+ if (uri.getScheme() == null) {
+ uri = Uri.parse(HTTPS_SCHEME_PREFIX + uri);
+ } else if ((uri.getScheme()).equals(HTTP_SCHEME)) {
+ // If the prefix is http, replace it with https.
+ uri = Uri.parse(uri.toString().replaceFirst(HTTP_SCHEME_PREFIX, HTTPS_SCHEME_PREFIX));
+ }
+
+ // Follow redirects to support URL shortening.
+ try {
+ Log.d(TAG, "Following redirects for original URI: " + uri);
+ uri = followCardboardParamRedirect(uri, MAX_REDIRECTS, urlFactory);
+ } catch (IOException e) {
+ Log.w(TAG, "Error while following URL redirect " + e);
+ return UriToParamsStatus.error(UriToParamsStatus.STATUS_CONNECTION_ERROR);
+ }
+
+ if (uri == null) {
+ Log.e(TAG, "Error when following URI redirects");
+ return UriToParamsStatus.error(UriToParamsStatus.STATUS_UNEXPECTED_FORMAT);
+ }
+
+ byte[] params = CardboardParamsUtils.createFromUri(uri);
+ if (params == null) {
+ Log.e(TAG, "Error when parsing device parameters from URI query string: " + uri);
+ return UriToParamsStatus.error(UriToParamsStatus.STATUS_UNEXPECTED_FORMAT);
+ }
+ return UriToParamsStatus.success(params);
+ }
+
+ /**
+ * Reads the device parameters from a predefined storage location by forwarding a call to {@code
+ * readDeviceParamsFromStorage()}.
+ *
+ *
Based on the API level, different behaviours are expected. When the API level is below
+ * Android Q´s API level external storage is used. When the API level is exactly the same as
+ * Android Q's API level, a migration from external storage to scoped storage is performed. When
+ * there are device parameters in both in external and scoped storage, scoped storage is prefered.
+ * When the API level is greater than Android Q's API level scoped storage is used.
+ *
+ * @param context The current Context. It is or wraps an Activity or an Application instance.
+ * @return A byte array with proto encoded device parameters.
+ */
+ @UsedByNative
+ public static byte[] readDeviceParams(Context context) {
+ if (!isAtLeastQ()) {
+ Log.d(TAG, "Reading device parameters from external storage.");
+ return readDeviceParamsFromStorage(StorageSource.EXTERNAL_STORAGE, context);
+ }
+
+ Log.d(TAG, "Reading device parameters from both scoped and external storage.");
+ byte[] externalDeviceParams =
+ readDeviceParamsFromStorage(StorageSource.EXTERNAL_STORAGE, context);
+ byte[] internalDeviceParams =
+ readDeviceParamsFromStorage(StorageSource.SCOPED_STORAGE, context);
+
+ // There are device parameters only in external storage --> a copy to internal storage is done.
+ if (externalDeviceParams != null && internalDeviceParams == null) {
+ Log.d(TAG, "About to copy external device parameters to scoped storage.");
+ if (!writeDeviceParamsToStorage(
+ externalDeviceParams, StorageSource.SCOPED_STORAGE, context)) {
+ Log.e(TAG, "Error writing device parameters to scoped storage.");
+ }
+ return externalDeviceParams;
+ }
+ return internalDeviceParams;
+ }
+
+ /**
+ * Writes the device parameters to a predefined storage location by forwarding a call to {@code
+ * writeDeviceParamsToStorage()}.
+ *
+ *
Based on the API level, different behaviours are expected. When the API level is below
+ * Android Q´s API level external storage is used. Otherwise, scoped storage is used.
+ *
+ * @param context The current Context. It is or wraps an Activity or an Application instance.
+ * @return true when the write operation is successful.
+ */
+ public static boolean writeDeviceParams(byte[] deviceParams, Context context) {
+ StorageSource storageSource;
+ if (isAtLeastQ()) {
+ storageSource = StorageSource.SCOPED_STORAGE;
+ Log.d(TAG, "Writing device parameters to scoped storage.");
+ } else {
+ storageSource = StorageSource.EXTERNAL_STORAGE;
+ Log.d(TAG, "Writing device parameters to external storage.");
+ }
+ return writeDeviceParamsToStorage(deviceParams, storageSource, context);
+ }
+
+ /**
+ * Obtains the physical parameters of a Cardboard headset from a Uri (as bytes).
+ *
+ * @param uri Uri to read the parameters from.
+ * @return A bytes buffer with the Cardboard headset parameters or null in case of error.
+ */
+ private static byte[] createFromUri(Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+
+ byte[] deviceParams;
+ if (isOriginalCardboardDeviceUri(uri)) {
+ deviceParams = CardboardV1DeviceParams.build().toByteArray();
+ } else if (isCardboardDeviceUri(uri)) {
+ deviceParams = readDeviceParamsFromUri(uri);
+ } else {
+ Log.w(TAG, String.format("URI \"%s\" not recognized as Cardboard viewer.", uri));
+ deviceParams = null;
+ }
+
+ return deviceParams;
+ }
+
+ /**
+ * Analyzes if the given URI identifies a Cardboard viewer.
+ *
+ * @param uri Uri to analyze.
+ * @return true if the given URI identifies a Cardboard viewer.
+ */
+ private static boolean isCardboardUri(Uri uri) {
+ return isOriginalCardboardDeviceUri(uri) || isCardboardDeviceUri(uri);
+ }
+
+ /**
+ * Analyzes if the given URI identifies an original Cardboard viewer (or equivalent).
+ *
+ * @param uri Uri to analyze.
+ * @return true if the given URI identifies an original Cardboard viewer (or equivalent).
+ */
+ private static boolean isOriginalCardboardDeviceUri(Uri uri) {
+ // Note for "cardboard:" scheme case we're lax about path, parameters, etc. since
+ // some viewers compatible with original Cardboard are known to take liberties.
+ return URI_ORIGINAL_CARDBOARD_QR_CODE.equals(uri);
+ }
+
+ /**
+ * Analyzes if the given URI identifies a Cardboard device using current scheme.
+ *
+ * @param uri Uri to analyze.
+ * @return true if the given URI identifies a Cardboard device using current scheme.
+ */
+ private static boolean isCardboardDeviceUri(Uri uri) {
+ return HTTPS_SCHEME.equals(uri.getScheme())
+ && URI_HOST_GOOGLE.equals(uri.getAuthority())
+ && ("/" + URI_PATH_CARDBOARD_CONFIG).equals(uri.getPath());
+ }
+
+ /**
+ * Decodes device parameters in URI from Base64 to bytes.
+ *
+ * @param uri Uri to get the parameters from.
+ * @return device parameters in bytes, or null in case of error.
+ */
+ private static byte[] readDeviceParamsFromUri(Uri uri) {
+ String paramsEncoded = uri.getQueryParameter(URI_KEY_PARAMS);
+ if (paramsEncoded == null) {
+ Log.w(TAG, "No Cardboard parameters in URI.");
+ return null;
+ }
+
+ try {
+ return Base64.decode(paramsEncoded, URI_CODING_PARAMS);
+ } catch (Exception e) {
+ Log.w(TAG, "Parsing Cardboard parameters from URI failed: " + e);
+ return null;
+ }
+ }
+
+ /**
+ * Reads the device parameters from a predefined storage location.
+ *
+ * @param storageSource When {@code StorageSource.SCOPED_STORAGE}, the path is in the scoped
+ * storage. Otherwise, the SD card is used.
+ * @param context The current Context. It is generally an Activity instance or wraps one, or an
+ * Application. It is used to read from scoped storage when @p storageSource is {@code
+ * StorageSource.SCOPED_STORAGE} via {@code Context.getFilesDir()}.
+ * @return The stored params. Null if the params do not exist or the read fails.
+ */
+ private static byte[] readDeviceParamsFromStorage(StorageSource storageSource, Context context) {
+ byte[] paramBytes = null;
+
+ try {
+ InputStream stream = null;
+ try {
+ stream = InputStreamProvider.get(getDeviceParamsFile(storageSource, context));
+ paramBytes = readDeviceParamsFromInputStream(stream);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // Pass
+ }
+ }
+ }
+ } catch (FileNotFoundException e) {
+ Log.d(TAG, "Parameters file not found for reading: " + e);
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Error reading parameters: " + e);
+ }
+ return paramBytes;
+ }
+
+ /**
+ * Reads the parameters from a given input stream.
+ *
+ * @param inputStream Input stream containing device params.
+ * @return A bytes buffer or null in case of error.
+ */
+ private static byte[] readDeviceParamsFromInputStream(InputStream inputStream) {
+ if (inputStream == null) {
+ return null;
+ }
+
+ try {
+ // Stream format is sentinel (4 byte int) + size (4 byte int) + proto.
+ // Values are big endian.
+ ByteBuffer header = ByteBuffer.allocate(2 * Integer.SIZE / Byte.SIZE);
+ if (inputStream.read(header.array(), 0, header.array().length) == -1) {
+ Log.e(TAG, "Error parsing param record: end of stream.");
+ return null;
+ }
+ int sentinel = header.getInt();
+ int length = header.getInt();
+ if (sentinel != CARDBOARD_DEVICE_PARAMS_STREAM_SENTINEL) {
+ Log.e(TAG, "Error parsing param record: incorrect sentinel.");
+ return null;
+ }
+ byte[] paramBytes = new byte[length];
+ if (inputStream.read(paramBytes, 0, paramBytes.length) == -1) {
+ Log.e(TAG, "Error parsing param record: end of stream.");
+ return null;
+ }
+ return paramBytes;
+ } catch (IOException e) {
+ Log.w(TAG, "Error reading parameters: " + e);
+ }
+ return null;
+ }
+
+ /**
+ * Writes device parameters to external storage.
+ *
+ * @param paramBytes The parameters to be written.
+ * @param storageSource When {@code StorageSource.SCOPED_STORAGE}, the path is in the scoped
+ * storage. Otherwise, the SD card is used.
+ * @param context The current Context. It is generally an Activity instance or wraps one, or an
+ * Application. It is used to write to scoped storage when {@code VERSION.SDK_INT >=
+ * VERSION_CODES.Q} via {@code Context.getFilesDir()}.
+ * @return whether the parameters were successfully written.
+ */
+ private static boolean writeDeviceParamsToStorage(
+ byte[] paramBytes, StorageSource storageSource, Context context) {
+ boolean success = false;
+ OutputStream stream = null;
+ try {
+ stream = OutputStreamProvider.get(getDeviceParamsFile(storageSource, context));
+ success = writeDeviceParamsToOutputStream(paramBytes, stream);
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Parameters file not found for writing: " + e);
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Error writing parameters: " + e);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // Pass
+ }
+ }
+ }
+ return success;
+ }
+
+ /**
+ * Attempts to write the parameters into the given output stream.
+ *
+ * @param paramBytes The parameters to be written.
+ * @param outputStream OutputStream in which the parameters are stored.
+ * @return whether the parameters were successfully written.
+ */
+ private static boolean writeDeviceParamsToOutputStream(
+ byte[] paramBytes, OutputStream outputStream) {
+ try {
+ // Stream format is sentinel (4 byte int) + size (4 byte int) + proto.
+ // Values are big endian.
+ ByteBuffer header = ByteBuffer.allocate(2 * Integer.SIZE / Byte.SIZE);
+ header.putInt(CARDBOARD_DEVICE_PARAMS_STREAM_SENTINEL);
+ header.putInt(paramBytes.length);
+ outputStream.write(header.array());
+ outputStream.write(paramBytes);
+ return true;
+ } catch (IOException e) {
+ Log.w(TAG, "Error writing parameters: " + e);
+ return false;
+ }
+ }
+
+ /**
+ * Returns a file in the Cardboard configuration folder of the device.
+ *
+ *
This method creates a folder named {@link #CARDBOARD_CONFIG_FOLDER} in either the scoped
+ * storage of the application or the SD card if not already present depending on the value of @p
+ * useScopedStorage.
+ *
+ *
Deprecation warnings are suppressed on this method given that {@code
+ * Environment.getExternalStorageDirectory()} is currently marked as deprecated but intentionally
+ * used in order to ease the storage migration process.
+ *
+ * @param storageSource When {@code StorageSource.SCOPED_STORAGE}, the path is in the scoped
+ * storage. Otherwise, the SD card is used.
+ * @param context The current Context. It is generally an Activity instance or wraps one, or an
+ * Application. It is used to write to scoped storage when @p storageSource is {@code
+ * StorageSource.SCOPED_STORAGE} via {@code Context.getFilesDir()}.
+ * @return The file object of the desired file. Note that the file might not exist.
+ * @throws IllegalStateException If the configuration folder path exists but it's not a folder.
+ */
+ @SuppressWarnings("deprecation")
+ private static File getDeviceParamsFile(StorageSource storageSource, Context context) {
+ File configFolder =
+ new File(
+ storageSource == StorageSource.SCOPED_STORAGE
+ ? context.getFilesDir()
+ : Environment.getExternalStorageDirectory(),
+ CARDBOARD_CONFIG_FOLDER);
+
+ if (!configFolder.exists()) {
+ configFolder.mkdirs();
+ } else if (!configFolder.isDirectory()) {
+ throw new IllegalStateException(
+ configFolder + " already exists as a file, but is expected to be a directory.");
+ }
+
+ return new File(configFolder, CARDBOARD_DEVICE_PARAMS_FILE);
+ }
+
+ /**
+ * Follow HTTPS redirect until we reach a valid Cardboard device URI.
+ *
+ *
Network access is only used if the given URI is not already a cardboard device. Only HTTPS
+ * headers are transmitted, and the final URI is not accessed.
+ *
+ * @param uri The initial URI.
+ * @param maxRedirects Maximum number of redirects to follow.
+ * @param urlFactory Factory for creating URL instance for HTTPS connection.
+ * @return Cardboard device URI, or null if there is an error.
+ */
+ @Nullable
+ private static Uri followCardboardParamRedirect(
+ Uri uri, int maxRedirects, final UrlFactory urlFactory) throws IOException {
+ int numRedirects = 0;
+ while (uri != null && !isCardboardUri(uri)) {
+ if (numRedirects >= maxRedirects) {
+ Log.d(TAG, "Exceeding the number of maximum redirects: " + maxRedirects);
+ return null;
+ }
+ uri = resolveHttpsRedirect(uri, urlFactory);
+ numRedirects++;
+ }
+ return uri;
+ }
+
+ /**
+ * Dereference an HTTPS redirect without reading resource body.
+ *
+ * @param uri The initial URI.
+ * @param urlFactory Factory for creating URL instance for HTTPS connection.
+ * @return Redirected URI, or null if there is no redirect or an error.
+ */
+ @Nullable
+ private static Uri resolveHttpsRedirect(Uri uri, UrlFactory urlFactory) throws IOException {
+ HttpURLConnection connection = urlFactory.openHttpsConnection(uri);
+ if (connection == null) {
+ return null;
+ }
+ // Rather than follow redirects internally, we follow one hop at the time.
+ // We don't want to issue even a HEAD request to the Cardboard URI.
+ connection.setInstanceFollowRedirects(false);
+ connection.setDoInput(false);
+ connection.setConnectTimeout(HTTPS_TIMEOUT_MS);
+ connection.setReadTimeout(HTTPS_TIMEOUT_MS);
+ // Workaround for Android bug with HEAD requests on KitKat devices.
+ // See: https://code.google.com/p/android/issues/detail?id=24672.
+ connection.setRequestProperty("Accept-Encoding", "");
+ try {
+ connection.setRequestMethod("HEAD");
+ } catch (ProtocolException e) {
+ Log.w(TAG, e.toString());
+ return null;
+ }
+ try {
+ connection.connect();
+ int responseCode = connection.getResponseCode();
+ Log.i(TAG, "Response code: " + responseCode);
+ if (responseCode != HttpURLConnection.HTTP_MOVED_PERM
+ && responseCode != HttpURLConnection.HTTP_MOVED_TEMP) {
+ return null;
+ }
+ String location = connection.getHeaderField("Location");
+ if (location == null) {
+ Log.d(TAG, "Returning null because of null location.");
+ return null;
+ }
+ Log.i(TAG, "Location: " + location);
+
+ Uri redirectUri = Uri.parse(location.replaceFirst(HTTP_SCHEME_PREFIX, HTTPS_SCHEME_PREFIX));
+ if (redirectUri == null || redirectUri.compareTo(uri) == 0) {
+ Log.d(TAG, "Returning null because of wrong redirect URI.");
+ return null;
+ }
+ Log.i(TAG, "Param URI redirect to " + redirectUri);
+ uri = redirectUri;
+ } finally {
+ connection.disconnect();
+ }
+ return uri;
+ }
+
+ /**
+ * Checks whether the current Android version is Q or greater.
+ *
+ * @return true if the current Android version is Q or greater, false otherwise.
+ */
+ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
+ private static boolean isAtLeastQ() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/InputStreamProvider.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/InputStreamProvider.java
new file mode 100644
index 00000000..521fa71b
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/InputStreamProvider.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+/**
+ * Provides an {@code InputStream} to read from a {@code File}.
+ *
+ *
This class is used to inject mock streams and test {@code CardboardParamsUtils}.
+ */
+public class InputStreamProvider {
+ /** Interface to provide an {@code InputStream} from a file. */
+ public interface Provider {
+ /**
+ * Returns an {@code InputStream} that wraps a file.
+ *
+ * @param[in] file A file to wrap with an {@code InputStream}.
+ * @return An {@code InputStream}.
+ * @throws FileNotFoundException When {@code file} cannot be openned.
+ */
+ InputStream get(File file) throws FileNotFoundException;
+ }
+
+ /**
+ * Default {@code Provider} implementation based on a {@code BufferedInputStream}.
+ */
+ private static class BufferedProvider implements Provider {
+ public BufferedProvider() {}
+
+ @Override
+ public InputStream get(File file) throws FileNotFoundException {
+ return new BufferedInputStream(new FileInputStream(file));
+ }
+ }
+
+ /**
+ * Default {@code Provider} implementation that returns a {@code BufferedInputStream} from {@code
+ * file}.
+ */
+ private static Provider provider = new BufferedProvider();
+
+ private InputStreamProvider() {}
+
+ /**
+ * Setter of a custom {@code Provider} implementation.
+ *
+ * @param[in] provider A custom {@code Provider} implementation.
+ */
+ public static void setProvider(Provider provider) {
+ InputStreamProvider.provider = provider;
+ }
+
+ /**
+ * Getter of a default {@code Provider} that uses a {@code BufferedInputStream}.
+ *
+ * @return A {@code Provider}.
+ */
+ public static Provider getDefaultProvider() {
+ return new BufferedProvider();
+ }
+
+ /**
+ * Gets an {@code InputStream} wrapping a file.
+ *
+ * @param[in] file A file to wrap with an {@code InputStream}.
+ * @return An {@code InputStream}.
+ * @throws FileNotFoundException When {@code file} cannot be openned.
+ */
+ public static InputStream get(File file) throws FileNotFoundException {
+ return provider.get(file);
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/OutputStreamProvider.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/OutputStreamProvider.java
new file mode 100644
index 00000000..28c2e257
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/OutputStreamProvider.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+
+/**
+ * Provides an {@code OutputStream} to write to a {@code File}.
+ *
+ *
This class is used to inject mock streams and test {@code CardboardParamsUtils}.
+ */
+public class OutputStreamProvider {
+ /** Interface to provide an {@code OutputStream} from a file. */
+ public interface Provider {
+ /**
+ * Returns an {@code OutputStream} that wraps a file.
+ *
+ * @param[in] file A file to wrap with an {@code OutputStream}.
+ * @return An {@code OutputStream}.
+ * @throws FileNotFoundException When {@code file} cannot be openned.
+ */
+ OutputStream get(File file) throws FileNotFoundException;
+ }
+
+ /**
+ * Default {@code Provider} implementation based on a {@code BufferedOutputStream}.
+ */
+ private static class BufferedProvider implements Provider {
+ public BufferedProvider() {}
+
+ @Override
+ public OutputStream get(File file) throws FileNotFoundException {
+ return new BufferedOutputStream(new FileOutputStream(file));
+ }
+ }
+
+ /**
+ * Default {@code Provider} implementation that returns a {@code BufferedOutputStream} from {@code
+ * file}.
+ */
+ private static Provider provider = new BufferedProvider();
+
+ private OutputStreamProvider() {}
+
+ /**
+ * Setter of a custom {@code Provider} implementation.
+ *
+ * @param[in] provider A custom {@code Provider} implementation.
+ */
+ public static void setProvider(Provider provider) {
+ OutputStreamProvider.provider = provider;
+ }
+
+ /**
+ * Getter of a default {@code Provider} that uses a {@code BufferedOutputStream}.
+ *
+ * @return A {@code Provider}.
+ */
+ public static Provider getDefaultProvider() {
+ return new BufferedProvider();
+ }
+
+ /**
+ * Gets an {@code OutputStream} wrapping a file.
+ *
+ * @param[in] file A file to wrap with an {@code OutputStream}.
+ * @return An {@code OutputStream}.
+ * @throws FileNotFoundException When {@code file} cannot be openned.
+ */
+ public static OutputStream get(File file) throws FileNotFoundException {
+ return provider.get(file);
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/QrCodeContentProcessor.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/QrCodeContentProcessor.java
new file mode 100644
index 00000000..5d1bb3c9
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/QrCodeContentProcessor.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode;
+
+import android.content.Context;
+import android.util.Log;
+import android.widget.Toast;
+import com.google.android.gms.vision.barcode.Barcode;
+import com.google.cardboard.sdk.R;
+
+/**
+ * Class for processing QR code data. The QR code content should be a URI which has a parameter
+ * named 'p' in the query string. This parameter contains the Cardboard Viewer Parameters encoded in
+ * Base64. If needed, the URI can be redirected up to MAX_REDIRECTS times.
+ */
+public class QrCodeContentProcessor {
+ private static final String TAG = QrCodeContentProcessor.class.getSimpleName();
+
+ private final Listener listener;
+
+ public QrCodeContentProcessor(Listener listener) {
+ this.listener = listener;
+ }
+
+ /**
+ * Consume the item instance detected from an Activity or Fragment level by implementing the
+ * QrCodeProcessListener interface method onQrCodeProcessed.
+ */
+ public interface Listener {
+ void onQrCodeSaved(boolean status);
+ }
+
+ /**
+ * Processes detected QR code and save obtained device parameters.
+ *
+ * @param context The current Context. It is generally an Activity instance or wraps one, or an
+ * Application. It is used to write device params to scoped storage via {@code
+ * Context.getFilesDir()}.
+ */
+ public void processAndSaveQrCode(Barcode qrCode, Context context) {
+ new ProcessAndSaveQrCodeTask(context).execute(qrCode);
+ }
+
+ /**
+ * Asynchronous Task to process QR code. Once it is processed, obtained parameters are saved in
+ * external storage.
+ */
+ public class ProcessAndSaveQrCodeTask
+ extends AsyncTask {
+ private final Context context;
+
+ /**
+ * Contructs a ProcessAndSaveQrCodeTask.
+ *
+ * @param context The current Context. It is generally an Activity instance or wraps one, or an
+ * Application. It is used to write device params to scoped storage via {@code
+ * Context.getFilesDir()}.
+ */
+ public ProcessAndSaveQrCodeTask(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ protected CardboardParamsUtils.UriToParamsStatus doInBackground(Barcode qrCode) {
+ UrlFactory urlFactory = new UrlFactory();
+ return getParamsFromQrCode(qrCode, urlFactory);
+ }
+
+ @Override
+ protected void onPostExecute(CardboardParamsUtils.UriToParamsStatus result) {
+ boolean status = false;
+ if (result.statusCode == CardboardParamsUtils.UriToParamsStatus.STATUS_UNEXPECTED_FORMAT) {
+ Log.d(TAG, String.valueOf(R.string.invalid_qr_code));
+ Toast.makeText(context, R.string.invalid_qr_code, Toast.LENGTH_LONG).show();
+ } else if (result.statusCode
+ == CardboardParamsUtils.UriToParamsStatus.STATUS_CONNECTION_ERROR) {
+ Log.d(TAG, String.valueOf(R.string.connection_error));
+ Toast.makeText(context, R.string.connection_error, Toast.LENGTH_LONG).show();
+ } else if (result.params != null) {
+ status = CardboardParamsUtils.writeDeviceParams(result.params, context);
+ Log.d(TAG, "Could " + (!status ? "not " : "") + "write Cardboard parameters to storage.");
+ }
+
+ listener.onQrCodeSaved(status);
+ }
+ }
+
+ /**
+ * Attempts to convert a QR code detection result into device parameters (as a protobuf).
+ *
+ * This function analyses the obtained string from a QR code in order to get the device
+ * parameters by calling {@code CardboardParamsUtils.getParamsFromUriString}.
+ *
+ * @param barcode The detected QR code.
+ * @param urlFactory Factory for creating URL instance for HTTPS connection.
+ * @return Cardboard device parameters, or null if there is an error.
+ */
+ private static CardboardParamsUtils.UriToParamsStatus getParamsFromQrCode(
+ Barcode barcode, UrlFactory urlFactory) {
+ if (barcode.valueFormat != Barcode.TEXT && barcode.valueFormat != Barcode.URL) {
+ Log.e(TAG, "Invalid QR code format: " + barcode.valueFormat);
+ return CardboardParamsUtils.UriToParamsStatus.error(
+ CardboardParamsUtils.UriToParamsStatus.STATUS_UNEXPECTED_FORMAT);
+ }
+
+ return CardboardParamsUtils.getParamsFromUriString(barcode.rawValue, urlFactory);
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/QrCodeTracker.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/QrCodeTracker.java
new file mode 100755
index 00000000..83a48498
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/QrCodeTracker.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode;
+
+import com.google.android.gms.vision.Tracker;
+import com.google.android.gms.vision.barcode.Barcode;
+
+/**
+ * QrCodeTracker is used for tracking or reading a QR code. This is used to receive newly detected
+ * items, add a graphical representation to an overlay, update the graphics as the item changes, and
+ * remove the graphics when the item goes away.
+ */
+public class QrCodeTracker extends Tracker {
+ private final Listener listener;
+
+ /**
+ * Consume the item instance detected from an Activity or Fragment level by implementing the
+ * Listener interface method onQrCodeDetected.
+ */
+ public interface Listener {
+ void onQrCodeDetected(Barcode qrCode);
+ }
+
+ QrCodeTracker(Listener listener) {
+ this.listener = listener;
+ }
+
+ /** Start tracking the detected item instance. */
+ @Override
+ public void onNewItem(int id, Barcode item) {
+ if (item.displayValue != null) {
+ listener.onQrCodeDetected(item);
+ }
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/QrCodeTrackerFactory.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/QrCodeTrackerFactory.java
new file mode 100755
index 00000000..300ed5ee
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/QrCodeTrackerFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode;
+
+import com.google.android.gms.vision.MultiProcessor;
+import com.google.android.gms.vision.Tracker;
+import com.google.android.gms.vision.barcode.Barcode;
+
+/**
+ * Factory for creating a tracker and associated graphic to be associated with a new QR code. The
+ * multi-processor uses this factory to create QR code trackers as needed -- one for each QR code.
+ */
+public class QrCodeTrackerFactory implements MultiProcessor.Factory {
+ private final QrCodeTracker.Listener listener;
+
+ public QrCodeTrackerFactory(QrCodeTracker.Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public Tracker create(Barcode qrCode) {
+ return new QrCodeTracker(listener);
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/UrlFactory.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/UrlFactory.java
new file mode 100644
index 00000000..803d8b0c
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/UrlFactory.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode;
+
+import android.net.Uri;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+/** UrlFactory for producing a HttpURLConnection connection. */
+public class UrlFactory {
+ public static final String TAG = UrlFactory.class.getSimpleName();
+ private static final String HTTPS_SCHEME = "https";
+
+ // Return connection object, or null on error.
+ @Nullable
+ public HttpURLConnection openHttpsConnection(@Nullable Uri uri) throws IOException {
+ URL url;
+ try {
+ // Always opens an HTTPS connection.
+ url = new URL(uri.buildUpon().scheme(HTTPS_SCHEME).build().toString());
+ } catch (MalformedURLException e) {
+ Log.w(TAG, e.toString());
+ return null;
+ }
+ URLConnection urlConnection = url.openConnection();
+ // Return type is HttpURLConnection. When using Cronet as the app's URLStreamHandlerFactory, we
+ // always get back an HttpURLConnection (see
+ // https://developer.android.com/guide/topics/connectivity/cronet/reference/org/chromium/net/CronetEngine.html#public-abstract-urlstreamhandlerfactory-createurlstreamhandlerfactory).
+ // Because the URL scheme is guaranteed to be https, we can safely return the type
+ // HttpURLConnection.
+ if (!(urlConnection instanceof HttpURLConnection)) {
+ Log.w(TAG, "Expected HttpURLConnection");
+ throw new IllegalArgumentException("Expected HttpURLConnection");
+ }
+ return (HttpURLConnection) urlConnection;
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/camera/CameraSource.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/camera/CameraSource.java
new file mode 100755
index 00000000..9af16026
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/camera/CameraSource.java
@@ -0,0 +1,600 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode.camera;
+
+import android.Manifest;
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.WindowManager;
+import androidx.annotation.RequiresPermission;
+import com.google.android.gms.common.images.Size;
+import com.google.android.gms.vision.Detector;
+import com.google.android.gms.vision.Frame;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages the camera in conjunction with an underlying detector. This receives preview frames from
+ * the camera at a specified rate, sending those frames to the detector as fast as it is able to
+ * process those frames.
+ *
+ * Deprecation warnings are suppressed on this class given that {@code android.hardware.Camera}
+ * is currently marked as deprecated but intentionally used in order to ease the camera usage.
+ */
+@SuppressWarnings("deprecation")
+public class CameraSource {
+ private static final String TAG = CameraSource.class.getSimpleName();
+
+ private static final float ASPECT_RATIO_TOLERANCE = 0.01f;
+
+ // Preferred width in pixels.
+ private static final int WIDTH = 1600;
+
+ // Preferred height in pixels.
+ private static final int HEIGHT = 1200;
+
+ /**
+ * These values may be requested by the caller. Due to hardware limitations, we may need to select
+ * close, but not exactly the same values for these.
+ */
+ private static final float FPS = 15.0f;
+
+ private final Context context;
+
+ private final Object cameraLock = new Object();
+
+ // Guarded by cameraLock
+ private android.hardware.Camera camera;
+
+ private int rotation;
+
+ private Size previewSize;
+
+ /**
+ * Dedicated thread and associated runnable for calling into the detector with frames, as the
+ * frames become available from the camera.
+ */
+ private Thread processingThread;
+
+ private final FrameProcessingRunnable frameProcessor;
+
+ /**
+ * Map to convert between a byte array, received from the camera, and its associated byte buffer.
+ */
+ private final Map bytesToByteBuffer = new HashMap<>();
+
+ /**
+ * Constructs a CameraSource.
+ *
+ * Creates a camera source builder with the supplied context and detector. Camera preview
+ * images will be streamed to the associated detector upon starting the camera source.
+ *
+ * @param context The Android's Application context.
+ * @param detector Expects a QR code detector.
+ * @throws IllegalArgumentException When any of the parameter preconditions is unmet.
+ */
+ public CameraSource(Context context, Detector> detector) {
+ // Prerequisite evaluation.
+ if (context == null) {
+ Log.e(TAG, "context is null.");
+ throw new IllegalArgumentException("No context supplied.");
+ }
+ if (detector == null) {
+ Log.e(TAG, "detector is null.");
+ throw new IllegalArgumentException("No detector supplied.");
+ }
+
+ this.context = context;
+ frameProcessor = new FrameProcessingRunnable(detector);
+ Log.i(TAG, "Successful CameraSource creation.");
+ }
+
+ /** Stops the camera and releases the resources of the camera and underlying detector. */
+ public void release() {
+ synchronized (cameraLock) {
+ stop();
+ frameProcessor.release();
+ }
+ }
+
+ /**
+ * Opens the camera and starts sending preview frames to the underlying detector. The supplied
+ * surface holder is used for the preview so frames can be displayed to the user.
+ *
+ * @param surfaceHolder the surface holder to use for the preview frames
+ * @throws IOException if the supplied surface holder could not be used as the preview display
+ */
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public CameraSource start(SurfaceHolder surfaceHolder) throws IOException {
+ synchronized (cameraLock) {
+ if (camera != null) {
+ return this;
+ }
+
+ camera = createCamera();
+ camera.setPreviewDisplay(surfaceHolder);
+ camera.startPreview();
+
+ processingThread = new Thread(frameProcessor);
+ frameProcessor.setActive(true);
+ processingThread.start();
+ }
+ return this;
+ }
+
+ /** Closes the camera and stops sending frames to the underlying frame detector. */
+ public void stop() {
+ synchronized (cameraLock) {
+ frameProcessor.setActive(false);
+ if (processingThread != null) {
+ try {
+ processingThread.join();
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Frame processing thread interrupted on release.");
+ }
+ processingThread = null;
+ }
+
+ // Clear the buffer to prevent OOM exceptions
+ bytesToByteBuffer.clear();
+
+ if (camera != null) {
+ camera.stopPreview();
+ camera.setPreviewCallbackWithBuffer(null);
+ try {
+ camera.setPreviewTexture(null);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to clear camera preview: " + e);
+ }
+ camera.release();
+ camera = null;
+ }
+ }
+ }
+
+ /** Returns the preview size that is currently in use by the underlying camera. */
+ public Size getPreviewSize() {
+ return previewSize;
+ }
+
+ /**
+ * Opens the camera and applies the user settings.
+ *
+ * @throws RuntimeException if the method fails
+ */
+ private android.hardware.Camera createCamera() {
+ int requestedCameraId =
+ getIdForRequestedCamera(android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK);
+ if (requestedCameraId == -1) {
+ Log.e(TAG, "Could not find requested camera.");
+ throw new RuntimeException("Could not find requested camera.");
+ }
+ android.hardware.Camera camera = android.hardware.Camera.open(requestedCameraId);
+
+ SizePair sizePair = selectSizePair(camera, WIDTH, HEIGHT);
+ if (sizePair == null) {
+ Log.e(TAG, "Could not find suitable preview size.");
+ throw new RuntimeException("Could not find suitable preview size.");
+ }
+ Size pictureSize = sizePair.pictureSize();
+ previewSize = sizePair.previewSize();
+
+ int[] previewFpsRange = selectPreviewFpsRange(camera, FPS);
+ if (previewFpsRange == null) {
+ Log.e(TAG, "Could not find suitable preview frames per second range.");
+ throw new RuntimeException("Could not find suitable preview frames per second range.");
+ }
+
+ android.hardware.Camera.Parameters parameters = camera.getParameters();
+
+ if (pictureSize != null) {
+ parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
+ }
+
+ parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight());
+ parameters.setPreviewFpsRange(
+ previewFpsRange[android.hardware.Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
+ previewFpsRange[android.hardware.Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
+ parameters.setPreviewFormat(ImageFormat.NV21);
+
+ setRotation(camera, parameters, requestedCameraId);
+
+ if (parameters
+ .getSupportedFocusModes()
+ .contains(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ parameters.setFocusMode(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
+ } else {
+ Log.i(
+ TAG, "Camera focus mode: FOCUS_MODE_CONTINUOUS_PICTURE is not supported on this device.");
+ }
+
+ camera.setParameters(parameters);
+
+ // Four frame buffers are needed for working with the camera:
+ //
+ // one for the frame that is currently being executed upon in doing detection
+ // one for the next pending frame to process immediately upon completing detection
+ // two for the frames that the camera uses to populate future preview images
+ camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback());
+ camera.addCallbackBuffer(createPreviewBuffer(previewSize));
+ camera.addCallbackBuffer(createPreviewBuffer(previewSize));
+ camera.addCallbackBuffer(createPreviewBuffer(previewSize));
+ camera.addCallbackBuffer(createPreviewBuffer(previewSize));
+
+ Log.i(TAG, "Successfull camera creation.");
+ return camera;
+ }
+
+ /**
+ * Gets the id for the camera specified by the direction it is facing. Returns -1 if no such
+ * camera was found.
+ *
+ * @param facing the desired camera (front-facing or rear-facing)
+ */
+ private static int getIdForRequestedCamera(int facing) {
+ android.hardware.Camera.CameraInfo cameraInfo = new android.hardware.Camera.CameraInfo();
+ for (int i = 0; i < android.hardware.Camera.getNumberOfCameras(); ++i) {
+ android.hardware.Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == facing) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Selects the most suitable preview and picture size, given the desired width and height.
+ *
+ * @param camera the camera to select a preview size from
+ * @param desiredWidth the desired width of the camera preview frames
+ * @param desiredHeight the desired height of the camera preview frames
+ * @return the selected preview and picture size pair
+ */
+ private static SizePair selectSizePair(
+ android.hardware.Camera camera, int desiredWidth, int desiredHeight) {
+ List validPreviewSizes = generateValidPreviewSizeList(camera);
+
+ // The method for selecting the best size is to minimize the sum of the differences between
+ // the desired values and the actual values for width and height.
+ SizePair selectedPair = null;
+ int minDiff = Integer.MAX_VALUE;
+ for (SizePair sizePair : validPreviewSizes) {
+ Size size = sizePair.previewSize();
+ int diff =
+ Math.abs(size.getWidth() - desiredWidth) + Math.abs(size.getHeight() - desiredHeight);
+ if (diff < minDiff) {
+ selectedPair = sizePair;
+ minDiff = diff;
+ }
+ }
+
+ return selectedPair;
+ }
+
+ /**
+ * Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted
+ * preview images on some devices, the picture size must be set to a size that is the same aspect
+ * ratio as the preview size or the preview may end up being distorted. If the picture size is
+ * null, then there is no picture size with the same aspect ratio as the preview size.
+ */
+ private static class SizePair {
+ private final Size preview;
+ private Size picture;
+
+ public SizePair(
+ android.hardware.Camera.Size previewSize, android.hardware.Camera.Size pictureSize) {
+ preview = new Size(previewSize.width, previewSize.height);
+ if (pictureSize != null) {
+ picture = new Size(pictureSize.width, pictureSize.height);
+ }
+ }
+
+ public Size previewSize() {
+ return preview;
+ }
+
+ public Size pictureSize() {
+ return picture;
+ }
+ }
+
+ /**
+ * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is not
+ * a corresponding picture size of the same aspect ratio. If there is a corresponding picture size
+ * of the same aspect ratio, the picture size is paired up with the preview size.
+ */
+ private static List generateValidPreviewSizeList(android.hardware.Camera camera) {
+ android.hardware.Camera.Parameters parameters = camera.getParameters();
+ List supportedPreviewSizes =
+ parameters.getSupportedPreviewSizes();
+ List supportedPictureSizes =
+ parameters.getSupportedPictureSizes();
+ List validPreviewSizes = new ArrayList<>();
+ for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) {
+ float previewAspectRatio = (float) previewSize.width / (float) previewSize.height;
+
+ // By looping through the picture sizes in order, we favor the higher resolutions.
+ for (android.hardware.Camera.Size pictureSize : supportedPictureSizes) {
+ float pictureAspectRatio = (float) pictureSize.width / (float) pictureSize.height;
+ if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) {
+ validPreviewSizes.add(new SizePair(previewSize, pictureSize));
+ break;
+ }
+ }
+ }
+
+ // If there are no picture sizes with the same aspect ratio as any preview sizes, allow all
+ // of the preview sizes and hope that the camera can handle it.
+ if (validPreviewSizes.isEmpty()) {
+ Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size");
+ for (android.hardware.Camera.Size previewSize : supportedPreviewSizes) {
+ // The null picture size will let us know that we shouldn't set a picture size.
+ validPreviewSizes.add(new SizePair(previewSize, null));
+ }
+ }
+
+ return validPreviewSizes;
+ }
+
+ /**
+ * Selects the most suitable preview frames per second range, given the desired frames per second.
+ *
+ * @param camera the camera to select a frames per second range from
+ * @param desiredPreviewFps the desired frames per second for the camera preview frames
+ * @return the selected preview frames per second range
+ */
+ private static int[] selectPreviewFpsRange(
+ android.hardware.Camera camera, float desiredPreviewFps) {
+ // The camera API uses integers scaled by a factor of 1000 frame rates.
+ int desiredPreviewFpsScaled = (int) (desiredPreviewFps * 1000.0f);
+
+ // The method for selecting the best range is to minimize the sum of the differences between
+ // the desired value and the upper and lower bounds of the range.
+ int[] selectedFpsRange = null;
+ int minDiff = Integer.MAX_VALUE;
+ List previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange();
+ for (int[] range : previewFpsRangeList) {
+ int deltaMin =
+ desiredPreviewFpsScaled - range[android.hardware.Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
+ int deltaMax =
+ desiredPreviewFpsScaled - range[android.hardware.Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
+ int diff = Math.abs(deltaMin) + Math.abs(deltaMax);
+ if (diff < minDiff) {
+ selectedFpsRange = range;
+ minDiff = diff;
+ }
+ }
+ return selectedFpsRange;
+ }
+
+ /**
+ * Calculates the correct rotation for the given camera id and sets the rotation in the
+ * parameters. It also sets the camera's display orientation and rotation.
+ *
+ * @param parameters the camera parameters for which to set the rotation
+ * @param cameraId the camera id to set rotation based on
+ */
+ private void setRotation(
+ android.hardware.Camera camera, android.hardware.Camera.Parameters parameters, int cameraId) {
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ int degrees = 0;
+ int rotation = windowManager.getDefaultDisplay().getRotation();
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ default:
+ Log.e(TAG, "Bad rotation value: " + rotation);
+ }
+
+ android.hardware.Camera.CameraInfo cameraInfo = new android.hardware.Camera.CameraInfo();
+ android.hardware.Camera.getCameraInfo(cameraId, cameraInfo);
+
+ int angle;
+ int displayAngle;
+ if (cameraInfo.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ angle = (cameraInfo.orientation + degrees) % 360;
+ displayAngle = (360 - angle) % 360; // compensate for it being mirrored
+ } else { // back-facing
+ angle = (cameraInfo.orientation - degrees + 360) % 360;
+ displayAngle = angle;
+ }
+
+ // This corresponds to the rotation constants in frame.
+ this.rotation = angle / 90;
+
+ camera.setDisplayOrientation(displayAngle);
+ parameters.setRotation(angle);
+ }
+
+ /**
+ * Creates one buffer for the camera preview callback. The size of the buffer is based off of the
+ * camera preview size and the format of the camera image.
+ *
+ * @return a new preview buffer of the appropriate size for the current camera settings
+ */
+ private byte[] createPreviewBuffer(Size previewSize) {
+ int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21);
+ long sizeInBits = (long) previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel;
+ int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1;
+
+ // Creating the byte array this way and wrapping it, as opposed to using .allocate(),
+ // should guarantee that there will be an array to work with.
+ byte[] byteArray = new byte[bufferSize];
+ ByteBuffer buffer = ByteBuffer.wrap(byteArray);
+ bytesToByteBuffer.put(byteArray, buffer);
+ return byteArray;
+ }
+
+ // ==============================================================================================
+ // Frame processing
+ // ==============================================================================================
+
+ /** Called when the camera has a new preview frame. */
+ private class CameraPreviewCallback implements android.hardware.Camera.PreviewCallback {
+ @Override
+ public void onPreviewFrame(byte[] data, android.hardware.Camera camera) {
+ frameProcessor.setNextFrame(data, camera);
+ }
+ }
+
+ /**
+ * This runnable controls access to the underlying receiver, calling it to process frames when
+ * available from the camera. This is designed to run detection on frames as fast as possible.
+ */
+ private class FrameProcessingRunnable implements Runnable {
+ private Detector> detector;
+ private final long startTimeMillis = SystemClock.elapsedRealtime();
+
+ // This lock guards all of the member variables below.
+ private final Object lock = new Object();
+ private boolean active = true;
+
+ // These pending variables hold the state associated with the new frame awaiting processing.
+ private long pendingTimeMillis;
+ private int pendingFrameId = 0;
+ private ByteBuffer pendingFrameData;
+
+ FrameProcessingRunnable(Detector> detector) {
+ this.detector = detector;
+ }
+
+ /**
+ * Releases the underlying receiver. This is only safe to do after the associated thread has
+ * completed, which is managed in camera source's release method above.
+ */
+ void release() {
+ detector.release();
+ detector = null;
+ }
+
+ /** Marks the runnable as active/not active. Signals any blocked threads to continue. */
+ void setActive(boolean active) {
+ synchronized (lock) {
+ this.active = active;
+ lock.notifyAll();
+ }
+ }
+
+ /**
+ * Sets the frame data received from the camera. This adds the previous unused frame buffer (if
+ * present) back to the camera, and keeps a pending reference to the frame data for future use.
+ */
+ void setNextFrame(byte[] data, android.hardware.Camera camera) {
+ synchronized (lock) {
+ if (pendingFrameData != null) {
+ camera.addCallbackBuffer(pendingFrameData.array());
+ pendingFrameData = null;
+ }
+
+ if (!bytesToByteBuffer.containsKey(data)) {
+ Log.d(
+ TAG,
+ "Skipping frame. Could not find ByteBuffer associated with the image"
+ + " "
+ + "data from the camera.");
+ return;
+ }
+
+ // Timestamp and frame ID are saved to aware when frames are dropped along the way.
+ pendingTimeMillis = SystemClock.elapsedRealtime() - startTimeMillis;
+ pendingFrameId++;
+ pendingFrameData = bytesToByteBuffer.get(data);
+
+ // Notify the processor thread if it is waiting on the next frame.
+ lock.notifyAll();
+ }
+ }
+
+ /**
+ * As long as the processing thread is active, this executes detection on frames continuously.
+ * The next pending frame is either immediately available or hasn't been received yet. Once it
+ * is available, we transfer the frame info to local variables and run detection on that frame.
+ * It immediately loops back for the next frame without pausing.
+ */
+ @Override
+ public void run() {
+ Frame outputFrame;
+ ByteBuffer data;
+
+ while (true) {
+ synchronized (lock) {
+ while (active && (pendingFrameData == null)) {
+ try {
+ // Wait for the next frame to be received from the camera, since we
+ // don't have it yet.
+ lock.wait();
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Frame processing loop terminated.", e);
+ return;
+ }
+ }
+
+ if (!active) {
+ // Exit the loop once this camera source is stopped or released.
+ return;
+ }
+
+ outputFrame =
+ new Frame.Builder()
+ .setImageData(
+ pendingFrameData,
+ previewSize.getWidth(),
+ previewSize.getHeight(),
+ ImageFormat.NV21)
+ .setId(pendingFrameId)
+ .setTimestampMillis(pendingTimeMillis)
+ .setRotation(rotation)
+ .build();
+
+ // Hold onto the frame data locally, so that we can use this for detection
+ // below. We need to clear pendingFrameData to ensure that this buffer isn't
+ // recycled back to the camera before we are done using that data.
+ data = pendingFrameData;
+ pendingFrameData = null;
+ }
+
+ // The code below needs to run outside of synchronization, because this will allow
+ // the camera to add pending frame(s) while we are running detection on the current
+ // frame.
+ try {
+ detector.receiveFrame(outputFrame);
+ } catch (Throwable t) {
+ Log.e(TAG, "Exception thrown from receiver.", t);
+ } finally {
+ camera.addCallbackBuffer(data.array());
+ }
+ }
+ }
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/camera/CameraSourcePreview.java b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/camera/CameraSourcePreview.java
new file mode 100755
index 00000000..4b33a9dc
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/java/com/google/cardboard/sdk/qrcode/camera/CameraSourcePreview.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cardboard.sdk.qrcode.camera;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+import androidx.annotation.RequiresPermission;
+import com.google.android.gms.common.images.Size;
+import java.io.IOException;
+
+/**
+ * Manages the surface that shows the camera preview, adapting it to the size and orientation of the
+ * phone.
+ */
+public class CameraSourcePreview extends ViewGroup {
+ private static final String TAG = CameraSourcePreview.class.getSimpleName();
+
+ private final Context context;
+ private final SurfaceView surfaceView;
+ private boolean startRequested;
+ private boolean surfaceAvailable;
+ private CameraSource cameraSource;
+
+ public CameraSourcePreview(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ this.context = context;
+ startRequested = false;
+ surfaceAvailable = false;
+
+ surfaceView = new SurfaceView(context);
+ surfaceView.getHolder().addCallback(new SurfaceCallback());
+ addView(surfaceView);
+ }
+
+ @RequiresPermission(Manifest.permission.CAMERA)
+ public void start(CameraSource cameraSource) throws IOException {
+ if (cameraSource == null) {
+ stop();
+ }
+
+ this.cameraSource = cameraSource;
+
+ if (this.cameraSource != null) {
+ startRequested = true;
+ startIfReady();
+ }
+ }
+
+ public void stop() {
+ if (cameraSource != null) {
+ cameraSource.stop();
+ }
+ }
+
+ public void release() {
+ if (cameraSource != null) {
+ cameraSource.release();
+ cameraSource = null;
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.CAMERA)
+ private void startIfReady() throws IOException {
+ if (startRequested && surfaceAvailable) {
+ cameraSource.start(surfaceView.getHolder());
+ startRequested = false;
+ }
+ }
+
+ private class SurfaceCallback implements SurfaceHolder.Callback {
+ @Override
+ public void surfaceCreated(SurfaceHolder surface) {
+ surfaceAvailable = true;
+ try {
+ startIfReady();
+ } catch (SecurityException se) {
+ Log.e(TAG, "Do not have permission to start the camera", se);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not start camera source.", e);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder surface) {
+ surfaceAvailable = false;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int width = 320;
+ int height = 240;
+ if (cameraSource != null) {
+ Size size = cameraSource.getPreviewSize();
+ if (size != null) {
+ width = size.getWidth();
+ height = size.getHeight();
+ }
+ }
+
+ // Swap width and height sizes when in portrait, since it will be rotated 90 degrees
+ if (isPortraitMode()) {
+ int tmp = width;
+ width = height;
+ height = tmp;
+ }
+
+ final int layoutWidth = right - left;
+ final int layoutHeight = bottom - top;
+
+ int childHeight;
+ int childWidth;
+
+ // Fits height when in portrait mode and with when in landscape mode.
+ if (isPortraitMode()) {
+ childHeight = layoutHeight;
+ childWidth = (int) (((float) layoutHeight / (float) height) * width);
+ } else {
+ childWidth = layoutWidth;
+ childHeight = (int) (((float) layoutWidth / (float) width) * height);
+ }
+
+ for (int i = 0; i < getChildCount(); ++i) {
+ getChildAt(i).layout(0, 0, childWidth, childHeight);
+ }
+
+ try {
+ startIfReady();
+ } catch (SecurityException se) {
+ Log.e(TAG, "Do not have permission to start the camera", se);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not start camera source.", e);
+ }
+ }
+
+ private boolean isPortraitMode() {
+ int orientation = context.getResources().getConfiguration().orientation;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ return false;
+ }
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ return true;
+ }
+
+ Log.d(TAG, "isPortraitMode returning false by default");
+ return false;
+ }
+}
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/qr_code.cc b/mode/libraries/vr/libs/sdk/qrcode/android/qr_code.cc
new file mode 100644
index 00000000..00ca8a29
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/qr_code.cc
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "qr_code.h"
+
+#include
+
+#include
+
+#include "jni_utils/android/jni_utils.h"
+
+#define JNI_METHOD(return_type, clazz, method_name) \
+ JNIEXPORT return_type JNICALL \
+ Java_com_google_cardboard_sdk_##clazz##_##method_name
+
+namespace cardboard::qrcode {
+
+namespace {
+JavaVM* vm_;
+jobject context_;
+jclass cardboard_params_utils_class_;
+jclass intent_class_;
+jclass component_name_class_;
+std::atomic device_params_changed_count_(0);
+
+// TODO(b/180938531): Release these global references.
+void LoadJNIResources(JNIEnv* env) {
+ cardboard_params_utils_class_ =
+ reinterpret_cast(env->NewGlobalRef(cardboard::jni::LoadJClass(
+ env, "com/google/cardboard/sdk/qrcode/CardboardParamsUtils")));
+ intent_class_ = reinterpret_cast(env->NewGlobalRef(
+ cardboard::jni::LoadJClass(env, "android/content/Intent")));
+ component_name_class_ = reinterpret_cast(env->NewGlobalRef(
+ cardboard::jni::LoadJClass(env, "android/content/ComponentName")));
+}
+
+void IncrementDeviceParamsChangedCount() {
+ device_params_changed_count_++;
+}
+
+} // anonymous namespace
+
+void initializeAndroid(JavaVM* vm, jobject context) {
+ vm_ = vm;
+ context_ = context;
+
+ JNIEnv* env;
+ cardboard::jni::LoadJNIEnv(vm_, &env);
+ LoadJNIResources(env);
+}
+
+std::vector getCurrentSavedDeviceParams() {
+ JNIEnv* env;
+ cardboard::jni::LoadJNIEnv(vm_, &env);
+
+ jmethodID readDeviceParams =
+ env->GetStaticMethodID(cardboard_params_utils_class_, "readDeviceParams",
+ "(Landroid/content/Context;)[B");
+ jbyteArray byteArray = static_cast(env->CallStaticObjectMethod(
+ cardboard_params_utils_class_, readDeviceParams, context_));
+ if (byteArray == nullptr) {
+ return {};
+ }
+
+ const int length = env->GetArrayLength(byteArray);
+
+ std::vector buffer;
+ buffer.resize(length);
+ env->GetByteArrayRegion(byteArray, 0, length,
+ reinterpret_cast(&buffer[0]));
+ return buffer;
+}
+
+void scanQrCodeAndSaveDeviceParams() {
+ // Get JNI environment pointer
+ JNIEnv* env;
+ cardboard::jni::LoadJNIEnv(vm_, &env);
+
+ // Get instance of Intent
+ jmethodID newIntent = env->GetMethodID(intent_class_, "", "()V");
+ jobject intentObject = env->NewObject(intent_class_, newIntent);
+
+ // Get instance of ComponentName
+ jmethodID newComponentName =
+ env->GetMethodID(component_name_class_, "",
+ "(Landroid/content/Context;Ljava/lang/String;)V");
+ jstring className =
+ env->NewStringUTF("com.google.cardboard.sdk.QrCodeCaptureActivity");
+ jobject componentNameObject = env->NewObject(
+ component_name_class_, newComponentName, context_, className);
+
+ // Set component in intent
+ jmethodID setComponent = env->GetMethodID(
+ intent_class_, "setComponent",
+ "(Landroid/content/ComponentName;)Landroid/content/Intent;");
+ env->CallObjectMethod(intentObject, setComponent, componentNameObject);
+
+ // Start activity using intent
+ jclass activityClass = env->GetObjectClass(context_);
+ jmethodID startActivity = env->GetMethodID(activityClass, "startActivity",
+ "(Landroid/content/Intent;)V");
+ env->CallVoidMethod(context_, startActivity, intentObject);
+}
+
+void saveDeviceParams(const uint8_t* uri, int size) {
+ // Get JNI environment pointer
+ JNIEnv* env;
+ cardboard::jni::LoadJNIEnv(vm_, &env);
+
+ // Allocate memory for uri_jbyte_array
+ jbyteArray uri_jbyte_array = env->NewByteArray(size);
+
+ // Copy the uint8_t* to a jbyteArray
+ jbyte* java_data_ptr = env->GetByteArrayElements(uri_jbyte_array, 0);
+ memcpy(java_data_ptr, uri, size);
+ env->SetByteArrayRegion(uri_jbyte_array, 0, size, java_data_ptr);
+
+ // Get the Java class method to be called
+ jmethodID save_params_from_uri_method =
+ env->GetStaticMethodID(cardboard_params_utils_class_, "saveParamsFromUri",
+ "([BLandroid/content/Context;)V");
+
+ // Call the Java class method
+ env->CallStaticVoidMethod(cardboard_params_utils_class_,
+ save_params_from_uri_method, uri_jbyte_array,
+ context_);
+
+ // Release memory allocated by uri_jbyte_array
+ env->ReleaseByteArrayElements(uri_jbyte_array, java_data_ptr, 0);
+
+ IncrementDeviceParamsChangedCount();
+}
+
+int getDeviceParamsChangedCount() { return device_params_changed_count_; }
+
+} // namespace cardboard::qrcode
+
+extern "C" {
+
+JNI_METHOD(void, QrCodeCaptureActivity, nativeIncrementDeviceParamsChangedCount)
+(JNIEnv* /*env*/, jobject /*obj*/) {
+ cardboard::qrcode::IncrementDeviceParamsChangedCount();
+}
+
+} // extern "C"
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/res/drawable-xxhdpi/qr_sample.png b/mode/libraries/vr/libs/sdk/qrcode/android/res/drawable-xxhdpi/qr_sample.png
new file mode 100644
index 00000000..5a873a89
Binary files /dev/null and b/mode/libraries/vr/libs/sdk/qrcode/android/res/drawable-xxhdpi/qr_sample.png differ
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/res/drawable-xxhdpi/tick_marks.png b/mode/libraries/vr/libs/sdk/qrcode/android/res/drawable-xxhdpi/tick_marks.png
new file mode 100644
index 00000000..9ac43518
Binary files /dev/null and b/mode/libraries/vr/libs/sdk/qrcode/android/res/drawable-xxhdpi/tick_marks.png differ
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/res/layout/qr_code_capture.xml b/mode/libraries/vr/libs/sdk/qrcode/android/res/layout/qr_code_capture.xml
new file mode 100644
index 00000000..8aac5fc9
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/res/layout/qr_code_capture.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/res/values/colors.xml b/mode/libraries/vr/libs/sdk/qrcode/android/res/values/colors.xml
new file mode 100644
index 00000000..38f7fccb
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #555
+ #FFFAFAFA
+ #FFEEEEEE
+
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/res/values/strings.xml b/mode/libraries/vr/libs/sdk/qrcode/android/res/values/strings.xml
new file mode 100644
index 00000000..b86f7bae
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/res/values/strings.xml
@@ -0,0 +1,43 @@
+
+
+
+ Cardboard SDK requires camera and write to external storage permission to read the QR code and save the encoded device parameters.
+
+ Camera permission is not granted and is needed to read QR codes
+
+ QrCodeCapture
+
+ Find this Cardboard symbol on your viewer
+
+ Can\'t find this symbol?
+
+ SKIP
+
+ HeadsetDetector
+
+ Viewer detected
+
+ Invalid QR Code
+
+ Connection error
+
+ QR Code detector dependency missing
+
diff --git a/mode/libraries/vr/libs/sdk/qrcode/android/res/values/styles.xml b/mode/libraries/vr/libs/sdk/qrcode/android/res/values/styles.xml
new file mode 100644
index 00000000..c451c230
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/android/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/mode/libraries/vr/libs/sdk/qrcode/cardboard_v1/cardboard_v1.cc b/mode/libraries/vr/libs/sdk/qrcode/cardboard_v1/cardboard_v1.cc
new file mode 100644
index 00000000..ed33dcc0
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/cardboard_v1/cardboard_v1.cc
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "qrcode/cardboard_v1/cardboard_v1.h"
+
+namespace cardboard::qrcode {
+
+std::vector getCardboardV1DeviceParams() {
+ return {
+ 0xa, 0xc, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2c, 0x20, 0x49,
+ 0x6e, 0x63, 0x2e, 0x12, 0xc, 0x43, 0x61, 0x72, 0x64, 0x62, 0x6f,
+ 0x61, 0x72, 0x64, 0x20, 0x76, 0x31, 0x1d, 0x31, 0x8, 0x2c, 0x3d,
+ 0x25, 0x8f, 0xc2, 0x75, 0x3d, 0x2a, 0x10, 0x0, 0x0, 0x20, 0x42,
+ 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x20, 0x42, 0x0, 0x0, 0x20,
+ 0x42, 0x35, 0x29, 0x5c, 0xf, 0x3d, 0x3a, 0x8, 0xc1, 0xca, 0xe1,
+ 0x3e, 0x77, 0xbe, 0x1f, 0x3e, 0x58, 0x0, 0x60, 0x1,
+ };
+}
+
+} // namespace cardboard::qrcode
diff --git a/mode/libraries/vr/libs/sdk/qrcode/cardboard_v1/cardboard_v1.h b/mode/libraries/vr/libs/sdk/qrcode/cardboard_v1/cardboard_v1.h
new file mode 100644
index 00000000..a0a0c63b
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/cardboard_v1/cardboard_v1.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#ifndef CARDBOARD_SDK_QRCODE_CARDBOARD_V1_CARDBOARD_V1_H_
+#define CARDBOARD_SDK_QRCODE_CARDBOARD_V1_CARDBOARD_V1_H_
+
+#include
+
+#include
+
+namespace cardboard::qrcode {
+
+/// Device params for Cardboard V1 released at Google I/O 2014.
+/// {@
+constexpr float kCardboardV1InterLensDistance = 0.06f;
+constexpr float kCardboardV1TrayToLensDistance = 0.035f;
+constexpr float kCardboardV1ScreenToLensDistance = 0.042f;
+constexpr float kCardboardV1FovHalfDegrees[] = {40.0f, 40.0f, 40.0f, 40.0f};
+constexpr float kCardboardV1DistortionCoeffs[] = {0.441f, 0.156f};
+constexpr int kCardboardV1DistortionCoeffsSize = 2;
+constexpr int kCardboardV1VerticalAlignmentType = 0;
+constexpr char kCardboardV1Vendor[] = "Google, Inc.";
+constexpr char kCardboardV1Model[] = "Cardboard v1";
+/// @}
+
+std::vector getCardboardV1DeviceParams();
+} // namespace cardboard::qrcode
+
+#endif // CARDBOARD_SDK_QRCODE_CARDBOARD_V1_CARDBOARD_V1_H_
diff --git a/mode/libraries/vr/libs/sdk/qrcode/ios/device_params_helper.h b/mode/libraries/vr/libs/sdk/qrcode/ios/device_params_helper.h
new file mode 100644
index 00000000..c65f4b50
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/ios/device_params_helper.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import
+
+/**
+ * Helper class to read and write cardboard device params.
+ */
+@interface CardboardDeviceParamsHelper : NSObject
+
+/**
+ * Reads currently saved device params. Returns nil if no params are saved yet.
+ *
+ * @return The currently saved encoded device params.
+ */
++ (nullable NSData *)readSerializedDeviceParams;
+
+/**
+ * Loads and validates the device parameters from the given URL. Upon success it saves the viewer
+ * profile data and calls the completion block with the result of validation and an optional error.
+ *
+ * @param url The URL to retrieve the device params from.
+ * @param completion Callback to be executed upon completion.
+ */
++ (void)resolveAndUpdateViewerProfileFromURL:(nullable NSURL *)url
+ withCompletion:
+ (nonnull void (^)(BOOL success, NSError *_Nullable))completion;
+
+/**
+ * Writes Cardboard V1 device parameters in storage.
+ */
++ (void)saveCardboardV1Params;
+
+@end
diff --git a/mode/libraries/vr/libs/sdk/qrcode/ios/device_params_helper.mm b/mode/libraries/vr/libs/sdk/qrcode/ios/device_params_helper.mm
new file mode 100644
index 00000000..f4245091
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/ios/device_params_helper.mm
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import "qrcode/ios/device_params_helper.h"
+
+#include
+#include
+
+#include "qrcode/cardboard_v1/cardboard_v1.h"
+#import "qrcode/ios/nsurl_session_data_handler.h"
+
+// The value is an array with the creation time and the device params data.
+static NSString *const kCardboardDeviceParamsAndTimeKey =
+ @"com.google.cardboard.sdk.DeviceParamsAndTime";
+
+@implementation CardboardDeviceParamsHelper
+
++ (NSData *)parseURL:(NSURL *)url {
+ if (!url) {
+ return nil;
+ }
+
+ if ([NSURLSessionDataHandler isOriginalCardboardDeviceUrl:url]) {
+ return [CardboardDeviceParamsHelper createCardboardV1Params];
+ } else if ([NSURLSessionDataHandler isCardboardDeviceUrl:url]) {
+ return [CardboardDeviceParamsHelper readDeviceParamsFromUrl:url];
+ }
+
+ return nil;
+}
+
++ (void)readFromUrl:(NSURL *)url
+ withCompletion:(void (^)(NSData *deviceParams, NSError *error))completion {
+ NSMutableURLRequest *request =
+ [NSMutableURLRequest requestWithURL:url
+ cachePolicy:NSURLRequestReloadIgnoringCacheData
+ timeoutInterval:10];
+
+ // Save bandwidth, just read the header.
+ [request setHTTPMethod:@"HEAD"];
+
+ NSURLSessionDataHandler *dataHandler =
+ [[NSURLSessionDataHandler alloc] initWithOnUrlCompletion:^(NSURL *targetUrl, NSError *error) {
+ if (!targetUrl) {
+ NSLog(@"failed to result the url = %@", url);
+ }
+ completion([CardboardDeviceParamsHelper parseURL:targetUrl], error);
+ }];
+
+ NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
+ NSURLSession *session = [NSURLSession sessionWithConfiguration:config
+ delegate:dataHandler
+ delegateQueue:[NSOperationQueue mainQueue]];
+ NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
+ [dataTask resume];
+}
+
++ (void)resolveAndUpdateViewerProfileFromURL:(NSURL *)url
+ withCompletion:(void (^)(BOOL success, NSError *error))completion {
+ // If the scheme is http, replace it with https.
+ NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
+ if ([components.scheme.lowercaseString isEqualToString:@"http"]) {
+ components.scheme = @"https";
+ url = [components URL];
+ }
+
+ // Try to parse the url if it has the params encoded in it. This can help avoid a network call.
+ NSData *viewerParams = [CardboardDeviceParamsHelper parseURL:url];
+ if (viewerParams) {
+ [CardboardDeviceParamsHelper update:viewerParams];
+ completion(YES, nil);
+ } else {
+ [CardboardDeviceParamsHelper readFromUrl:url
+ withCompletion:^(NSData *deviceParams, NSError *error) {
+ if (deviceParams) {
+ [CardboardDeviceParamsHelper update:deviceParams];
+ }
+ completion(deviceParams != nil, error);
+ }];
+ }
+}
+
++ (void)saveCardboardV1Params {
+ NSData *deviceParams = [CardboardDeviceParamsHelper createCardboardV1Params];
+ [CardboardDeviceParamsHelper update:deviceParams];
+}
+
++ (NSData *)createCardboardV1Params {
+ std::vector deviceParams = cardboard::qrcode::getCardboardV1DeviceParams();
+ return [NSData dataWithBytes:deviceParams.data() length:deviceParams.size()];
+}
+
++ (NSData *)readDeviceParamsFromUrl:(NSURL *)url {
+ NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url
+ resolvingAgainstBaseURL:NO];
+ for (NSURLQueryItem *queryItem in urlComponents.queryItems) {
+ // Device parameters decoded data is after the p paramers in the url, for example:
+ // https://google.com/cardboard/cfg?p=device_param_encoded_string.
+ if ([queryItem.name isEqualToString:@"p"]) {
+ NSString *encodedDeviceParams =
+ [CardboardDeviceParamsHelper convertToNormalBase64String:queryItem.value];
+ return [[NSData alloc] initWithBase64EncodedString:encodedDeviceParams options:0];
+ }
+ }
+
+ NSLog(@"No Cardboard parameters in URL: %@", url);
+ return nil;
+}
+
++ (void)update:(NSData *)deviceParams {
+ [CardboardDeviceParamsHelper
+ writeDeviceParams:std::string((char *)deviceParams.bytes, deviceParams.length)];
+}
+
++ (NSData *)readSerializedDeviceParams {
+ std::string deviceParams = [CardboardDeviceParamsHelper readDeviceParams];
+ return [NSData dataWithBytes:deviceParams.c_str() length:deviceParams.length()];
+}
+
+// Convert the url safe base64 string into normal base64 string with padding if needed.
++ (NSString *)convertToNormalBase64String:(NSString *)original {
+ original = [original stringByReplacingOccurrencesOfString:@"_" withString:@"/"];
+ original = [original stringByReplacingOccurrencesOfString:@"-" withString:@"+"];
+ // Add the padding with "=" if needed.
+ NSUInteger paddingCount = 4 - (original.length % 4);
+ if (paddingCount == 1) {
+ original = [NSString stringWithFormat:@"%@=", original];
+ } else if (paddingCount == 2) {
+ original = [NSString stringWithFormat:@"%@==", original];
+ } else if (paddingCount == 3) {
+ original = [NSString stringWithFormat:@"%@===", original];
+ }
+ return original;
+}
+
++ (std::string)readDeviceParams {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSArray *array = [defaults arrayForKey:kCardboardDeviceParamsAndTimeKey];
+ NSData *serializedDeviceParams = array ? array[1] : nil;
+ if (serializedDeviceParams) {
+ return std::string((char *)serializedDeviceParams.bytes, serializedDeviceParams.length);
+ } else {
+ return std::string("");
+ }
+}
+
++ (bool)writeDeviceParams:(const std::string &)device_params {
+ NSData *deviceParams = [NSData dataWithBytes:device_params.c_str() length:device_params.length()];
+ NSArray *array = @[ [NSDate date], deviceParams ];
+ [[NSUserDefaults standardUserDefaults] setObject:array forKey:kCardboardDeviceParamsAndTimeKey];
+ return true;
+}
+
+@end
diff --git a/mode/libraries/vr/libs/sdk/qrcode/ios/nsurl_session_data_handler.h b/mode/libraries/vr/libs/sdk/qrcode/ios/nsurl_session_data_handler.h
new file mode 100644
index 00000000..f511cb44
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/ios/nsurl_session_data_handler.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import
+
+/** The completion will be called once the target url is fetched. */
+typedef void (^OnUrlCompletion)(NSURL* _Nullable targetUrl, NSError* _Nullable error);
+
+/**
+ * Helper class that handles NSURL session data.
+ */
+@interface NSURLSessionDataHandler : NSObject
+
+/**
+ * Analyzes if the given URL identifies an original Cardboard viewer (or equivalent).
+ *
+ * @param url URL to analyze.
+ * @return true if the given URL identifies an original Cardboard viewer (or equivalent).
+ */
++ (BOOL)isOriginalCardboardDeviceUrl:(nonnull NSURL*)url;
+
+/**
+ * Analyzes if the given URL identifies a Cardboard device using current scheme.
+ *
+ * @param url URL to analyze.
+ * @return true if the given URL identifies a Cardboard device using current scheme.
+ */
++ (BOOL)isCardboardDeviceUrl:(nonnull NSURL*)url;
+
+/**
+ * Inits handler with OnUrlCompletion callback.
+ *
+ * @param completion Callback to be executed upon completion.
+ */
+- (nonnull instancetype)initWithOnUrlCompletion:(nonnull OnUrlCompletion)completion;
+
+@end
diff --git a/mode/libraries/vr/libs/sdk/qrcode/ios/nsurl_session_data_handler.mm b/mode/libraries/vr/libs/sdk/qrcode/ios/nsurl_session_data_handler.mm
new file mode 100644
index 00000000..e3bc2df0
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/ios/nsurl_session_data_handler.mm
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import "qrcode/ios/nsurl_session_data_handler.h"
+
+static NSString *const kCardboardDeviceParamsUrlPrefix = @"https://google.com/cardboard/cfg";
+static NSString *const kOriginalCardboardDeviceParamsUrl = @"https://g.co/cardboard";
+
+@implementation NSURLSessionDataHandler {
+ OnUrlCompletion _completion;
+ BOOL _success;
+}
+
++ (BOOL)isOriginalCardboardDeviceUrl:(NSURL *)url {
+ return [[url absoluteString] isEqualToString:kOriginalCardboardDeviceParamsUrl];
+}
+
++ (BOOL)isCardboardDeviceUrl:(NSURL *)url {
+ return [[url absoluteString] hasPrefix:kCardboardDeviceParamsUrlPrefix];
+}
+
+/**
+ * Analyzes if the given URL identifies a Cardboard viewer.
+ *
+ * @param url URL to analyze.
+ * @return true if the given URL identifies a Cardboard viewer.
+ */
++ (BOOL)isCardboardUrl:(NSURL *)url {
+ return [NSURLSessionDataHandler isOriginalCardboardDeviceUrl:url] ||
+ [NSURLSessionDataHandler isCardboardDeviceUrl:url];
+}
+
+- (instancetype)initWithOnUrlCompletion:(OnUrlCompletion)completion {
+ self = [super init];
+ if (self) {
+ _completion = completion;
+ _success = NO;
+ }
+ return self;
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ willPerformHTTPRedirection:(NSHTTPURLResponse *)response
+ newRequest:(NSURLRequest *)request
+ completionHandler:(void (^)(NSURLRequest *))completionHandler {
+ if (request.URL) {
+ NSURL *secureUrl = request.URL;
+
+ // If the scheme is http, replace it with https.
+ NSURLComponents *components = [NSURLComponents componentsWithURL:secureUrl
+ resolvingAgainstBaseURL:NO];
+ if ([components.scheme.lowercaseString isEqualToString:@"http"]) {
+ components.scheme = @"https";
+ secureUrl = [components URL];
+ }
+
+ if ([NSURLSessionDataHandler isCardboardUrl:secureUrl]) {
+ _success = YES;
+ _completion(secureUrl, nil);
+ }
+ }
+
+ if (_success) {
+ completionHandler(nil);
+ } else {
+ completionHandler(request);
+ }
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+ didReceiveResponse:(NSURLResponse *)response
+ completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
+ completionHandler(NSURLSessionResponseAllow);
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
+ NSURLCredential *_Nullable))completionHandler {
+ completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didCompleteWithError:(NSError *)error {
+ if (error) {
+ _success = NO;
+ _completion(nil, error);
+ } else if (!_success) {
+ _completion(nil, nil);
+ }
+}
+
+@end
diff --git a/mode/libraries/vr/libs/sdk/qrcode/ios/qr_code.mm b/mode/libraries/vr/libs/sdk/qrcode/ios/qr_code.mm
new file mode 100644
index 00000000..1cb94d6d
--- /dev/null
+++ b/mode/libraries/vr/libs/sdk/qrcode/ios/qr_code.mm
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import "qr_code.h"
+
+#import
+#import
+
+#import