Skip to content
Open
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
8 changes: 0 additions & 8 deletions .idea/modules.xml

This file was deleted.

4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ android {

targetSdk = 35

versionCode = 417
versionName = '2.6.20'
versionCode = 418
versionName = '2.6.21'

multiDexEnabled = true

Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/com/prey/PreyConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -1673,6 +1673,20 @@ public void setDailyLocation(String dailyLocation){
saveString(PreyConfig.DAILY_LOCATION, dailyLocation);
}

public static final String LOCATION_SCHEDULE = "LOCATION_SCHEDULE";

/**
* @return the raw {@code location_schedule} JSON from status.json, or "" when unset.
*/
public String getLocationSchedule(){
return getString(PreyConfig.LOCATION_SCHEDULE, "");
}

public void setLocationSchedule(String locationSchedule){
PreyLogger.d(String.format("DAILY setLocationSchedule [%s]", locationSchedule));
saveString(PreyConfig.LOCATION_SCHEDULE, locationSchedule);
}

public static final String MINUTES_TO_QUERY_SERVER = "MINUTES_TO_QUERY_SERVER";

public int getMinutesToQueryServer() {
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/com/prey/PreyStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ public void initConfig(Context ctx){
}catch(Exception e){
aware = false;
}
try {
JSONObject jsnobjectLocal = jsnobjectSettings.getJSONObject("local");
if (jsnobjectLocal.has("location_schedule")
&& !jsnobjectLocal.isNull("location_schedule")) {
String locationSchedule = jsnobjectLocal.getJSONObject("location_schedule").toString();
PreyConfig.getPreyConfig(ctx).setLocationSchedule(locationSchedule);
} else {
PreyConfig.getPreyConfig(ctx).setLocationSchedule("");
}
}catch(Exception e){
PreyConfig.getPreyConfig(ctx).setLocationSchedule("");
}
try {
JSONObject jsnobjectGlobal = jsnobjectSettings.getJSONObject("global");
autoconnect = jsnobjectGlobal.getBoolean("auto_connect");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public void onReceive(Context context, Intent intent) {
PreyLogger.d("DAILY______________________________");
PreyLogger.d("DAILY----------AlarmLocationReceiver onReceive");
final Context ctx = context;
// Re-arm the next check first so a failure below never breaks the schedule.
LocationScheduled.getInstance().scheduleNext(ctx);
new Thread() {
public void run() {
new DailyLocation().run(ctx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import com.prey.PreyLogger;
import com.prey.PreyPhone;
import com.prey.actions.location.LocationUpdatesService;
import com.prey.actions.location.LocationUtil;
import com.prey.actions.location.PreyLocation;
import com.prey.actions.location.PreyLocationManager;
import com.prey.net.PreyHttpResponse;
Expand All @@ -22,53 +21,84 @@
import org.json.JSONObject;

import java.net.HttpURLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class DailyLocation {

/** Maximum attempts to obtain a fix (widens the acquisition window). */
private static final int MAXIMUM_OF_ATTEMPTS = 6;

/** Seconds to wait before reading each attempt. */
private static final int[] SLEEP_OF_ATTEMPTS = new int[]{2, 2, 3, 3, 4, 4};

/** Accuracy (meters) considered good enough to stop early. */
private static final float GOOD_ACCURACY_METERS = 50f;

/**
* @return a UTC {@code yyyy-MM-dd} formatter so the "already sent today" boundary is
* evaluated in UTC rather than the device-local timezone.
*/
private static SimpleDateFormat utcDayFormat() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
return sdf;
}

/**
* Method checks if it should send a location
*
* @param context
*/
public void run(Context context) {
Date now = new Date();
String dailyLocation = PreyConfig.getPreyConfig(context).getDailyLocation();
String nowDailyLocation = PreyConfig.FORMAT_SDF_AWARE.format(new Date());
String nowDailyLocation = utcDayFormat().format(now);
boolean isAirplaneModeOn = PreyPhone.isAirplaneModeOn(context);
PreyLogger.d(String.format("DailyLocation run isAirplaneModeOn:%s", isAirplaneModeOn));
if (!nowDailyLocation.equals(dailyLocation) && !isAirplaneModeOn) {
String schedule = PreyConfig.getPreyConfig(context).getLocationSchedule();
boolean withinWindow = LocationScheduleWindow.isWithinAllowedWindow(schedule, now);
PreyLogger.d(String.format("DailyLocation run isAirplaneModeOn:%s withinWindow:%s", isAirplaneModeOn, withinWindow));
if (nowDailyLocation.equals(dailyLocation)) {
PreyLogger.d("DAILY location already sent");
return;
}
if (isAirplaneModeOn || !withinWindow) {
PreyLogger.d("DAILY skipped: airplane mode or outside allowed window");
return;
}
try {
PreyLocationManager.getInstance(context).setLastLocation(null);
try {
PreyLocationManager.getInstance(context).setLastLocation(null);
new LocationUpdatesService().startForegroundService(context);
PreyLocation preyLocation = null;
int i = 0;
while (i < LocationUtil.MAXIMUM_OF_ATTEMPTS) {
PreyLogger.d(String.format("DAILY getPreyLocationApp[%s]", i));
try {
Thread.sleep(LocationUtil.SLEEP_OF_ATTEMPTS[i] * 1000);
} catch (InterruptedException e) {
PreyLogger.e(String.format("DAILY error :%s", e.getMessage()), e);
}
preyLocation = PreyLocationManager.getInstance(context).getLastLocation();
if (preyLocation != null) {
preyLocation.setMethod("native");
} else {
PreyLogger.d(String.format("DAILY null[%s]", i));
}
if (preyLocation != null && preyLocation.getLat() != 0 && preyLocation.getLng() != 0) {
break;
}
i++;
new LocationUpdatesService().startForegroundService(context);
PreyLocation bestLocation = null;
for (int i = 0; i < MAXIMUM_OF_ATTEMPTS; i++) {
PreyLogger.d(String.format("DAILY getPreyLocationApp[%s]", i));
try {
Thread.sleep(SLEEP_OF_ATTEMPTS[i] * 1000L);
} catch (InterruptedException e) {
PreyLogger.e(String.format("DAILY error :%s", e.getMessage()), e);
}
PreyLocation preyLocation = PreyLocationManager.getInstance(context).getLastLocation();
if (preyLocation == null || preyLocation.getLat() == 0 || preyLocation.getLng() == 0) {
PreyLogger.d(String.format("DAILY null[%s]", i));
continue;
}
if (preyLocation != null && preyLocation.getLat() != 0 && preyLocation.getLng() != 0) {
sendLocation(context, preyLocation);
preyLocation.setMethod("native");
// Keep the most accurate (lowest accuracy value) fix seen so far.
if (bestLocation == null || preyLocation.getAccuracy() < bestLocation.getAccuracy()) {
bestLocation = preyLocation;
}
if (bestLocation.getAccuracy() > 0 && bestLocation.getAccuracy() <= GOOD_ACCURACY_METERS) {
break;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
PreyLogger.d("DAILY location already sent");
// Send the best fix obtained; a mediocre fix still beats skipping the day.
if (bestLocation != null && bestLocation.getLat() != 0 && bestLocation.getLng() != 0) {
sendLocation(context, bestLocation);
}
} catch (Exception e) {
PreyLogger.e(String.format("DAILY run error:%s", e.getMessage()), e);
}
}

Expand Down Expand Up @@ -100,7 +130,7 @@ public static void sendLocation(Context context, PreyLocation preyLocation) thro
int statusCode = preyResponse.getStatusCode();
PreyLogger.d(String.format("DAILY getStatusCode :%s", statusCode));
if (statusCode == HttpURLConnection.HTTP_OK || statusCode == HttpURLConnection.HTTP_CREATED) {
PreyConfig.getPreyConfig(context).setDailyLocation(PreyConfig.FORMAT_SDF_AWARE.format(new Date()));
PreyConfig.getPreyConfig(context).setDailyLocation(utcDayFormat().format(new Date()));
}
PreyLogger.d(String.format("DAILY sendNowAware:%s", preyLocation.toString()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*******************************************************************************
* Created by Patricio Jofré
* Copyright 2026 Prey Inc. All rights reserved.
* License: GPLv3
* Full license at "/LICENSE"
******************************************************************************/
package com.prey.actions.location.daily;

import com.prey.PreyLogger;

import org.json.JSONObject;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
* Pure evaluation of the server-configured allowed window for the daily location.
* <p>
* The configuration comes from {@code settings.local.location_schedule} in
* {@code /devices/:key/status.json} and has the shape:
* <pre>
* {
* "start_at": "10:00", "end_at": "11:00",
* "sunday": false, "monday": true, "tuesday": true, "wednesday": true,
* "thursday": true, "friday": true, "saturday": false
* }
* </pre>
* where each weekday is a boolean flag and {@code start_at}/{@code end_at} are
* {@code "HH:mm"}. Both days and hours are evaluated in <b>UTC</b> and the hour range
* is inclusive on both ends.
* <p>
* When the configuration is absent, empty or malformed, the location may be sent at any
* time (fail-open) so the daily-location feature keeps working when no window is set.
*/
public class LocationScheduleWindow {

/** UTC day-of-week keys, indexed so {@code Calendar.SUNDAY - 1 == 0}. */
private static final String[] DAY_KEYS = {
"sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"
};

private LocationScheduleWindow() {
}

/**
* Decides whether a location may be acquired/sent at {@code now} given the schedule.
*
* @param scheduleJson the raw {@code location_schedule} JSON, or null/empty when unset
* @param now the instant to evaluate (interpreted in UTC)
* @return true when there is no restriction or {@code now} falls inside the window
*/
public static boolean isWithinAllowedWindow(String scheduleJson, Date now) {
if (scheduleJson == null || scheduleJson.trim().isEmpty()) {
return true;
}
try {
JSONObject schedule = new JSONObject(scheduleJson);
Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
calendar.setTime(now);
return isDayAllowed(schedule, calendar) && isTimeAllowed(schedule, calendar);
} catch (Exception e) {
PreyLogger.d(String.format("DAILY window malformed schedule, sending ASAP:%s", e.getMessage()));
return true;
}
}

/**
* @return true when the schedule carries no weekday flags at all (any day), or the flag
* for the current UTC day-of-week is present and {@code true}. A weekday flag that is
* present and {@code false}, or absent while other weekdays are set, blocks that day.
*/
private static boolean isDayAllowed(JSONObject schedule, Calendar calendar) {
boolean hasAnyDayFlag = false;
for (String dayKey : DAY_KEYS) {
if (schedule.has(dayKey)) {
hasAnyDayFlag = true;
break;
}
}
if (!hasAnyDayFlag) {
return true;
}
String today = DAY_KEYS[calendar.get(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY]; // SUNDAY == 1
return schedule.optBoolean(today, false);
}

/**
* @return true when {@code start_at}/{@code end_at} are missing/unparseable, or
* {@code start_at <= now <= end_at} in UTC minutes-of-day (inclusive).
*/
private static boolean isTimeAllowed(JSONObject schedule, Calendar calendar) {
int start = parseMinuteOfDay(schedule.optString("start_at", null));
int end = parseMinuteOfDay(schedule.optString("end_at", null));
if (start < 0 || end < 0) {
return true;
}
int nowMinutes = calendar.get(Calendar.HOUR_OF_DAY) * 60 + calendar.get(Calendar.MINUTE);
return nowMinutes >= start && nowMinutes <= end;
}

/**
* Parses a {@code "HH:mm"} string into minutes since midnight.
*
* @return minutes-of-day, or -1 when the value is missing or unparseable
*/
private static int parseMinuteOfDay(String value) {
if (value == null || value.trim().isEmpty()) {
return -1;
}
String[] parts = value.trim().split(":");
if (parts.length != 2) {
return -1;
}
try {
int hour = Integer.parseInt(parts[0].trim());
int minute = Integer.parseInt(parts[1].trim());
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
return -1;
}
return hour * 60 + minute;
} catch (NumberFormatException e) {
return -1;
}
}
}
Loading
Loading