Developer mode lets other apps, automations and the launcher work the glasses' camera through Visor. They can take a photo, start and stop a video, import a capture, hear about new captures, and get the Live view's picture.
Open Settings, Advanced, and turn on Developer mode under Tools.
The glasses take a photo for the phone only when a Meta switch, wearables_robusta.demo_enabled, is on. Developer mode turns it on, and turns it off again when you turn the mode off. If Visor has never read the glasses' configuration, it reads it first, which can take a minute over Bluetooth.
While the mode is on:
- Visor answers the actions below.
- The launcher offers two shortcuts on Visor's icon: Take photo and Record video.
- Visor broadcasts the events below.
When the mode is off, Android does not deliver the actions to Visor at all.
With the mode on, Advanced shows a Token under the switch. Tap it to copy it. The arrow next to it makes a new one, and the old one stops working at once.
An app needs the token for the Live frames. For the actions, it needs the token only when Actions need the token is on. Then Visor ignores an action without the right token extra and says so in the event log. The launcher's shortcuts carry the token on their own.
Send each action as a broadcast to the package com.macedonga.visor, or start it as an activity. The activity shows nothing and closes at once.
| Action | Extras | What it does |
|---|---|---|
com.macedonga.visor.action.TAKE_PHOTO |
none | Takes a photo. |
com.macedonga.visor.action.START_VIDEO |
capture_type (optional): video or hyperlapse |
Starts a video. |
com.macedonga.visor.action.STOP_VIDEO |
none | Stops the video. |
com.macedonga.visor.action.TOGGLE_VIDEO |
capture_type (optional) |
Stops the video if one runs, else starts one. |
com.macedonga.visor.action.IMPORT_MEDIA |
id: the id from MEDIA_AVAILABLE |
Imports that capture to the phone. |
With Actions need the token on, add token to every action.
If the glasses are not connected, Visor tries to connect and waits up to 20 seconds before it acts.
From a computer:
adb shell am broadcast -p com.macedonga.visor -a com.macedonga.visor.action.TAKE_PHOTO
adb shell am broadcast -p com.macedonga.visor -a com.macedonga.visor.action.IMPORT_MEDIA --es id <id>
From an app:
context.sendBroadcast(Intent("com.macedonga.visor.action.TAKE_PHOTO").setPackage("com.macedonga.visor").putExtra("token", token))Set the package. Android does not deliver a broadcast without one to Visor. In Tasker, use Send Intent with the package com.macedonga.visor and the target Broadcast Receiver.
Visor sends these as broadcasts while developer mode is on.
| Event | When | Extras |
|---|---|---|
com.macedonga.visor.event.MEDIA_AVAILABLE |
The glasses have a capture that this phone does not have yet. | id, type, time, size |
com.macedonga.visor.event.IMPORT_PROGRESS |
The photo or video of a capture is on its way, once per whole percent. | id, received, size, percent |
com.macedonga.visor.event.NEW_MEDIA |
A capture is in the gallery. | id, type, time, uri, path |
com.macedonga.visor.event.IMPORT_FAILED |
An import did not finish. | id, reason |
com.macedonga.visor.event.VIDEO_STARTED |
The glasses started a video. | time |
com.macedonga.visor.event.VIDEO_STOPPED |
The glasses stopped a video. | time |
The extras:
id: the capture's id on the glasses.IMPORT_MEDIAtakes it.type:photo,live(a photo with its clip) orvideo.time: milliseconds since 1970. For a capture, when the glasses took it. For a video event, when Visor saw it.sizeandreceived: bytes, as longs.percent: 0 to 100, as an int.uri: the capture'scontent://media/...address in the gallery.path: the file, for example/storage/emulated/0/Pictures/Visor/Visor_20261021_103433.jpg. Photos go toPictures/Visorand videos toMovies/Visor.reason: why the import failed.NOT_FOUNDmeans the glasses have no capture with that id.WIFI_OFF,NOT_ALLOWED,CUT_OFFand the others come from Visor's import.
To receive the events, register a receiver while your app runs:
val filter = IntentFilter().apply {
addAction("com.macedonga.visor.event.MEDIA_AVAILABLE")
addAction("com.macedonga.visor.event.NEW_MEDIA")
}
ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_EXPORTED)A receiver declared in your manifest does not get them, because Android delivers broadcasts without a package only to receivers registered at run time.
While a Live runs on Visor, an app that holds the token can get its picture as JPEG stills. Visor decodes each still from the stream on its own, so it asks for one every 200 ms at most, and how many it manages depends on the phone.
- Declare Visor in your manifest, or Android hides it from your app:
<queries><package android:name="com.macedonga.visor" /></queries>. - Bind the service with the action
com.macedonga.visor.action.LIVE_FRAMESand the packagecom.macedonga.visor. - Send message 1 (subscribe) with
tokenand, if you want,interval_ms(default 1000, at least 200). SetreplyToto your ownMessenger. - Read message 3 (frame):
jpegis the picture as a byte array,timeis when Visor took it, in milliseconds since 1970. - Send message 2 (unsubscribe), or unbind, to stop.
Message 4 (refused) comes instead when the token is wrong or the mode is off. Its reason is TOKEN or OFF. It also comes later if someone makes a new token or turns the mode off.
val frames = Messenger(Handler(Looper.getMainLooper()) { msg ->
when (msg.what) {
3 -> msg.data.getByteArray("jpeg")?.let { show(BitmapFactory.decodeByteArray(it, 0, it.size)) }
4 -> Log.w("frames", "refused: ${msg.data.getString("reason")}")
}
true
})
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
val subscribe = Message.obtain(null, 1).apply {
data = bundleOf("token" to token, "interval_ms" to 500L)
replyTo = frames
}
Messenger(binder).send(subscribe)
}
override fun onServiceDisconnected(name: ComponentName) {}
}
context.bindService(
Intent("com.macedonga.visor.action.LIVE_FRAMES").setPackage("com.macedonga.visor"),
connection,
Context.BIND_AUTO_CREATE,
)Nothing comes while no Live runs. Frames start again by themselves when the next Live starts.
- A typical flow: take a photo, wait for its
MEDIA_AVAILABLE, sendIMPORT_MEDIAwith that id, and read the file fromNEW_MEDIA. In a test,MEDIA_AVAILABLEcame less than a second after the photo. - An import goes over Wi-Fi Direct, so the phone's Wi-Fi must be on.
- The glasses also send captures still waiting in their own queue from an earlier import. Each one gets its own
NEW_MEDIA, so check theid. IMPORT_MEDIAfor a capture already on the phone imports nothing and sendsNEW_MEDIAagain.- After Visor restarts, it sends
MEDIA_AVAILABLEagain for every capture still waiting on the glasses. - Visor's own imports, from the gallery or on their own, send
IMPORT_PROGRESS,NEW_MEDIAandIMPORT_FAILEDtoo. - To read the file at
uriorpath, your app needs Android's permission to read photos and videos. - While the mode is on and Actions need the token is off, any app on the phone can send these actions. The events go to every app that listens. The glasses' capture light still turns on for every photo and video.
- The glasses' call app also refers to
wearables_robusta.demo_enabled. What the switch does there is not known. - Visor's event log (Settings, Advanced, Event log) shows each event it sends, with its extras.