Skip to content

Commit 51944f7

Browse files
authored
Fixes Deutsche Bahn, VRS, VVS, add Poland, add button for location update, add alert service via context menu (#992)
* add location button in FROM field to enable refresh #786 * rename onFindGpsLocation to updateGpsLocation when refreshing location wait one iteration longer for more accuracy * Update public-transport-enabler to 2713727d07df8408e064e5ffae2733862711a10b Fix provider Deutsche Bahn #981 Fix provider VRS: Update client certificate #993 * Alert service Set alert for a station via context menu * increased priority for alert stop watchdog at arrival code improvements * Fix VVS https://gitlab.com/oeffi/oeffi/-/commit/6d7979e8411c5b641a9857ff22b1f72e23bb35cf * use Transportr icon in notification show chequered flag at arrival * enable vibration * use string resource for channel name German translations * add Poland https://gitlab.com/oeffi/oeffi/-/commit/2736bc82dd08937aebebf26a582949cc6a8c3205 * add manifest permission for background location (not requested actively, but can be necessary on some devices) improvements in alert service update version code and version name update fastlane
1 parent ed094aa commit 51944f7

File tree

19 files changed

+356
-18
lines changed

19 files changed

+356
-18
lines changed

.clabot

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"Jean-BaptisteC",
2525
"Bnyro",
2626
"mesinger",
27-
"newhinton"
27+
"newhinton",
28+
"woheller69"
2829
],
2930
"label": "cla-signed ✔️",
3031
"message": "Thank you for your pull request and welcome to our community! We require contributors to sign our [Contributor License Agreement](https://github.com/grote/Transportr/blob/master/CLA.md), and we don't seem to have the user {{usersWithoutCLA}} on file. In order for your code to get reviewed and merged, please explicitly state that you accept the agreement. Alternatively, you can add a commit that adds yourself to https://github.com/grote/Transportr/blob/master/.clabot"

app/build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ apply from: 'witness.gradle'
88
android {
99

1010
defaultConfig {
11-
versionCode 127
12-
versionName "2.2.3"
11+
versionCode 128
12+
versionName "2.2.4"
1313

1414
applicationId "de.grobox.liberario"
1515
minSdkVersion 21
@@ -142,7 +142,7 @@ dependencies {
142142
exclude module: 'failureaccess'
143143
exclude group: 'com.google.j2objc'
144144
}
145-
implementation('com.gitlab.opentransitmap:public-transport-enabler:7f29ab5fc5a02f71e044c5623117741e36e73d3f') {
145+
implementation('com.gitlab.opentransitmap:public-transport-enabler:2713727d07df8408e064e5ffae2733862711a10b') {
146146
exclude group: 'com.google.guava' // included above
147147
exclude group: 'org.json', module: 'json' // provided by Android
148148
exclude group: 'net.sf.kxml', module: 'kxml2' // provided by Android

app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
1010
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
1111
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
12+
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
1213
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
1314
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
15+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
16+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
17+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
1418

1519
<uses-feature
1620
android:name="android.hardware.WIFI"
@@ -95,6 +99,11 @@
9599
android:name=".about.ContributorsActivity"
96100
android:label="@string/drawer_contributors"/>
97101

102+
<service
103+
android:exported="false"
104+
android:foregroundServiceType="location"
105+
android:name=".AlertService" />
106+
98107
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
99108
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
100109
<!-- Version >= 3.0. DeX Dual Mode support -->
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Transportr
3+
*
4+
* Copyright (c) 2013 - 2025 Torsten Grote
5+
*
6+
* This program is Free Software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as
8+
* published by the Free Software Foundation, either version 3 of the
9+
* License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package de.grobox.transportr;
21+
22+
import android.Manifest;
23+
import android.app.PendingIntent;
24+
import android.app.Service;
25+
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_MAX;
26+
import android.content.Context;
27+
import android.content.Intent;
28+
import android.content.pm.PackageManager;
29+
import android.content.pm.ServiceInfo;
30+
import android.location.Location;
31+
import android.location.LocationListener;
32+
import android.location.LocationManager;
33+
import android.os.Build;
34+
import android.os.Handler;
35+
import android.os.IBinder;
36+
import android.os.Looper;
37+
38+
import androidx.annotation.NonNull;
39+
import androidx.annotation.Nullable;
40+
import androidx.core.app.ActivityCompat;
41+
import androidx.core.app.NotificationChannelCompat;
42+
import androidx.core.app.NotificationCompat;
43+
import androidx.core.app.NotificationManagerCompat;
44+
import androidx.core.app.NotificationCompat.Builder;
45+
46+
public class AlertService extends Service implements LocationListener {
47+
private LocationManager mLocManager = null;
48+
private boolean isWatchdogRunning = false;
49+
private long lastLocationUpdate = 0;
50+
private NotificationManagerCompat mNotifManager = null;
51+
private static final int NOTIF_ID = 111;
52+
private static final String CHANNEL_ID = "alert";
53+
private Builder mNotifBuilder;
54+
private String destinationName;
55+
private String arrivalTime;
56+
private long arrivalTimeLong;
57+
private Location destination;
58+
private PendingIntent stopPendingIntent;
59+
private static final long ARRIVAL_THRESHOLD_METERS = 150;
60+
private static final long ARRIVAL_THRESHOLD_SEC = 30;
61+
private static final long WATCHDOG_INTERVAL_MS = 1_000; // 1 second
62+
private static final long LOCATION_INTERVAL_MS = 2_000; // 2 seconds
63+
private static final long LOCATION_TIMEOUT_MS = 10_000; // 10 seconds
64+
private static final String ACTION_STOP = "STOP";
65+
private final Handler handler = new Handler(Looper.getMainLooper());
66+
private final Runnable watchdogRunnable = new Runnable() {
67+
@Override
68+
public void run() {
69+
long now = System.currentTimeMillis();
70+
if (now - lastLocationUpdate > LOCATION_TIMEOUT_MS) {
71+
// No location update in the last 10 seconds → take action
72+
onLocationUpdateTimeout();
73+
}
74+
// Reschedule the check
75+
if (isWatchdogRunning) handler.postDelayed(this, WATCHDOG_INTERVAL_MS);
76+
}
77+
};
78+
79+
@Nullable
80+
@Override
81+
public IBinder onBind(Intent intent) {
82+
return null;
83+
}
84+
85+
@Override
86+
public void onCreate() {
87+
super.onCreate();
88+
mLocManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
89+
mNotifManager = NotificationManagerCompat.from(getApplicationContext());
90+
NotificationChannelCompat notifChannel = new NotificationChannelCompat.Builder(CHANNEL_ID, IMPORTANCE_MAX).setName(getResources().getString(R.string.action_alert)).setVibrationEnabled(true).build();
91+
mNotifManager.createNotificationChannel(notifChannel);
92+
93+
Intent stopIntent = new Intent(this, AlertService.class);
94+
stopIntent.setAction(ACTION_STOP);
95+
stopPendingIntent = PendingIntent.getService(
96+
this,
97+
0,
98+
stopIntent,
99+
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
100+
);
101+
}
102+
103+
@Override
104+
public int onStartCommand(Intent intent, int flags, int startId) {
105+
if (intent == null) {
106+
// Service restarted by system, but we lack destination data → stop.
107+
stopSelf();
108+
return START_NOT_STICKY;
109+
}
110+
if (!ACTION_STOP.equals(intent.getAction())) {
111+
handler.removeCallbacksAndMessages(null);
112+
arrivalTime = intent.getStringExtra("EXTRA_TIME_STR");
113+
arrivalTimeLong = intent.getLongExtra("EXTRA_TIME_LONG",0L);
114+
destinationName = intent.getStringExtra("EXTRA_LOCATION_NAME");
115+
116+
double latitude = intent.getDoubleExtra("EXTRA_LATITUDE", 0.0);
117+
double longitude = intent.getDoubleExtra("EXTRA_LONGITUDE", 0.0);
118+
destination = new Location("manual");
119+
destination.setLatitude(latitude);
120+
destination.setLongitude(longitude);
121+
lastLocationUpdate = 0;
122+
showNotif();
123+
startGpsLocListener();
124+
isWatchdogRunning = true;
125+
handler.postDelayed(watchdogRunnable, WATCHDOG_INTERVAL_MS);
126+
return START_STICKY;
127+
} else {
128+
stopSelf();
129+
return START_NOT_STICKY;
130+
}
131+
}
132+
133+
private void showNotif() {
134+
updateNotification(null, false);
135+
136+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
137+
startForeground(NOTIF_ID, mNotifBuilder.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
138+
} else {
139+
startForeground(NOTIF_ID, mNotifBuilder.build());
140+
}
141+
142+
}
143+
144+
private void startGpsLocListener() {
145+
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
146+
return;
147+
}
148+
mLocManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, LOCATION_INTERVAL_MS, 0, this);
149+
}
150+
151+
@Override
152+
public void onLocationChanged(@NonNull Location location) {
153+
lastLocationUpdate = System.currentTimeMillis();
154+
long timeToDestination = (arrivalTimeLong - System.currentTimeMillis()) / 1000;
155+
String timeString = (timeToDestination > 60) ? getString(R.string.in_x_minutes, Math.round(timeToDestination / 60.0)) : getString(R.string.seconds, timeToDestination);
156+
long distanceToDestination = (long) destination.distanceTo(location);
157+
if (distanceToDestination > ARRIVAL_THRESHOLD_METERS){
158+
updateNotification(getString(R.string.meter, distanceToDestination) + " / " + timeString , false);
159+
} else {
160+
updateNotification(null, true);
161+
mLocManager.removeUpdates(this);
162+
isWatchdogRunning = false;
163+
handler.postDelayed(this::stopSelf, 30000);
164+
}
165+
}
166+
167+
private void onLocationUpdateTimeout() {
168+
long timeToDestination = (arrivalTimeLong - System.currentTimeMillis()) / 1000;
169+
String timeString = (timeToDestination > 60) ? getString(R.string.in_x_minutes, Math.round(timeToDestination / 60.0)) : getString(R.string.seconds, timeToDestination);
170+
if (timeToDestination > ARRIVAL_THRESHOLD_SEC){
171+
updateNotification( timeString , false);
172+
} else {
173+
updateNotification(null, true);
174+
mLocManager.removeUpdates(this);
175+
isWatchdogRunning = false;
176+
handler.postDelayed(this::stopSelf, 30000);
177+
}
178+
}
179+
180+
private void updateNotification(@Nullable String contentText, boolean hasArrived) {
181+
mNotifBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
182+
.setSilent(!hasArrived)
183+
.setOnlyAlertOnce(!hasArrived) // or adjust as needed
184+
.setSmallIcon(R.drawable.ic_transportr)
185+
.setPriority(hasArrived ? NotificationCompat.PRIORITY_MAX : NotificationCompat.PRIORITY_DEFAULT) //ignored on Android 8+
186+
.setAutoCancel(false)
187+
.setOngoing(true)
188+
.addAction(R.drawable.ic_stop, getString(R.string.action_stop), stopPendingIntent)
189+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
190+
.setContentTitle((hasArrived ? "\ud83c\udfc1 " : "") + destinationName + " " + arrivalTime); //Unicode Character "🏁" (U+1F3C1)
191+
192+
if (contentText != null) {
193+
mNotifBuilder.setContentText(contentText);
194+
}
195+
196+
mNotifManager.notify(NOTIF_ID, mNotifBuilder.build());
197+
}
198+
199+
@Override
200+
public void onDestroy() {
201+
handler.removeCallbacks(watchdogRunnable);
202+
mLocManager.removeUpdates(this);
203+
super.onDestroy();
204+
}
205+
}

app/src/main/java/de/grobox/transportr/locations/LocationGpsView.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class LocationGpsView(context: Context, attrs: AttributeSet) : LocationView(cont
6262

6363
@RequiresPermission(ACCESS_FINE_LOCATION)
6464
fun setSearching() {
65+
ui.gps.visibility = INVISIBLE
6566
if (isSearching) return
6667
isSearching = true
6768

@@ -81,6 +82,7 @@ class LocationGpsView(context: Context, attrs: AttributeSet) : LocationView(cont
8182
}
8283

8384
fun clearSearching() {
85+
ui.gps.visibility = VISIBLE
8486
if (!isSearching) return
8587

8688
ui.status.clearAnimation()

app/src/main/java/de/grobox/transportr/locations/LocationView.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ open class LocationView @JvmOverloads constructor(context: Context, attrs: Attri
142142
val location: AutoCompleteTextView = view.findViewById(R.id.location)
143143
internal val progress: ProgressBar = view.findViewById(R.id.progress)
144144
val clear: ImageButton = view.findViewById(R.id.clearButton)
145+
val gps: ImageButton = view.findViewById(R.id.gpsButton)
145146
}
146147

147148
/* State Saving and Restoring */

0 commit comments

Comments
 (0)