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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import android.content.SharedPreferences;
import android.provider.Settings;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class ConfigRepository {
Expand All @@ -25,6 +28,8 @@ public class ConfigRepository {

public static final String LAST_RUN_TIMESTAMP_KEY = "last-run-timestamp";

public static final String ENABLED_LOCATION_PROVIDERS_KEY = "enabled-location-providers";

/**
* How long do we stay in high accuracy mode after a trigger
*/
Expand Down Expand Up @@ -117,4 +122,25 @@ public long getLastRunTimestamp() {
public void setLastRunTimestamp(long lastRunTime) {
prefs.edit().putLong(LAST_RUN_TIMESTAMP_KEY, lastRunTime).apply();
}

/**
* Get the list of enabled location providers in order.
* Returns an empty list if not set.
*/
public List<String> getEnabledLocationProviders() {
String providersString = prefs.getString(ENABLED_LOCATION_PROVIDERS_KEY, "");
if (providersString.isEmpty()) {
return new ArrayList<>();
}
return new ArrayList<>(Arrays.asList(providersString.split(",")));
}

/**
* Set the list of enabled location providers in order.
* The list is stored as a comma-separated string.
*/
public void setEnabledLocationProviders(List<String> providers) {
String providersString = String.join(",", providers);
prefs.edit().putString(ENABLED_LOCATION_PROVIDERS_KEY, providersString).apply();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ public ListenableFuture<SetLocationResponse> sendLocation(String deviceId, Beaco
beaconRequest.getLat(),
beaconRequest.getLon(),
(double) beaconRequest.getAccuracy(),
beaconRequest.getTimestamp()
beaconRequest.getTimestamp(),
beaconRequest.getMetadata()
);
ListenableFuture<SetLocationResponse> future = createStub().setLocation(request);
Futures.addCallback(future, callback, threadExecutor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@

import android.location.Location;

import java.util.HashMap;
import java.util.Map;

import lombok.Value;

@Value
public class BeaconRequest {
long timestamp;
double lat;
double lon;
float accuracy;
private long timestamp;
private double lat;
private double lon;
private float accuracy;
private Map<String, String> metadata;

public static BeaconRequest fromLocation(Location location) {
return new BeaconRequest(
System.currentTimeMillis(),
location.getLatitude(),
location.getLongitude(),
location.getAccuracy()
location.getAccuracy(),
new HashMap<String, String>() {{
put("provider", location.getProvider());
}}
Comment on lines +24 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of double-brace initialization for creating and initializing the HashMap is generally considered an anti-pattern. It creates an anonymous inner class, which can have a performance overhead and potential for memory leaks.

A more standard and efficient approach is to use Collections.singletonMap(), especially since only one entry is being added and the map is effectively immutable. Please also add the required import for java.util.Collections.

                Collections.singletonMap("provider", location.getProvider())

);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import com.jackpf.locationhistory.RegisterPushHandlerRequest;
import com.jackpf.locationhistory.SetLocationRequest;

import java.util.Map;

public class Requests {
private static Device device(String deviceId, String deviceName) {
return Device.newBuilder()
Expand All @@ -20,12 +22,14 @@ private static Device device(String deviceId, String deviceName) {
private static Location location(
Double lat,
Double lon,
Double accuracy
Double accuracy,
Map<String, String> metadata
) {
return Location.newBuilder()
.setLat(lat)
.setLon(lon)
.setAccuracy(accuracy)
.putAllMetadata(metadata)
.build();
}

Expand Down Expand Up @@ -54,12 +58,13 @@ public static SetLocationRequest setLocationRequest(
Double lat,
Double lon,
Double accuracy,
Long timestamp
Long timestamp,
Map<String, String> metadata
) {
return SetLocationRequest
.newBuilder()
.setDeviceId(deviceId)
.setLocation(location(lat, lon, accuracy))
.setLocation(location(lat, lon, accuracy, metadata))
.setTimestamp(timestamp)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

public class LocationService {
private final LocationManager locationManager;
Expand All @@ -36,13 +38,18 @@ public class LocationService {
* Network should be relatively fast (~5s for radio wakeup or near instant if using WiFi location)
*/
private static final int NETWORK_TIMEOUT = 10_000;
/**
* If we don't know what we're calling
*/
private static final int DEFAULT_TIMEOUT = 30_000;
/**
* Timeout doesn't apply to cache requests
*/
private static final int CACHE_TIMEOUT = -1;

@Getter
@AllArgsConstructor
@ToString
private static class ProviderRequest {
private String source;
private int timeout;
Expand Down Expand Up @@ -74,6 +81,18 @@ public static LocationService create(Context context,
);
}

public List<String> getAvailableSources() {
return filterEnabledSources(locationManager.getAllProviders()).stream()
.filter(p -> !p.equals(LocationManager.PASSIVE_PROVIDER))
.collect(Collectors.toList());
}

private List<String> filterEnabledSources(List<String> sources) {
return sources.stream()
.filter(locationManager::isProviderEnabled)
.collect(Collectors.toList());
}

private void callSequentialProviders(Iterator<ProviderRequest> providers, Consumer<LocationData> consumer) {
if (!providers.hasNext()) {
consumer.accept(null); // All providers invalid/failed
Expand Down Expand Up @@ -123,49 +142,68 @@ private LocationData chooseBestLocation(List<LocationData> locations) {
return best;
}

private List<ProviderRequest> createRequests(List<String> sources, LocationProvider provider) {
List<ProviderRequest> requests = new ArrayList<>();

for (String source : filterEnabledSources(sources)) {
int timeout = timeoutForSource(source);
requests.add(new ProviderRequest(source, timeout, provider));
}

return requests;
}

private int timeoutForSource(String source) {
switch (source) {
case LocationManager.GPS_PROVIDER:
return GPS_TIMEOUT;
case LocationManager.NETWORK_PROVIDER:
return NETWORK_TIMEOUT;
default:
return DEFAULT_TIMEOUT;
}
}

/**
* @param accuracy Defines priority of accuracy vs battery use (GPS v.s. network v.s. cached location sources)
* @param consumer Callback for location data
* @throws SecurityException If we're missing location permissions
*/
public void getLocation(RequestedAccuracy accuracy, Consumer<LocationData> consumer) throws SecurityException {
public void getLocation(RequestedAccuracy accuracy,
List<String> sources,
Consumer<LocationData> consumer) throws SecurityException {
if (!permissionsManager.hasLocationPermissions()) {
throw new SecurityException("No location permissions");
}

List<ProviderRequest> providers = new ArrayList<>();

if (optimisedProvider.isSupported()) {
/* Optimised provider will automatically use the location cache if available
* and return a fresh (< ~30s) location if available */
log.d("Using optimised location manager");

ProviderRequest gpsRequest = new ProviderRequest(LocationManager.GPS_PROVIDER, GPS_TIMEOUT, optimisedProvider);
ProviderRequest networkRequest = new ProviderRequest(LocationManager.NETWORK_PROVIDER, NETWORK_TIMEOUT, optimisedProvider);
List<ProviderRequest> requests = createRequests(sources, optimisedProvider);
log.d("Requesting location from providers: %s", Arrays.toString(requests.toArray()));

if (accuracy == RequestedAccuracy.HIGH) {
providers.addAll(Arrays.asList(gpsRequest, networkRequest));
callParallelProviders(providers.iterator(), this::chooseBestLocation, consumer);
callParallelProviders(requests.iterator(), this::chooseBestLocation, consumer);
} else {
providers.addAll(Arrays.asList(networkRequest, gpsRequest));
callSequentialProviders(providers.iterator(), consumer);
callSequentialProviders(requests.iterator(), consumer);
}
} else {
/* The legacy provider will directly request location from the hardware,
* so we've gotta implement our own cache checks */
log.d("Using legacy location manager");

ProviderRequest highGps = new ProviderRequest(LocationManager.GPS_PROVIDER, GPS_TIMEOUT, legacyHighAccuracyProvider);
ProviderRequest highNetwork = new ProviderRequest(LocationManager.NETWORK_PROVIDER, NETWORK_TIMEOUT, legacyHighAccuracyProvider);
ProviderRequest cachedGps = new ProviderRequest(LocationManager.GPS_PROVIDER, CACHE_TIMEOUT, legacyCachedProvider);
ProviderRequest cachedNetwork = new ProviderRequest(LocationManager.NETWORK_PROVIDER, CACHE_TIMEOUT, legacyCachedProvider);
List<ProviderRequest> cachedRequests = createRequests(sources, legacyCachedProvider);
List<ProviderRequest> highAccuracyRequests = createRequests(sources, legacyHighAccuracyProvider);
List<ProviderRequest> allRequests = Stream.concat(cachedRequests.stream(), highAccuracyRequests.stream())
.collect(Collectors.toList());
Comment on lines +199 to +200
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Stream.concat to merge two lists can be less readable and performant than using standard list operations. For improved clarity and performance, consider using ArrayList's constructor and the addAll method.

Suggested change
List<ProviderRequest> allRequests = Stream.concat(cachedRequests.stream(), highAccuracyRequests.stream())
.collect(Collectors.toList());
List<ProviderRequest> allRequests = new ArrayList<>(cachedRequests);
allRequests.addAll(highAccuracyRequests);

log.d("Requesting location from providers: %s", Arrays.toString(allRequests.toArray()));

if (accuracy == RequestedAccuracy.HIGH) {
providers.addAll(Arrays.asList(highGps, highNetwork, cachedGps, cachedNetwork));
callParallelProviders(providers.iterator(), this::chooseBestLocation, consumer);
callParallelProviders(allRequests.iterator(), this::chooseBestLocation, consumer);
} else {
providers.addAll(Arrays.asList(cachedGps, cachedNetwork, highNetwork, highGps));
callSequentialProviders(providers.iterator(), consumer);
callSequentialProviders(allRequests.iterator(), consumer);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.jackpf.locationhistory.client.ui;

import android.annotation.SuppressLint;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;

import com.jackpf.locationhistory.client.R;

import java.util.Collections;
import java.util.List;

import lombok.Setter;

public class LocationProviderAdapter extends RecyclerView.Adapter<LocationProviderAdapter.ViewHolder> {

private final List<LocationProviderItem> providers;
@Setter
private ItemTouchHelper itemTouchHelper;

public LocationProviderAdapter(List<LocationProviderItem> providers) {
this.providers = providers;
}

@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_location_provider, parent, false);
return new ViewHolder(view);
}

@SuppressLint("ClickableViewAccessibility")
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
LocationProviderItem item = providers.get(position);

holder.providerName.setText(item.getProviderName());
holder.providerCheckbox.setChecked(item.isEnabled());

holder.providerCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
item.setEnabled(isChecked);
});

holder.dragHandle.setOnTouchListener((v, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && itemTouchHelper != null) {
itemTouchHelper.startDrag(holder);
}
return false;
});
}

@Override
public int getItemCount() {
return providers.size();
}

public List<LocationProviderItem> getProviders() {
return providers;
}

public void onItemMove(int fromPosition, int toPosition) {
if (fromPosition < toPosition) {
for (int i = fromPosition; i < toPosition; i++) {
Collections.swap(providers, i, i + 1);
}
} else {
for (int i = fromPosition; i > toPosition; i--) {
Collections.swap(providers, i, i - 1);
}
}
notifyItemMoved(fromPosition, toPosition);
}

static class ViewHolder extends RecyclerView.ViewHolder {
final ImageView dragHandle;
final CheckBox providerCheckbox;
final TextView providerName;

ViewHolder(View itemView) {
super(itemView);
dragHandle = itemView.findViewById(R.id.dragHandle);
providerCheckbox = itemView.findViewById(R.id.providerCheckbox);
providerName = itemView.findViewById(R.id.providerName);
}
}

public static class DragCallback extends ItemTouchHelper.Callback {
private final LocationProviderAdapter adapter;

public DragCallback(LocationProviderAdapter adapter) {
this.adapter = adapter;
}

@Override
public boolean isLongPressDragEnabled() {
return false; // We use the drag handle instead
}

@Override
public boolean isItemViewSwipeEnabled() {
return false;
}

@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
return makeMovementFlags(dragFlags, 0);
}

@Override
public boolean onMove(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder target) {
adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}

@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
// Not used
}
}
}
Loading