package nodomain.freeyourgadget.gadgetbridge.womp; import android.Manifest; 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.IntentFilter; 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.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.json.JSONObject; import java.io.Serializable; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.util.PendingIntentUtils; /** * 前台保活:通知展示手环心率/步数,后台静默接收手环数据并定时上报位置。 */ public class WompKeepAliveService extends Service implements LocationListener { private final Handler handler = new Handler(Looper.getMainLooper()); private Location lastLocation; private int lastHeartRate; private int lastSteps; private long lastNotifyAt; private long lastAbnormalAt; private boolean locating; private final android.content.BroadcastReceiver sampleReceiver = new android.content.BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (!DeviceService.ACTION_REALTIME_SAMPLES.equals(intent.getAction())) { return; } Serializable extra = intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE); if (extra instanceof ActivitySample) { ActivitySample sample = (ActivitySample) extra; boolean changed = false; if (sample.getHeartRate() > 0 && sample.getHeartRate() != lastHeartRate) { lastHeartRate = sample.getHeartRate(); changed = true; maybeReportAbnormal(sample.getHeartRate()); } if (sample.getSteps() > 0) { if (sample.getSteps() >= lastSteps || sample.getSteps() > 200) { lastSteps = sample.getSteps(); } else { lastSteps += sample.getSteps(); } changed = true; } if (changed) { refreshNotification(false); } } } }; public static void start(Context context) { Intent i = new Intent(context, WompKeepAliveService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(i); } else { context.startService(i); } } public static void stop(Context context) { context.stopService(new Intent(context, WompKeepAliveService.class)); } public static void onAttendanceChanged(Context context) { start(context); } @Override public void onCreate() { super.onCreate(); createChannel(); Notification notification = buildNotification(); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground(WompConfig.NOTIFY_KEEPALIVE, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION); } else { startForeground(WompConfig.NOTIFY_KEEPALIVE, notification); } } catch (Exception e) { startForeground(WompConfig.NOTIFY_KEEPALIVE, notification); } IntentFilter filter = new IntentFilter(DeviceService.ACTION_REALTIME_SAMPLES); LocalBroadcastManager.getInstance(this).registerReceiver(sampleReceiver, filter); syncLocationTracking(); handler.post(gpsTask); handler.postDelayed(healthTask, 8_000); handler.postDelayed(notifyTask, 15_000); } private final Runnable gpsTask = new Runnable() { @Override public void run() { syncLocationTracking(); if (WompSession.isCheckedIn(WompKeepAliveService.this)) { reportGps(); } handler.postDelayed(this, WompConfig.GPS_INTERVAL_MS); } }; private final Runnable healthTask = new Runnable() { @Override public void run() { if (WompSession.isCheckedIn(WompKeepAliveService.this)) { reportHealth("周期"); } handler.postDelayed(this, WompConfig.HEALTH_INTERVAL_MS); } }; private final Runnable notifyTask = new Runnable() { @Override public void run() { refreshNotification(true); handler.postDelayed(this, 20_000); } }; private void syncLocationTracking() { if (WompSession.isCheckedIn(this)) { startLocation(); } else { stopLocation(); } } private void startLocation() { if (locating) { return; } if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return; } boolean started = false; try { lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, WompConfig.GPS_INTERVAL_MS, 5, this, Looper.getMainLooper()); started = true; } catch (Exception ignored) { } try { lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, WompConfig.GPS_INTERVAL_MS, 5, this, Looper.getMainLooper()); started = true; } catch (Exception ignored) { } try { lastLocation = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); if (lastLocation == null) { lastLocation = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER); } WompLocation.remember(lastLocation); } catch (Exception ignored) { } locating = started; } private void stopLocation() { if (!locating) { return; } LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE); try { lm.removeUpdates(this); } catch (Exception ignored) { } locating = false; } private void reportGps() { if (!WompSession.isLoggedIn(this) || !WompSession.isCheckedIn(this) || lastLocation == null) { return; } try { JSONObject body = new JSONObject(); body.put("sorterId", WompSession.getSorterId(this)); body.put("longitude", lastLocation.getLongitude()); body.put("latitude", lastLocation.getLatitude()); body.put("height", lastLocation.hasAltitude() ? lastLocation.getAltitude() : 0); body.put("speed", lastLocation.hasSpeed() ? lastLocation.getSpeed() : 0); body.put("accuracy", lastLocation.hasAccuracy() ? lastLocation.getAccuracy() : 0); WompApi.post("/app/push/sorter/gps", body, null); } catch (Exception ignored) { } } private void reportHealth(String dataType) { if (!WompSession.isLoggedIn(this)) { return; } if (lastHeartRate <= 0 && lastSteps <= 0) { return; } try { JSONObject body = new JSONObject(); body.put("sorterId", WompSession.getSorterId(this)); if (lastHeartRate > 0) { body.put("heartRate", lastHeartRate); } body.put("stepCount", lastSteps); body.put("dataType", dataType); WompApi.post("/app/push/sorter/health", body, null); } catch (Exception ignored) { } } private void maybeReportAbnormal(int heartRate) { if (heartRate < WompConfig.HR_ABNORMAL_LOW || heartRate > WompConfig.HR_ABNORMAL_HIGH) { long now = System.currentTimeMillis(); if (now - lastAbnormalAt < WompConfig.ABNORMAL_INTERVAL_MS) { return; } lastAbnormalAt = now; reportHealth("异常"); } } @Override public void onLocationChanged(Location location) { lastLocation = location; WompLocation.remember(location); } @Override public void onStatusChanged(String provider, int status, Bundle extras) {} @Override public void onProviderEnabled(String provider) {} @Override public void onProviderDisabled(String provider) {} @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { syncLocationTracking(); return START_STICKY; } @Override public void onDestroy() { handler.removeCallbacksAndMessages(null); LocalBroadcastManager.getInstance(this).unregisterReceiver(sampleReceiver); locating = true; stopLocation(); super.onDestroy(); } private void createChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel ch = new NotificationChannel( WompConfig.CHANNEL_KEEPALIVE, "健康手环", NotificationManager.IMPORTANCE_LOW); ch.setDescription("实时显示手环心率和步数"); ch.setShowBadge(false); ch.enableLights(false); ch.enableVibration(false); ch.setSound(null, null); NotificationManager nm = getSystemService(NotificationManager.class); nm.createNotificationChannel(ch); } } private void refreshNotification(boolean force) { long now = System.currentTimeMillis(); if (!force && now - lastNotifyAt < 1500) { return; } lastNotifyAt = now; NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); nm.notify(WompConfig.NOTIFY_KEEPALIVE, buildNotification()); } private Notification buildNotification() { Intent open = new Intent(this, ControlCenterv2.class); PendingIntent pi = PendingIntentUtils.getActivity(this, 0, open, 0, false); String hr = lastHeartRate > 0 ? (lastHeartRate + " 次/分") : "--"; String steps = lastSteps > 0 ? String.valueOf(lastSteps) : "--"; String text = "心率 " + hr + " 步数 " + steps; NotificationCompat.Builder builder = new NotificationCompat.Builder(this, WompConfig.CHANNEL_KEEPALIVE) .setSmallIcon(R.drawable.ic_heart) .setContentTitle("健康手环") .setContentText(text) .setStyle(new NotificationCompat.BigTextStyle().bigText(text)) .setOngoing(true) .setOnlyAlertOnce(true) .setShowWhen(false) .setSilent(true) .setContentIntent(pi) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE); } return builder.build(); } }