+5
.reuse/dep5
+5
.reuse/dep5
···
136
136
Copyright: No copyright
137
137
License: CC0-1.0
138
138
Comment: Computer-generated files do not have a copyright per US law.
139
+
140
+
Files: src/xrt/auxiliary/android_cardboard/src/main/res/drawable/cardboard_oss_qr.png
141
+
Copyright: 2019 Google LLC
142
+
License: Apache-2.0
143
+
Comment: SPDX-License-Identifier missing.
+3
build.gradle
+3
build.gradle
+13
settings.gradle
+13
settings.gradle
···
17
17
repositories {
18
18
google()
19
19
mavenCentral()
20
+
maven { url 'https://jitpack.io' }
20
21
}
21
22
}
22
23
23
24
24
25
rootProject.name = 'monado'
25
26
27
+
// Typically required by all Monado-based runtimes on AOSP platforms
26
28
include ':src:xrt:auxiliary'
27
29
project(':src:xrt:auxiliary').projectDir = new File(rootDir, 'src/xrt/auxiliary/android')
28
30
31
+
// Only used by Google Cardboard support in the installable Monado
32
+
include ':src:xrt:auxiliary:cardboard'
33
+
project(':src:xrt:auxiliary:cardboard').projectDir = new File(rootDir, 'src/xrt/auxiliary/android_cardboard')
34
+
35
+
// Required for out-of-process builds
29
36
include ':src:xrt:ipc'
30
37
project(':src:xrt:ipc').projectDir = new File(rootDir, 'src/xrt/ipc/android')
31
38
39
+
// Typically required by all Monado-based runtimes on AOSP platforms
40
+
// Use dependency injection to customize
32
41
include ':src:xrt:targets:android_common'
42
+
43
+
// End-level (leaf) target - builds on android_common with dependency injection
44
+
// to make an installable APK providing a runtime with a Cardboard-like experience
45
+
// by default.
33
46
include ':src:xrt:targets:openxr_android'
+26
src/xrt/auxiliary/android/src/main/java/org/freedesktop/monado/auxiliary/ScannerProvider.kt
+26
src/xrt/auxiliary/android/src/main/java/org/freedesktop/monado/auxiliary/ScannerProvider.kt
···
1
+
// Copyright 2025, Collabora, Ltd.
2
+
// SPDX-License-Identifier: BSL-1.0
3
+
/*!
4
+
* @file
5
+
* @brief Interface for target-specific scanner-related things on Android.
6
+
* @author Simon Zeni <simon.zeni@collabora.com>
7
+
* @ingroup aux_android
8
+
*/
9
+
10
+
package org.freedesktop.monado.auxiliary
11
+
12
+
import android.content.Intent
13
+
import android.graphics.drawable.Drawable
14
+
15
+
/**
16
+
* Scanner activity. This interface must be provided by any Android "XRT Target".
17
+
*
18
+
* Intended for use in dependency injection.
19
+
*/
20
+
interface ScannerProvider {
21
+
/** Get a drawable icon for use the UI, for the runtime/Monado-incorporating target. */
22
+
fun getIcon(): Drawable
23
+
24
+
/** Make a {@code Intent} to launch a scanner activity, if provided by the target. */
25
+
fun makeScannerIntent(): Intent
26
+
}
+71
src/xrt/auxiliary/android_cardboard/build.gradle
+71
src/xrt/auxiliary/android_cardboard/build.gradle
···
1
+
// Copyright 2020-2025, Collabora, Ltd.
2
+
// SPDX-License-Identifier: BSL-1.0
3
+
4
+
plugins {
5
+
id 'com.android.library'
6
+
id 'kotlin-android'
7
+
id 'com.diffplug.spotless'
8
+
}
9
+
10
+
android {
11
+
compileSdk project.sharedCompileSdk
12
+
buildToolsVersion = buildToolsVersion
13
+
14
+
defaultConfig {
15
+
minSdkVersion 24
16
+
targetSdkVersion project.sharedTargetSdk
17
+
}
18
+
19
+
buildTypes {
20
+
release {
21
+
minifyEnabled false
22
+
// Gradle plugin produces proguard-android-optimize.txt from @Keep annotations
23
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24
+
}
25
+
}
26
+
27
+
compileOptions {
28
+
sourceCompatibility JavaVersion.VERSION_17
29
+
targetCompatibility JavaVersion.VERSION_17
30
+
}
31
+
kotlinOptions {
32
+
jvmTarget = JavaVersion.VERSION_17
33
+
}
34
+
packagingOptions {
35
+
resources {
36
+
excludes += ['META-INF/*.kotlin_module']
37
+
}
38
+
}
39
+
namespace 'org.freedesktop.monado.auxiliary.cardboard'
40
+
lint {
41
+
fatal 'StopShip'
42
+
}
43
+
buildFeatures {
44
+
viewBinding true
45
+
}
46
+
}
47
+
48
+
dependencies {
49
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
50
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
51
+
implementation "androidx.constraintlayout:constraintlayout:$androidxConstraintLayoutVersion"
52
+
implementation "com.google.android.material:material:$materialVersion"
53
+
implementation "com.github.markusfisch:BarcodeScannerView:$barcodeScannerViewVersion"
54
+
implementation 'com.google.dagger:hilt-android:2.51.1'
55
+
}
56
+
57
+
spotless {
58
+
java {
59
+
target 'src/main/java/**/*.java'
60
+
// apply a specific flavor of google-java-format.
61
+
googleJavaFormat('1.18.1').aosp().reflowLongStrings()
62
+
// fix formatting of type annotations.
63
+
formatAnnotations()
64
+
}
65
+
66
+
kotlin {
67
+
target 'src/main/java/**/*.kt'
68
+
// Use ktfmt(https://github.com/facebook/ktfmt) as the default Kotlin formatter.
69
+
ktfmt("$ktfmtVersion").kotlinlangStyle()
70
+
}
71
+
}
+4
src/xrt/auxiliary/android_cardboard/proguard-rules.pro
+4
src/xrt/auxiliary/android_cardboard/proguard-rules.pro
+23
src/xrt/auxiliary/android_cardboard/src/main/AndroidManifest.xml
+23
src/xrt/auxiliary/android_cardboard/src/main/AndroidManifest.xml
···
1
+
<?xml version="1.0" encoding="utf-8"?>
2
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+
<!--
4
+
Copyright 2020-2025, Collabora, Ltd.
5
+
SPDX-License-Identifier: BSL-1.0
6
+
-->
7
+
8
+
<!-- Cardboard QR code scanner. -->
9
+
<uses-permission android:name="android.permission.INTERNET"/>
10
+
<uses-permission android:name="android.permission.CAMERA"/>
11
+
<uses-feature android:name="android.hardware.camera"/>
12
+
<uses-feature android:name="android.hardware.camera.autofocus"
13
+
android:required="false"/>
14
+
15
+
<application
16
+
android:requestLegacyExternalStorage="true">
17
+
<activity android:name=".QrScannerActivity"
18
+
android:exported="true"
19
+
android:theme="@style/Theme.AppCompat.NoActionBar"
20
+
android:label="Monado"/>
21
+
</application>
22
+
23
+
</manifest>
+129
src/xrt/auxiliary/android_cardboard/src/main/java/org/freedesktop/monado/auxiliary/cardboard/CardboardParamUtils.kt
+129
src/xrt/auxiliary/android_cardboard/src/main/java/org/freedesktop/monado/auxiliary/cardboard/CardboardParamUtils.kt
···
1
+
// Copyright 2024-2025, Collabora, Ltd.
2
+
// SPDX-License-Identifier: BSL-1.0
3
+
/*!
4
+
* @file
5
+
* @brief Utilities for Google Cardboard codes.
6
+
* @author Simon Zeni <simon.zeni@collabora.com>
7
+
* @author Rylie Pavlik <rylie.pavlik@collabora.com>
8
+
*/
9
+
10
+
package org.freedesktop.monado.auxiliary.cardboard
11
+
12
+
import android.content.Context
13
+
import android.net.Uri
14
+
import android.util.Base64
15
+
import android.util.Log
16
+
import androidx.core.net.toUri
17
+
import java.io.File
18
+
import java.io.FileOutputStream
19
+
import java.net.HttpURLConnection
20
+
import java.net.URL
21
+
22
+
private const val TAG = "CardboardParamUtils"
23
+
24
+
@Suppress("SpellCheckingInspection")
25
+
private const val cardboardV1ParamValue: String =
26
+
"CgxHb29nbGUsIEluYy4SDENhcmRib2FyZCB2MR0xCCw9JY_CdT0qEAAAIEIAACBCAAAgQgAAIEJYADUpXA89OgiuR-E-mpkZPlABYAE"
27
+
private const val googleDomain = "google.com"
28
+
29
+
private const val cardboardConfigPath = "/cardboard/cfg"
30
+
31
+
private const val cardboardV1EquivUri =
32
+
"https://$googleDomain$cardboardConfigPath?p=$cardboardV1ParamValue"
33
+
34
+
/** Cardboard V1 had a QR code with no parameters encoded. */
35
+
fun isOriginalCardboard(uri: Uri): Boolean {
36
+
return uri.authority.equals("g.co") && uri.path.equals("/cardboard")
37
+
}
38
+
39
+
/** Newer Cardboard-compatible devices have parameters encoded in a URI (eventually) */
40
+
fun isCardboardParamUri(uri: Uri): Boolean {
41
+
return uri.authority.equals(googleDomain) && uri.path.equals(cardboardConfigPath)
42
+
}
43
+
44
+
/** Cardboard URIs include those with params and the original human-targeted Cardboard V1 URI. */
45
+
fun isCardboardUri(uri: Uri): Boolean {
46
+
return isOriginalCardboard(uri) || isCardboardParamUri(uri)
47
+
}
48
+
49
+
/**
50
+
* Some QR codes are missing the scheme. Further, we do not want to visit plain http URIs looking
51
+
* for a redirect, therefore we replace the scheme if applicable.
52
+
*/
53
+
private fun sanitizeAndParseUri(value: String): Uri {
54
+
return if (!value.startsWith("http")) {
55
+
// missing protocol
56
+
"https://$value".toUri()
57
+
} else if (value.startsWith("http://")) {
58
+
// non-https
59
+
value.replaceFirst("http", "https").toUri()
60
+
} else {
61
+
value.toUri()
62
+
}
63
+
}
64
+
65
+
/** Save current_device_params with approximate Cardboard V1 params */
66
+
fun saveCardboardV1Params(qrScannerActivity: QrScannerActivity) {
67
+
saveCardboardParamsFromResolvedUri(qrScannerActivity, cardboardV1EquivUri.toUri())
68
+
}
69
+
70
+
/** Save current_device_params from a Cardboard param URI. */
71
+
fun saveCardboardParamsFromResolvedUri(context: Context, uri: Uri) {
72
+
assert(isCardboardParamUri(uri))
73
+
val paramEncoded = uri.getQueryParameter("p")
74
+
val flags = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
75
+
val data = Base64.decode(paramEncoded, flags)
76
+
77
+
val configFile = File(context.filesDir, "current_device_params")
78
+
configFile.createNewFile()
79
+
80
+
Log.d(TAG, "Saving to file $configFile")
81
+
FileOutputStream(configFile).use { it.write(data) }
82
+
}
83
+
84
+
/**
85
+
* Given some data from a QR Code, try to get to a Cardboard URI: either the original Cardboard web
86
+
* site, or a parameter URI.
87
+
*
88
+
* @return null if we could not get to a Cardboard URI even after several redirects.
89
+
*/
90
+
fun qrCodeValueToCardboardURI(value: String): Uri? {
91
+
92
+
Log.i(TAG, "QR code scan data $value")
93
+
94
+
var uri = sanitizeAndParseUri(value)
95
+
96
+
Log.i(TAG, "QR code URL $uri")
97
+
98
+
// Try fetching the URL, following redirects, repeatedly.
99
+
for (i in 0..5) {
100
+
if (isCardboardUri(uri)) {
101
+
// got one!
102
+
break
103
+
}
104
+
val connection = URL(uri.toString()).openConnection() as HttpURLConnection
105
+
connection.setRequestMethod("HEAD")
106
+
connection.instanceFollowRedirects = false
107
+
connection.connect()
108
+
val code = connection.getResponseCode()
109
+
if (
110
+
code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_MOVED_TEMP
111
+
) {
112
+
// Sanitize: we only consider visiting https, for privacy
113
+
uri = sanitizeAndParseUri(connection.getHeaderField("Location"))
114
+
115
+
Log.i(TAG, "followed url to $uri")
116
+
} else {
117
+
// was not a redirect, all done
118
+
break
119
+
}
120
+
}
121
+
122
+
Log.i(TAG, "ending with url $uri")
123
+
124
+
if (!isCardboardUri(uri)) {
125
+
Log.e(TAG, "url is not cardboard config")
126
+
return null
127
+
}
128
+
return uri
129
+
}
+120
src/xrt/auxiliary/android_cardboard/src/main/java/org/freedesktop/monado/auxiliary/cardboard/QrScannerActivity.kt
+120
src/xrt/auxiliary/android_cardboard/src/main/java/org/freedesktop/monado/auxiliary/cardboard/QrScannerActivity.kt
···
1
+
// Copyright 2024-2025, Collabora, Ltd.
2
+
// SPDX-License-Identifier: BSL-1.0
3
+
/*!
4
+
* @file
5
+
* @brief QR code scanner activity for Google Cardboard codes.
6
+
* @author Simon Zeni <simon.zeni@collabora.com>
7
+
* @author Rylie Pavlik <rylie.pavlik@collabora.com>
8
+
*/
9
+
10
+
package org.freedesktop.monado.auxiliary.cardboard
11
+
12
+
import android.Manifest
13
+
import android.content.pm.PackageManager
14
+
import android.os.Bundle
15
+
import android.util.Log
16
+
import android.widget.Toast
17
+
import androidx.appcompat.app.AppCompatActivity
18
+
import androidx.core.app.ActivityCompat
19
+
import androidx.core.content.ContextCompat
20
+
import de.markusfisch.android.barcodescannerview.widget.BarcodeScannerView
21
+
import org.freedesktop.monado.auxiliary.cardboard.databinding.ActivityScannerBinding
22
+
23
+
class QrScannerActivity : AppCompatActivity() {
24
+
private lateinit var viewBinding: ActivityScannerBinding
25
+
private lateinit var scannerView: BarcodeScannerView
26
+
27
+
companion object {
28
+
private const val TAG = "MonadoQrScanner"
29
+
private const val PERMISSION_REQUEST_CODE = 200
30
+
}
31
+
32
+
private fun tryGetPermissions(): Boolean {
33
+
34
+
if (
35
+
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
36
+
PackageManager.PERMISSION_GRANTED
37
+
) {
38
+
Log.i(TAG, "Camera permission has been granted")
39
+
return true
40
+
}
41
+
Log.i(TAG, "Camera permission has not been granted, requesting")
42
+
val permissionStrings = arrayOf(Manifest.permission.CAMERA)
43
+
ActivityCompat.requestPermissions(this, permissionStrings, PERMISSION_REQUEST_CODE)
44
+
return false
45
+
}
46
+
47
+
override fun onCreate(savedInstanceState: Bundle?) {
48
+
super.onCreate(savedInstanceState)
49
+
50
+
viewBinding = ActivityScannerBinding.inflate(layoutInflater)
51
+
setContentView(viewBinding.root)
52
+
if (tryGetPermissions()) {
53
+
startScanner()
54
+
}
55
+
}
56
+
57
+
private fun startScanner() {
58
+
scannerView = findViewById(R.id.barcode_scanner)
59
+
scannerView.cropRatio = .75f
60
+
61
+
scannerView.setOnBarcodeListener { result -> processScan(result.text) }
62
+
}
63
+
64
+
public override fun onResume() {
65
+
super.onResume()
66
+
if (this::scannerView.isInitialized) {
67
+
68
+
scannerView.openAsync()
69
+
} else {
70
+
tryGetPermissions()
71
+
}
72
+
}
73
+
74
+
public override fun onPause() {
75
+
super.onPause()
76
+
if (this::scannerView.isInitialized) {
77
+
scannerView.close()
78
+
}
79
+
}
80
+
81
+
override fun onRequestPermissionsResult(
82
+
requestCode: Int,
83
+
permissions: Array<String>,
84
+
grantResults: IntArray,
85
+
) {
86
+
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
87
+
Log.i(TAG, "onRequestPermissionResult")
88
+
if (requestCode == PERMISSION_REQUEST_CODE) {
89
+
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
90
+
startScanner()
91
+
}
92
+
}
93
+
}
94
+
95
+
private fun processScan(value: String): Boolean {
96
+
val uri = qrCodeValueToCardboardURI(value)
97
+
if (uri == null) {
98
+
Log.e(TAG, "url is not cardboard config")
99
+
runOnUiThread { Toast.makeText(this, "QR code invalid", Toast.LENGTH_SHORT).show() }
100
+
return true
101
+
}
102
+
103
+
if (isOriginalCardboard(uri)) {
104
+
// This is an approximation of the original Cardboard config - only 2 digits for
105
+
// distortion, not 3, but good enough.
106
+
Log.d(TAG, "QR code is for Cardboard V1 with no parameters in it, using default")
107
+
saveCardboardV1Params(this)
108
+
} else {
109
+
saveCardboardParamsFromResolvedUri(this, uri)
110
+
}
111
+
112
+
Log.d(TAG, "QR code valid")
113
+
runOnUiThread {
114
+
Toast.makeText(applicationContext, "Cardboard parameters saved", Toast.LENGTH_SHORT)
115
+
.show()
116
+
}
117
+
finish()
118
+
return false
119
+
}
120
+
}
src/xrt/auxiliary/android_cardboard/src/main/res/drawable/cardboard_oss_qr.png
src/xrt/auxiliary/android_cardboard/src/main/res/drawable/cardboard_oss_qr.png
This is a binary file and will not be displayed.
+48
src/xrt/auxiliary/android_cardboard/src/main/res/layout/activity_scanner.xml
+48
src/xrt/auxiliary/android_cardboard/src/main/res/layout/activity_scanner.xml
···
1
+
<?xml version="1.0" encoding="utf-8"?>
2
+
<!--
3
+
Copyright 2024, Collabora, Ltd.
4
+
SPDX-License-Identifier: BSL-1.0
5
+
-->
6
+
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
7
+
xmlns:app="http://schemas.android.com/apk/res-auto"
8
+
xmlns:tools="http://schemas.android.com/tools"
9
+
android:layout_width="match_parent"
10
+
android:layout_height="match_parent"
11
+
tools:context=".QrScannerActivity">
12
+
13
+
<de.markusfisch.android.barcodescannerview.widget.BarcodeScannerView
14
+
android:id="@+id/barcode_scanner"
15
+
android:layout_width="match_parent"
16
+
android:layout_height="match_parent"/>
17
+
18
+
<LinearLayout
19
+
android:layout_width="match_parent"
20
+
android:layout_height="wrap_content"
21
+
android:orientation="horizontal"
22
+
android:paddingTop="32dp"
23
+
android:paddingBottom="32dp"
24
+
android:paddingLeft="24dp"
25
+
android:paddingRight="24dp"
26
+
android:background="?attr/colorPrimary"
27
+
app:layout_constraintBottom_toBottomOf="parent">
28
+
29
+
<ImageView
30
+
android:id="@+id/imageView3"
31
+
android:layout_width="0dp"
32
+
android:layout_height="64dp"
33
+
android:layout_weight="0"
34
+
android:importantForAccessibility="no" app:srcCompat="@drawable/cardboard_oss_qr" />
35
+
36
+
<TextView
37
+
android:layout_width="match_parent"
38
+
android:layout_height="wrap_content"
39
+
android:lineSpacingExtra="4dp"
40
+
android:paddingLeft="24dp"
41
+
android:paddingTop="10dp"
42
+
android:text="@string/look_for_the_qr_code_on_your_cardboard_headset"
43
+
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
44
+
</LinearLayout>
45
+
46
+
47
+
</androidx.constraintlayout.widget.ConstraintLayout>
48
+
+10
src/xrt/auxiliary/android_cardboard/src/main/res/values/strings.xml
+10
src/xrt/auxiliary/android_cardboard/src/main/res/values/strings.xml
···
1
+
<resources>
2
+
<!--
3
+
Copyright 2025, Collabora, Ltd.
4
+
SPDX-License-Identifier: BSL-1.0
5
+
-->
6
+
7
+
<string name="qr">Cardboard QR scan</string>
8
+
<string name="look_for_the_qr_code_on_your_cardboard_headset">Look for the QR code on your Cardboard headset</string>
9
+
10
+
</resources>