Documentation
¶
Overview ¶
Package android captures the Android screen from a pure-Go, CGO-free application, for an XR compositor that redraws every frame.
What Android permits, and what it does not ¶
Everything here is shaped by one measured fact: an ordinary, unprivileged APK may MIRROR the screen and may draw its own content on an attached display, and it may do nothing else. It cannot give itself extra desktops. On Android 15 (API 35), creating a virtual display and launching an activity on it fails with
SecurityException: Permission Denial: starting Intent { ... }
... with launchDisplayId=N
for ANY activity, including the caller's own; asking for VIRTUAL_DISPLAY_FLAG_TRUSTED — which is not in the public SDK at all — fails with "Requires ADD_TRUSTED_DISPLAY permission", a signature|role permission; and VirtualDeviceManager.createVirtualDevice fails with "Access denied, requires: android.permission.CREATE_VIRTUAL_DEVICE", which is internal|role. See the README for the whole transcript. So on Android an XR ribbon carries the phone's real screen, and the application's own content, and that is all.
The split, and why there is a Java host ¶
Android hands no drawable surface, and no MediaProjection, to a process that is not the app: both are behind JNI, and JNI needs cgo. So the app is two processes, exactly as in go-widgets/android — a small Java host owning the Android objects, and an ordinary CGO_ENABLED=0 GOOS=android executable owning everything above the pixels. This package is the application half.
The hot path ¶
Stream.Frame hands back a BORROWED view of the most recent frame: the bytes are the shared mapping the host wrote into, not a copy, and the borrow lasts until the next Stream.Frame, Stream.WaitFrame or Stream.Close. In steady state a Frame call performs no allocation at all. Copy out with Frame.CopyTight or Frame.NRGBA to keep it longer.
Stride ¶
A captured frame's rows may be PADDED: Stride is the number of BYTES per row and it is not necessarily Width*4. Always index with Stride, or use Frame.Row. The Android 15 emulator measured here reported stride 4320 for a 1080-wide capture — exactly Width*4, no padding — which is precisely why the API carries the number instead of trusting it.
Frames only arrive when something changes ¶
A MediaProjection capture is change-driven, like ScreenCaptureKit on macOS. Options.FPS is a CEILING, not a rate: with a still screen, this package's own probe took 39 frames in 4.0 s and then nothing at all; with the screen scrolling it took 574 frames in 11.9 s. Do not treat a missing frame as a failure; the second return value of Stream.Frame is the truth about whether anything changed.
Consent ¶
Every capture session needs the user to agree through a system dialog, and the agreement dies with the session. Authorized reports whether the host already holds a projection token; RequestAuthorization asks the host to put the dialog in front of the user and blocks until they answer.
Index ¶
- Constants
- Variables
- func ArtifactDir() (string, error)
- func Authorized() bool
- func Available() bool
- func DecodeBuffer(b []byte) (slots int, slotSize int64, err error)
- func DecodeConsent(b []byte) (bool, error)
- func DeriveSocket(env string, envOK bool, home string) string
- func DeriveWallSocket(env string, envOK bool, home string) string
- func EncodeBuffer(slots int, slotSize int64) []byte
- func EncodeConfig(c ConfigMessage) []byte
- func EncodeConsent(granted bool) []byte
- func EncodeDisplays(ds []Display) []byte
- func EncodeError(e ErrorMessage) []byte
- func EncodeFrame(f FrameMsg) []byte
- func EncodeOpenDisplay(m OpenDisplayMessage) []byte
- func EncodeOpenScreen(m OpenScreenMessage) []byte
- func EncodePresent(m PresentMessage) []byte
- func EncodePresented(m PresentedMessage) []byte
- func EncodeStart(s StartMessage) []byte
- func EncodeStopped(s StoppedMessage) []byte
- func FrameMessage(typ uint8, body []byte) []byte
- func ReadMessage(r io.Reader) (typ uint8, body []byte, err error)
- func RequestAuthorization(context.Context) (bool, error)
- func SaveTranscript(name, text string) (string, error)
- func SentinelColorAt(w, h, x, y int) (uint32, bool)
- func WallAvailable() bool
- func WriteMessage(w io.Writer, typ uint8, body []byte) error
- type Canvas
- type ConfigMessage
- type Content
- type Display
- type DisplaySpec
- type ErrorMessage
- type Frame
- type FrameMsg
- type OpenDisplayMessage
- type OpenScreenMessage
- type Options
- type OwnedDisplay
- type PixelFormat
- type PresentMessage
- type PresentedMessage
- type Screen
- func (s *Screen) Close() error
- func (s *Screen) DisplayID() int
- func (s *Screen) Err() error
- func (s *Screen) Next(ctx context.Context) (Canvas, error)
- func (s *Screen) Options() ScreenOptions
- func (s *Screen) Present(ctx context.Context, c Canvas) error
- func (s *Screen) Size() (int, int)
- func (s *Screen) Stats() ScreenStats
- func (s *Screen) Stride() int
- func (s *Screen) String() string
- type ScreenOptions
- type ScreenStats
- type Sentinel
- type StartMessage
- type Stats
- type StoppedMessage
- type Stream
- func (st *Stream) Close() error
- func (st *Stream) Err() error
- func (st *Stream) Format() PixelFormat
- func (st *Stream) Frame() (Frame, bool)
- func (st *Stream) Options() Options
- func (st *Stream) Size() (int, int)
- func (st *Stream) Stats() Stats
- func (st *Stream) String() string
- func (st *Stream) WaitFrame(context.Context) (Frame, error)
- type Wall
- type Web
Constants ¶
const ( // MsgDisplays answers MsgListDisplays with everything the host can see. MsgDisplays uint8 = 0x01 // MsgConfig announces a started capture: the frame geometry, the pixel // format and the layout of the shared buffer the frames will appear in. MsgConfig uint8 = 0x02 // MsgFrame says a frame has been written into one slot of the shared // buffer. It carries no pixels: the pixels are already there. MsgFrame uint8 = 0x03 // MsgStopped says the capture ended, and why. The user revoking the // projection from the system UI arrives here, not as an error on a call. MsgStopped uint8 = 0x04 // MsgConsent answers MsgConsentRequest with the user's decision. MsgConsent uint8 = 0x05 // MsgError reports a failure the host could not answer in-band. MsgError uint8 = 0x06 // MsgBuffer hands the application the shared frame buffer, as an ancillary // descriptor, once a capture has been configured. It is the ONE message // carrying a descriptor, so it is always written in a single sendmsg. // // The buffer belongs to the HOST, not to the application, and that is // forced rather than chosen: an Android app cannot map a descriptor it // received read-write. SharedMemory.fromFileDescriptor rejects a Go memfd // outright ("FileDescriptor is not a valid ashmem fd"), and reopening it // through /proc/self/fd is refused with EACCES. Only a region the host // itself created with SharedMemory.create is writable by the host, so the // host creates it and lends it here. MsgBuffer uint8 = 0x07 // MsgPresented says one frame the application wrote has reached the view on // an existing display, so its slot is free again. // // It is the ONLY back-pressure [Screen] has. The pixels travel the opposite // way there — this process draws and the host blits — so nothing else stops // the application overwriting a slot the host has not finished reading, and // a tear in a headset is not a cosmetic defect. MsgPresented uint8 = 0x08 // MsgListDisplays asks the host what displays exist. MsgListDisplays uint8 = 0x81 // MsgConsentRequest asks the host to put the screen-capture consent dialog // in front of the user. Only the host can: consent is an Activity result, // and the application owns no Activity. MsgConsentRequest uint8 = 0x82 // MsgStart asks the host to begin capturing. MsgStart uint8 = 0x83 // MsgStop asks the host to end the capture but stay connected. MsgStop uint8 = 0x84 // MsgBye tells the host the application is going away. MsgBye uint8 = 0x85 // MsgOpenDisplay asks the wall host to create ONE virtual display, show a // Presentation carrying the named content on it, and stream the pixels // Android renders there. The host answers with MsgConfig — whose DisplayID // carries the id the platform assigned — and then MsgBuffer, exactly as // MsgStart does, so one Stream implementation serves both. MsgOpenDisplay uint8 = 0x86 // MsgOpenScreen asks the host to show a Presentation on a display that // ALREADY EXISTS — the glasses — carrying a view this process paints into. // // It is the mirror image of MsgOpenDisplay, and the difference is the // direction of the pixels, not the kind of window. MsgOpenDisplay exists to // get at what Android can render and a CGO-free Go process cannot; this // exists because the headset is an OUTPUT, and what belongs on it is the // ribbon this process composited. The host answers with MsgConfig and then // MsgBuffer, exactly as the other two do. MsgOpenScreen uint8 = 0x87 // MsgPresent says the application has finished drawing one slot and the // host should put it on the screen. It carries no pixels: the pixels are // already in the shared buffer. MsgPresent uint8 = 0x88 )
Message types. Host→app messages are below 0x80 and app→host at or above it, so a misrouted message is a decode error rather than a plausible other message.
const ( // StopUser is the user revoking the projection — from the cast chip in the // status bar, or from the notification the foreground service must show. // It is the normal way a capture ends and is NOT a malfunction. StopUser uint8 = 0 // StopSystem is the platform stopping us: a new projection started // elsewhere, the device locked, the service was killed. StopSystem uint8 = 1 // StopApp is this application having asked, with MsgStop or MsgBye. StopApp uint8 = 2 )
Reasons a capture stopped, carried by StoppedMessage.
const ( // MinScreenEdge is the smallest frame a screen will carry. Smaller than // this is a mistake rather than a frugal choice. MinScreenEdge = 16 // MaxScreenPixels bounds one frame, and with it the shared buffer. // // The WHOLE buffer has to fit an int32, which is what Android's // SharedMemory.create takes, and it is MaxQueueDepth slots of four bytes a // pixel: 32 megapixels is the largest frame that leaves the product below // two gigabytes at every accepted queue depth. It is eight 4K panels' worth, // so it constrains nothing anybody would ask for and still refuses a width // times a height that overflowed somewhere upstream. MaxScreenPixels = 32 << 20 )
Limits on a screen's frames.
const ( FlagSupportsProtectedBuffers uint32 = 1 << 0 FlagSecure uint32 = 1 << 1 FlagPrivate uint32 = 1 << 2 FlagPresentation uint32 = 1 << 3 FlagRound uint32 = 1 << 4 )
Display flags, mirroring android.view.Display's public FLAG_* constants. The two that matter to an XR application are FlagPresentation, which says the system considers a display suitable for an app's own full-screen content, and FlagPrivate, which says it belongs to one app.
const ( // DefaultFPS is the frame-rate ceiling used when Options.FPS is zero. DefaultFPS = 60.0 // DefaultQueueDepth is the slot count used when Options.QueueDepth is zero. DefaultQueueDepth = 3 // MinQueueDepth is the smallest slot count this package accepts. MinQueueDepth = 3 // MaxQueueDepth bounds the slot count. A slot is a whole framebuffer — // 10.4 MB for a 1080x2400 phone — so this is a real memory limit, not a // formality. MaxQueueDepth = 8 // MaxDimension is the largest frame edge accepted, a sanity bound well // above any real display. MaxDimension = 32768 )
Defaults applied to the zero value of the corresponding Options field.
const ( // DefaultMaxDisplays is the limit [NewWall] applies when it is given 0. A // ribbon uses six to eight panels, so this is already generous. DefaultMaxDisplays = 8 // MaxDisplays is the largest limit [NewWall] accepts, however explicitly a // caller asks. It is nowhere near the count that killed system_server, and // the Java host refuses past it too. MaxDisplays = 32 // DefaultDensityDPI is the density given to an owned display whose spec // leaves DensityDPI at zero. It is Android's DENSITY_XHIGH, the density a // 1920x1080 panel of the size XR glasses present usually reports. DefaultDensityDPI = 320 // MinDisplayEdge is the smallest edge an owned display may have. A display // smaller than this cannot lay a view hierarchy out at all. MinDisplayEdge = 16 )
Limits on how many displays one application may create.
const ( SentinelBackground uint32 = 0xFF0000 // red SentinelTopLeft uint32 = 0x00FF00 // green SentinelBottomRight uint32 = 0x0000FF // blue )
The colours Sentinel paints, as packed 0xRRGGBB.
const ArtifactEnv = "XRKIT_ARTIFACT_DIR"
ArtifactEnv names the directory artefacts are written to, overriding the default.
const DefaultDisplayID = 0
DefaultDisplayID is android.view.Display.DEFAULT_DISPLAY.
const EnvSocket = "XR_ANDROID_SOCKET"
EnvSocket names the environment variable carrying the abstract socket the XR host listens on. It is declared here too so a build script or a test can name it from any platform.
const EnvWallSocket = "XR_ANDROID_WALL_SOCKET"
EnvWallSocket names the environment variable carrying the wall host's socket, the counterpart of EnvSocket.
const MaxPayload = 1 << 16
MaxPayload bounds one decoded message body. The largest message a host legitimately sends is a display list, so a frame beyond this is a desynchronised stream — refused rather than allocated.
const SocketSuffix = ".xr"
SocketSuffix is appended to the application's package name to form the abstract socket the host listens on when nothing names one explicitly. It matches XrHostService.SOCKET_SUFFIX.
const WallSocketSuffix = ".xrwall"
WallSocketSuffix is appended to the package name to get the abstract socket the WALL host listens on, matching XrWallService.SOCKET_SUFFIX.
It is a second socket, and a second service, because the two have nothing in common but a wire format. Capture needs a MediaProjection, a consent dialog and — from API 34 — a mediaProjection foreground service, none of which an owned display needs: it asks for no permission at all. Keeping them apart means creating a ribbon panel cannot put a "recording your screen" chip in the status bar, and that a capture session ending cannot take a panel with it. The capture host also serves exactly one connection, whereas the wall serves one PER DISPLAY.
Variables ¶
var ( // ErrUnsupported is reported on every platform that is not Android, and on // Android when the process was not started by a host that speaks this // protocol. The module still builds and vets everywhere, so an application // keeps cross-building on a developer's workstation. ErrUnsupported = errors.New("android: screen capture is unavailable " + "(not running under a go-xrkit Android host)") // ErrPermissionDenied is reported when the user refused the screen-capture // consent dialog, or revoked a projection already running. Its message // names the remedy. ErrPermissionDenied = errors.New("android: screen-capture consent denied — " + "Android asks the user for permission once per capture session and the " + "answer cannot be remembered; call RequestAuthorization again and accept " + "the system dialog") // ErrNoDisplay is reported when a capture was asked for and the host // listed no display at all. ErrNoDisplay = errors.New("android: no capturable display") // ErrNotFound is reported when a display ID does not name anything the // host can see. ErrNotFound = errors.New("android: no such display") // ErrNotCapturable is reported for a display that exists but that an // unprivileged app may not capture. MediaProjection mirrors the DEFAULT // display and nothing else: capturing a second display needs // CAPTURE_VIDEO_OUTPUT, which is a signature permission. ErrNotCapturable = errors.New("android: only the default display can be captured " + "by an unprivileged app (capturing another display needs the signature " + "permission CAPTURE_VIDEO_OUTPUT)") // ErrClosed is reported by every [Stream] method after [Stream.Close]. ErrClosed = errors.New("android: stream is closed") // ErrNoFrame is reported by [Stream.WaitFrame] when no frame arrived // before its context expired. It is NOT a malfunction: a motionless screen // legitimately produces no frames. ErrNoFrame = errors.New("android: no frame available") // ErrInvalidOption is reported by [Options.Validate] and wraps a // description of the offending field. ErrInvalidOption = errors.New("android: invalid option") // ErrShortBuffer is reported by [Frame.CopyTight] when the destination is // too small to hold the frame. ErrShortBuffer = errors.New("android: destination buffer too short") )
Sentinel errors. All are stable and may be matched with errors.Is.
var ErrBadPayload = errors.New("android: malformed message payload")
ErrBadPayload reports a message whose body is structurally impossible — a string longer than the body that holds it, a negative count.
var ErrInRepository = errors.New("a capture must never be written where it can be committed")
ErrInRepository is reported when the chosen artefact directory is inside a git work tree.
var ErrNotPresentable = errors.New("android: display will not take a presentation")
ErrNotPresentable reports a display that cannot carry a Presentation.
The built-in panel is the usual one: it is where the launcher and every other application live, and the platform will not let an ordinary app put a Presentation over them. Display.Presentation says in advance, which is why this is answered here without asking the host.
var ErrScreenClosed = errors.New("android: screen is closed")
ErrScreenClosed is reported by Screen.Next and Screen.Present after Screen.Close.
var ErrShortPayload = errors.New("android: truncated message payload")
ErrShortPayload reports a message whose body is too short for its type.
var ErrTooManyDisplays = errors.New("android: too many virtual displays")
ErrTooManyDisplays is reported by NewWall when the requested limit exceeds MaxDisplays, and by Wall.Open when the wall is already at its limit. Nothing is created in either case.
var ErrWallClosed = errors.New("android: wall is closed")
ErrWallClosed is reported by Wall.Open after Wall.Close.
Functions ¶
func ArtifactDir ¶ added in v0.4.0
ArtifactDir is where a command may write what it captured, created if it is not there. The rule above is what it enforces.
func Authorized ¶
func Authorized() bool
Authorized reports whether the host already holds a projection token. Off Android, never.
func Available ¶
func Available() bool
Available reports whether this process is running under a host that speaks this protocol. Off Android, never.
func DecodeBuffer ¶
DecodeBuffer decodes the layout of the shared buffer.
func DecodeConsent ¶
DecodeConsent decodes the user's decision.
func DeriveSocket ¶
DeriveSocket works out which abstract socket to dial.
An explicit name wins: a host that spawned this process sets EnvSocket and that is the end of it. But in the composition this package is really for, the process is spawned by a DIFFERENT host — go-widgets/android's Activity, which owns the drawing surface and knows nothing about capture — and never sets it. Android gives such a process no way to ask its own package name either.
What it does give it is HOME, which that host sets to the app's private files directory: "/data/user/0/org.example.app/files". The package name is in there, and the host's socket name follows from it. That is why the two hosts can live in one APK without either one having to know about the other.
It returns "" when neither route yields a name.
func DeriveWallSocket ¶ added in v0.2.0
DeriveWallSocket works out which abstract socket the wall host listens on, exactly as DeriveSocket does for the capture host.
func EncodeBuffer ¶
EncodeBuffer encodes the layout of the shared buffer accompanying MsgBuffer.
func EncodeConsent ¶
EncodeConsent encodes the user's decision.
func EncodeDisplays ¶
EncodeDisplays encodes a display list.
func EncodeOpenDisplay ¶ added in v0.2.0
func EncodeOpenDisplay(m OpenDisplayMessage) []byte
EncodeOpenDisplay encodes an OpenDisplayMessage.
func EncodeOpenScreen ¶ added in v0.5.0
func EncodeOpenScreen(m OpenScreenMessage) []byte
EncodeOpenScreen encodes an OpenScreenMessage.
func EncodePresent ¶ added in v0.5.0
func EncodePresent(m PresentMessage) []byte
EncodePresent encodes a PresentMessage.
func EncodePresented ¶ added in v0.5.0
func EncodePresented(m PresentedMessage) []byte
EncodePresented encodes a PresentedMessage.
func EncodeStopped ¶
func EncodeStopped(s StoppedMessage) []byte
EncodeStopped encodes a StoppedMessage.
func FrameMessage ¶
FrameMessage returns one framed message: a 4-byte big-endian length covering the type byte and the body, then the type byte, then the body.
It exists as bytes rather than as writes because a message that carries an ancillary descriptor has to reach the host in ONE sendmsg: split across two writes, the host could attribute the descriptor to the wrong message.
func ReadMessage ¶
ReadMessage reads one framed message. It returns io.EOF when the stream ends cleanly between messages, so a caller can tell a closed host from a truncated one.
func RequestAuthorization ¶
RequestAuthorization asks the host for the screen-capture consent dialog. Off Android it reports ErrUnsupported.
func SaveTranscript ¶ added in v0.5.0
SaveTranscript writes a command's transcript where it can still be read after the cable has been somewhere else, and returns the path it used.
⛔ THE APP'S OWN files DIRECTORY, NOT ITS EXTERNAL ONE. A file in /storage/emulated/0/Android/data/<pkg>/ is hidden from the shell user on a modern Android, and `adb shell run-as` cannot traverse the FUSE layer that mediates it: a whole run was measured, written there, and unreadable. The private directory -- which the host hands over as HOME -- is what run-as can read, and this is where that lands.
Off a device there is no HOME of that kind, so it falls back to ArtifactDir. A transcript is text this process composed rather than a picture of anybody's screen, so the rule about work trees does not govern it -- but the fallback obeys it anyway, because the cheapest way to keep a rule is to have one path.
func SentinelColorAt ¶ added in v0.2.0
SentinelColorAt is the colour a correct Sentinel frame holds at (x, y) on a display of the given size, as 0xRRGGBB. It reports false for a point outside the display, or one close enough to a quadrant edge or to the label that anti-aliasing makes the exact value unsafe to assert on.
Sample with it rather than comparing whole images: the label is rendered by Android's own text stack and will not be byte-identical between releases, while the quadrants are flat colour and are.
func WallAvailable ¶ added in v0.2.0
func WallAvailable() bool
WallAvailable reports whether this process is running under a host that serves owned displays. Off Android, never.
Types ¶
type Canvas ¶ added in v0.5.0
type Canvas struct {
// Pix is the frame's pixels, Stride bytes a row, Height rows.
Pix []byte
// Stride is the distance in bytes between two rows. It is at least
// Width*4 and is usually more: the host announces the alignment its
// surface wants, and writing Width*4 instead would shear the image.
Stride int
// Width and Height are the frame's size in pixels.
Width, Height int
// contains filtered or unexported fields
}
A Canvas is one frame slot of a screen's shared buffer, lent to the application to draw into.
The bytes are BORROWED and belong to the host: they are valid from the Screen.Next that returned them until the Screen.Present that hands them back, and touching them afterwards races the blit. A Canvas is not safe for concurrent use.
Pix is RGBA, eight bits a channel — the same memory layout as image.RGBA, which is what Canvas.RGBA hands back so the whole of image/draw can paint a frame.
func (Canvas) RGBA ¶ added in v0.5.0
RGBA returns the canvas as an image.RGBA sharing the same memory, so a frame can be painted with image/draw, a font renderer, or anything else that takes a draw.Image.
It allocates only the header: the pixels are the host's, and drawing through this writes straight into the shared buffer.
func (Canvas) Row ¶ added in v0.5.0
Row returns the bytes of one row, for a compositor that writes spans rather than going through image/draw. It reports nil for a row outside the frame.
The slice stops at Width*4 and cannot be appended past the row: the padding a stride leaves is the host's, and a span that ran into it would corrupt the next row rather than fail.
type ConfigMessage ¶
type ConfigMessage struct {
// Width and Height are the frame size in pixels.
Width, Height int
// Stride is the number of BYTES per row in the shared buffer. It is the
// host's ImageReader row stride and is NOT necessarily Width*4.
Stride int
// Format is the pixel layout, always [FormatRGBA] today.
Format PixelFormat
// Slots is how many frame slots the shared buffer holds.
Slots int
// SlotSize is the byte size of one slot, Stride*Height rounded up to the
// host's alignment.
SlotSize int64
// DisplayID names the display being captured.
DisplayID int
}
ConfigMessage announces a started capture.
func DecodeConfig ¶
func DecodeConfig(b []byte) (ConfigMessage, error)
DecodeConfig decodes a ConfigMessage.
type Content ¶ added in v0.2.0
type Content interface {
// Describe renders the content for logs.
Describe() string
// Validate reports whether the content is usable, wrapping
// [ErrInvalidOption].
Validate() error
// contains filtered or unexported methods
}
Content is what Android renders on an owned display.
The seam, and why it is a DECLARATION rather than pixels ¶
The obvious seam would hand the host a framebuffer for it to blit. That is the wrong way round here, and would be a round trip with no product: pixels this process drew, sent to Android, read back unchanged. If the application is drawing, it should composite in Go and skip the display entirely.
The reason to route a ribbon panel through a real Android display is the opposite one — to get at what Android renders and this process CANNOT: a WebView, a MediaCodec surface, a PdfRenderer, a maps view. None of those can be reached from a CGO-free Go process, all of them are ordinary Views, and a Presentation is how a View gets onto a display. So Content NAMES what should render, the Java host builds it, and the pixels come back through the same borrowed-frame path as a screen capture.
That also keeps the rule the rest of this package is built on: the host owns the Android objects and decides nothing. A URL is a decision the application made; a WebView is the host obeying it.
The set is deliberately closed — a Content implemented outside this package would name a Java class the host does not have. Implementations are Web and Sentinel.
type Display ¶
type Display struct {
// ID is the android.view.Display display id. 0 is the built-in screen.
ID int
// Name is what the system calls the display. For the built-in panel this
// is a device string ("Built-in Screen" on the Android 15 emulator); for a
// display attached over USB-C DP Alt Mode it comes from the sink's EDID,
// which is how XR glasses announce their model.
Name string
// Width and Height are the display's real size in pixels.
Width, Height int
// DensityDPI is the display's density in dots per inch.
DensityDPI int
// RefreshRate is the active mode's rate in hertz.
RefreshRate float64
// Flags are android.view.Display's FLAG_* bits; see [FlagPresentation].
Flags uint32
}
Display is a display the host can see.
Width and Height are in PIXELS — Android reports no separate point space, so unlike the macOS sibling there is only one pair of numbers, and DensityDPI carries what a scale factor would carry elsewhere.
func DecodeDisplays ¶
DecodeDisplays decodes a display list.
func DefaultDisplay ¶
DefaultDisplay returns the built-in screen. Off Android it reports ErrUnsupported.
func Displays ¶
Displays returns every display the host can see. Off Android it reports ErrUnsupported.
func Screens ¶ added in v0.5.0
Screens returns the displays an application may put a Screen on. Off Android there is no host to ask, so it reports ErrUnsupported.
func (Display) Default ¶
Default reports whether this is the built-in screen, the only display an unprivileged app may capture.
func (Display) Presentation ¶
Presentation reports whether the system considers this display suitable for an application's own full-screen content — which is what an XR headset attached over DP Alt Mode is for.
type DisplaySpec ¶ added in v0.2.0
type DisplaySpec struct {
// Width and Height are the display's size in PIXELS. Both are required.
//
// Making them small does NOT buy more displays — the count that kills
// system_server was the same at 640x480 and at 1920x1080 — so choose the
// size the content wants to be read at.
Width, Height int
// DensityDPI is the display's density. Zero means [DefaultDensityDPI].
// Android lays the view hierarchy out against this, so it decides how
// large text and controls come out in the ribbon.
DensityDPI int
// Content is what Android renders on the display. It is required; see
// [Content].
Content Content
// QueueDepth is how many frame slots the shared buffer holds, exactly as
// in [Options.QueueDepth]. Zero means [DefaultQueueDepth].
QueueDepth int
}
DisplaySpec says what an owned display should be.
The zero DisplaySpec is not usable: a display has no natural size to fall back on the way a capture does, so Width, Height and Content must be given.
func (DisplaySpec) String ¶ added in v0.2.0
func (s DisplaySpec) String() string
String renders the spec for logs.
func (DisplaySpec) Validate ¶ added in v0.2.0
func (s DisplaySpec) Validate() error
Validate reports whether the spec describes a display Android could make, wrapping ErrInvalidOption. It does not consult the system, and it does not apply defaults: a zero DensityDPI or QueueDepth is refused here and filled in by Wall.Open before this is reached.
type ErrorMessage ¶
type ErrorMessage struct {
// Code is the host's own numeric code, 0 when it has none.
Code int
// Op names what failed, e.g. "getMediaProjection".
Op string
// Detail is the platform's message, e.g. a SecurityException's text.
Detail string
}
ErrorMessage is a failure the host could not answer in-band.
func DecodeError ¶
func DecodeError(b []byte) (ErrorMessage, error)
DecodeError decodes an ErrorMessage.
func (ErrorMessage) Error ¶
func (e ErrorMessage) Error() string
Error renders the operation, the code and the platform's message.
type Frame ¶
type Frame struct {
// Pix is the frame's bytes in [FormatRGBA], Stride bytes per row, Height
// rows. len(Pix) == Stride*Height.
Pix []byte
// Width and Height are the frame's size in pixels.
Width, Height int
// Stride is the number of BYTES per row. It may be padded and is NOT
// necessarily Width*4.
Stride int
// Seq counts frames since the stream started; it is 0 before the first
// frame and strictly increases afterwards.
Seq uint64
// At is when the host's ImageReader delivered the frame.
At time.Time
}
Frame is a BORROWED view of one captured frame.
Pix aliases the shared mapping the host wrote into. It stays valid only until the next Stream.Frame, Stream.WaitFrame or Stream.Close on the stream that produced it. Do not retain it; copy with Frame.CopyTight or Frame.NRGBA if you need it to outlive the borrow.
func (Frame) CopyTight ¶
CopyTight copies the frame into dst with the row padding removed, so dst holds Width*4*Height bytes of contiguous RGBA. It reports how many bytes it wrote, or ErrShortBuffer if dst is too small. It allocates nothing.
func (Frame) NRGBA ¶
NRGBA copies the frame into a freshly allocated image.NRGBA. It is the convenience path for saving a frame to disk; it allocates, so it does not belong in a per-frame loop.
Android's RGBA_8888 is already image.NRGBA's byte order, so unlike the macOS sibling this is a copy with no channel swap — but it is still a copy, because the source is padded and borrowed.
func (Frame) Row ¶
Row returns row y of the frame, Width*4 bytes with the padding trimmed off. It does not allocate. It returns nil for an out-of-range y or an invalid frame.
type FrameMsg ¶
type FrameMsg struct {
// Seq counts frames since the capture started, from 1.
Seq uint64
// Slot is which slot of the shared buffer holds it.
Slot int
// Width, Height and Stride describe THIS frame. They normally repeat the
// config, but the host may resize a capture in flight — a phone rotating
// under a capture does exactly that — so every frame carries its own
// geometry rather than inheriting one that may be stale.
Width, Height, Stride int
// AtUnixNano is when the host's ImageReader delivered it.
AtUnixNano int64
}
FrameMsg says a frame has been written into a slot of the shared buffer.
type OpenDisplayMessage ¶ added in v0.2.0
type OpenDisplayMessage struct {
// Width and Height are the display's size in pixels.
Width, Height int
// DensityDPI is the density Android lays the view hierarchy out against.
DensityDPI int
// Slots is how many frame slots the shared buffer holds.
Slots int
// ContentKind names the View the host should build; see [Content].
ContentKind uint32
// ContentPayload is that content's one string argument — a URL for a
// WebView, a label for the sentinel.
ContentPayload string
}
OpenDisplayMessage asks the wall host for one owned display.
func DecodeOpenDisplay ¶ added in v0.2.0
func DecodeOpenDisplay(b []byte) (OpenDisplayMessage, error)
DecodeOpenDisplay decodes an OpenDisplayMessage.
type OpenScreenMessage ¶ added in v0.5.0
type OpenScreenMessage struct {
// DisplayID is the android.view.Display id to present on. It must be a
// display the host can see and whose FLAG_PRESENTATION is set; the host
// refuses anything else rather than guessing.
DisplayID int
// Width and Height are the size of the frames this process will write, in
// pixels. They need not match the display: the host scales the bitmap to
// fill it, which is how a ribbon rendered at one resolution reaches panels
// of another.
Width, Height int
// Slots is how many frame slots the shared buffer holds.
Slots int
}
OpenScreenMessage asks the host for a Presentation on a display that already exists, and for the shared buffer this process will paint into.
func DecodeOpenScreen ¶ added in v0.5.0
func DecodeOpenScreen(b []byte) (OpenScreenMessage, error)
DecodeOpenScreen decodes an OpenScreenMessage.
type Options ¶
type Options struct {
// Width and Height are the requested frame size in PIXELS. Zero means the
// display's native size.
Width, Height int
// FPS is the CEILING on the frame rate, not a guarantee: a MediaProjection
// only produces a frame when the content changed. Zero means [DefaultFPS].
FPS float64
// QueueDepth is how many frame slots the shared buffer holds. Zero means
// [DefaultQueueDepth]. It must leave room for the frame lent out and the
// one being written, so values below [MinQueueDepth] are rejected — with
// two slots a host that produced two frames while the consumer held one
// would overwrite the borrow.
QueueDepth int
}
Options configures a capture stream.
The zero Options is usable: it captures the default display at its native pixel size, at up to 60 frames per second.
func (Options) Validate ¶
Validate reports whether the options are self-consistent, wrapping ErrInvalidOption. It does not consult the system.
type OwnedDisplay ¶ added in v0.2.0
type OwnedDisplay struct {
*Stream
// contains filtered or unexported fields
}
OwnedDisplay is one display this application created, and the feed of pixels Android rendered on it.
It embeds Stream, so a ribbon consumes it exactly as it consumes a screen capture — Stream.Frame lends borrowed pixels with the stride carried and says whether they are fresh, Stream.WaitFrame blocks for the next one. That is the point: the ribbon does not care whether a panel is the mirrored phone or a display we made.
func (*OwnedDisplay) Close ¶ added in v0.2.0
func (d *OwnedDisplay) Close() error
Close releases the display and its feed, and gives the slot back to the wall. It is idempotent.
func (*OwnedDisplay) ID ¶ added in v0.2.0
func (d *OwnedDisplay) ID() int
ID is the android.view.Display id the platform gave this display. It is only meaningful for logs and for matching against `adb shell dumpsys display`; no other application may do anything with it.
func (*OwnedDisplay) Spec ¶ added in v0.2.0
func (d *OwnedDisplay) Spec() DisplaySpec
Spec is the specification this display was opened with, with its zero fields filled in.
func (*OwnedDisplay) String ¶ added in v0.2.0
func (d *OwnedDisplay) String() string
String renders the display for logs.
type PixelFormat ¶
type PixelFormat uint32
PixelFormat names the layout of a captured frame, using Android's own android.graphics.PixelFormat constants so the value on the wire is the one the host read off the ImageReader.
const FormatRGBA PixelFormat = 1 // android.graphics.PixelFormat.RGBA_8888
FormatRGBA is 32-bit RGBA, android.graphics.PixelFormat.RGBA_8888. It is the only format this package streams, and it is what ImageReader produces for a MediaProjection mirror. It is also byte-for-byte what a go-widgets painter writes, so a captured frame composites with no conversion.
This differs from the sibling packages on other platforms: macOS ScreenCaptureKit hands back BGRA. Frame.NRGBA hides the difference, and anything compositing raw bytes must ask Stream.Format.
func (PixelFormat) BytesPerPixel ¶
func (f PixelFormat) BytesPerPixel() int
BytesPerPixel is the size of one pixel in this format, 0 for a format this package does not stream.
func (PixelFormat) String ¶
func (f PixelFormat) String() string
String renders the format for logs.
type PresentMessage ¶ added in v0.5.0
type PresentMessage struct {
// Seq numbers the frame, from 1. It comes back in [PresentedMessage].
Seq int64
// Slot is which slot of the shared buffer holds the pixels.
Slot int
// Width, Height and Stride describe the pixels in that slot.
Width, Height, Stride int
}
PresentMessage hands one slot of the shared buffer to the host to put on the screen. The geometry travels with it rather than being implied by the config, for the same reason FrameMsg carries it: the one place a wrong number would become an out-of-range slice over shared memory is the place to state it explicitly and check it.
func DecodePresent ¶ added in v0.5.0
func DecodePresent(b []byte) (PresentMessage, error)
DecodePresent decodes a PresentMessage.
type PresentedMessage ¶ added in v0.5.0
type PresentedMessage struct {
// Seq is the [PresentMessage.Seq] of the frame that was presented.
Seq int64
// Slot is the slot it occupied, which the application may now draw into.
Slot int
}
PresentedMessage says one frame reached the screen and its slot is free.
func DecodePresented ¶ added in v0.5.0
func DecodePresented(b []byte) (PresentedMessage, error)
DecodePresented decodes a PresentedMessage.
type Screen ¶ added in v0.5.0
type Screen struct {
// contains filtered or unexported fields
}
A Screen is a Presentation on a display that ALREADY EXISTS — the glasses — carrying pixels this process painted.
Why this is a different thing from a Wall ¶
A Wall creates displays and reads what Android renders on them. A Screen does neither: the display is the headset, the platform made it when the cable was plugged in, and the pixels travel the other way. The two share a host, a socket and a wire, and nothing else — a Wall is an INPUT, a Screen is the OUTPUT, and an XR application wants both at once.
Why an application may have one at all ¶
An ordinary APK may not start an activity on a display it does not own: that is refused with a SecurityException, and the flag that would lift it needs a signature permission. A Presentation is not an activity start, so it is not what that check refuses, and it is the only route there is. Whether a given display will take one is the platform's own answer, carried in Display.Presentation — measured on a Pixel 11 Pro Fold with VITURE Beast glasses on the USB-C port:
APPEARED id 12 "VITURE Beast" 1920x1080 @110dpi 60.0Hz flags 0x8088 presentation true
No permission, no consent dialog and no MediaProjection are involved, exactly as for a Wall.
The back-pressure is the point ¶
The host lends a shared buffer of ScreenOptions.QueueDepth slots. Screen.Next hands out a free one, the application draws into it, Screen.Present gives it to the host, and the slot comes back only once the host says the frame reached the view. Nothing else stops the application overwriting a slot mid-blit: the pixels go the other way here, so there is no stream of host frames to pace against, and a tear in a headset is not a cosmetic defect.
A Screen is safe for concurrent use. A Canvas is not: it is a window onto shared memory, and two goroutines drawing into one race.
func ShowOn ¶ added in v0.5.0
ShowOn puts a Presentation on an existing display and returns the screen the application draws into.
It refuses a display the platform will not take a Presentation on with ErrNotPresentable, BEFORE dialling anything: the answer is already in the Display the host handed over, and a round trip would come back saying less. It wraps ErrInvalidOption for options that cannot describe a frame, and reports ErrUnsupported where there is no host.
Close the screen when done; it holds a socket, a mapping and a window.
func (*Screen) Close ¶ added in v0.5.0
Close takes the Presentation down, unmaps the buffer and releases the socket. It is idempotent.
Any Canvas still out is invalid afterwards: the memory under it is gone.
func (*Screen) DisplayID ¶ added in v0.5.0
DisplayID is the android.view.Display id this screen presents on.
func (*Screen) Err ¶ added in v0.5.0
Err reports the failure that ended the screen, or nil while it is live.
func (*Screen) Next ¶ added in v0.5.0
Next returns a canvas to draw the next frame into, waiting for a slot if every one is still in flight.
The pixels it hands back are BORROWED from the shared buffer and are whatever the frame that last used that slot left there, not blank: a compositor that paints the whole frame wants that, and one that does not must clear it.
It reports the context's error if the wait is abandoned, ErrScreenClosed after Screen.Close, and the host's failure if the host went away.
func (*Screen) Options ¶ added in v0.5.0
func (s *Screen) Options() ScreenOptions
Options are the options this screen was opened with, with the zero fields filled in.
func (*Screen) Present ¶ added in v0.5.0
Present hands a drawn canvas to the host to put on the screen. The canvas is invalid afterwards whether this succeeded or not.
It refuses a canvas this screen did not lend, and one presented twice, with ErrInvalidOption: the slot number reaches the host as an offset into shared memory, and freeing a slot that is in use would let the next frame be drawn over one being blitted.
func (*Screen) Size ¶ added in v0.5.0
Size is the frame size in pixels, which is what Screen.Next hands back. The display may be a different size; the host scales to fill it.
func (*Screen) Stats ¶ added in v0.5.0
func (s *Screen) Stats() ScreenStats
Stats reports what the screen has done.
type ScreenOptions ¶ added in v0.5.0
type ScreenOptions struct {
// Width and Height are the size of the frames the application will draw,
// in pixels. Zero means the display's own size.
//
// They need not match the display. The host scales the frame to fill it,
// so a ribbon composited once at one resolution reaches panels of another —
// and a screen rendered deliberately small is how a weak device keeps its
// frame rate.
Width, Height int
// QueueDepth is how many frame slots the shared buffer holds. Zero means
// [DefaultQueueDepth], and it is bounded by [MinQueueDepth] and
// [MaxQueueDepth] exactly as a capture's is: the floor leaves room for the
// frame being drawn, the one in flight and the one being blitted.
QueueDepth int
}
ScreenOptions says what frames an application will put on a screen.
The zero value is usable and means: frames the size of the display, with DefaultQueueDepth slots.
func (ScreenOptions) String ¶ added in v0.5.0
func (o ScreenOptions) String() string
String renders the options for logs.
func (ScreenOptions) Validate ¶ added in v0.5.0
func (o ScreenOptions) Validate() error
Validate reports whether the options describe frames a host could carry, wrapping ErrInvalidOption. It does not apply defaults: a zero Width or QueueDepth is refused here and filled in by ShowOn before this is reached.
type ScreenStats ¶ added in v0.5.0
type ScreenStats struct {
// Presented is how many frames the application handed over.
Presented int64
// Acknowledged is how many of those the host reported on the screen. The
// difference is what is in flight.
Acknowledged int64
// Waited is how many times [Screen.Next] had to block because every slot
// was in flight. A large number against a small QueueDepth means the
// application is drawing faster than the headset can take it.
Waited int64
}
ScreenStats counts what a screen has done.
func (ScreenStats) String ¶ added in v0.5.0
func (s ScreenStats) String() string
String renders the counters for logs.
type Sentinel ¶ added in v0.2.0
type Sentinel struct {
// Label is drawn across the middle in white. An empty label draws none.
Label string
}
Sentinel renders quadrants of exactly known colour with a label across them: green top-left, blue bottom-right, red elsewhere, white text.
It is this package's SELF-TEST content, and it is exported because a consumer needs it too. A feed that "works" and delivers a black buffer is the classic silent failure of this whole mechanism, and the only way to catch it is to draw something whose pixels are known in advance and then SAMPLE them rather than look at them. SentinelColorAt says what a correct frame must hold at a given point, and that is what this package's live proof asserts on.
type StartMessage ¶
type StartMessage struct {
// DisplayID is the display to capture. Only the default display can be
// captured by an unprivileged app; see [ErrNotCapturable].
DisplayID int
// Width and Height are the requested frame size in pixels; zero means the
// display's native size.
Width, Height int
// MilliFPS is the frame-rate ceiling in thousandths of a frame per second,
// so 60 fps is 60000 and a rate below one frame a second is still exact.
MilliFPS uint32
// Slots is how many frame slots the shared buffer holds.
Slots int
}
StartMessage asks the host to begin capturing.
func DecodeStart ¶
func DecodeStart(b []byte) (StartMessage, error)
DecodeStart decodes a StartMessage.
type Stats ¶
type Stats struct {
// Frames is the number of frames the host delivered.
Frames uint64
// Superseded is the number of delivered frames replaced by a newer one
// before the consumer ever asked for them. A large value next to Frames
// means the consumer is slower than the capture.
Superseded uint64
// Last is when the most recent frame arrived.
Last time.Time
// Interval is the gap between the two most recent frames.
Interval time.Duration
}
Stats reports what a stream has seen since it started.
func (Stats) FPS ¶
FPS is the instantaneous rate implied by Stats.Interval, 0 when fewer than two frames have arrived.
type StoppedMessage ¶
type StoppedMessage struct {
// Reason is one of [StopUser], [StopSystem] or [StopApp].
Reason uint8
// Detail is the host's own wording, which may be empty.
Detail string
}
StoppedMessage says a capture ended and why.
func DecodeStopped ¶
func DecodeStopped(b []byte) (StoppedMessage, error)
DecodeStopped decodes a StoppedMessage.
func (StoppedMessage) String ¶
func (s StoppedMessage) String() string
String renders the reason and the detail for a log or an error.
type Stream ¶
type Stream struct {
// contains filtered or unexported fields
}
Stream is a running capture. Off Android none can exist, so every method answers as a closed one would; the type is present so a consumer's code still type-checks.
func CaptureDisplay ¶
CaptureDisplay starts streaming a display. Off Android it reports ErrUnsupported.
func (*Stream) Close ¶
Close ends the capture. Off Android there is nothing to end, and it is idempotent all the same.
func (*Stream) Err ¶
Err reports why the system stopped the capture. Off Android it is always ErrUnsupported.
func (*Stream) Format ¶
func (st *Stream) Format() PixelFormat
Format returns the pixel layout of every frame this stream produces.
type Wall ¶ added in v0.2.0
type Wall struct {
// contains filtered or unexported fields
}
A Wall is a set of displays this application created and owns, and the limit on how many of them may exist at once.
Why an application may have these at all ¶
An ordinary APK may not manufacture desktops for OTHER people's applications: launching an activity onto a display the app created fails with a SecurityException even for the app's own activity, and the flag that would lift it, VIRTUAL_DISPLAY_FLAG_TRUSTED, needs a signature permission. That half of the refusal is real and is not going anywhere.
It may, however, create a virtual display and show a [Presentation] on it — a Dialog attached to a Display, which is not an activity start and so does not meet the check that refuses the other thing. Measured on Android 15 (API 35, arm64):
Q1 ANSWER: Presentation.show() SUCCEEDED on the app's OWN virtual display 3 Q2 frame 640x480 ... black=0/307200 topLeft=#FF00FF00 bottomRight=#FF0000FF
No permission, no consent dialog and no MediaProjection are involved, which is why a Wall needs none of RequestAuthorization's machinery. So an XR ribbon on Android carries the mirrored phone — see CaptureDisplay — AND as many screens of Android-rendered content as the application asks for.
THE LIMIT IS THE SAFETY, AND IT IS NOT A FORMALITY ¶
On the emulator this package was measured against, creating the 304th virtual display did not fail: it killed system_server with android.view.Surface$OutOfResourcesException: NO_MEMORY out of SurfaceControl.nativeCreate, and the device soft-rebooted. An unprivileged APK with no permissions at all did that to the machine it was running on.
READ THIS BEFORE RAISING THE LIMIT: the ceiling was the SAME COUNT at 640x480 and at 1920x1080 — nine times the pixels, same number, with the app's Java heap at 14 MiB of 192. It is SurfaceControl handles, not graphics memory. So MAKING THE SCREENS SMALLER DOES NOT HELP, which is exactly the mitigation somebody reaches for. 304 is one emulator's measurement and is deliberately not a constant here; a real device's number is unknown and could be lower.
A Wall therefore refuses past its limit with ErrTooManyDisplays instead of asking the platform and hoping. DefaultMaxDisplays is 8, which is more than a ribbon uses; MaxDisplays is 32, which is the most this package will let a caller ask for however explicit they are about wanting it. The Java host enforces the same 32 independently, so a second process cannot get past it either.
A Wall is safe for concurrent use.
func NewWall ¶ added in v0.2.0
NewWall returns a wall that will hold at most max displays at once. A max of 0 means DefaultMaxDisplays; a negative max, or one above MaxDisplays, is refused with ErrTooManyDisplays and no wall is returned.
NewWall talks to nothing and can fail for no other reason, so a caller may build one before knowing whether a host is there.
func (*Wall) Close ¶ added in v0.2.0
Close releases every display the wall still holds and refuses further Wall.Open calls. It is idempotent, and reports the first failure a display reported while still closing the rest.
func (*Wall) Open ¶ added in v0.2.0
func (w *Wall) Open(ctx context.Context, spec DisplaySpec) (*OwnedDisplay, error)
Open creates one display, shows a Presentation carrying spec.Content on it, and returns a feed of the pixels Android rendered there.
It refuses with ErrTooManyDisplays when the wall is already full, and it refuses BEFORE asking the host, so a full wall costs no round trip and creates nothing. It refuses with ErrWallClosed after Wall.Close, and wraps ErrInvalidOption for a spec that cannot describe a display.
type Web ¶ added in v0.2.0
type Web struct {
// URL is what the WebView loads. http, https and data URLs are accepted.
URL string
}
Web renders a WebView on the display.
It is the content that justifies the whole mechanism: a CGO-free Go process cannot host a web engine, and a ribbon of web panels is a real product that falls straight out of a Presentation on an owned display.
The host's WebView has JavaScript enabled and no file access. Reaching a http(s) URL needs android.permission.INTERNET in the APK, which is protectionLevel normal; a data: URL needs nothing and is what this package's own tests use.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
xrcapture
command
Command xrcapture is the on-device proof for github.com/go-xrkit/android: an ordinary CGO_ENABLED=0 GOOS=android binary that asks the host for consent, captures the screen, and reports what it actually got.
|
Command xrcapture is the on-device proof for github.com/go-xrkit/android: an ordinary CGO_ENABLED=0 GOOS=android binary that asks the host for consent, captures the screen, and reports what it actually got. |
|
xrglasses
command
Command xrglasses waits for XR glasses to be attached to the phone and says exactly what Android makes of them.
|
Command xrglasses waits for XR glasses to be attached to the phone and says exactly what Android makes of them. |
|
xrscreen
command
Command xrscreen paints frames from Go onto the glasses and says how many of them Android took.
|
Command xrscreen paints frames from Go onto the glasses and says how many of them Android took. |
|
xrwall
command
Command xrwall proves, on a real device, that this application can create displays of its own, render Android content on them and read the pixels back — and that the limit that keeps it from rebooting the phone actually refuses.
|
Command xrwall proves, on a real device, that this application can create displays of its own, render Android content on them and read the pixels back — and that the limit that keeps it from rebooting the phone actually refuses. |
|
xrwide
command
Command xrwide measures how WIDE a display this application owns can be, on the device it is running on, and whether the pixels actually arrive at that width.
|
Command xrwide measures how WIDE a display this application owns can be, on the device it is running on, and whether the pixels actually arrive at that width. |
