mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb0736a3b3 | ||
|
|
4ac389d2fd | ||
|
|
95fddf5a55 | ||
|
|
206dd67d98 | ||
|
|
817b413eb1 | ||
|
|
03f27c4dda | ||
|
|
42b151034a | ||
|
|
7a774b4091 | ||
|
|
2b8b221d1b |
No files matched your search
@@ -1,31 +1,13 @@
|
||||
Gearslip 0.3.39 runs as a head unit's home screen, connects to more cars, and brings Google Maps and Waze turns to the car.
|
||||
Gearslip 0.3.40 is a test build for a few cars that still don't connect.
|
||||
|
||||
This is a pre-release. Many of these fixes target cars we don't own, so please try it and tell us how your car does. Attach a log to your issue: open Gearslip, tap Logs, then Share log.
|
||||
|
||||
## New
|
||||
- Launcher mode: on a head unit that runs Android, or a spare phone or tablet on the dash, Gearslip can be the home screen. Turn it on under Settings > Connection. An All apps page opens everything else.
|
||||
- Turns from Google Maps or Waze on the phone show in the car's nav bar and on its instrument cluster.
|
||||
- An Apps card on the phone's home screen lets you hide apps from the car's drawer. Thanks to @johnneerdael.
|
||||
- A display density setting makes text and buttons bigger in car apps on small screens. Thanks to @johnneerdael.
|
||||
- A Language setting picks Gearslip's language, and spoken directions use a voice in that language.
|
||||
- Alerts from car apps show on the car screen, such as Waze asking whether a hazard is still there.
|
||||
- AntennaPod gets a Next chapter button.
|
||||
- The car list shows the names you gave your Bluetooth devices. Thanks to @theo330.
|
||||
- A car you once turned away can connect on its own again, from the car list.
|
||||
If your car works on 0.3.39, you don't need this one. If you're following #18, #39 or #57, please try it and attach a log to your issue: open Gearslip, tap Logs, then Share log.
|
||||
|
||||
## Fixed
|
||||
- Cars that waited for the music channel before showing a picture now show Gearslip's screen. This should fix the black screen or endless "Starting video" on the 2017 Pilot, 2020 Civic, 2021 CX-5 and 2024 Mirage.
|
||||
- Gearslip sends the car its icon and a session ID, as a real phone does. Older Hyundais that stopped after the first request should now connect.
|
||||
- Gearslip pings the car once a second, as a real phone does. This may stop the 2021 Bolt from hanging up after 25 seconds.
|
||||
- A still car screen gets a fresh frame every second, which should fix the black screen on some aftermarket boxes.
|
||||
- Wireless keeps calling the car until it answers, starts over after a failed step, and stays connected with the phone locked.
|
||||
- Music playing through Gearslip comes back after a call.
|
||||
- Car maps keep the phone's position while the phone is locked.
|
||||
- Spoken directions start the speech engine again when it fails to start.
|
||||
- Error messages say when the car's clock is wrong, and what to try when a connection gets stuck.
|
||||
- A car that holds back its screen is asked for it again with the detail older cars expect. This is for the 2017 Pilot (#57) and other cars on the oldest Android Auto version.
|
||||
- Music no longer carries on from the phone's speaker after you turn the car off. An app that started playing again by itself is paused a second time (#39).
|
||||
- A liked song lights up the whole like button, as shuffle and repeat do. Before, only the heart filled in for apps such as Metrolist.
|
||||
|
||||
## For a few cars
|
||||
- Directions over the music channel, under Spoken directions in the car's settings: for a car that drops the connection or restarts after a spoken turn.
|
||||
- Phone's Bluetooth address, under Settings > Connection: for a car that drops the phone's Bluetooth when Gearslip connects. Copy the address from Android's Settings > About phone.
|
||||
## Testing
|
||||
- GM cars open two more channels, Bluetooth and Wi-Fi projection, and log what the car sends on them. This is to find out why the 2021 Bolt hangs up about 20 seconds in (#18). If you drive a GM car, type your phone's Bluetooth address under Settings > Connection > Phone's Bluetooth address. You'll find it in Android's Settings under About phone.
|
||||
|
||||
New text is in English until translators catch up. Help with your language on [Weblate](https://hosted.weblate.org/engage/gearslip/).
|
||||
Help translate Gearslip on [Weblate](https://hosted.weblate.org/engage/gearslip/).
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Translations
|
||||
|
||||
# Weblate is the only thing that writes translated strings: it pushes straight to experimental,
|
||||
# never through a pull request. A PR that edits them collides with Weblate's own copy, and
|
||||
# Weblate locks itself until someone resets it. Edit the English strings.xml only.
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "gearslip/src/main/res/values-*/strings.xml"
|
||||
|
||||
jobs:
|
||||
weblate-only:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Translations go through Weblate
|
||||
run: |
|
||||
echo "::error::This pull request edits a translation. Please make translation changes on Weblate instead: https://hosted.weblate.org/engage/gearslip/ (it can upload a whole strings.xml under Files > Upload translation). See CONTRIBUTING.md."
|
||||
exit 1
|
||||
@@ -4,124 +4,93 @@
|
||||
|
||||
An Android Auto alternative built for everyone.
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="80">](https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://github.com/twinksoftware/gearslip)
|
||||
|
||||

|
||||
|
||||
[](https://forthebadge.com)
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="80">](https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://github.com/twinksoftware/gearslip)
|
||||
|
||||
[Fix or add your language on Weblate](https://hosted.weblate.org/engage/gearslip/).
|
||||
Translations go through Weblate only; pull requests that edit them get closed.
|
||||
|
||||
[](https://hosted.weblate.org/engage/gearslip/)
|
||||
|
||||
Questions, ideas?
|
||||
[Join the discussion on Matrix](https://matrix.to/#/!mf0_mgBd5KqKpc9FfX_wHp1jFCcwAtPrndIn_oGIyMk?via=matrix.org).
|
||||
Gearslip speaks the Android Auto protocol straight to your car, over USB or wireless, with no
|
||||
Google software in the path. Map, music, and messaging apps draw their car screens through
|
||||
Gearslip, and it adds a player, calls, voice, and a web browser of its own.
|
||||
|
||||
> [!WARNING]
|
||||
> Gearslip is **alpha software**. It can drop the connection, freeze the car screen, or fail
|
||||
> to start, and a new release can break a car that worked before. Set it up while parked, and
|
||||
> keep another way to navigate.
|
||||
|
||||
> [!WARNING]
|
||||
> On GrapheneOS, make sure **USB-C port control** isn't set to **Charging-only**. That setting
|
||||
> blocks USB data, so the car never sees the phone, even with a data cable. Set it to allow
|
||||
> data transfer.
|
||||
Questions, ideas? [Join the discussion on Matrix](https://matrix.to/#/!mf0_mgBd5KqKpc9FfX_wHp1jFCcwAtPrndIn_oGIyMk?via=matrix.org).
|
||||
|
||||
## Which cars work
|
||||
|
||||
Most cars work. Starting around 2020, some cars added newer security checks that reject
|
||||
the aasdk certificate, and in those cars Gearslip needs a certificate of your own. [CARS.md](CARS.md) lists the
|
||||
cars drivers have confirmed.
|
||||
|
||||
## What Gearslip is
|
||||
|
||||
Gearslip is an Android Auto client for the phone. It speaks the Android Auto protocol
|
||||
directly to a car head unit, over USB or wirelessly, with no Google software in the path.
|
||||
Gearslip acts as a Car App Library host: an app that ships a car interface hands Gearslip
|
||||
a template, and Gearslip renders that template natively on the car screen. Nothing is
|
||||
mirrored from the phone. For apps without a car interface, Gearslip offers a separate
|
||||
screen-sharing mode.
|
||||
## What works
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
| Your car apps in one place. | Vela Maps navigating, with the player beside it. |
|
||||
|  |  |
|
||||
| Metrolist in Gearslip's player. | A new message, read aloud at a tap, over the map. |
|
||||
|  |  |
|
||||
| Lyrics light up word by word as the song plays. | Browse the web while parked. |
|
||||
| **Cars** | Most cars, over USB or wireless. Some cars from about 2020 on need a certificate of your own. [CARS.md](CARS.md) lists the cars drivers have confirmed. |
|
||||
| **Maps** | Organic Maps, Vela, OsmAnd, Waze, Flitsmeister, MAPS.ME, and MapQuest on the car screen, alerts included. Turns from Google Maps or Waze on the phone show in the nav bar and on the instrument cluster. |
|
||||
| **Music** | Any app built for car screens, with Now Playing, Up Next, search, and lyrics. Spotify plays through Gearslip's own player. |
|
||||
| **Calls** | Dial or search contacts, and answer or decline on the car screen. Calls use the car's Bluetooth. |
|
||||
| **Messages** | Read aloud on arrival, with a reply by voice or a quick reply. |
|
||||
| **Voice** | "Navigate to the nearest gas station", "play Daft Punk", "call Mom". Speech stays on the phone. Commands are in English for now. |
|
||||
| **Controls** | Touch, steering wheel buttons, control knobs, and D-pads. |
|
||||
| **Languages** | English and 19 more, some only partly translated. |
|
||||
| **Head units with Android** | Gearslip can be the home screen of an Android head unit, or of a spare phone or tablet on the dash. Turn on Use as launcher in Settings > Connection. |
|
||||
| **Doesn't work** | Spotify's and Google Messages' own car screens, which accept only Android Auto. TIDAL lets only approved apps browse its library. |
|
||||
|
||||
## See it
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
| Your car apps in one place. | Metrolist in Gearslip's player. |
|
||||
|  |  |
|
||||
| A new message, read aloud at a tap, over the map. | Lyrics light up word by word as the song plays. |
|
||||
|  |  |
|
||||
| Ask for a place, a song, or an app. | Pick a route to a coffee shop. Each one shows its time and distance. |
|
||||
| Ask for a place, a song, or an app. | Pick a route. Each one shows its time and distance. |
|
||||
|  | |
|
||||
| Browse the web while parked. | |
|
||||
|
||||
## Features
|
||||
|
||||
- **Phone calls.** Dial from a keypad, or search your contacts, then confirm before the
|
||||
call goes out. When a call comes in, answer or decline it on the car screen. During a
|
||||
call, the nav bar shows who you're talking to, and you mute or hang up from there.
|
||||
- **Navigation.** Gearslip hosts real Car App Library map apps on the car screen.
|
||||
Organic Maps, Vela, OsmAnd, Flitsmeister, MAPS.ME, and MapQuest work today. While you
|
||||
navigate, a big turn card on the left shows the arrow, the distance, and your lane, and
|
||||
the map keeps the right side.
|
||||
- **Music.** Browse, play, and queue tracks from a car-enabled media app, with a Now
|
||||
Playing view, an Up Next queue, and lyrics. Type a search, and apps that accept one
|
||||
play the best match.
|
||||
- **The web.** A browser with an address bar, back and forward, and its own on-screen
|
||||
keyboard, for anything that doesn't ship a car interface of its own.
|
||||
- **Weather and the day ahead.** A dashboard screen shows the week's calendar next to
|
||||
the current and coming weather.
|
||||
- **Voice.** Tap the mic, or press the talk button on the wheel, and say "navigate to the
|
||||
nearest gas station", "play Daft Punk", or "call Mom". Say "help" for the full list.
|
||||
Speech turns into text on the phone, and nothing you say leaves it.
|
||||
- **Messages.** New messages appear as a small card on the car screen. Gearslip reads
|
||||
each one aloud, and you reply by voice or with a quick reply. Music pauses while
|
||||
Gearslip speaks or listens. Speech runs on the phone, so it needs no Google account.
|
||||
- **Notifications.** Only messages, missed calls, alarms, and reminders interrupt you.
|
||||
The rest wait on the dashboard.
|
||||
- **Your apps, your order.** The launcher groups apps by kind: phone, maps, music,
|
||||
messaging, and the rest. Press and hold an app to move it.
|
||||
- **Car controls.** Steering wheel buttons, control knobs, and D-pads all work.
|
||||
- **Your phone's colors.** The car screen and the phone app take their colors from your
|
||||
phone's Material You palette, and every card and button matches.
|
||||
- **Themes.** Choose flat buttons or skeuomorphic ones that look like real keys.
|
||||
- **Settings on the phone.** The car's settings sit on the phone's home screen too, so you
|
||||
can set things up before you drive. A change on either screen shows on both.
|
||||
- **Navigation.** Gearslip hosts real map apps on the car screen. While you navigate, a big
|
||||
turn card shows the arrow, the distance, and your lane, and the map keeps the rest.
|
||||
- **Music.** Browse, play, and queue from any car-ready media app. Type a search, and apps
|
||||
that accept one play the best match.
|
||||
- **Phone calls.** During a call, the nav bar shows who you're talking to, and you mute or
|
||||
hang up from there.
|
||||
- **Messages and notifications.** Only messages, missed calls, alarms, and reminders
|
||||
interrupt you. The rest wait on the dashboard. Music pauses while Gearslip speaks or
|
||||
listens.
|
||||
- **Voice.** Tap the mic or press the talk button on the wheel. Say "help" for the full list.
|
||||
- **The web.** A browser with an address bar, back and forward, and its own keyboard.
|
||||
- **Weather and the day ahead.** A dashboard shows the week's calendar beside the weather.
|
||||
- **Your apps, your order.** Apps group by kind. Press and hold one to move it, and hide
|
||||
the ones you don't use from the Apps card on the phone.
|
||||
- **Your look.** The car screen takes your phone's Material You colors. Choose flat
|
||||
buttons or ones that look like real keys, and make text bigger on small screens.
|
||||
- **Settings on the phone.** Set things up before you drive. A change on either screen
|
||||
shows on both.
|
||||
|
||||
## Status
|
||||
## Install
|
||||
|
||||
- **Wired or wireless.** Plug the phone into the car's USB port, or, in a car with wireless
|
||||
Android Auto, tap Connect wirelessly and pick the car. Pair the phone with the car over
|
||||
Bluetooth first. After the first wireless drive, Android asks once whether Gearslip may
|
||||
watch for that car; from then on, Gearslip connects whenever the car's Bluetooth does.
|
||||
- **Setup walks you through it.** First launch asks for the Android Auto certificate,
|
||||
then one permission at a time, each with a plain reason and an Allow or Skip. A help
|
||||
screen inside the car answers anything setup didn't cover.
|
||||
- **Templated apps work.** Gearslip renders the Car App Library's templates on the car
|
||||
screen, and it draws its own keyboard for search, sign-in, and text fields. It has been
|
||||
tested against several real navigation and media apps.
|
||||
- **Gearslip works with [LIVI](https://github.com/f-io/LIVI),** the open-source head unit
|
||||
used in homemade car dashboards.
|
||||
- **Some apps refuse Gearslip.** Spotify, Waze, and Google Messages use a version of
|
||||
Google's car library that accepts only Android Auto itself. Spotify still plays through
|
||||
Gearslip's own player. TIDAL lets only approved apps browse its library.
|
||||
- **Calls use the car's Bluetooth,** as they do in Android Auto. Pair the phone with the
|
||||
car to hear calls through its speakers.
|
||||
- **Music reaches the car over Bluetooth or through Gearslip.** Setup asks which.
|
||||
Bluetooth is the default: Android never asks for permission, and your map app's voice
|
||||
reaches the car too. Through Gearslip, the sound travels over the car connection, cable
|
||||
or wireless. It sounds a little clearer, with no delay, but each drive Android asks you
|
||||
to share your screen, which lets Gearslip send the sound. Gearslip never reads the
|
||||
screen; capturing playback is the only way an ordinary app can pass another app's audio
|
||||
along.
|
||||
- **Gearslip needs no adb, root, or Shizuku.** Every feature relies on an ordinary
|
||||
Android permission or an on-device consent dialog. For audio through Gearslip, setup offers two
|
||||
optional adb commands: one removes the audio dialog for good, and the other keeps
|
||||
notifications visible while audio plays.
|
||||
- **Updates come from GitHub.** Once a day, Gearslip checks this repository's latest
|
||||
release. When a new version is out, it downloads the APK, checks it against GitHub's
|
||||
SHA-256, and installs it through Android's own installer. Turn off automatic checks in
|
||||
Settings.
|
||||
1. Install Gearslip from [the releases page](https://github.com/twinksoftware/Gearslip/releases)
|
||||
or through Obtainium.
|
||||
2. Open it. Setup asks where your Android Auto certificate comes from, then for one
|
||||
permission at a time, each with a plain reason. A help screen in the car answers the rest.
|
||||
3. Plug the phone into the car's USB port. In a car with wireless Android Auto, pair the
|
||||
phone with the car over Bluetooth, then tap Connect wirelessly and pick the car.
|
||||
|
||||
## Verify your download
|
||||
After the first wireless drive, Android asks once whether Gearslip may watch for that car.
|
||||
From then on, Gearslip connects whenever the car's Bluetooth does.
|
||||
|
||||
> [!NOTE]
|
||||
> On GrapheneOS, make sure **USB-C port control** isn't set to **Charging-only**. That
|
||||
> setting blocks USB data, so the car never sees the phone, even with a data cable.
|
||||
|
||||
Gearslip updates itself from this repository. Once a day it checks the latest release,
|
||||
downloads the APK, checks it against GitHub's SHA-256, and installs it through Android's
|
||||
own installer. Turn off automatic checks in Settings.
|
||||
|
||||
### Verify your download
|
||||
|
||||
Check that your copy of Gearslip is genuine with
|
||||
[AppVerifier](https://github.com/soupslurpr/AppVerifier): share this block to it, or compare it
|
||||
@@ -132,6 +101,24 @@ app.seb3thehacker.gearslip
|
||||
4C:C5:6E:BD:D1:3A:54:D2:60:EC:A9:B7:E2:B2:AE:6B:30:E2:F2:25:BF:DA:8F:50:8F:07:09:ED:F0:84:94:8B
|
||||
```
|
||||
|
||||
## Good to know
|
||||
|
||||
- **Music reaches the car over Bluetooth or through Gearslip.** Setup asks which.
|
||||
Bluetooth is the default. Android never asks for permission, and your map app's voice
|
||||
reaches the car too. Through Gearslip, the sound travels over the car connection, cable
|
||||
or wireless. It sounds a little clearer, with no delay, but each drive Android asks you
|
||||
to share your screen, which lets Gearslip send the sound. Gearslip never reads the
|
||||
screen. Capturing playback is the only way an ordinary app can pass another app's audio
|
||||
along.
|
||||
- **Gearslip needs no adb, root, or Shizuku.** Every feature relies on an ordinary
|
||||
Android permission or an on-device consent dialog. For audio through Gearslip, setup
|
||||
offers two optional adb commands. One removes the audio dialog for good, and the other
|
||||
keeps notifications visible while audio plays.
|
||||
- **Apps without a car screen** can still reach the car through a separate screen-sharing
|
||||
mode.
|
||||
- **Gearslip works with [LIVI](https://github.com/f-io/LIVI),** the open-source head unit
|
||||
used in homemade car dashboards.
|
||||
|
||||
## Usage notes
|
||||
|
||||
Gearslip can send short notes that show which cars it works in. They stay off unless you
|
||||
@@ -143,9 +130,9 @@ turn them on, in setup or in Settings.
|
||||
those boxes cover: the Android version and whether the phone runs GrapheneOS, the phone
|
||||
model, whether a car connected that day, and each car's head unit, model, year, screen
|
||||
size, and connection result, with whether that drive used the cable or wireless and
|
||||
whether music went through Gearslip or over Bluetooth. If you tick it, how each drive went: how far
|
||||
the connection got, how long it lasted, and how it ended. Settings shows the exact note. Untick a phone detail, and
|
||||
the server drops the copy it kept.
|
||||
whether music went through Gearslip or over Bluetooth. If you tick it, how each drive
|
||||
went: how far the connection got, how long it lasted, and how it ended. Settings shows
|
||||
the exact note. Untick a phone detail, and the server drops the copy it kept.
|
||||
- **What they leave out.** Your name, accounts, location, contacts, messages, and
|
||||
anything you type or play.
|
||||
- **How phones are counted.** A random number counts each phone once. The server keeps
|
||||
@@ -155,6 +142,19 @@ turn them on, in setup or in Settings.
|
||||
|
||||
Turn the notes off, and they stop at once.
|
||||
|
||||
## Help out
|
||||
|
||||
Bug reports from real cars help most, and pull requests are welcome.
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md) explains what to put in a report, which branch to work
|
||||
on, and the one-time agreement you sign before your first pull request merges.
|
||||
|
||||
To translate Gearslip into your language, join
|
||||
[Gearslip on Weblate](https://hosted.weblate.org/engage/gearslip/). No coding needed.
|
||||
Translations go through Weblate only, since it holds its own copy of every language and
|
||||
changes made in git collide with it.
|
||||
|
||||
[](https://hosted.weblate.org/engage/gearslip/)
|
||||
|
||||
## How Gearslip is made
|
||||
|
||||
Gearslip is my beloved child. I decide what it does and how it looks, and Claude writes nearly
|
||||
@@ -164,17 +164,6 @@ open-source head unit software. I watch the phone's temperature too, so a new fe
|
||||
cook it. Then I beg my friends to try it in their cars, so it works in as many makes, models,
|
||||
and years as possible. If something breaks, that's on me, so please open an issue.
|
||||
|
||||
## Contributing
|
||||
|
||||
Bug reports from real cars help most, and pull requests are welcome.
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md) explains what to put in a report, which branch to work
|
||||
on, and the one-time agreement you sign before your first pull request merges.
|
||||
|
||||
To help translate Gearslip into your language, join
|
||||
[Gearslip on Weblate](https://hosted.weblate.org/engage/gearslip/). No coding needed. Please
|
||||
don't send translations as pull requests: Weblate holds its own copy of every language, and
|
||||
changes made in git collide with it.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
|
||||
@@ -38,9 +38,9 @@ android {
|
||||
applicationId = "app.seb3thehacker.gearslip"
|
||||
minSdk = 31
|
||||
targetSdk = 37
|
||||
versionCode = 14
|
||||
versionCode = 15
|
||||
// -PgearslipVersionName=0.1.99-test makes a build that thinks it's older, to try the updater.
|
||||
versionName = project.findProperty("gearslipVersionName")?.toString() ?: "0.3.39-alpha"
|
||||
versionName = project.findProperty("gearslipVersionName")?.toString() ?: "0.3.40-alpha"
|
||||
manifestPlaceholders["appLabel"] = "Gearslip"
|
||||
// Where opted-in phones send their daily usage note: the stats-worker/ Worker's address,
|
||||
// set as gearslipStatsUrl in gradle.properties. Left empty, nothing is ever sent.
|
||||
|
||||
@@ -10,6 +10,14 @@ object Changelog {
|
||||
class Release(val versionCode: Int, val versionName: String, val changes: List<String>)
|
||||
|
||||
val releases = listOf(
|
||||
Release(
|
||||
15, "0.3.40-alpha",
|
||||
listOf(
|
||||
"A liked song lights up the whole like button, as shuffle and repeat do.",
|
||||
"Music no longer carries on from the phone after you turn the car off.",
|
||||
"Cars that hold back their screen are asked for it again in a way older cars understand.",
|
||||
),
|
||||
),
|
||||
Release(
|
||||
14, "0.3.39-alpha",
|
||||
listOf(
|
||||
|
||||
@@ -107,6 +107,8 @@ class GearslipRunner(
|
||||
@Volatile private var audioLink: AudioLink? = null
|
||||
@Volatile private var guidanceLink: AudioLink? = null
|
||||
@Volatile private var vendorChannelId = -1
|
||||
/** The car's Wi-Fi projection channel, opened in GM cars only (see [startVendorChannel]); -1 when closed. */
|
||||
@Volatile private var wifiChannelId = -1
|
||||
private var audioMessagesSeen = 0
|
||||
private var touchWidth = 0
|
||||
private var touchHeight = 0
|
||||
@@ -260,6 +262,8 @@ class GearslipRunner(
|
||||
CarBluetooth.onMessage(messageId, body)
|
||||
} else if (frame.channel == vendorChannelId) {
|
||||
onVendorMessage(messageId, body)
|
||||
} else if (frame.channel == wifiChannelId) {
|
||||
onWifiProjectionMessage(messageId, body)
|
||||
} else if (frame.channel == CarMic.current?.channelId) {
|
||||
CarMic.current?.onMessage(messageId, body)
|
||||
} else if (frame.channel == audioLink?.channelId) {
|
||||
@@ -726,6 +730,23 @@ class GearslipRunner(
|
||||
vendorChannelId = gm.serviceId
|
||||
log.i("-> ChannelOpenRequest on GM's channel ${gm.serviceId}")
|
||||
openChannel(gm.serviceId)
|
||||
// Opening GM's channel and pinging the car didn't stop the Bolt hanging up, so a GM car
|
||||
// also gets the two channels Gearslip had never opened: Bluetooth and Wi-Fi projection.
|
||||
// Only what arrives on them is logged, by message id and size, until one turns out to matter.
|
||||
CarBluetooth.announce("a GM car, issue #18")
|
||||
ServiceDiscovery.findWifiProjectionService(serviceDiscoveryResponse)?.let { channel ->
|
||||
wifiChannelId = channel
|
||||
log.i("-> ChannelOpenRequest on the Wi-Fi projection channel $channel (a GM car, issue #18)")
|
||||
openChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onWifiProjectionMessage(messageId: Int, body: ByteArray) {
|
||||
if (messageId == MSG_CHANNEL_OPEN_RESPONSE) {
|
||||
log.i("<- Wi-Fi projection channel open: status=${Protobuf.readInt32Field(body, 1) ?: 0}")
|
||||
} else {
|
||||
log.i("<- Wi-Fi projection message id=$messageId (${body.size} bytes)")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onVendorMessage(messageId: Int, body: ByteArray) {
|
||||
@@ -1317,15 +1338,20 @@ class GearslipRunner(
|
||||
}
|
||||
}
|
||||
|
||||
/** Asks the car for its screen; the answer comes to [onVideoFocus]. */
|
||||
private fun requestProjectedFocus() {
|
||||
// VideoFocusRequestNotification { mode = 2; reason = 3 }. Reason 1 means "the phone's
|
||||
// screen is off", which a 2020 Civic may have taken as a reason to keep its own
|
||||
// screen (issue #43); 0 is UNKNOWN.
|
||||
val focus = Protobuf.varintField(2, VIDEO_FOCUS_PROJECTED.toLong()) +
|
||||
/**
|
||||
* Asks the car for its screen; the answer comes to [onVideoFocus]. [withDisplay] names the
|
||||
* display's channel too, as the request for the car's own screen does (see [requestNativeFocus]):
|
||||
* the field is deprecated, but a car older than that may ignore a request without it.
|
||||
*/
|
||||
private fun requestProjectedFocus(withDisplay: Boolean = false) {
|
||||
// VideoFocusRequestNotification { disp_channel_id = 1; mode = 2; reason = 3 }. Reason 1
|
||||
// means "the phone's screen is off", which a 2020 Civic may have taken as a reason to
|
||||
// keep its own screen (issue #43); 0 is UNKNOWN.
|
||||
val display = if (withDisplay) Protobuf.varintField(1, videoChannelId.toLong()) else ByteArray(0)
|
||||
val focus = display + Protobuf.varintField(2, VIDEO_FOCUS_PROJECTED.toLong()) +
|
||||
Protobuf.varintField(3, 0L)
|
||||
send(MSG_VIDEO_FOCUS_REQUEST, focus, encrypted = true, channel = videoChannelId)
|
||||
log.i("-> VideoFocusRequest(mode=PROJECTED)")
|
||||
log.i(if (withDisplay) "-> VideoFocusRequest(display=$videoChannelId, mode=PROJECTED)" else "-> VideoFocusRequest(mode=PROJECTED)")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1335,17 +1361,23 @@ class GearslipRunner(
|
||||
* music over Bluetooth that channel waits for the car to switch source, so if the screen
|
||||
* hasn't come a few seconds after asking, open it and ask again. Cars that hand the screen
|
||||
* straight over keep their own source as before.
|
||||
* The 2017 Pilot (protocol 1.0) still kept its screen with the channel open, so the second
|
||||
* request also names the display's channel, as cars of the Pilot's age may need.
|
||||
*/
|
||||
private fun watchForWithheldScreen() {
|
||||
Thread({
|
||||
Thread.sleep(WITHHELD_SCREEN_MS)
|
||||
if (!running || videoStarted) return@Thread
|
||||
val open = CarAudio.openSink ?: return@Thread
|
||||
log.w("no screen from the car after ${WITHHELD_SCREEN_MS / 1000}s - opening the media channel and asking again")
|
||||
open()
|
||||
// Give the car the channel's open response before it hears the request again.
|
||||
Thread.sleep(1_000)
|
||||
if (running && !videoStarted) requestProjectedFocus()
|
||||
val open = CarAudio.openSink
|
||||
if (open != null) {
|
||||
log.w("no screen from the car after ${WITHHELD_SCREEN_MS / 1000}s - opening the media channel and asking again")
|
||||
open()
|
||||
// Give the car the channel's open response before it hears the request again.
|
||||
Thread.sleep(1_000)
|
||||
} else {
|
||||
log.w("no screen from the car after ${WITHHELD_SCREEN_MS / 1000}s - asking again")
|
||||
}
|
||||
if (running && !videoStarted) requestProjectedFocus(withDisplay = true)
|
||||
}, "gearslip-focus-fallback").apply { isDaemon = true }.start()
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,13 @@ object ServiceDiscovery {
|
||||
VendorService(id, name)
|
||||
}
|
||||
|
||||
/** The channel of the car's Wi-Fi projection service (Service field 14), or null. */
|
||||
fun findWifiProjectionService(response: ByteArray): Int? =
|
||||
Wire.allBytes(Wire.fields(response), 1).firstNotNullOfOrNull { channel ->
|
||||
val service = Wire.fields(channel)
|
||||
if (Wire.bytes(service, 14) == null) null else Wire.varint(service, 1)?.toInt()
|
||||
}
|
||||
|
||||
fun findBluetoothService(response: ByteArray): BluetoothService? {
|
||||
for (channel in Wire.allBytes(Wire.fields(response), 1)) {
|
||||
val service = Wire.fields(channel)
|
||||
|
||||
@@ -105,6 +105,20 @@ object CarBluetooth {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the car's Bluetooth channel though there's no pairing to do, as a real phone does on
|
||||
* every connection. Issue #18: a 2021 Bolt hangs up about 20 seconds in, and this is one of
|
||||
* the two channels it offers that Gearslip never opened. Without an address the driver typed,
|
||||
* it only opens the channel: a made-up address could set the car pairing with the wrong phone.
|
||||
*/
|
||||
fun announce(why: String) {
|
||||
val c = car ?: return
|
||||
if (pairing || announcing) return
|
||||
announcing = true
|
||||
log.i("-> ChannelOpenRequest on channel ${c.service.serviceId} ($why)")
|
||||
c.open()
|
||||
}
|
||||
|
||||
fun onCarDisconnected() {
|
||||
car = null
|
||||
pairing = false
|
||||
@@ -157,6 +171,10 @@ object CarBluetooth {
|
||||
else main.post { startBond("the car wouldn't open its Bluetooth channel") }
|
||||
return
|
||||
}
|
||||
if (announcing && typedAddress() == null) {
|
||||
log.i("Bluetooth channel open; no address typed in Settings, so no PairingRequest")
|
||||
return
|
||||
}
|
||||
val method = c.service.pairingMethods.firstOrNull { it == METHOD_NUMERIC_COMPARISON }
|
||||
?: c.service.pairingMethods.firstOrNull() ?: METHOD_NUMERIC_COMPARISON
|
||||
c.send(
|
||||
|
||||
@@ -31,6 +31,11 @@ object CarServices {
|
||||
|
||||
private var context: Context? = null
|
||||
|
||||
/** Buffering counts as playing: an app between songs is about to make sound again. */
|
||||
private val PLAYING_STATES = setOf(PlaybackState.STATE_PLAYING, PlaybackState.STATE_BUFFERING)
|
||||
/** How long after pausing for the car leaving to look again for an app that started by itself. */
|
||||
private const val REPAUSE_MS = 2_000L
|
||||
|
||||
val nav: CarAppConnection by lazy { CarAppConnection(requireNotNull(context)) }
|
||||
val media: CarMedia by lazy { CarMedia(requireNotNull(context)) }
|
||||
|
||||
@@ -253,11 +258,22 @@ object CarServices {
|
||||
* the phone's speaker. Unplugging a USB accessory isn't "audio becoming noisy" the way
|
||||
* headphones are, so the apps won't pause themselves. Every playing session is paused, not
|
||||
* just the one Gearslip opened: anything the car was playing was going through the car.
|
||||
* An app can start again on its own once the speaker is given back a moment later, which a
|
||||
* Deezer driver heard after turning the car off (issue #39), so the ones it paused are
|
||||
* looked at once more and paused again if they're playing.
|
||||
*/
|
||||
fun pausePhoneMedia(why: String = "the car left") {
|
||||
pausedForNow = null
|
||||
pausedOwnForNow = false
|
||||
pause(why)
|
||||
val paused = pause(why)
|
||||
if (paused.isEmpty()) return
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||
val again = paused.filter { it.playbackState?.state in PLAYING_STATES }
|
||||
again.forEach { runCatching { it.transportControls.pause() } }
|
||||
if (again.isNotEmpty()) {
|
||||
app.seb3thehacker.gearslip.GearslipLog.i("media: paused ${again.joinToString { it.packageName }} again: it started by itself after $why")
|
||||
}
|
||||
}, REPAUSE_MS)
|
||||
}
|
||||
|
||||
/** What [pausePhoneMediaForNow] paused, for [resumePhoneMedia] to play again. */
|
||||
@@ -293,7 +309,7 @@ object CarServices {
|
||||
val manager = ctx.getSystemService(MediaSessionManager::class.java)
|
||||
val listener = ComponentName(ctx, GearslipNotificationListener::class.java)
|
||||
val playing = try {
|
||||
manager.getActiveSessions(listener).filter { it.playbackState?.state == PlaybackState.STATE_PLAYING }
|
||||
manager.getActiveSessions(listener).filter { it.playbackState?.state in PLAYING_STATES }
|
||||
} catch (e: SecurityException) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ internal fun PlayModeRow(
|
||||
// Gearslip's own stand in for apps that report a heart rating or shuffle and repeat modes.
|
||||
val likeAction = now.likeAction
|
||||
if (likeAction != null) {
|
||||
CustomActionButton(likeAction, media, size, toggle = true)
|
||||
CustomActionButton(likeAction, media, size, on = liked == true)
|
||||
} else {
|
||||
GsIconButton(
|
||||
if (liked == true) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||
@@ -158,7 +158,7 @@ internal fun PlayModeRow(
|
||||
}
|
||||
val shuffleAction = now.shuffleAction
|
||||
if (shuffleAction != null) {
|
||||
CustomActionButton(shuffleAction, media, size, toggle = true)
|
||||
CustomActionButton(shuffleAction, media, size, on = shuffleAction.on)
|
||||
} else {
|
||||
GsIconButton(
|
||||
MediaIcons.Shuffle, stringResource(if (shuffleOn) R.string.player_shuffle_off else R.string.player_shuffle_on),
|
||||
@@ -170,7 +170,7 @@ internal fun PlayModeRow(
|
||||
}
|
||||
val repeatAction = now.repeatAction
|
||||
if (repeatAction != null) {
|
||||
CustomActionButton(repeatAction, media, size, toggle = true)
|
||||
CustomActionButton(repeatAction, media, size, on = repeatAction.on)
|
||||
} else {
|
||||
GsIconButton(
|
||||
if (modes.repeat == PlaybackStateCompat.REPEAT_MODE_ONE) MediaIcons.RepeatOne else MediaIcons.Repeat,
|
||||
@@ -191,12 +191,12 @@ internal fun PlayModeRow(
|
||||
* not Gearslip's, so it's loaded from there. Its name stands in if the icon won't load.
|
||||
*/
|
||||
@Composable
|
||||
private fun CustomActionButton(action: CustomAction, media: CarMedia, size: Dp, toggle: Boolean = false) {
|
||||
private fun CustomActionButton(action: CustomAction, media: CarMedia, size: Dp, on: Boolean = false) {
|
||||
val icon = rememberActionIcon(action)
|
||||
GsIconBox(
|
||||
onClick = { media.custom(action) },
|
||||
modifier = Modifier.size(size),
|
||||
colors = gsColors(if (toggle && action.on) GsTone.Primary else GsTone.Neutral),
|
||||
colors = gsColors(if (on) GsTone.Primary else GsTone.Neutral),
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(icon, action.name, Modifier.size(size * 0.52f))
|
||||
|
||||
@@ -44,15 +44,27 @@ class MediaEntry(
|
||||
val group: String? = null,
|
||||
)
|
||||
|
||||
/** [icon] is a drawable id inside the media app's own package, not Gearslip's; 0 for none. */
|
||||
class CustomAction(val id: String, val name: String, val extras: Bundle?, val icon: Int = 0, val iconName: String = "") {
|
||||
/**
|
||||
* [icon] is a drawable id inside the media app's own package, not Gearslip's; 0 for none.
|
||||
* [hasOutline] says the app also has an outline of the same icon (ic_heart beside ic_heart_outline).
|
||||
*/
|
||||
class CustomAction(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val extras: Bundle?,
|
||||
val icon: Int = 0,
|
||||
val iconName: String = "",
|
||||
val hasOutline: Boolean = false,
|
||||
) {
|
||||
/**
|
||||
* Whether a toggle like heart or shuffle is on, read from the icon's name - Tidal swaps
|
||||
* ic_favorite for ic_favorite_filled and ic_shuffle for ic_shuffle_active, and keeps the
|
||||
* button's name the same. Whole words only, so "inactive" doesn't count as "active".
|
||||
* Metrolist swaps ic_heart_outline for a plain ic_heart, so a plain icon that has an outline
|
||||
* twin counts as on too.
|
||||
*/
|
||||
val on: Boolean
|
||||
get() = iconName.lowercase().split('_', '.', '-').any { it in ON_WORDS }
|
||||
get() = hasOutline || iconName.lowercase().split('_', '.', '-').any { it in ON_WORDS }
|
||||
|
||||
private companion object {
|
||||
val ON_WORDS = setOf("filled", "fill", "active", "on", "selected", "checked", "enabled", "solid", "one", "all")
|
||||
@@ -118,8 +130,8 @@ class NowPlaying(
|
||||
* only says which way it goes next by its name ("Remove from Liked Songs"), so that's read.
|
||||
*/
|
||||
val liked: Boolean?
|
||||
get() = heart ?: likeAction?.name?.let { name ->
|
||||
listOf("remove", "unlike", "unfav", "unheart").any { name.contains(it, ignoreCase = true) }
|
||||
get() = heart ?: likeAction?.let { action ->
|
||||
action.on || listOf("remove", "unlike", "unfav", "unheart").any { action.name.contains(it, ignoreCase = true) }
|
||||
}
|
||||
|
||||
val playing get() = state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_BUFFERING
|
||||
@@ -392,7 +404,10 @@ class CarMedia(private val context: Context) {
|
||||
speed = state?.playbackSpeed ?: 1f,
|
||||
updatedAt = state?.lastPositionUpdateTime ?: 0,
|
||||
actions = state?.actions ?: 0,
|
||||
custom = withNextChapter(state?.customActions.orEmpty().map { CustomAction(it.action, it.name.toString(), it.extras, it.icon, iconName(it.icon)) }),
|
||||
custom = withNextChapter(state?.customActions.orEmpty().map {
|
||||
val iconName = iconName(it.icon)
|
||||
CustomAction(it.action, it.name.toString(), it.extras, it.icon, iconName, hasOutline(it.icon, iconName))
|
||||
}),
|
||||
error = state?.errorMessage?.toString(),
|
||||
activeQueueItemId = state?.activeQueueItemId ?: MediaSession.QueueItem.UNKNOWN_ID.toLong(),
|
||||
heart = if (controller?.ratingType == Rating.RATING_HEART) {
|
||||
@@ -405,7 +420,6 @@ class CarMedia(private val context: Context) {
|
||||
|
||||
private var appResources: Pair<String, android.content.res.Resources?>? = null
|
||||
|
||||
/** The icon's resource name in the app's package, or "" when it can't be read. */
|
||||
/**
|
||||
* AntennaPod takes a Next chapter command whenever it's playing, but offers the button only
|
||||
* when the driver turns it on in its notification settings. Gearslip offers it either way,
|
||||
@@ -424,16 +438,30 @@ class CarMedia(private val context: Context) {
|
||||
private fun resourcesOf(pkg: String): android.content.res.Resources? {
|
||||
if (appResources?.first != pkg) {
|
||||
appResources = pkg to runCatching { context.packageManager.getResourcesForApplication(pkg) }.getOrNull()
|
||||
outlines.clear()
|
||||
}
|
||||
return appResources?.second
|
||||
}
|
||||
|
||||
/** The icon's resource name in the app's package, or "" when it can't be read. */
|
||||
private fun iconName(icon: Int): String {
|
||||
val pkg = controller?.packageName ?: return ""
|
||||
val res = resourcesOf(pkg) ?: return ""
|
||||
return runCatching { res.getResourceEntryName(icon) }.getOrDefault("")
|
||||
}
|
||||
|
||||
/** Whether each icon has an outline twin, by icon id: looked up once, since state changes often. */
|
||||
private val outlines = HashMap<Int, Boolean>()
|
||||
|
||||
private fun hasOutline(icon: Int, name: String): Boolean {
|
||||
val pkg = controller?.packageName ?: return false
|
||||
val res = resourcesOf(pkg) ?: return false
|
||||
if (name.isEmpty() || OUTLINE_WORDS.any { name.contains(it, ignoreCase = true) }) return false
|
||||
return outlines.getOrPut(icon) {
|
||||
OUTLINE_WORDS.any { res.getIdentifier("${name}_$it", "drawable", pkg) != 0 }
|
||||
}
|
||||
}
|
||||
|
||||
private var loggedButtons: String? = null
|
||||
|
||||
/** The app's own buttons, by id, name and icon, each time they change. */
|
||||
@@ -822,6 +850,8 @@ class CarMedia(private val context: Context) {
|
||||
/** How long an app gets to start playing a search before Gearslip says it didn't. */
|
||||
const val SEARCH_WAIT_MS = 6_000L
|
||||
const val SEARCH_NOTICE_MS = 6_000L
|
||||
/** How apps name the empty version of an icon. */
|
||||
val OUTLINE_WORDS = listOf("outline", "outlined", "border", "empty")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user