mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
initial standards implemented + new docs like how to build apps
This commit is contained in:
1 parent
cb895f0658
commit
e1732f8365
5 files changed
+471
-3
No files matched your search
@@ -12,7 +12,14 @@ local.properties
|
||||
# The head unit certificate. Never committed or shared beyond who Seb sends it to directly.
|
||||
/cert
|
||||
|
||||
# The release signing key and its passwords. Losing these breaks app updates; leaking them
|
||||
# lets anyone sign an APK as Gearslip.
|
||||
*.jks
|
||||
*.keystore
|
||||
keystore.properties
|
||||
|
||||
# Local investigation script; not meant for the public repo.
|
||||
/docs/cert-sweep.sh
|
||||
archive/
|
||||
CLAUDE.md
|
||||
*.pdf
|
||||
@@ -28,7 +28,7 @@ Real progress, with one hard limit that's worth knowing before you build on this
|
||||
doesn't ship one.** A head unit only accepts a phone identity that chains to Google's
|
||||
Automotive Link certificate authority - something only Google issues. Without one, Gearslip
|
||||
still runs and renders everything correctly against a debug harness, but a real car will
|
||||
reject the connection outright. See [SPIKE_FINDINGS.md](SPIKE_FINDINGS.md) for what was
|
||||
reject the connection outright. See [docs/SPIKE_FINDINGS.md](docs/SPIKE_FINDINGS.md) for what was
|
||||
tested, including one publicly-known leaked certificate that works on some older head
|
||||
units and is rejected by newer firmware and by Google's own reference tooling.
|
||||
- **No adb, root, or Shizuku is required to run it.** Every feature is gated on an ordinary
|
||||
@@ -45,9 +45,11 @@ The APK lands at `build/Gearslip-debug.apk`.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [SPIKE_FINDINGS.md](SPIKE_FINDINGS.md) - the certificate investigation in full: what was
|
||||
- [docs/BUILDING_APPS.md](docs/BUILDING_APPS.md) - how to write apps Gearslip can render:
|
||||
templated apps, media apps, and the Gearslip app protocol.
|
||||
- [docs/SPIKE_FINDINGS.md](docs/SPIKE_FINDINGS.md) - the certificate investigation in full: what was
|
||||
tried, what a real head unit does and doesn't accept, and why.
|
||||
- [CUSTOM_HEADUNIT_SCOPING.md](CUSTOM_HEADUNIT_SCOPING.md) - the original scoping of what a
|
||||
- [docs/CUSTOM_HEADUNIT_SCOPING.md](docs/CUSTOM_HEADUNIT_SCOPING.md) - the original scoping of what a
|
||||
custom head unit client would need.
|
||||
|
||||
## License
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
# Building apps for Gearslip
|
||||
|
||||
Gearslip renders three kinds of app on a car screen. This page specifies all three.
|
||||
|
||||
| Kind | What it is | Status |
|
||||
| --- | --- | --- |
|
||||
| [Templated apps](#templated-apps) | An AndroidX Car App Library app. Gearslip hosts it. | Works today |
|
||||
| [Media apps](#media-apps) | A `MediaBrowserService` that opted in to Android Auto. | Works today |
|
||||
| [Gearslip apps](#gearslip-apps) | A native Gearslip screen, described over binder. | Specified here; not yet implemented |
|
||||
|
||||
Write a templated app when your app fits the Car App Library's templates, because it runs today
|
||||
and it also runs on Android Auto. Write a media app when you ship audio. Write a Gearslip app
|
||||
when you want a screen the Car App Library cannot describe.
|
||||
|
||||
Gearslip's package name is `app.seb3thehacker.gearslip`. It declares
|
||||
`android.car.permission.TEMPLATE_RENDERER`.
|
||||
|
||||
---
|
||||
|
||||
## Templated apps
|
||||
|
||||
A templated app exports a `CarAppService`, describes each screen as a `Template`, and lets the
|
||||
host draw it. Gearslip implements the host half of that protocol, so an existing Android Auto app
|
||||
needs no Gearslip-specific code. Build it against the AndroidX Car App Library as you would for
|
||||
any other host.
|
||||
|
||||
### Letting Gearslip in
|
||||
|
||||
Your app decides which hosts may render it, through the `HostValidator` your `CarAppService`
|
||||
returns.
|
||||
|
||||
Gearslip declares `android.car.permission.TEMPLATE_RENDERER`, and the library's own
|
||||
`HostValidator` accepts any host holding it. If you build your validator with
|
||||
`HostValidator.Builder` and no allowlist, Gearslip already passes, and you need nothing below.
|
||||
Add an allowlist entry when you wrote a custom validator, or when you want to name Gearslip
|
||||
explicitly:
|
||||
|
||||
```xml
|
||||
<!-- res/values/arrays.xml -->
|
||||
<array name="hosts_allowlist">
|
||||
<item>4cc56ebdd13a54d260eca9b7e2b2ae6b30e2f225bfda8f508f0709edf084948b,app.seb3thehacker.gearslip</item>
|
||||
</array>
|
||||
```
|
||||
|
||||
```java
|
||||
@Override
|
||||
public HostValidator createHostValidator() {
|
||||
return new HostValidator.Builder(getApplicationContext())
|
||||
.addAllowedHosts(R.array.hosts_allowlist)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
That digest belongs to Gearslip's release signing certificate. A Gearslip you built yourself
|
||||
carries your own debug key and a different digest, so read the fingerprint off the APK you
|
||||
actually installed when an allowlist entry fails to match.
|
||||
|
||||
Write each entry as the digest first, then a comma, then the package name. The digest is the
|
||||
SHA-256 of the host's signing certificate as 64 lowercase hex characters, with no colons and no
|
||||
`0x` prefix. The builder lowercases the entry and strips spaces, but it keeps colons, so convert
|
||||
`keytool` output before pasting it:
|
||||
|
||||
```
|
||||
keytool -list -v -keystore <keystore> -alias <alias> \
|
||||
| grep 'SHA256:' | cut -d' ' -f3 | tr -d ':' | tr 'A-Z' 'a-z'
|
||||
```
|
||||
|
||||
Reading the installed APK is more reliable, because it fingerprints the key that actually signed
|
||||
the build, and `apksigner` already prints the required format:
|
||||
|
||||
```
|
||||
apksigner verify --print-certs Gearslip.apk | grep 'SHA-256 digest'
|
||||
```
|
||||
|
||||
During development, `HostValidator.ALLOW_ALL_HOSTS_VALIDATOR` accepts every host. Ship a real
|
||||
validator instead.
|
||||
|
||||
An app that refuses a host returns no binder at all. Gearslip reports that as
|
||||
`the app refused to bind - host not accepted`. Your own side logs the reason under the tag
|
||||
`CarApp.Val`, including the exact `addAllowedHost` call to add, so read logcat there first.
|
||||
|
||||
### Discovery
|
||||
|
||||
Gearslip finds templated apps by querying `androidx.car.app.CarAppService`, then re-querying with
|
||||
each known category to recover which ones your intent filter matched. Declare the categories you
|
||||
belong to, or Gearslip files you under `app`:
|
||||
|
||||
```xml
|
||||
<service android:name=".MyCarAppService" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.car.app.CarAppService" />
|
||||
<category android:name="androidx.car.app.category.NAVIGATION" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
```
|
||||
|
||||
Gearslip recognises the `NAVIGATION`, `POI`, `PARKING`, `CHARGING`, `MESSAGING`, `CALLING`,
|
||||
`SETTINGS`, `IOT` and `WEATHER` categories. Navigation apps start automatically on the home
|
||||
screen when the driver used them last.
|
||||
|
||||
### The handshake
|
||||
|
||||
Gearslip offers car API level 8. It negotiates down to
|
||||
`min(8, yourLatestCarAppApiLevel)` and refuses the app when that falls below your
|
||||
`minCarAppApiLevel`, rather than drawing it wrongly. Keep your minimum at 8 or below.
|
||||
|
||||
### Templates Gearslip renders
|
||||
|
||||
Gearslip renders all sixteen templates. Ten describe their own content:
|
||||
|
||||
`ListTemplate`, `GridTemplate`, `PaneTemplate`, `MessageTemplate`, `LongMessageTemplate`,
|
||||
`SearchTemplate`, `SignInTemplate`, `TabTemplate`, `SectionedItemTemplate`,
|
||||
`MediaPlaybackTemplate`.
|
||||
|
||||
Six draw a map underneath and take chrome over the top:
|
||||
|
||||
`NavigationTemplate`, `MapWithContentTemplate`, `MapTemplate`, `PlaceListNavigationTemplate`,
|
||||
`RoutePreviewNavigationTemplate`, `PlaceListMapTemplate`.
|
||||
|
||||
`SearchTemplate` and `SignInTemplate` need text. A phone keyboard cannot appear on the car's
|
||||
virtual display, so Gearslip draws its own and feeds the result back through the template's
|
||||
listener.
|
||||
|
||||
### Content limits
|
||||
|
||||
Gearslip reports these limits through the constraint service, and expects you to honour them:
|
||||
|
||||
| Limit | Value |
|
||||
| --- | --- |
|
||||
| Grid items | 12 |
|
||||
| Pane rows | 4 |
|
||||
| Route list rows | 3 |
|
||||
| Anything else | 12 |
|
||||
|
||||
Gearslip enables app-driven refresh, so you may invalidate your own template without the driver
|
||||
touching the screen.
|
||||
|
||||
### Drawing a map
|
||||
|
||||
A navigation app draws its own pixels. Gearslip lends it a real `Surface` taken from a
|
||||
`SurfaceView` inside the car's virtual display, so your map reaches the head unit through the
|
||||
existing video pipe. Gearslip never reads those pixels back.
|
||||
|
||||
Gearslip then reports which part of that surface the driver can see. Treat the visible area as
|
||||
the region safe for your own controls, and the stable area as the region that never gets
|
||||
covered. Gearslip keeps its chrome to a top bar, so both areas are the surface minus that bar.
|
||||
|
||||
Your app receives four gestures, never raw touches: `onClick`, `onScroll`, `onFling` and
|
||||
`onScale`. Coordinates are surface pixels. Scroll distances follow `GestureDetector`'s
|
||||
convention, which reports the previous position minus the current one.
|
||||
|
||||
### Car hardware
|
||||
|
||||
A phone knows nothing about a car's sensors, so Gearslip answers every car-hardware request with
|
||||
"unsupported". It answers rather than ignores: an app whose sensor manager waits for a reply
|
||||
hangs when the host stays silent. Design for a host that reports no vehicle data at all.
|
||||
|
||||
Gearslip accepts the suggestions an app pushes and discards them, because no screen shows them
|
||||
yet. Your app will not fail for offering them.
|
||||
|
||||
### Day and night
|
||||
|
||||
Gearslip decides whether it is dark outside and pushes that to your app as a configuration
|
||||
change, with `UI_MODE_NIGHT_YES` or `UI_MODE_NIGHT_NO`. Take your map style from the
|
||||
configuration the host reports. The configuration also carries the car's density, screen size and
|
||||
orientation, and always sets `UI_MODE_TYPE_CAR`.
|
||||
|
||||
---
|
||||
|
||||
## Media apps
|
||||
|
||||
Gearslip draws the media UI itself: browsing, artwork, lyrics, transport controls and a
|
||||
now-playing pill in the nav bar. Your app supplies a browse tree and a media session.
|
||||
|
||||
Gearslip lists a media app when it exports a `MediaBrowserService` **and** declares the Android
|
||||
Auto meta-data. Both halves are required, because the service alone also appears on phone-only
|
||||
players that expose a browser to Bluetooth and Wear:
|
||||
|
||||
```xml
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
</application>
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- res/xml/automotive_app_desc.xml -->
|
||||
<automotiveApp>
|
||||
<uses name="media" />
|
||||
</automotiveApp>
|
||||
```
|
||||
|
||||
Publish playback state, metadata, duration and artwork through your `MediaSession`, and support
|
||||
play, pause, next, previous and seek. Gearslip reconnects to the app the driver used last and
|
||||
can start it playing on connection.
|
||||
|
||||
---
|
||||
|
||||
## Gearslip apps
|
||||
|
||||
This section specifies protocol version 1. Gearslip does not yet implement it.
|
||||
|
||||
A Gearslip app describes a screen and lets Gearslip render it in Compose, using the car's theme,
|
||||
the driver's chosen scale and the current day or night state. Your app draws nothing and receives
|
||||
no surface. Everything the driver sees comes from the model below, which means your screen
|
||||
matches the rest of the car UI and cannot be made unreadable.
|
||||
|
||||
Write a Gearslip app to reach the parts of the car UI the Car App Library does not cover. Write a
|
||||
templated app if you need to draw a map, because version 1 lends no surface.
|
||||
|
||||
### Discovery
|
||||
|
||||
Export a service with the Gearslip action and declare the protocol version it speaks:
|
||||
|
||||
```xml
|
||||
<service
|
||||
android:name=".GearslipService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="app.seb3thehacker.gearslip.APP" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="app.seb3thehacker.gearslip.version"
|
||||
android:value="1" />
|
||||
</service>
|
||||
```
|
||||
|
||||
Gearslip scans for these services once per car session and shows each one as a tile in the
|
||||
launcher, under your application's own label and icon. To override either on the car screen, add
|
||||
`app.seb3thehacker.gearslip.label` or `app.seb3thehacker.gearslip.icon`.
|
||||
|
||||
Gearslip binds to any app that declares this service. It asks the driver for nothing, exactly as
|
||||
a head unit asks nothing before running an Android Auto app.
|
||||
|
||||
Gearslip binds your service when the driver opens your tile and unbinds it when they leave. Keep
|
||||
no state that a rebind cannot rebuild.
|
||||
|
||||
### The interface
|
||||
|
||||
Copy these three files into `src/main/aidl/app/seb3thehacker/gearslip/api/`. A published artifact
|
||||
will replace them.
|
||||
|
||||
```aidl
|
||||
// IGearslipApp.aidl
|
||||
package app.seb3thehacker.gearslip.api;
|
||||
|
||||
import app.seb3thehacker.gearslip.api.IGearslipHost;
|
||||
import app.seb3thehacker.gearslip.api.IScreenCallback;
|
||||
|
||||
interface IGearslipApp {
|
||||
/** Return the highest version both sides speak. Gearslip passes the version it offers. */
|
||||
int negotiate(int hostVersion);
|
||||
|
||||
void onCreate(IGearslipHost host);
|
||||
void onStart();
|
||||
void onStop();
|
||||
|
||||
/** Describe the current screen. Answer through the callback, on any thread. */
|
||||
void getScreen(IScreenCallback callback);
|
||||
|
||||
void onAction(String actionId);
|
||||
void onTextInput(String fieldId, String text);
|
||||
}
|
||||
```
|
||||
|
||||
```aidl
|
||||
// IGearslipHost.aidl
|
||||
package app.seb3thehacker.gearslip.api;
|
||||
|
||||
interface IGearslipHost {
|
||||
/** Tell Gearslip the screen changed. It calls getScreen again. */
|
||||
void invalidate();
|
||||
|
||||
/** Open the car keyboard. The result arrives at onTextInput. */
|
||||
void requestTextInput(String fieldId, String prompt, String initialText);
|
||||
|
||||
String getEnvironment();
|
||||
String getNowPlaying();
|
||||
|
||||
/** One of play, pause, toggle, next, previous. */
|
||||
void mediaCommand(String command);
|
||||
|
||||
/** Leave the app and return to the launcher. */
|
||||
void finish();
|
||||
}
|
||||
```
|
||||
|
||||
```aidl
|
||||
// IScreenCallback.aidl
|
||||
package app.seb3thehacker.gearslip.api;
|
||||
|
||||
interface IScreenCallback {
|
||||
void onScreen(String screenJson);
|
||||
void onError(String message);
|
||||
}
|
||||
```
|
||||
|
||||
### Versioning
|
||||
|
||||
Gearslip calls `negotiate` first, passing the version it offers. Return the highest version you
|
||||
also speak. Gearslip uses that version for the rest of the connection and drops the app when the
|
||||
answer exceeds what it offered. This mirrors how Gearslip negotiates car API levels with
|
||||
templated apps, and it lets either side move first.
|
||||
|
||||
Version 1 is the only version. Reject anything you do not recognise by returning 1.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
1. Gearslip binds your service and calls `negotiate`.
|
||||
2. Gearslip calls `onCreate`, handing you the host interface. Keep it.
|
||||
3. Gearslip calls `onStart`, then `getScreen`.
|
||||
4. You answer through `onScreen`. Gearslip renders it.
|
||||
5. The driver taps something. Gearslip calls `onAction` with that element's id.
|
||||
6. Your screen changes, so you call `invalidate`. Gearslip calls `getScreen` again.
|
||||
7. The driver leaves. Gearslip calls `onStop` and unbinds.
|
||||
|
||||
Every call arrives on a binder thread. Answer `getScreen` promptly; Gearslip shows the previous
|
||||
screen until you do. Gearslip coalesces repeated `invalidate` calls.
|
||||
|
||||
### The screen model
|
||||
|
||||
Answer `getScreen` with a JSON object.
|
||||
|
||||
```json
|
||||
{
|
||||
"protocol": 1,
|
||||
"type": "list",
|
||||
"title": "Stations",
|
||||
"items": [
|
||||
{ "id": "r6", "title": "BBC Radio 6", "subtitle": "Music", "icon": "play" },
|
||||
{ "id": "r4", "title": "BBC Radio 4", "subtitle": "Speech", "icon": "play" }
|
||||
],
|
||||
"actions": [
|
||||
{ "id": "refresh", "title": "Refresh", "icon": "refresh", "primary": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`type` takes one of four values:
|
||||
|
||||
| Type | Renders as | Uses |
|
||||
| --- | --- | --- |
|
||||
| `list` | A vertical list of rows | `items` |
|
||||
| `grid` | A grid of tiles | `items` |
|
||||
| `pane` | A short detail pane beside its actions | `items`, `actions` |
|
||||
| `message` | A single message and its actions | `text`, `actions` |
|
||||
|
||||
An item takes `id`, `title`, and optionally `subtitle`, `icon` and `enabled`. Gearslip calls
|
||||
`onAction` with the item's `id` when the driver taps it. An item with `"type": "input"` renders
|
||||
as a text field carrying `value`; tapping it opens the car keyboard, and the result arrives at
|
||||
`onTextInput` under the item's id.
|
||||
|
||||
An action takes `id`, `title`, and optionally `icon` and `primary`. Gearslip renders at most two
|
||||
actions and emphasises the primary one.
|
||||
|
||||
`icon` names a built-in glyph: `play`, `pause`, `next`, `previous`, `search`, `settings`, `home`,
|
||||
`apps`, `notifications`, `info`, `share`, `place`, `favorite`, `add`, `remove`, `refresh`,
|
||||
`check`, `error`. Version 1 carries no images. Gearslip drops an unknown name and renders the
|
||||
row without a glyph.
|
||||
|
||||
Gearslip truncates any list past the limits in [Content limits](#content-limits) and shortens
|
||||
long strings. Send a screen that fits, rather than one Gearslip has to cut.
|
||||
|
||||
### Reading the car
|
||||
|
||||
`getEnvironment` returns the car's current state:
|
||||
|
||||
```json
|
||||
{
|
||||
"protocol": 1,
|
||||
"darkOutside": true,
|
||||
"moving": false,
|
||||
"frameWidth": 800,
|
||||
"frameHeight": 480,
|
||||
"densityDpi": 160,
|
||||
"batteryPercent": 82,
|
||||
"charging": true,
|
||||
"vehicle": "Uconnect"
|
||||
}
|
||||
```
|
||||
|
||||
`darkOutside` reports whether Gearslip believes it is dark, from the light sensor or the clock.
|
||||
`moving` reports whether the phone's GPS shows the car moving. Both change during a session, so
|
||||
read them each time you build a screen rather than caching them.
|
||||
|
||||
Treat `moving` as a comfort signal, not a safety guarantee. It reads false whenever the fix is
|
||||
stale or missing, which includes tunnels and car parks.
|
||||
|
||||
### Controlling media
|
||||
|
||||
`getNowPlaying` returns what the car is playing, whichever app owns it:
|
||||
|
||||
```json
|
||||
{ "title": "Sleepwalk", "artist": "Santo & Johnny", "playing": true, "durationMs": 142000 }
|
||||
```
|
||||
|
||||
`mediaCommand` sends one of `play`, `pause`, `toggle`, `next` or `previous` to that app. Both
|
||||
work regardless of which app is playing, so use them to put transport controls on your own
|
||||
screen. Ship audio through a media app instead of driving these.
|
||||
|
||||
### Text input
|
||||
|
||||
A phone keyboard cannot appear on the car's virtual display, so Gearslip draws its own. Call
|
||||
`requestTextInput` with a field id, a prompt and any existing text. Gearslip opens the keyboard
|
||||
and calls `onTextInput` as the driver types, so treat each call as the current contents rather
|
||||
than a final answer.
|
||||
|
||||
### What version 1 leaves out
|
||||
|
||||
Version 1 lends no surface, so no Gearslip app draws its own pixels. It carries no images beyond
|
||||
the built-in glyphs. It places apps only in the launcher; contributing a dashboard card or a
|
||||
home-screen pane comes later. Anything that needs its own rendering belongs in a templated app.
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- [README.md](../README.md) - what Gearslip is and how to build it.
|
||||
- [Android for Cars App Library](https://developer.android.com/training/cars/apps) - the
|
||||
reference for templated apps.
|
||||
@@ -1,3 +1,4 @@
|
||||
import java.util.Properties
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||
|
||||
@@ -13,6 +14,16 @@ plugins {
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
// The release signing key, read from an untracked keystore.properties beside this file or at the
|
||||
// repository root. Third-party apps allowlist Gearslip by the SHA-256 of this certificate, so the
|
||||
// key must stay the same for the life of the project - and must never be committed.
|
||||
val keystoreProperties = Properties().apply {
|
||||
listOf(
|
||||
rootProject.file("keystore.properties"),
|
||||
project.file("keystore.properties"),
|
||||
).firstOrNull { it.exists() }?.inputStream()?.use { load(it) }
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.seb3thehacker.gearslip"
|
||||
compileSdk = 37
|
||||
@@ -25,6 +36,25 @@ android {
|
||||
versionName = "0.0.02-pre"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
// Declared only when the properties file is present, so a clone without the key still
|
||||
// builds debug; assembleRelease then falls back to unsigned rather than failing here.
|
||||
if (keystoreProperties.getProperty("storeFile") != null) {
|
||||
create("release") {
|
||||
storeFile = rootProject.file(keystoreProperties.getProperty("storeFile"))
|
||||
storePassword = keystoreProperties.getProperty("storePassword")
|
||||
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.findByName("release")
|
||||
}
|
||||
}
|
||||
|
||||
// Keeps build output out of the module folder: <root>/build/gearslip/... instead of
|
||||
// <root>/gearslip/build/..., so everything this project produces lands under one
|
||||
// root-level build/ directory.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copy to keystore.properties and fill in. That file is gitignored; this one is not, so never
|
||||
# put real passwords here.
|
||||
#
|
||||
# storeFile is resolved from the repository root.
|
||||
storeFile=gearslip-release.jks
|
||||
storePassword=
|
||||
keyAlias=gearslip
|
||||
keyPassword=
|
||||
Reference in new issue
Block a user