generated from Grigo/AndroidTemplate
added sleepmode
This commit is contained in:
@@ -6,6 +6,12 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -30,6 +36,10 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name=".LoraForegroundService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="location|dataSync" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class CommandPoller {
|
public class CommandPoller {
|
||||||
@@ -32,6 +34,11 @@ public class CommandPoller {
|
|||||||
private final TrackRecorder trackRecorder;
|
private final TrackRecorder trackRecorder;
|
||||||
private final PeerStatsCache peerStatsCache;
|
private final PeerStatsCache peerStatsCache;
|
||||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
|
Thread t = new Thread(r, "CommandPoller");
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
|
||||||
@@ -75,34 +82,28 @@ public class CommandPoller {
|
|||||||
if (!running.compareAndSet(false, true)) {
|
if (!running.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
scheduleCommandPoll();
|
scheduler.scheduleWithFixedDelay(
|
||||||
schedulePairedPoll();
|
this::pollCommandsSafe, 0, COMMAND_POLL_MS, TimeUnit.MILLISECONDS);
|
||||||
|
scheduler.scheduleWithFixedDelay(
|
||||||
|
this::pollPairedSafe, 0, PAIRED_POLL_MS, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
running.set(false);
|
running.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleCommandPoll() {
|
private void pollCommandsSafe() {
|
||||||
executor.execute(() -> {
|
if (!running.get()) {
|
||||||
if (running.get()) {
|
return;
|
||||||
pollCommands();
|
}
|
||||||
}
|
pollCommands();
|
||||||
if (running.get()) {
|
|
||||||
mainHandler.postDelayed(this::scheduleCommandPoll, COMMAND_POLL_MS);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void schedulePairedPoll() {
|
private void pollPairedSafe() {
|
||||||
executor.execute(() -> {
|
if (!running.get()) {
|
||||||
if (running.get()) {
|
return;
|
||||||
pollPairedSession();
|
}
|
||||||
}
|
pollPairedSession();
|
||||||
if (running.get()) {
|
|
||||||
mainHandler.postDelayed(this::schedulePairedPoll, PAIRED_POLL_MS);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pollCommands() {
|
private void pollCommands() {
|
||||||
@@ -181,7 +182,7 @@ public class CommandPoller {
|
|||||||
}
|
}
|
||||||
startedSessionId = session.id;
|
startedSessionId = session.id;
|
||||||
pendingAckSessionId = session.id;
|
pendingAckSessionId = session.id;
|
||||||
mainHandler.post(trackRecorder::start);
|
trackRecorder.start();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w(TAG, "paired poll failed", e);
|
Log.w(TAG, "paired poll failed", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.grigowashere.loratester;
|
|||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
|
||||||
import com.grigowashere.loratester.api.ServerApi;
|
import com.grigowashere.loratester.api.ServerApi;
|
||||||
|
import com.grigowashere.loratester.location.LocationTracker;
|
||||||
import com.grigowashere.loratester.net.NetworkMonitor;
|
import com.grigowashere.loratester.net.NetworkMonitor;
|
||||||
import com.grigowashere.loratester.track.TrackRecorder;
|
import com.grigowashere.loratester.track.TrackRecorder;
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ public class LoraApp extends Application {
|
|||||||
private NetworkMonitor networkMonitor;
|
private NetworkMonitor networkMonitor;
|
||||||
private PeerStatsCache peerStatsCache;
|
private PeerStatsCache peerStatsCache;
|
||||||
private CommandPoller commandPoller;
|
private CommandPoller commandPoller;
|
||||||
|
private LocationTracker locationTracker;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
@@ -76,6 +78,22 @@ public class LoraApp extends Application {
|
|||||||
return commandPoller;
|
return commandPoller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void startLocationUpdates() {
|
||||||
|
if (locationTracker == null) {
|
||||||
|
locationTracker = new LocationTracker(this, (lat, lon, alt) -> {
|
||||||
|
telemetryUploader.updateLocation(lat, lon);
|
||||||
|
trackRecorder.updateLocation(lat, lon, alt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
locationTracker.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void stopLocationUpdates() {
|
||||||
|
if (locationTracker != null) {
|
||||||
|
locationTracker.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void refreshTrackRecorder() {
|
public void refreshTrackRecorder() {
|
||||||
if (commandPoller != null) {
|
if (commandPoller != null) {
|
||||||
commandPoller.stop();
|
commandPoller.stop();
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package com.grigowashere.loratester;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.PowerManager;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
|
import com.grigowashere.loratester.track.TrackRecorder;
|
||||||
|
|
||||||
|
public class LoraForegroundService extends Service {
|
||||||
|
|
||||||
|
private static final String CHANNEL_ID = "lora_background";
|
||||||
|
private static final int NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
private PowerManager.WakeLock wakeLock;
|
||||||
|
private LoraApp app;
|
||||||
|
|
||||||
|
private final Runnable notificationTicker = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateNotification();
|
||||||
|
handler.postDelayed(this, 5000L);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void ensureRunning(Context context) {
|
||||||
|
Context appContext = context.getApplicationContext();
|
||||||
|
Intent intent = new Intent(appContext, LoraForegroundService.class);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
appContext.startForegroundService(intent);
|
||||||
|
} else {
|
||||||
|
appContext.startService(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
app = (LoraApp) getApplication();
|
||||||
|
createNotificationChannel();
|
||||||
|
acquireWakeLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
Notification notification = buildNotification();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
notification,
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
||||||
|
| ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, notification);
|
||||||
|
}
|
||||||
|
app.startLocationUpdates();
|
||||||
|
handler.removeCallbacks(notificationTicker);
|
||||||
|
handler.post(notificationTicker);
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
handler.removeCallbacks(notificationTicker);
|
||||||
|
releaseWakeLock();
|
||||||
|
app.stopLocationUpdates();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTaskRemoved(Intent rootIntent) {
|
||||||
|
TelemetryUploader uploader = app.getTelemetryUploader();
|
||||||
|
if (uploader != null) {
|
||||||
|
uploader.stopTelnet();
|
||||||
|
}
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||||
|
stopSelf();
|
||||||
|
super.onTaskRemoved(rootIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acquireWakeLock() {
|
||||||
|
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||||
|
if (pm == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "LoraTester::Background");
|
||||||
|
wakeLock.setReferenceCounted(false);
|
||||||
|
wakeLock.acquire();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseWakeLock() {
|
||||||
|
if (wakeLock != null && wakeLock.isHeld()) {
|
||||||
|
wakeLock.release();
|
||||||
|
}
|
||||||
|
wakeLock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
getString(R.string.notification_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
);
|
||||||
|
channel.setDescription(getString(R.string.notification_channel_desc));
|
||||||
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||||
|
if (nm != null) {
|
||||||
|
nm.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Notification buildNotification() {
|
||||||
|
Intent open = new Intent(this, MainActivity.class);
|
||||||
|
open.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
|
PendingIntent pending = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
open,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||||
|
);
|
||||||
|
|
||||||
|
TelemetryUploader uploader = app.getTelemetryUploader();
|
||||||
|
TrackRecorder recorder = app.getTrackRecorder();
|
||||||
|
SettingsRepository settings = app.getSettingsRepository();
|
||||||
|
|
||||||
|
boolean telnetOn = settings.isTelnetEnabled();
|
||||||
|
boolean telnetConnected = uploader != null && uploader.isTelnetConnected();
|
||||||
|
boolean recording = recorder != null && recorder.isRecording();
|
||||||
|
int points = recorder != null ? recorder.getPointCount() : 0;
|
||||||
|
|
||||||
|
String telnetLine = telnetOn
|
||||||
|
? getString(telnetConnected ? R.string.telnet_connected : R.string.telnet_disconnected)
|
||||||
|
: getString(R.string.telnet_disabled_short);
|
||||||
|
String trackLine = recording
|
||||||
|
? getString(R.string.notification_track_recording, points)
|
||||||
|
: getString(R.string.notification_track_idle);
|
||||||
|
|
||||||
|
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_service)
|
||||||
|
.setContentTitle(getString(R.string.notification_title))
|
||||||
|
.setContentText(telnetLine + " · " + trackLine)
|
||||||
|
.setSubText(getString(R.string.notification_subtitle))
|
||||||
|
.setContentIntent(pending)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNotification() {
|
||||||
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||||
|
if (nm != null) {
|
||||||
|
nm.notify(NOTIFICATION_ID, buildNotification());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
package com.grigowashere.loratester;
|
package com.grigowashere.loratester;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.PowerManager;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.EdgeToEdge;
|
import androidx.activity.EdgeToEdge;
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
@@ -17,19 +23,30 @@ import androidx.viewpager2.widget.ViewPager2;
|
|||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import com.google.android.material.tabs.TabLayoutMediator;
|
import com.google.android.material.tabs.TabLayoutMediator;
|
||||||
import com.grigowashere.loratester.location.LocationTracker;
|
|
||||||
import com.grigowashere.loratester.track.TrackRecorder;
|
|
||||||
import com.grigowashere.loratester.ui.MainPagerAdapter;
|
import com.grigowashere.loratester.ui.MainPagerAdapter;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private TelemetryUploader telemetryUploader;
|
private LoraApp app;
|
||||||
private LocationTracker locationTracker;
|
private SettingsRepository settings;
|
||||||
|
private boolean backgroundLocationRequested;
|
||||||
|
|
||||||
private final ActivityResultLauncher<String[]> locationPermissionLauncher =
|
private final ActivityResultLauncher<String[]> locationPermissionLauncher =
|
||||||
registerForActivityResult(
|
registerForActivityResult(
|
||||||
new ActivityResultContracts.RequestMultiplePermissions(),
|
new ActivityResultContracts.RequestMultiplePermissions(),
|
||||||
result -> startLocationIfPermitted()
|
result -> onForegroundLocationReady()
|
||||||
|
);
|
||||||
|
|
||||||
|
private final ActivityResultLauncher<String> backgroundLocationLauncher =
|
||||||
|
registerForActivityResult(
|
||||||
|
new ActivityResultContracts.RequestPermission(),
|
||||||
|
granted -> startBackgroundWork()
|
||||||
|
);
|
||||||
|
|
||||||
|
private final ActivityResultLauncher<String> notificationPermissionLauncher =
|
||||||
|
registerForActivityResult(
|
||||||
|
new ActivityResultContracts.RequestPermission(),
|
||||||
|
granted -> startBackgroundWork()
|
||||||
);
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -43,9 +60,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
return insets;
|
return insets;
|
||||||
});
|
});
|
||||||
|
|
||||||
LoraApp app = (LoraApp) getApplication();
|
app = (LoraApp) getApplication();
|
||||||
telemetryUploader = app.getTelemetryUploader();
|
settings = app.getSettingsRepository();
|
||||||
SettingsRepository settings = app.getSettingsRepository();
|
|
||||||
|
|
||||||
ViewPager2 pager = findViewById(R.id.viewPager);
|
ViewPager2 pager = findViewById(R.id.viewPager);
|
||||||
TabLayout tabs = findViewById(R.id.tabLayout);
|
TabLayout tabs = findViewById(R.id.tabLayout);
|
||||||
@@ -62,41 +78,92 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
tab.setText(titleRes);
|
tab.setText(titleRes);
|
||||||
}).attach();
|
}).attach();
|
||||||
|
|
||||||
TrackRecorder trackRecorder = app.getTrackRecorder();
|
requestStartupPermissions();
|
||||||
locationTracker = new LocationTracker(this, (lat, lon, alt) -> {
|
|
||||||
telemetryUploader.updateLocation(lat, lon);
|
|
||||||
trackRecorder.updateLocation(lat, lon, alt);
|
|
||||||
});
|
|
||||||
|
|
||||||
requestLocationPermission();
|
|
||||||
if (settings.isTelnetEnabled()) {
|
if (settings.isTelnetEnabled()) {
|
||||||
telemetryUploader.startTelnet();
|
app.getTelemetryUploader().startTelnet();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestLocationPermission() {
|
private void requestStartupPermissions() {
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
if (hasForegroundLocation()) {
|
||||||
|
onForegroundLocationReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
locationPermissionLauncher.launch(new String[]{
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onForegroundLocationReady() {
|
||||||
|
if (!hasForegroundLocation()) {
|
||||||
|
Toast.makeText(this, R.string.background_location_required, Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestNotificationPermissionIfNeeded();
|
||||||
|
requestBackgroundLocationIfNeeded();
|
||||||
|
startBackgroundWork();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestNotificationPermissionIfNeeded() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
== PackageManager.PERMISSION_GRANTED) {
|
== PackageManager.PERMISSION_GRANTED) {
|
||||||
startLocationIfPermitted();
|
return;
|
||||||
} else {
|
}
|
||||||
locationPermissionLauncher.launch(new String[]{
|
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
}
|
||||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
||||||
});
|
private void requestBackgroundLocationIfNeeded() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasBackgroundLocation()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (backgroundLocationRequested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
backgroundLocationRequested = true;
|
||||||
|
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) {
|
||||||
|
Toast.makeText(this, R.string.background_location_rationale, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
backgroundLocationLauncher.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startBackgroundWork() {
|
||||||
|
LoraForegroundService.ensureRunning(this);
|
||||||
|
if (settings.isTelnetEnabled()) {
|
||||||
|
app.getTelemetryUploader().startTelnet();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startLocationIfPermitted() {
|
private boolean hasForegroundLocation() {
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
== PackageManager.PERMISSION_GRANTED) {
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
locationTracker.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean hasBackgroundLocation() {
|
||||||
protected void onDestroy() {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
locationTracker.stop();
|
return hasForegroundLocation();
|
||||||
telemetryUploader.stopTelnet();
|
}
|
||||||
super.onDestroy();
|
return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openBatteryOptimizationSettings(@NonNull android.content.Context context) {
|
||||||
|
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||||
|
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isIgnoringBatteryOptimizations(@NonNull android.content.Context context) {
|
||||||
|
PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE);
|
||||||
|
if (pm == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return pm.isIgnoringBatteryOptimizations(context.getPackageName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ public class LocationTracker {
|
|||||||
}
|
}
|
||||||
LocationRequest request = new LocationRequest.Builder(
|
LocationRequest request = new LocationRequest.Builder(
|
||||||
Priority.PRIORITY_HIGH_ACCURACY, 10_000L
|
Priority.PRIORITY_HIGH_ACCURACY, 10_000L
|
||||||
).setMinUpdateIntervalMillis(5_000L).build();
|
)
|
||||||
|
.setMinUpdateIntervalMillis(5_000L)
|
||||||
|
.setMaxUpdateDelayMillis(15_000L)
|
||||||
|
.setWaitForAccurateLocation(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
callback = new LocationCallback() {
|
callback = new LocationCallback() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import androidx.fragment.app.Fragment;
|
|||||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||||
import com.google.android.material.textfield.TextInputEditText;
|
import com.google.android.material.textfield.TextInputEditText;
|
||||||
import com.grigowashere.loratester.LoraApp;
|
import com.grigowashere.loratester.LoraApp;
|
||||||
|
import com.grigowashere.loratester.LoraForegroundService;
|
||||||
|
import com.grigowashere.loratester.MainActivity;
|
||||||
import com.grigowashere.loratester.R;
|
import com.grigowashere.loratester.R;
|
||||||
import com.grigowashere.loratester.SettingsRepository;
|
import com.grigowashere.loratester.SettingsRepository;
|
||||||
import com.grigowashere.loratester.TelemetryUploader;
|
import com.grigowashere.loratester.TelemetryUploader;
|
||||||
@@ -44,6 +46,7 @@ public class SettingsFragment extends Fragment {
|
|||||||
TextInputEditText editRange = view.findViewById(R.id.editRangeRegex);
|
TextInputEditText editRange = view.findViewById(R.id.editRangeRegex);
|
||||||
TextInputEditText editDeviceLabel = view.findViewById(R.id.editDeviceLabel);
|
TextInputEditText editDeviceLabel = view.findViewById(R.id.editDeviceLabel);
|
||||||
SwitchMaterial switchTelnet = view.findViewById(R.id.switchTelnet);
|
SwitchMaterial switchTelnet = view.findViewById(R.id.switchTelnet);
|
||||||
|
Button batteryBtn = view.findViewById(R.id.btnBatteryOptimization);
|
||||||
TextView deviceIdLabel = view.findViewById(R.id.deviceIdLabel);
|
TextView deviceIdLabel = view.findViewById(R.id.deviceIdLabel);
|
||||||
Button save = view.findViewById(R.id.btnSaveSettings);
|
Button save = view.findViewById(R.id.btnSaveSettings);
|
||||||
|
|
||||||
@@ -59,6 +62,14 @@ public class SettingsFragment extends Fragment {
|
|||||||
switchTelnet.setChecked(settings.isTelnetEnabled());
|
switchTelnet.setChecked(settings.isTelnetEnabled());
|
||||||
deviceIdLabel.setText(getString(R.string.device_id_label, settings.getOrCreateDeviceId()));
|
deviceIdLabel.setText(getString(R.string.device_id_label, settings.getOrCreateDeviceId()));
|
||||||
|
|
||||||
|
batteryBtn.setOnClickListener(v -> {
|
||||||
|
if (MainActivity.isIgnoringBatteryOptimizations(requireContext())) {
|
||||||
|
Toast.makeText(requireContext(), R.string.battery_optimization_done, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MainActivity.openBatteryOptimizationSettings(requireContext());
|
||||||
|
});
|
||||||
|
|
||||||
save.setOnClickListener(v -> {
|
save.setOnClickListener(v -> {
|
||||||
settings.setServerUrl(textOf(editServer, SettingsRepository.DEFAULT_SERVER));
|
settings.setServerUrl(textOf(editServer, SettingsRepository.DEFAULT_SERVER));
|
||||||
settings.setTelnetHost(textOf(editHost, SettingsRepository.DEFAULT_TELNET_HOST));
|
settings.setTelnetHost(textOf(editHost, SettingsRepository.DEFAULT_TELNET_HOST));
|
||||||
@@ -78,6 +89,7 @@ public class SettingsFragment extends Fragment {
|
|||||||
} else {
|
} else {
|
||||||
uploader.stopTelnet();
|
uploader.stopTelnet();
|
||||||
}
|
}
|
||||||
|
LoraForegroundService.ensureRunning(requireContext());
|
||||||
Toast.makeText(requireContext(), R.string.saved, Toast.LENGTH_SHORT).show();
|
Toast.makeText(requireContext(), R.string.saved, Toast.LENGTH_SHORT).show();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" />
|
||||||
|
</vector>
|
||||||
@@ -93,6 +93,20 @@
|
|||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="@string/telnet_enabled" />
|
android:text="@string/telnet_enabled" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:text="@string/battery_optimization_hint"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnBatteryOptimization"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/battery_optimization" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/deviceIdLabel"
|
android:id="@+id/deviceIdLabel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -113,4 +113,16 @@
|
|||||||
<string name="map_heatmap_legend_level">уровень</string>
|
<string name="map_heatmap_legend_level">уровень</string>
|
||||||
<string name="map_heatmap_legend_low">низина</string>
|
<string name="map_heatmap_legend_low">низина</string>
|
||||||
<string name="chat_self_label">Вы</string>
|
<string name="chat_self_label">Вы</string>
|
||||||
|
<string name="notification_channel_name">Фоновая работа</string>
|
||||||
|
<string name="notification_channel_desc">Telnet, GPS и запись трека при свёрнутом приложении</string>
|
||||||
|
<string name="notification_title">LoraTester активен</string>
|
||||||
|
<string name="notification_subtitle">Работа в фоне</string>
|
||||||
|
<string name="notification_track_recording">трек: %1$d точек</string>
|
||||||
|
<string name="notification_track_idle">трек: нет</string>
|
||||||
|
<string name="telnet_disabled_short">telnet: выкл</string>
|
||||||
|
<string name="background_location_rationale">Для GPS при выключенном экране разрешите геолокацию «Всегда»</string>
|
||||||
|
<string name="background_location_required">Нужен доступ к геолокации для трека и карты</string>
|
||||||
|
<string name="battery_optimization">Без ограничений батареи</string>
|
||||||
|
<string name="battery_optimization_hint">Рекомендуется на MIUI / ColorOS / EMUI для стабильного telnet</string>
|
||||||
|
<string name="battery_optimization_done">Ограничения батареи уже отключены</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user