Skip to content

Commit a009f33

Browse files
malysclaude
andcommitted
release: EVABRPUploader 2.1.2 — permissions declared but never granted
- Foreground service type: the service declares `location`, and the plain two-argument startForeground claims every declared type. Since API 34 each is validated against the permissions held right then, so on a car where position was never granted the service threw SecurityException in onCreate and never uploaded. The type is now claimed only while its permission is held, and re-asserted on every start so a later grant applies without a reboot. - Ordering: the activity requested position *after* starting the service, which made the first launch on a new install the one case that could never work. - CAR_EXTERIOR_ENVIRONMENT and CONTROL_CAR_CLIMATE were declared and read since 2.1.0 but never requested, so outside and cabin temperature were dropped from every payload on cars that would have answered. - POST_NOTIFICATIONS was declared and never requested: the service's only status display was silently dropped from Android 13 on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent a246ad1 commit a009f33

4 files changed

Lines changed: 98 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ All notable changes to this project are documented here. Format follows
44
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
55
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [2.1.2] - 2026-08-25
8+
9+
### Fixed
10+
11+
- **The uploader starts on a car that has not granted position yet.** The service declares the
12+
`location` foreground type, and since Android 14 every declared type is checked against the
13+
permissions held at the moment it enters the foreground — so a fresh install, or a boot
14+
before the app had ever been opened, killed the service in its own `onCreate` before the
15+
first tick. The type is now claimed only while the permission behind it is held, and the
16+
activity asks for the permission *before* starting the service rather than after.
17+
- **Outside and cabin temperature reach ABRP again.** Both were declared and read since 2.1.0
18+
but never requested at runtime, so both properties answered as unreadable and were dropped
19+
from every payload.
20+
- **The service notification is visible.** `POST_NOTIFICATIONS` was declared and never asked
21+
for; the only status the service has was being dropped silently on Android 13 and later.
22+
- A permission granted while the service is already running now takes effect at once instead
23+
of at the next boot.
24+
725
## [2.1.1] - 2026-08-23
826

927
### Fixed

‎app/build.gradle‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ android {
3434
applicationId "com.evsuite.abrp"
3535
minSdk 27
3636
targetSdk 36
37-
versionCode 9
38-
versionName "2.1.1"
37+
versionCode 10
38+
versionName "2.1.2"
3939

4040
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
4141
}

‎app/src/main/java/com/evsuite/abrp/AbrpUploadService.java‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import android.app.PendingIntent;
77
import android.app.Service;
88
import android.content.Intent;
9+
import android.content.pm.PackageManager;
10+
import android.content.pm.ServiceInfo;
911
import android.content.SharedPreferences;
1012
import android.location.Location;
1113
import android.location.LocationListener;
@@ -16,7 +18,11 @@
1618
import android.os.Looper;
1719
import android.util.Log;
1820

21+
import android.Manifest;
22+
1923
import androidx.core.app.NotificationCompat;
24+
import androidx.core.app.ServiceCompat;
25+
import androidx.core.content.ContextCompat;
2026

2127
import com.evsuite.hardware.EVHardware;
2228

@@ -107,6 +113,8 @@ public class AbrpUploadService extends Service {
107113
private final ChargeMeter chargeMeter = new ChargeMeter();
108114
private volatile UploadSettings settings = UploadSettings.defaults();
109115
/** False while no GPS subscription is delivering — drives the watchdog re-arm. */
116+
/** What the foreground notification currently says, so re-asserting it does not blank it. */
117+
private volatile String lastStatus = "Starting…";
110118
private volatile boolean locationUpdatesActive = false;
111119
private volatile long lastLocationRequestMs = 0L;
112120

@@ -127,7 +135,7 @@ public void onCreate() {
127135
});
128136

129137
createNotificationChannel();
130-
startForeground(NOTIF_ID, buildNotification("Starting…"));
138+
startInForeground();
131139
running = true;
132140
prefs.edit().putBoolean("service_running", true).apply();
133141

@@ -158,9 +166,45 @@ public int onStartCommand(Intent intent, int flags, int startId) {
158166
stopSelf();
159167
return START_NOT_STICKY;
160168
}
169+
// Re-asserted on every start: a position grant that arrives after the service is up
170+
// changes which foreground types it may hold, and the activity starts it again as
171+
// soon as the user answers. Without this the grant only took effect at the next boot.
172+
startInForeground();
161173
return START_STICKY;
162174
}
163175

176+
/**
177+
* Enters the foreground holding only the service types this app currently has the
178+
* permissions for.
179+
*
180+
* The service declares {@code foregroundServiceType="location"}, and since API 34 the
181+
* platform validates every declared type against what is held at the moment
182+
* startForeground runs — the plain two-argument call claims them all. On a car where
183+
* position had not been granted yet (a fresh install, or the boot receiver starting the
184+
* service before the app was ever opened) that threw a SecurityException and the uploader
185+
* died before its first tick. Without the permission the service still runs and still
186+
* uploads; it simply has no position to send, exactly as before.
187+
*/
188+
private void startInForeground() {
189+
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q) {
190+
// Service types do not exist before API 29: nothing is declared and nothing is
191+
// checked, so the plain call is the only correct one there.
192+
startForeground(NOTIF_ID, buildNotification(lastStatus));
193+
return;
194+
}
195+
int types = hasLocationPermission()
196+
? ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
197+
: ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
198+
ServiceCompat.startForeground(this, NOTIF_ID, buildNotification(lastStatus), types);
199+
}
200+
201+
private boolean hasLocationPermission() {
202+
return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
203+
== PackageManager.PERMISSION_GRANTED
204+
|| ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
205+
== PackageManager.PERMISSION_GRANTED;
206+
}
207+
164208
@Override
165209
public IBinder onBind(Intent intent) { return null; }
166210

@@ -542,6 +586,7 @@ private Notification buildNotification(String status) {
542586
}
543587

544588
private void updateNotification(String status) {
589+
lastStatus = status;
545590
getSystemService(NotificationManager.class).notify(NOTIF_ID, buildNotification(status));
546591
}
547592
}

‎app/src/main/java/com/evsuite/abrp/MainActivity.java‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,12 @@ protected void onCreate(Bundle savedInstanceState) {
223223
boolean haveCreds = !securePrefs.getString(SecurePrefs.KEY_TOKEN, "").trim().isEmpty()
224224
&& !securePrefs.getString(SecurePrefs.KEY_API_KEY, "").trim().isEmpty();
225225
if (enabled && haveCreds) {
226-
startForegroundService(new Intent(this, AbrpUploadService.class));
226+
// Asked for first, and deliberately: the service declares the `location`
227+
// foreground type, and since API 34 starting it without the permission behind
228+
// that type is a SecurityException in its own onCreate. Started this way round,
229+
// the very first launch after an install could never bring the uploader up.
227230
requestLocationPermissionIfNeeded();
231+
startForegroundService(new Intent(this, AbrpUploadService.class));
228232
}
229233

230234
abrpPane.findViewById(R.id.save_button).setOnClickListener(v -> saveCredentials());
@@ -675,11 +679,30 @@ private String textOf(TextInputEditText field) {
675679
"android.car.permission.CAR_SPEED",
676680
"android.car.permission.CAR_ENERGY",
677681
"android.car.permission.CAR_ENERGY_PORTS",
682+
// Outside temperature and cabin temperature: declared and read since 2.1.0, but
683+
// never asked for, so both came back unreadable and were dropped from every
684+
// payload on a car that would have answered them.
685+
"android.car.permission.CAR_EXTERIOR_ENVIRONMENT",
686+
"android.car.permission.CONTROL_CAR_CLIMATE",
678687
};
679688

689+
/**
690+
* Asked for alongside the telemetry permissions from API 33, and separately because a
691+
* denial costs something different: the foreground notification is the only status this
692+
* service has, and without this it is dropped silently while the service runs on.
693+
*/
694+
private static String[] runtimePermissions() {
695+
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) {
696+
return RUNTIME_PERMISSIONS;
697+
}
698+
String[] all = java.util.Arrays.copyOf(RUNTIME_PERMISSIONS, RUNTIME_PERMISSIONS.length + 1);
699+
all[RUNTIME_PERMISSIONS.length] = Manifest.permission.POST_NOTIFICATIONS;
700+
return all;
701+
}
702+
680703
private void requestLocationPermissionIfNeeded() {
681704
java.util.List<String> missing = new java.util.ArrayList<>();
682-
for (String p : RUNTIME_PERMISSIONS) {
705+
for (String p : runtimePermissions()) {
683706
if (ContextCompat.checkSelfPermission(this, p)
684707
!= PackageManager.PERMISSION_GRANTED) {
685708
missing.add(p);
@@ -714,6 +737,13 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis
714737
}
715738
}
716739

740+
// A grant that arrives after the service is already up changes which foreground
741+
// types it may hold, and which properties it may read. Starting it again is what
742+
// applies both without waiting for the next boot.
743+
if (prefs.getBoolean("service_enabled", false)) {
744+
startForegroundService(new Intent(this, AbrpUploadService.class));
745+
}
746+
717747
if (locationDenied && carDataDenied) {
718748
setConnectionStatus(COLOR_ERROR, getString(R.string.perm_denied_all));
719749
} else if (locationDenied) {

0 commit comments

Comments
 (0)