Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .clabot
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"Jean-BaptisteC",
"Bnyro",
"mesinger",
"newhinton"
"newhinton",
"woheller69"
],
"label": "cla-signed ✔️",
"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"
Expand Down
6 changes: 3 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ apply from: 'witness.gradle'
android {

defaultConfig {
versionCode 127
versionName "2.2.3"
versionCode 128
versionName "2.2.4"

applicationId "de.grobox.liberario"
minSdkVersion 21
Expand Down Expand Up @@ -142,7 +142,7 @@ dependencies {
exclude module: 'failureaccess'
exclude group: 'com.google.j2objc'
}
implementation('com.gitlab.opentransitmap:public-transport-enabler:7f29ab5fc5a02f71e044c5623117741e36e73d3f') {
implementation('com.gitlab.opentransitmap:public-transport-enabler:2713727d07df8408e064e5ffae2733862711a10b') {
exclude group: 'com.google.guava' // included above
exclude group: 'org.json', module: 'json' // provided by Android
exclude group: 'net.sf.kxml', module: 'kxml2' // provided by Android
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<uses-feature
android:name="android.hardware.WIFI"
Expand Down Expand Up @@ -95,6 +99,11 @@
android:name=".about.ContributorsActivity"
android:label="@string/drawer_contributors"/>

<service
android:exported="false"
android:foregroundServiceType="location"
android:name=".AlertService" />

<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
<!-- Version >= 3.0. DeX Dual Mode support -->
Expand Down
205 changes: 205 additions & 0 deletions app/src/main/java/de/grobox/transportr/AlertService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Transportr
*
* Copyright (c) 2013 - 2025 Torsten Grote
*
* This program is Free Software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package de.grobox.transportr;

import android.Manifest;
import android.app.PendingIntent;
import android.app.Service;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_MAX;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.NotificationCompat.Builder;

public class AlertService extends Service implements LocationListener {
private LocationManager mLocManager = null;
private boolean isWatchdogRunning = false;
private long lastLocationUpdate = 0;
private NotificationManagerCompat mNotifManager = null;
private static final int NOTIF_ID = 111;
private static final String CHANNEL_ID = "alert";
private Builder mNotifBuilder;
private String destinationName;
private String arrivalTime;
private long arrivalTimeLong;
private Location destination;
private PendingIntent stopPendingIntent;
private static final long ARRIVAL_THRESHOLD_METERS = 150;
private static final long ARRIVAL_THRESHOLD_SEC = 30;
private static final long WATCHDOG_INTERVAL_MS = 1_000; // 1 second
private static final long LOCATION_INTERVAL_MS = 2_000; // 2 seconds
private static final long LOCATION_TIMEOUT_MS = 10_000; // 10 seconds
private static final String ACTION_STOP = "STOP";
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable watchdogRunnable = new Runnable() {
@Override
public void run() {
long now = System.currentTimeMillis();
if (now - lastLocationUpdate > LOCATION_TIMEOUT_MS) {
// No location update in the last 10 seconds → take action
onLocationUpdateTimeout();
}
// Reschedule the check
if (isWatchdogRunning) handler.postDelayed(this, WATCHDOG_INTERVAL_MS);
}
};

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();
mLocManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
mNotifManager = NotificationManagerCompat.from(getApplicationContext());
NotificationChannelCompat notifChannel = new NotificationChannelCompat.Builder(CHANNEL_ID, IMPORTANCE_MAX).setName(getResources().getString(R.string.action_alert)).setVibrationEnabled(true).build();
mNotifManager.createNotificationChannel(notifChannel);

Intent stopIntent = new Intent(this, AlertService.class);
stopIntent.setAction(ACTION_STOP);
stopPendingIntent = PendingIntent.getService(
this,
0,
stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
// Service restarted by system, but we lack destination data → stop.
stopSelf();
return START_NOT_STICKY;
}
if (!ACTION_STOP.equals(intent.getAction())) {
handler.removeCallbacksAndMessages(null);
arrivalTime = intent.getStringExtra("EXTRA_TIME_STR");
arrivalTimeLong = intent.getLongExtra("EXTRA_TIME_LONG",0L);
destinationName = intent.getStringExtra("EXTRA_LOCATION_NAME");

double latitude = intent.getDoubleExtra("EXTRA_LATITUDE", 0.0);
double longitude = intent.getDoubleExtra("EXTRA_LONGITUDE", 0.0);
destination = new Location("manual");
destination.setLatitude(latitude);
destination.setLongitude(longitude);
lastLocationUpdate = 0;
showNotif();
startGpsLocListener();
isWatchdogRunning = true;
handler.postDelayed(watchdogRunnable, WATCHDOG_INTERVAL_MS);
return START_STICKY;
} else {
stopSelf();
return START_NOT_STICKY;
}
}

private void showNotif() {
updateNotification(null, false);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, mNotifBuilder.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
} else {
startForeground(NOTIF_ID, mNotifBuilder.build());
}

}

private void startGpsLocListener() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return;
}
mLocManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, LOCATION_INTERVAL_MS, 0, this);
}

@Override
public void onLocationChanged(@NonNull Location location) {
lastLocationUpdate = System.currentTimeMillis();
long timeToDestination = (arrivalTimeLong - System.currentTimeMillis()) / 1000;
String timeString = (timeToDestination > 60) ? getString(R.string.in_x_minutes, Math.round(timeToDestination / 60.0)) : getString(R.string.seconds, timeToDestination);
long distanceToDestination = (long) destination.distanceTo(location);
if (distanceToDestination > ARRIVAL_THRESHOLD_METERS){
updateNotification(getString(R.string.meter, distanceToDestination) + " / " + timeString , false);
} else {
updateNotification(null, true);
mLocManager.removeUpdates(this);
isWatchdogRunning = false;
handler.postDelayed(this::stopSelf, 30000);
}
}

private void onLocationUpdateTimeout() {
long timeToDestination = (arrivalTimeLong - System.currentTimeMillis()) / 1000;
String timeString = (timeToDestination > 60) ? getString(R.string.in_x_minutes, Math.round(timeToDestination / 60.0)) : getString(R.string.seconds, timeToDestination);
if (timeToDestination > ARRIVAL_THRESHOLD_SEC){
updateNotification( timeString , false);
} else {
updateNotification(null, true);
mLocManager.removeUpdates(this);
isWatchdogRunning = false;
handler.postDelayed(this::stopSelf, 30000);
}
}

private void updateNotification(@Nullable String contentText, boolean hasArrived) {
mNotifBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSilent(!hasArrived)
.setOnlyAlertOnce(!hasArrived) // or adjust as needed
.setSmallIcon(R.drawable.ic_transportr)
.setPriority(hasArrived ? NotificationCompat.PRIORITY_MAX : NotificationCompat.PRIORITY_DEFAULT) //ignored on Android 8+
.setAutoCancel(false)
.setOngoing(true)
.addAction(R.drawable.ic_stop, getString(R.string.action_stop), stopPendingIntent)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle((hasArrived ? "\ud83c\udfc1 " : "") + destinationName + " " + arrivalTime); //Unicode Character "🏁" (U+1F3C1)

if (contentText != null) {
mNotifBuilder.setContentText(contentText);
}

mNotifManager.notify(NOTIF_ID, mNotifBuilder.build());
}

@Override
public void onDestroy() {
handler.removeCallbacks(watchdogRunnable);
mLocManager.removeUpdates(this);
super.onDestroy();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class LocationGpsView(context: Context, attrs: AttributeSet) : LocationView(cont

@RequiresPermission(ACCESS_FINE_LOCATION)
fun setSearching() {
ui.gps.visibility = INVISIBLE
if (isSearching) return
isSearching = true

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

fun clearSearching() {
ui.gps.visibility = VISIBLE
if (!isSearching) return

ui.status.clearAnimation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ open class LocationView @JvmOverloads constructor(context: Context, attrs: Attri
val location: AutoCompleteTextView = view.findViewById(R.id.location)
internal val progress: ProgressBar = view.findViewById(R.id.progress)
val clear: ImageButton = view.findViewById(R.id.clearButton)
val gps: ImageButton = view.findViewById(R.id.gpsButton)
}

/* State Saving and Restoring */
Expand Down
Loading
Loading