Compare commits

...

4 commits

Author SHA1 Message Date
Morten Delenk
a146ae77f1
Replace the rxjava adapters with better alternatives 2022-08-15 14:20:33 +01:00
Morten Delenk
eefd8134c3
don’t throw error if location is not set 2022-08-15 10:42:29 +01:00
Morten Delenk
df839b4c03
make image loading work again 2022-08-15 10:35:02 +01:00
Morten Delenk
9043b99914
Add missing text labels 2022-08-15 10:26:04 +01:00
45 changed files with 370 additions and 432 deletions

View file

@ -51,6 +51,7 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
/** /**
* Converts an RGB value into a luminance value. * Converts an RGB value into a luminance value.
*
* @param r The red channel in the range [0, 1]. * @param r The red channel in the range [0, 1].
* @param g The green channel in the range [0, 1]. * @param g The green channel in the range [0, 1].
* @param b The blue channel in the range [0, 1]. * @param b The blue channel in the range [0, 1].
@ -62,6 +63,7 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
/** /**
* Converts an RGB value into a luminance value. * Converts an RGB value into a luminance value.
*
* @param r The red channel in the range [0, 255]. * @param r The red channel in the range [0, 255].
* @param g The green channel in the range [0, 255]. * @param g The green channel in the range [0, 255].
* @param b The blue channel in the range [0, 255]. * @param b The blue channel in the range [0, 255].
@ -76,6 +78,7 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
/** /**
* Retrieves a plane proxy from the image proxy. * Retrieves a plane proxy from the image proxy.
*
* @param planeIndex the plane index to retrieve. * @param planeIndex the plane index to retrieve.
* @return the plane proxy. * @return the plane proxy.
*/ */
@ -85,6 +88,7 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
/** /**
* Retrieves a single pixel on a plane from the image. * Retrieves a single pixel on a plane from the image.
*
* @param planeIndex the plane index to retrieve the pixel from. * @param planeIndex the plane index to retrieve the pixel from.
* @param x the x coordinate of the pixel. * @param x the x coordinate of the pixel.
* @param y the y coordinate of the pixel. * @param y the y coordinate of the pixel.
@ -101,6 +105,7 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
/** /**
* Retrieves the luminance value of a single pixel from the image. * Retrieves the luminance value of a single pixel from the image.
*
* @param x the x coordinate of the pixel. * @param x the x coordinate of the pixel.
* @param y the y coordinate of the pixel. * @param y the y coordinate of the pixel.
* @return the pixel. * @return the pixel.

View file

@ -13,7 +13,7 @@ import rs.chir.compat.java.util.function.Function;
/** /**
* {@link ImageAnalysis.Analyzer} for QR codes. * {@link ImageAnalysis.Analyzer} for QR codes.
* * <p>
* When a QR code is found, the analyzer will call the {@link QRAnalyzerListener} callback. * When a QR code is found, the analyzer will call the {@link QRAnalyzerListener} callback.
* *
* @author Morten Delenk * @author Morten Delenk
@ -31,6 +31,7 @@ public class QRAnalyzer implements ImageAnalysis.Analyzer {
/** /**
* Creates a new QR analyzer. * Creates a new QR analyzer.
*
* @param listener The listener to call when a QR code is found. * @param listener The listener to call when a QR code is found.
*/ */
public QRAnalyzer(@NonNull QRAnalyzerListener listener) { public QRAnalyzer(@NonNull QRAnalyzerListener listener) {
@ -52,6 +53,7 @@ public class QRAnalyzer implements ImageAnalysis.Analyzer {
/** /**
* Analyzes the given {@link ImageProxy}. * Analyzes the given {@link ImageProxy}.
*
* @param image The image to analyze. * @param image The image to analyze.
*/ */
@Override @Override
@ -67,6 +69,7 @@ public class QRAnalyzer implements ImageAnalysis.Analyzer {
public interface QRAnalyzerListener { public interface QRAnalyzerListener {
/** /**
* Called when a QR code is found. * Called when a QR code is found.
*
* @param qrCode The contents of the QR code. * @param qrCode The contents of the QR code.
*/ */
void onQRFound(@NonNull String qrCode); void onQRFound(@NonNull String qrCode);

View file

@ -1,13 +1,11 @@
/** /**
* Module for computer vision on Android. * Module for computer vision on Android.
* * <p>
* Currently supports: * Currently supports:
* *
* <ul> * <ul>
* <li>QR Code scanning: {@link rs.chir.cv.QRAnalyzer}</li> * <li>QR Code scanning: {@link rs.chir.cv.QRAnalyzer}</li>
* <li>Conversion of an {@link androidx.camera.core.ImageProxy} into a {@link com.google.zxing.LuminanceSource}: {@link rs.chir.cv.QRAnalyzer}</li> * <li>Conversion of an {@link androidx.camera.core.ImageProxy} into a {@link com.google.zxing.LuminanceSource}: {@link rs.chir.cv.QRAnalyzer}</li>
* </ul> * </ul>
*
*
*/ */
package rs.chir.cv; package rs.chir.cv;

View file

@ -36,11 +36,13 @@ public class CachedClientImpl extends Client {
private final Context context; private final Context context;
/** /**
* Cached objects. * Cached objects.
*
* @see TrackedItem * @see TrackedItem
*/ */
private final Map<Long, TrackedItem> objects = new java.util.HashMap<>(10); private final Map<Long, TrackedItem> objects = new java.util.HashMap<>(10);
/** /**
* Cached public key. * Cached public key.
*
* @see PublicKey * @see PublicKey
*/ */
private Optional<PublicKey> publicKey = Optional.empty(); private Optional<PublicKey> publicKey = Optional.empty();
@ -51,6 +53,7 @@ public class CachedClientImpl extends Client {
/** /**
* Creates a new cached client. * Creates a new cached client.
*
* @param context the context used to retrieve network connectivity information * @param context the context used to retrieve network connectivity information
* @param client the underlying client * @param client the underlying client
*/ */
@ -61,6 +64,7 @@ public class CachedClientImpl extends Client {
/** /**
* Returns whether aggressive caching is desired. * Returns whether aggressive caching is desired.
*
* @return true if network connectivity is limited * @return true if network connectivity is limited
*/ */
private boolean aggressivelyCache() { private boolean aggressivelyCache() {
@ -96,6 +100,7 @@ public class CachedClientImpl extends Client {
/** /**
* Caches the given object. * Caches the given object.
*
* @param trackedItem the object to cache * @param trackedItem the object to cache
*/ */
private void cacheObject(TrackedItem trackedItem) { private void cacheObject(TrackedItem trackedItem) {

View file

@ -15,7 +15,7 @@ import rs.chir.invtracker.model.GeoRect;
import rs.chir.invtracker.model.PasetoToken; import rs.chir.invtracker.model.PasetoToken;
import rs.chir.invtracker.model.PublicKey; import rs.chir.invtracker.model.PublicKey;
import rs.chir.invtracker.model.TrackedItem; import rs.chir.invtracker.model.TrackedItem;
import rs.chir.invtracker.utils.CursorStreamable; import rs.chir.invtracker.utils.RXJavaAdapters;
/** /**
* The API Client abstract base class. * The API Client abstract base class.
@ -29,6 +29,7 @@ public abstract class Client {
/** /**
* Gets the public key. * Gets the public key.
*
* @return A single that emits the public key * @return A single that emits the public key
* @see PublicKey * @see PublicKey
* @see Single * @see Single
@ -38,6 +39,7 @@ public abstract class Client {
/** /**
* Attempts to log in with the given credentials. * Attempts to log in with the given credentials.
*
* @param username the username * @param username the username
* @param password the password * @param password the password
* @return A single that emits the API token * @return A single that emits the API token
@ -49,6 +51,7 @@ public abstract class Client {
/** /**
* Retrieves an item with the given ID. * Retrieves an item with the given ID.
*
* @param id the item ID * @param id the item ID
* @return A single that emits the item * @return A single that emits the item
* @see TrackedItem * @see TrackedItem
@ -59,6 +62,7 @@ public abstract class Client {
/** /**
* Retrieves a page of items. * Retrieves a page of items.
*
* @param cont An optional continuation token * @param cont An optional continuation token
* @return A single that emits a page of items * @return A single that emits a page of items
* @see TrackedItem * @see TrackedItem
@ -70,17 +74,19 @@ public abstract class Client {
/** /**
* Retrieve all items * Retrieve all items
*
* @return A flowable that emits all items * @return A flowable that emits all items
* @see TrackedItem * @see TrackedItem
* @see Flowable * @see Flowable
*/ */
@NonNull @NonNull
public Flowable<TrackedItem> streamObjects() { public Flowable<TrackedItem> streamObjects() {
return new CursorStreamable<>(this::getObjects); return RXJavaAdapters.fromCursor(this::getObjects);
} }
/** /**
* Retieves a page of items that are within the given rectangle. * Retieves a page of items that are within the given rectangle.
*
* @param rect The rectangle * @param rect The rectangle
* @param cont An optional continuation token * @param cont An optional continuation token
* @return A single that emits a page of items * @return A single that emits a page of items
@ -93,6 +99,7 @@ public abstract class Client {
/** /**
* Retrieves all items that are within the given rectangle. * Retrieves all items that are within the given rectangle.
*
* @param rect The rectangle * @param rect The rectangle
* @return A flowable that emits all items * @return A flowable that emits all items
* @see TrackedItem * @see TrackedItem
@ -100,11 +107,12 @@ public abstract class Client {
*/ */
@NonNull @NonNull
public Flowable<TrackedItem> streamObjects(@NonNull GeoRect rect) { public Flowable<TrackedItem> streamObjects(@NonNull GeoRect rect) {
return new CursorStreamable<>(id -> this.getObjects(rect, id)); return RXJavaAdapters.fromCursor(id -> this.getObjects(rect, id));
} }
/** /**
* Retrieves all items that are in an optional rectangle. * Retrieves all items that are in an optional rectangle.
*
* @param rect The rectangle * @param rect The rectangle
* @return A flowable that emits all matching items * @return A flowable that emits all matching items
* @see TrackedItem * @see TrackedItem
@ -117,6 +125,7 @@ public abstract class Client {
/** /**
* Retrieves a page of items that are within the given rectangle. * Retrieves a page of items that are within the given rectangle.
*
* @param rect The rectangle * @param rect The rectangle
* @param cont An optional continuation token * @param cont An optional continuation token
* @return A single that emits a page of items * @return A single that emits a page of items
@ -128,6 +137,7 @@ public abstract class Client {
/** /**
* Updates the location of the given item. * Updates the location of the given item.
*
* @param item The item * @param item The item
* @param location The location * @param location The location
* @return A single that emits true on success * @return A single that emits true on success
@ -140,6 +150,7 @@ public abstract class Client {
/** /**
* Updates the location of the given item. * Updates the location of the given item.
*
* @param itemId The item ID * @param itemId The item ID
* @param location The location * @param location The location
* @return A single that emits true on success * @return A single that emits true on success
@ -150,6 +161,7 @@ public abstract class Client {
/** /**
* Starts a download for an image. * Starts a download for an image.
*
* @param imageUrl The image URL * @param imageUrl The image URL
* @return A single that emits the image input stream * @return A single that emits the image input stream
*/ */
@ -158,6 +170,7 @@ public abstract class Client {
/** /**
* Retrieves a page of locations for the given item. * Retrieves a page of locations for the given item.
*
* @param itemId The item ID * @param itemId The item ID
* @param cont An optional continuation token * @param cont An optional continuation token
* @return A single that emits a page of locations * @return A single that emits a page of locations
@ -169,6 +182,7 @@ public abstract class Client {
/** /**
* Retrieves all locations for the given item. * Retrieves all locations for the given item.
*
* @param itemId The item ID * @param itemId The item ID
* @return A flowable that emits all locations * @return A flowable that emits all locations
* @see GeoLocation * @see GeoLocation
@ -176,11 +190,12 @@ public abstract class Client {
*/ */
@NonNull @NonNull
public Flowable<GeoLocation> streamLocations(long itemId) { public Flowable<GeoLocation> streamLocations(long itemId) {
return new CursorStreamable<>(id -> this.getLocations(itemId, id)); return RXJavaAdapters.fromCursor(id -> this.getLocations(itemId, id));
} }
/** /**
* Uploads an image to the server. * Uploads an image to the server.
*
* @param uri The source URI * @param uri The source URI
* @return The URL of the uploaded image * @return The URL of the uploaded image
* @see Single * @see Single
@ -191,6 +206,7 @@ public abstract class Client {
/** /**
* Updates an item. * Updates an item.
*
* @param item The item * @param item The item
* @return A single that emits the updated item * @return A single that emits the updated item
* @see Single * @see Single
@ -201,6 +217,7 @@ public abstract class Client {
/** /**
* Creates a new item. * Creates a new item.
*
* @param item The item * @param item The item
* @return A single that emits the created item * @return A single that emits the created item
* @see Single * @see Single
@ -211,6 +228,7 @@ public abstract class Client {
/** /**
* Deletes an item. * Deletes an item.
*
* @param id The item id * @param id The item id
* @return A single that emits true on success * @return A single that emits true on success
* @see Single * @see Single

View file

@ -8,11 +8,13 @@ import rs.chir.invtracker.utils.SingleBackoff;
/** /**
* An extended version of {@link SingleBackoff} that errors out on {@link UnauthorizedException}. * An extended version of {@link SingleBackoff} that errors out on {@link UnauthorizedException}.
*
* @param <T> the type of the result * @param <T> the type of the result
*/ */
class ClientBackoff<T> extends SingleBackoff<T> { class ClientBackoff<T> extends SingleBackoff<T> {
/** /**
* Creates a new client backoff. * Creates a new client backoff.
*
* @param singleSupplier the supplier of the result * @param singleSupplier the supplier of the result
*/ */
ClientBackoff(@NonNull Supplier<Single<T>> singleSupplier) { ClientBackoff(@NonNull Supplier<Single<T>> singleSupplier) {

View file

@ -16,8 +16,6 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerException;
@ -32,7 +30,6 @@ import okio.BufferedSink;
import okio.Okio; import okio.Okio;
import rs.chir.compat.java.util.Base64; import rs.chir.compat.java.util.Base64;
import rs.chir.compat.java.util.Optional; import rs.chir.compat.java.util.Optional;
import rs.chir.invtracker.client.BuildConfig;
import rs.chir.invtracker.client.model.APIKey; import rs.chir.invtracker.client.model.APIKey;
import rs.chir.invtracker.model.Cursor; import rs.chir.invtracker.model.Cursor;
import rs.chir.invtracker.model.GeoLocation; import rs.chir.invtracker.model.GeoLocation;
@ -40,7 +37,7 @@ import rs.chir.invtracker.model.GeoRect;
import rs.chir.invtracker.model.PasetoToken; import rs.chir.invtracker.model.PasetoToken;
import rs.chir.invtracker.model.PublicKey; import rs.chir.invtracker.model.PublicKey;
import rs.chir.invtracker.model.TrackedItem; import rs.chir.invtracker.model.TrackedItem;
import rs.chir.invtracker.utils.CallAdapter; import rs.chir.invtracker.utils.RXJavaAdapters;
import rs.chir.utils.xml.SimpleXML; import rs.chir.utils.xml.SimpleXML;
import rs.chir.utils.xml.XMLSerializable; import rs.chir.utils.xml.XMLSerializable;
@ -75,6 +72,7 @@ public class ClientImpl extends Client {
/** /**
* Creates a new client. * Creates a new client.
*
* @param context the context used to retrieve the API token * @param context the context used to retrieve the API token
* @param cronetEngine the Cronet engine * @param cronetEngine the Cronet engine
*/ */
@ -87,6 +85,7 @@ public class ClientImpl extends Client {
/** /**
* Creates a new client. * Creates a new client.
*
* @param context the context used * @param context the context used
*/ */
@NonNull @NonNull
@ -97,6 +96,7 @@ public class ClientImpl extends Client {
/** /**
* Returns the HTTP client. * Returns the HTTP client.
*
* @return the HTTP client * @return the HTTP client
*/ */
@NonNull @NonNull
@ -106,6 +106,7 @@ public class ClientImpl extends Client {
/** /**
* Parses a response * Parses a response
*
* @param response A single emitting the response * @param response A single emitting the response
* @param clazz The expected class of the response * @param clazz The expected class of the response
* @param <T> The expected class of the response * @param <T> The expected class of the response
@ -129,6 +130,7 @@ public class ClientImpl extends Client {
/** /**
* Encodes a request body. * Encodes a request body.
*
* @param body the body to encode * @param body the body to encode
* @param <T> the type of the body * @param <T> the type of the body
* @return the encoded body * @return the encoded body
@ -156,12 +158,13 @@ public class ClientImpl extends Client {
Request request = new Request.Builder() Request request = new Request.Builder()
.url(API_URL + "/public-key") .url(API_URL + "/public-key")
.build(); .build();
return new CallAdapter(this.client.newCall(request)); return RXJavaAdapters.fromCall(this.client.newCall(request));
}), PublicKey.class); }), PublicKey.class);
} }
/** /**
* Creates a new {@link Request.Builder} for the given API route. * Creates a new {@link Request.Builder} for the given API route.
*
* @param route the API route * @param route the API route
* @return A single emitting the {@link Request.Builder} * @return A single emitting the {@link Request.Builder}
*/ */
@ -188,7 +191,7 @@ public class ClientImpl extends Client {
.header("Authorization", authHeader) .header("Authorization", authHeader)
.post(RequestBody.create(EMPTY_BODY)) .post(RequestBody.create(EMPTY_BODY))
.build(); .build();
return new CallAdapter(this.client.newCall(request)); return RXJavaAdapters.fromCall(this.client.newCall(request));
}), PasetoToken.class); }), PasetoToken.class);
} }
@ -197,14 +200,14 @@ public class ClientImpl extends Client {
public Single<TrackedItem> getObject(long id) { public Single<TrackedItem> getObject(long id) {
return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects/" + id).map(Request.Builder::build) return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects/" + id).map(Request.Builder::build)
.map(this.client::newCall) .map(this.client::newCall)
.flatMap(CallAdapter::new)), TrackedItem.class); .flatMap(RXJavaAdapters::fromCall)), TrackedItem.class);
} }
@Override @Override
@NonNull @NonNull
public Single<Cursor<TrackedItem>> getObjects(@NonNull Optional<String> cont) { public Single<Cursor<TrackedItem>> getObjects(@NonNull Optional<String> cont) {
return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects" + cont.map(c -> "?start=" + c).orElse("")) return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects" + cont.map(c -> "?start=" + c).orElse(""))
.flatMap(request -> new CallAdapter(this.client.newCall(request.get().build())))), .flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.get().build())))),
Cursor.class) Cursor.class)
.map(cursor -> (Cursor<TrackedItem>) cursor); .map(cursor -> (Cursor<TrackedItem>) cursor);
} }
@ -216,7 +219,7 @@ public class ClientImpl extends Client {
.flatMap(request -> { .flatMap(request -> {
var req = request.method("POST", RequestBody.create(this.encodeBody(rect))) var req = request.method("POST", RequestBody.create(this.encodeBody(rect)))
.build(); .build();
return new CallAdapter(this.client.newCall(req)); return RXJavaAdapters.fromCall(this.client.newCall(req));
})), })),
Cursor.class) Cursor.class)
.map(cursor -> (Cursor<TrackedItem>) cursor); .map(cursor -> (Cursor<TrackedItem>) cursor);
@ -229,7 +232,7 @@ public class ClientImpl extends Client {
.flatMap(builder -> { .flatMap(builder -> {
var req = builder.method("POST", RequestBody.create(this.encodeBody(location))) var req = builder.method("POST", RequestBody.create(this.encodeBody(location)))
.build(); .build();
return new CallAdapter(this.client.newCall(req)); return RXJavaAdapters.fromCall(this.client.newCall(req));
}).map(__ -> true); }).map(__ -> true);
} }
@ -246,7 +249,7 @@ public class ClientImpl extends Client {
rb = Single.just(new Request.Builder() rb = Single.just(new Request.Builder()
.url(imageUrl)); .url(imageUrl));
} }
return rb.flatMap(request -> new ClientBackoff<>(() -> new CallAdapter(this.client.newCall(request.build())))) return rb.flatMap(request -> new ClientBackoff<>(() -> RXJavaAdapters.fromCall(this.client.newCall(request.build()))))
.map(Response::body).map(ResponseBody::byteStream); .map(Response::body).map(ResponseBody::byteStream);
} }
@ -254,7 +257,7 @@ public class ClientImpl extends Client {
@NonNull @NonNull
public Single<Cursor<GeoLocation>> getLocations(long itemId, @NonNull Optional<String> cont) { public Single<Cursor<GeoLocation>> getLocations(long itemId, @NonNull Optional<String> cont) {
return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects/" + itemId + "/locations" + cont.map(c -> "?start=" + c).orElse("")) return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects/" + itemId + "/locations" + cont.map(c -> "?start=" + c).orElse(""))
.flatMap(request -> new CallAdapter(this.client.newCall(request.get().build())))), .flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.get().build())))),
Cursor.class) Cursor.class)
.map(cursor -> (Cursor<GeoLocation>) cursor); .map(cursor -> (Cursor<GeoLocation>) cursor);
} }
@ -280,7 +283,7 @@ public class ClientImpl extends Client {
return MediaType.parse(contentType); return MediaType.parse(contentType);
} }
}; };
return new CallAdapter(this.client.newCall(request.method("POST", rb).build())); return RXJavaAdapters.fromCall(this.client.newCall(request.method("POST", rb).build()));
})).map(Response::headers).map(headers -> headers.get("Location")).map(header -> API_URL + header); })).map(Response::headers).map(headers -> headers.get("Location")).map(header -> API_URL + header);
} }
@ -288,7 +291,7 @@ public class ClientImpl extends Client {
@Override @Override
public Single<TrackedItem> updateObject(@NonNull TrackedItem item) { public Single<TrackedItem> updateObject(@NonNull TrackedItem item) {
return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects/" + item.id()) return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects/" + item.id())
.flatMap(request -> new CallAdapter(this.client.newCall(request.method("PATCH", RequestBody.create(this.encodeBody(item))) .flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.method("PATCH", RequestBody.create(this.encodeBody(item)))
.build())))), .build())))),
TrackedItem.class); TrackedItem.class);
} }
@ -297,7 +300,7 @@ public class ClientImpl extends Client {
@Override @Override
public Single<TrackedItem> createObject(@NonNull TrackedItem item) { public Single<TrackedItem> createObject(@NonNull TrackedItem item) {
return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects") return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects")
.flatMap(request -> new CallAdapter(this.client.newCall(request.method("POST", RequestBody.create(this.encodeBody(item))) .flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.method("POST", RequestBody.create(this.encodeBody(item)))
.build())))), .build())))),
TrackedItem.class); TrackedItem.class);
} }
@ -306,7 +309,7 @@ public class ClientImpl extends Client {
@Override @Override
public Single<Boolean> deleteObject(long id) { public Single<Boolean> deleteObject(long id) {
return this.createRequest("/objects/" + id) return this.createRequest("/objects/" + id)
.flatMap(request -> new CallAdapter(this.client.newCall(request.method("DELETE", RequestBody.create(EMPTY_BODY)).build()))) .flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.method("DELETE", RequestBody.create(EMPTY_BODY)).build())))
.map(__ -> true); .map(__ -> true);
} }
} }

View file

@ -14,7 +14,7 @@ import java.io.File;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import rs.chir.compat.java.util.Optional; import rs.chir.compat.java.util.Optional;
import rs.chir.invtracker.utils.TaskAdapter; import rs.chir.invtracker.utils.RXJavaAdapters;
/** /**
* Utility class for creating a {@link CronetEngine} instance. * Utility class for creating a {@link CronetEngine} instance.
@ -36,6 +36,7 @@ public enum CronetEngineProvider {
/** /**
* Creates a new {@link CronetEngine} instance. * Creates a new {@link CronetEngine} instance.
*
* @param context The context used for installing cronet * @param context The context used for installing cronet
* @return A single that emits the {@link CronetEngine} instance * @return A single that emits the {@link CronetEngine} instance
*/ */
@ -43,7 +44,7 @@ public enum CronetEngineProvider {
public static Single<CronetEngine> getInstance(@NonNull Context context) { public static Single<CronetEngine> getInstance(@NonNull Context context) {
if (!ENGINE.isPresent()) { if (!ENGINE.isPresent()) {
var installTask = CronetProviderInstaller.installProvider(context); var installTask = CronetProviderInstaller.installProvider(context);
return TaskAdapter.fromTask(installTask) return RXJavaAdapters.fromTask(installTask)
.map(_void -> new CronetEngine.Builder(context)) .map(_void -> new CronetEngine.Builder(context))
.onErrorResumeNext(throwable -> { .onErrorResumeNext(throwable -> {
Log.e("CronetEngineProvider", "Failed to install Cronet provider " + throwable); Log.e("CronetEngineProvider", "Failed to install Cronet provider " + throwable);

View file

@ -1,6 +1,6 @@
/** /**
* API client tools. * API client tools.
* * <p>
* You can retrieve an API client using {@link rs.chir.invtracker.client.Application#getClient()}. * You can retrieve an API client using {@link rs.chir.invtracker.client.Application#getClient()}.
*/ */
package rs.chir.invtracker.api; package rs.chir.invtracker.api;

View file

@ -36,6 +36,7 @@ public class Application extends android.app.Application {
/** /**
* Returns the Application instance for a context * Returns the Application instance for a context
*
* @param context the context to get the Application instance for * @param context the context to get the Application instance for
* @return the Application instance for the context * @return the Application instance for the context
*/ */
@ -64,6 +65,7 @@ public class Application extends android.app.Application {
/** /**
* Returns the preferences datastore. * Returns the preferences datastore.
*
* @return the preferences datastore * @return the preferences datastore
*/ */
@NonNull @NonNull
@ -73,6 +75,7 @@ public class Application extends android.app.Application {
/** /**
* Returns the API client. * Returns the API client.
*
* @return A single that completes with the API client. * @return A single that completes with the API client.
*/ */
@NonNull @NonNull

View file

@ -1,10 +1,8 @@
package rs.chir.invtracker.client; package rs.chir.invtracker.client;
import static android.app.Activity.RESULT_OK;
import static android.provider.MediaStore.ACTION_PICK_IMAGES; import static android.provider.MediaStore.ACTION_PICK_IMAGES;
import android.Manifest; import android.Manifest;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@ -20,7 +18,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -187,6 +184,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
/** /**
* Handler for when the saving of the item is successful. * Handler for when the saving of the item is successful.
*
* @param item the item that was saved. * @param item the item that was saved.
*/ */
private void onActionSuccess(TrackedItem trackedItem) { private void onActionSuccess(TrackedItem trackedItem) {
@ -201,6 +199,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
/** /**
* Pick an @{link ActivityResultLauncher} to use for picking an image. * Pick an @{link ActivityResultLauncher} to use for picking an image.
*
* @return the @{link ActivityResultLauncher} to use for picking an image. * @return the @{link ActivityResultLauncher} to use for picking an image.
*/ */
private ActivityResultLauncher<String> getImagePickerLauncher() { private ActivityResultLauncher<String> getImagePickerLauncher() {
@ -220,6 +219,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
/** /**
* Callback for when an image was taken. * Callback for when an image was taken.
*
* @param success <code>true</code> if the image was taken successfully. * @param success <code>true</code> if the image was taken successfully.
*/ */
private void onPictureTaken(@NonNull Boolean success) { private void onPictureTaken(@NonNull Boolean success) {
@ -234,6 +234,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
/** /**
* Upload an image to the server. * Upload an image to the server.
*
* @param uri the source image URI. * @param uri the source image URI.
*/ */
private void uploadImage(Uri uri) { private void uploadImage(Uri uri) {
@ -255,6 +256,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
/** /**
* Attempts to take a picture with the camera. * Attempts to take a picture with the camera.
*
* @throws IOException if the camera cannot be accessed. * @throws IOException if the camera cannot be accessed.
*/ */
private void takePicture() throws IOException { private void takePicture() throws IOException {
@ -277,6 +279,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
/** /**
* Prefill the fragment with the given item. * Prefill the fragment with the given item.
*
* @param item the item to prefill with. * @param item the item to prefill with.
*/ */
private void prefillItem(@NonNull TrackedItem trackedItem) { private void prefillItem(@NonNull TrackedItem trackedItem) {

View file

@ -5,7 +5,6 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.IdRes; import androidx.annotation.IdRes;
@ -22,8 +21,8 @@ import androidx.viewbinding.ViewBinding;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import java.util.ConcurrentModificationException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.ConcurrentModificationException;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
@ -38,6 +37,7 @@ import rs.chir.utils.TypeParameterGetter;
/** /**
* Base class for all fragments. * Base class for all fragments.
*
* @param <FragmentBinding> the type of the view binding. * @param <FragmentBinding> the type of the view binding.
*/ */
public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment implements TypeParameterGetter<FragmentBinding> { public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment implements TypeParameterGetter<FragmentBinding> {
@ -56,9 +56,23 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
*/ */
@Nullable @Nullable
private MenuProvider menuProvider; private MenuProvider menuProvider;
private String requestedPermission;
/**
* Permissions request launcher
*/
private final ActivityResultLauncher<String> permissionLauncher = this.registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
var permission = this.requestedPermission;
this.requestedPermission = null;
if (isGranted) {
this.onPermissionGranted(permission);
} else {
this.onPermissionDenied(permission);
}
});
/** /**
* Retrieves the fragment binding * Retrieves the fragment binding
*
* @return the fragment binding * @return the fragment binding
*/ */
@NonNull @NonNull
@ -71,6 +85,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Sets the fragment binding * Sets the fragment binding
*
* @param binding the fragment binding * @param binding the fragment binding
*/ */
protected void setBinding(@NonNull FragmentBinding binding) { protected void setBinding(@NonNull FragmentBinding binding) {
@ -79,6 +94,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Called when the view is created. * Called when the view is created.
*
* @param inflater the layout inflater * @param inflater the layout inflater
* @param container the container view * @param container the container view
* @param savedInstanceState the saved instance state * @param savedInstanceState the saved instance state
@ -92,6 +108,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Called when the view is created. * Called when the view is created.
*
* @param inflater the layout inflater * @param inflater the layout inflater
* @param container the container view * @param container the container view
* @param savedInstanceState the saved instance state * @param savedInstanceState the saved instance state
@ -115,6 +132,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Adds a disposable to the {@link CompositeDisposable} * Adds a disposable to the {@link CompositeDisposable}
*
* @param disposable the disposable to add * @param disposable the disposable to add
*/ */
protected void setAction(@NonNull Disposable action) { protected void setAction(@NonNull Disposable action) {
@ -127,6 +145,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Subscribe to the given observable and add it to the {@link CompositeDisposable} * Subscribe to the given observable and add it to the {@link CompositeDisposable}
*
* @param value the observable to subscribe to * @param value the observable to subscribe to
* @param consumer the consumer to call when the observable emits an item * @param consumer the consumer to call when the observable emits an item
*/ */
@ -136,6 +155,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Subscribe to the given single and add it to the {@link CompositeDisposable} * Subscribe to the given single and add it to the {@link CompositeDisposable}
*
* @param value the single to subscribe to * @param value the single to subscribe to
* @param consumer the consumer to call when the single emits an item * @param consumer the consumer to call when the single emits an item
*/ */
@ -145,6 +165,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Subscribe to the given single and add it to the {@link CompositeDisposable} * Subscribe to the given single and add it to the {@link CompositeDisposable}
*
* @param value the single to subscribe to * @param value the single to subscribe to
* @param consumer the consumer to call when the observable emits an item * @param consumer the consumer to call when the observable emits an item
* @param error the consumer to call when an error occurs * @param error the consumer to call when an error occurs
@ -155,6 +176,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Subscribe to the given observable and add it to the {@link CompositeDisposable} * Subscribe to the given observable and add it to the {@link CompositeDisposable}
*
* @param value the observable to subscribe to * @param value the observable to subscribe to
* @param consumer the consumer to call when the observable emits an item * @param consumer the consumer to call when the observable emits an item
* @param error the consumer to call when an error occurs * @param error the consumer to call when an error occurs
@ -167,6 +189,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Called when the fragment is attached to the view. * Called when the fragment is attached to the view.
*
* @param view the view * @param view the view
* @param savedInstanceState the saved instance state * @param savedInstanceState the saved instance state
*/ */
@ -181,6 +204,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Creates this fragments menu provider. * Creates this fragments menu provider.
*
* @return the menu provider, or null if no menu provider is needed * @return the menu provider, or null if no menu provider is needed
*/ */
@Nullable @Nullable
@ -213,6 +237,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Returns the menu host * Returns the menu host
*
* @return the menu host * @return the menu host
*/ */
@NonNull @NonNull
@ -223,6 +248,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Returns the application * Returns the application
*
* @return the application * @return the application
*/ */
@NonNull @NonNull
@ -232,6 +258,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Returns the API client * Returns the API client
*
* @return the API client * @return the API client
*/ */
@NonNull @NonNull
@ -241,6 +268,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Returns the navigation controller * Returns the navigation controller
*
* @return the navigation controller * @return the navigation controller
*/ */
@NonNull @NonNull
@ -250,15 +278,16 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Navigate to the given fragment * Navigate to the given fragment
*
* @param id the action id * @param id the action id
*/ */
void navigate(@IdRes int id) { void navigate(@IdRes int id) {
this.getNavController().navigate(id); this.getNavController().navigate(id);
} }
/** /**
* Checks if the app has the given permission * Checks if the app has the given permission
*
* @param permission the permission to check * @param permission the permission to check
* @return true if the app has the permission, false otherwise * @return true if the app has the permission, false otherwise
*/ */
@ -268,6 +297,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Called on action error * Called on action error
*
* @param throwable the throwable * @param throwable the throwable
*/ */
protected void onActionError(@NonNull Throwable throwable) { protected void onActionError(@NonNull Throwable throwable) {
@ -279,35 +309,24 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Navigates to the given fragment * Navigates to the given fragment
*
* @param fragmentDirection the fragment direction * @param fragmentDirection the fragment direction
*/ */
void navigate(NavDirections fragmentDirection) { void navigate(NavDirections fragmentDirection) {
this.getNavController().navigate(fragmentDirection); this.getNavController().navigate(fragmentDirection);
} }
private String requestedPermission;
/**
* Permissions request launcher
*/
private final ActivityResultLauncher<String> permissionLauncher = this.registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
var permission = this.requestedPermission;
this.requestedPermission = null;
if (isGranted) {
this.onPermissionGranted(permission);
} else {
this.onPermissionDenied(permission);
}
});
/** /**
* Called when the requested permission is granted * Called when the requested permission is granted
*
* @param permission the permission * @param permission the permission
*/ */
protected void onPermissionGranted(@NonNull String permission) { protected void onPermissionGranted(@NonNull String permission) {
} }
/** /**
* Called when the requested permission is denied * Called when the requested permission is denied
*
* @param permission the permission * @param permission the permission
*/ */
protected void onPermissionDenied(@NonNull String permission) { protected void onPermissionDenied(@NonNull String permission) {
@ -316,6 +335,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
/** /**
* Requests the given permission * Requests the given permission
*
* @param permission the permission to request * @param permission the permission to request
*/ */
protected void requestPermission(@NonNull String permission) { protected void requestPermission(@NonNull String permission) {

View file

@ -11,7 +11,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.view.MenuProvider; import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import java.io.File; import java.io.File;
@ -60,6 +59,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
/** /**
* Called when the fragment is created. * Called when the fragment is created.
*
* @param savedInstanceState The saved instance state. * @param savedInstanceState The saved instance state.
*/ */
@Override @Override
@ -72,6 +72,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
/** /**
* Called when the fragment is attached to the activity. * Called when the fragment is attached to the activity.
*
* @param view The view. * @param view The view.
* @param savedInstanceState The saved instance state. * @param savedInstanceState The saved instance state.
*/ */
@ -134,6 +135,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
/** /**
* Fetches the image from the server * Fetches the image from the server
*
* @param image The image URI * @param image The image URI
*/ */
private void fetchImage(@NonNull URI image) { private void fetchImage(@NonNull URI image) {
@ -143,6 +145,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
/** /**
* Called when the URL is loaded * Called when the URL is loaded
*
* @param image The image URI * @param image The image URI
* @param inputStream the response body * @param inputStream the response body
*/ */
@ -151,7 +154,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
var fileName = image.getPath().substring(image.getPath().lastIndexOf('/')); var fileName = image.getPath().substring(image.getPath().lastIndexOf('/'));
var path = this.requireActivity().getCacheDir().getAbsolutePath(); var path = this.requireActivity().getCacheDir().getAbsolutePath();
new File(path, "images").mkdirs(); new File(path, "images").mkdirs();
path += new File(new File(path, "images"), fileName); path += "/images/" + fileName;
// TODO: check if there is a better buffer size to use // TODO: check if there is a better buffer size to use
// ideally I would use {@link java.nio.file.Files#copy(InputStream, Path, CopyOption...)} but it is not available on API level 21 // ideally I would use {@link java.nio.file.Files#copy(InputStream, Path, CopyOption...)} but it is not available on API level 21
@ -171,13 +174,15 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
/** /**
* Loads and returns the image if present * Loads and returns the image if present
*
* @param image The image URI * @param image The image URI
* @return The image if present * @return The image if present
*/ */
private Optional<Bitmap> getImageIfPresent(@NonNull URI uri) { private Optional<Bitmap> getImageIfPresent(@NonNull URI uri) {
var fileName = uri.getPath().substring(uri.getPath().lastIndexOf('/')); var fileName = uri.getPath().substring(uri.getPath().lastIndexOf('/'));
var path = this.requireActivity().getCacheDir().getAbsolutePath(); var path = this.requireActivity().getCacheDir().getAbsolutePath();
var file = new File(new File(path, "images"), fileName); path += "/images/" + fileName;
var file = new File(path);
// Check if the image is present in the cache // Check if the image is present in the cache
if (file.exists()) { if (file.exists()) {
try { try {
@ -194,6 +199,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
/** /**
* Returns the item id * Returns the item id
*
* @return The item id * @return The item id
*/ */
long getmId() { long getmId() {
@ -207,7 +213,8 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
var builder = new AlertDialog.Builder(this.requireContext()); var builder = new AlertDialog.Builder(this.requireContext());
builder.setMessage(R.string.delete_warning) builder.setMessage(R.string.delete_warning)
.setPositiveButton(R.string.yes, (__1, __2) -> this.reallyDelete()) .setPositiveButton(R.string.yes, (__1, __2) -> this.reallyDelete())
.setNegativeButton(R.string.no, (__1, __2) -> {}); .setNegativeButton(R.string.no, (__1, __2) -> {
});
builder.create().show(); builder.create().show();
} }

View file

@ -18,6 +18,7 @@ class ItemDetailMenuProvider implements MenuProvider {
/** /**
* Creates a new instance of {@link ItemDetailMenuProvider} * Creates a new instance of {@link ItemDetailMenuProvider}
*
* @param itemDetailFragment the fragment * @param itemDetailFragment the fragment
*/ */
ItemDetailMenuProvider(ItemDetailFragment itemDetailFragment) { ItemDetailMenuProvider(ItemDetailFragment itemDetailFragment) {
@ -26,6 +27,7 @@ class ItemDetailMenuProvider implements MenuProvider {
/** /**
* Called when the menu is created * Called when the menu is created
*
* @param menu the menu * @param menu the menu
* @param inflater the menu inflater * @param inflater the menu inflater
*/ */
@ -36,6 +38,7 @@ class ItemDetailMenuProvider implements MenuProvider {
/** /**
* Called when an item is selected * Called when an item is selected
*
* @param item the item * @param item the item
* @return true if the item was handled, false otherwise * @return true if the item was handled, false otherwise
*/ */

View file

@ -19,6 +19,7 @@ public class LoadFragment extends FragmentBase<FragmentLoadBinding> {
/** /**
* Called when the API key is set. * Called when the API key is set.
*
* @param hasToken {@code true} if the API key is set, {@code false} otherwise. * @param hasToken {@code true} if the API key is set, {@code false} otherwise.
*/ */
private void tokenResponse(boolean hasToken) { private void tokenResponse(boolean hasToken) {

View file

@ -40,6 +40,7 @@ public class LoginFragment extends FragmentBase<FragmentLoginBinding> {
/** /**
* Called when the login button is clicked. * Called when the login button is clicked.
*
* @param view The view. * @param view The view.
*/ */
private void login(View view) { private void login(View view) {
@ -54,6 +55,7 @@ public class LoginFragment extends FragmentBase<FragmentLoginBinding> {
/** /**
* Called when the login response is received. * Called when the login response is received.
*
* @param token The token. * @param token The token.
*/ */
private void loginResponse(@NonNull PasetoToken pasetoToken) { private void loginResponse(@NonNull PasetoToken pasetoToken) {

View file

@ -45,6 +45,7 @@ public class MainActivity extends AppCompatActivity {
/** /**
* Returns the activity binding. * Returns the activity binding.
*
* @return The activity binding. * @return The activity binding.
*/ */
@NonNull @NonNull

View file

@ -1,7 +1,6 @@
package rs.chir.invtracker.client; package rs.chir.invtracker.client;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -18,9 +17,6 @@ import org.osmdroid.config.Configuration;
import org.osmdroid.util.GeoPoint; import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.Marker;
import java.util.ArrayList;
import java.util.List;
import rs.chir.invtracker.client.databinding.FragmentMapBinding; import rs.chir.invtracker.client.databinding.FragmentMapBinding;
import rs.chir.invtracker.model.TrackedItem; import rs.chir.invtracker.model.TrackedItem;
import rs.chir.invtracker.utils.SingleLocation; import rs.chir.invtracker.utils.SingleLocation;
@ -70,6 +66,7 @@ public class MapFragment extends FragmentBase<FragmentMapBinding> {
/** /**
* Creates and adds a marker for a tracked item * Creates and adds a marker for a tracked item
*
* @param item the tracked item to add a marker for * @param item the tracked item to add a marker for
*/ */
private void addMarker(@NonNull TrackedItem item) { private void addMarker(@NonNull TrackedItem item) {
@ -103,7 +100,8 @@ public class MapFragment extends FragmentBase<FragmentMapBinding> {
var map = this.getBinding().map; var map = this.getBinding().map;
map.getOverlays().clear(); map.getOverlays().clear();
this.subscribe(this.getClient().toObservable() this.subscribe(this.getClient().toObservable()
.flatMap(client -> client.streamObjects().toObservable()), .flatMap(client -> client.streamObjects().toObservable())
.filter(item -> item.lastKnownLocation().isPresent()),
this::addMarker); this::addMarker);
} }

View file

@ -18,6 +18,7 @@ public class MapMenuProvider implements MenuProvider {
/** /**
* Creates a new instance of {@link MapMenuProvider} * Creates a new instance of {@link MapMenuProvider}
*
* @param mapFragment the fragment * @param mapFragment the fragment
*/ */
public MapMenuProvider(MapFragment mapFragment) { public MapMenuProvider(MapFragment mapFragment) {

View file

@ -58,6 +58,7 @@ public class NearbyFragment extends FragmentBase<FragmentNearbyBinding> {
/** /**
* Change the filter mode. * Change the filter mode.
*
* @param filterMode The new filter mode. * @param filterMode The new filter mode.
*/ */
void switchFilter(ListFilterMode filterMode) { void switchFilter(ListFilterMode filterMode) {
@ -87,6 +88,7 @@ public class NearbyFragment extends FragmentBase<FragmentNearbyBinding> {
/** /**
* Called when the location response is received. * Called when the location response is received.
*
* @param location The location. * @param location The location.
*/ */
private void onLocationResponse(@NonNull GeoLocation location) { private void onLocationResponse(@NonNull GeoLocation location) {

View file

@ -6,7 +6,6 @@ import android.view.MenuItem;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.view.MenuProvider; import androidx.core.view.MenuProvider;
import androidx.navigation.fragment.NavHostFragment;
/** /**
* The menu provider for {@link NearbyFragment} * The menu provider for {@link NearbyFragment}
@ -19,6 +18,7 @@ class NearbyMenuProvider implements MenuProvider {
/** /**
* Creates a new instance of {@link NearbyMenuProvider} * Creates a new instance of {@link NearbyMenuProvider}
*
* @param nearbyFragment the fragment * @param nearbyFragment the fragment
*/ */
public NearbyMenuProvider(NearbyFragment nearbyFragment) { public NearbyMenuProvider(NearbyFragment nearbyFragment) {

View file

@ -18,6 +18,7 @@ class NearbyOnTouchListener implements RecyclerView.OnItemTouchListener {
/** /**
* Creates a new instance of {@link NearbyOnTouchListener} * Creates a new instance of {@link NearbyOnTouchListener}
*
* @param nearbyFragment the fragment * @param nearbyFragment the fragment
*/ */
public NearbyOnTouchListener(NearbyFragment nearbyFragment) { public NearbyOnTouchListener(NearbyFragment nearbyFragment) {

View file

@ -8,7 +8,6 @@ import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.print.PrintHelper; import androidx.print.PrintHelper;
import com.google.zxing.BarcodeFormat; import com.google.zxing.BarcodeFormat;

View file

@ -29,7 +29,7 @@ import java.util.regex.Pattern;
import rs.chir.cv.QRAnalyzer; import rs.chir.cv.QRAnalyzer;
import rs.chir.invtracker.client.databinding.FragmentQrScanBinding; import rs.chir.invtracker.client.databinding.FragmentQrScanBinding;
import rs.chir.invtracker.model.GeoLocation; import rs.chir.invtracker.model.GeoLocation;
import rs.chir.invtracker.utils.ListenableFutureAdapter; import rs.chir.invtracker.utils.RXJavaAdapters;
import rs.chir.invtracker.utils.SingleLocation; import rs.chir.invtracker.utils.SingleLocation;
import rs.chir.utils.math.Vec2; import rs.chir.utils.math.Vec2;
@ -121,6 +121,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
/** /**
* Returns the size of the application window in pixels * Returns the size of the application window in pixels
*
* @return the size of the application window in pixels * @return the size of the application window in pixels
*/ */
@NonNull @NonNull
@ -139,7 +140,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
private void startCamera() { private void startCamera() {
// request the camera provider // request the camera provider
this.subscribe(new ListenableFutureAdapter<>(ProcessCameraProvider.getInstance(this.requireContext()), ContextCompat.getMainExecutor(this.requireContext())), this::openedCamera); this.subscribe(RXJavaAdapters.fromListenableFuture(ProcessCameraProvider.getInstance(this.requireContext()), ContextCompat.getMainExecutor(this.requireContext())), this::openedCamera);
} }
@Override @Override
@ -164,6 +165,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
/** /**
* Show or hide the viewfinder * Show or hide the viewfinder
*
* @param hide true to hide the viewfinder, false to show it * @param hide true to hide the viewfinder, false to show it
*/ */
private void hideUI(boolean b) { private void hideUI(boolean b) {
@ -175,6 +177,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
/** /**
* Update the location of the object * Update the location of the object
*
* @param id the object id * @param id the object id
* @param location the location of the object * @param location the location of the object
*/ */
@ -193,6 +196,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
/** /**
* Show the detail view of the object * Show the detail view of the object
*
* @param id the object id * @param id the object id
*/ */
private void showDetailView(long id) { private void showDetailView(long id) {
@ -201,6 +205,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
/** /**
* Initializes the camera * Initializes the camera
*
* @param cameraProvider the camera provider * @param cameraProvider the camera provider
*/ */
private void openedCamera(@NonNull ProcessCameraProvider cameraProvider) { private void openedCamera(@NonNull ProcessCameraProvider cameraProvider) {

View file

@ -7,7 +7,6 @@ import androidx.datastore.preferences.core.Preferences;
import androidx.datastore.preferences.core.PreferencesKeys; import androidx.datastore.preferences.core.PreferencesKeys;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import rs.chir.compat.java.util.Objects;
import rs.chir.compat.java.util.Optional; import rs.chir.compat.java.util.Optional;
import rs.chir.invtracker.client.Application; import rs.chir.invtracker.client.Application;

View file

@ -14,13 +14,10 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import rs.chir.compat.java.util.Optional; import rs.chir.compat.java.util.Optional;
import rs.chir.invtracker.api.Client;
import rs.chir.invtracker.client.Application; import rs.chir.invtracker.client.Application;
import rs.chir.invtracker.client.R; import rs.chir.invtracker.client.R;
import rs.chir.invtracker.model.GeoRect; import rs.chir.invtracker.model.GeoRect;
@ -166,6 +163,7 @@ public class ItemListAdapter extends RecyclerView.Adapter<ItemListAdapter.ViewHo
/** /**
* Creates a new view holder. * Creates a new view holder.
*
* @param itemView the view to use. * @param itemView the view to use.
*/ */
ViewHolder(@NonNull View view) { ViewHolder(@NonNull View view) {
@ -178,6 +176,7 @@ public class ItemListAdapter extends RecyclerView.Adapter<ItemListAdapter.ViewHo
/** /**
* Binds the data to the view. * Binds the data to the view.
*
* @param item the item to bind. * @param item the item to bind.
*/ */
void bind(@NonNull TrackedItem item) { void bind(@NonNull TrackedItem item) {

View file

@ -22,12 +22,9 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import rs.chir.invtracker.api.Client;
import rs.chir.invtracker.client.Application; import rs.chir.invtracker.client.Application;
import rs.chir.invtracker.client.R; import rs.chir.invtracker.client.R;
import rs.chir.invtracker.model.GeoLocation; import rs.chir.invtracker.model.GeoLocation;
@ -83,6 +80,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
/** /**
* Creates a new view holder. * Creates a new view holder.
*
* @param parent the parent view group. * @param parent the parent view group.
* @param viewType the view type. * @param viewType the view type.
*/ */
@ -97,6 +95,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
/** /**
* Binds the view holder to the data. * Binds the view holder to the data.
*
* @param holder the view holder. * @param holder the view holder.
* @param position the position in the list. * @param position the position in the list.
*/ */
@ -120,6 +119,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
/** /**
* Returns the number of items in the list. * Returns the number of items in the list.
*
* @return the number of items in the list. * @return the number of items in the list.
*/ */
@Override @Override
@ -151,6 +151,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
/** /**
* Creates a new view holder. * Creates a new view holder.
*
* @param view the view to use. * @param view the view to use.
* @param context the context to use for accessing the map resources. * @param context the context to use for accessing the map resources.
*/ */
@ -170,6 +171,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
/** /**
* Binds the view holder to the data. * Binds the view holder to the data.
*
* @param location the location to bind to. * @param location the location to bind to.
*/ */
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")

View file

@ -29,6 +29,7 @@ public class LocationRounder {
/** /**
* Constructs a new {@link LocationRounder}. * Constructs a new {@link LocationRounder}.
*
* @param digits the number of decimal places to round to. * @param digits the number of decimal places to round to.
* @param method the method to use for rounding. * @param method the method to use for rounding.
*/ */
@ -40,6 +41,7 @@ public class LocationRounder {
/** /**
* Constructs a new location rounder from the application preferences. * Constructs a new location rounder from the application preferences.
*
* @param context the context to use for accessing the preferences datastore. * @param context the context to use for accessing the preferences datastore.
* @return a {@link LocationRounder} constructed from the application preferences. * @return a {@link LocationRounder} constructed from the application preferences.
*/ */
@ -53,6 +55,7 @@ public class LocationRounder {
/** /**
* Rounds double to the nearest multiple of 10^-digits. * Rounds double to the nearest multiple of 10^-digits.
*
* @param value the value to round. * @param value the value to round.
* @return the rounded value. * @return the rounded value.
*/ */
@ -63,6 +66,7 @@ public class LocationRounder {
/** /**
* Truncates double to a multiple of 10^-digits. * Truncates double to a multiple of 10^-digits.
*
* @param value the value to truncate. * @param value the value to truncate.
* @return the truncated value. * @return the truncated value.
*/ */
@ -75,6 +79,7 @@ public class LocationRounder {
/** /**
* Randomizes double into the range <code>[trunc(val), trunc(val) + 1)</code>. * Randomizes double into the range <code>[trunc(val), trunc(val) + 1)</code>.
*
* @param val the value to randomize. * @param val the value to randomize.
* @return the randomized value. * @return the randomized value.
*/ */
@ -86,6 +91,7 @@ public class LocationRounder {
/** /**
* Performs the rounding operation. * Performs the rounding operation.
*
* @param val the value to round. * @param val the value to round.
* @return the rounded value. * @return the rounded value.
*/ */
@ -100,6 +106,7 @@ public class LocationRounder {
/** /**
* Rounds a {@link GeoLocation}. * Rounds a {@link GeoLocation}.
*
* @param location the location to round. * @param location the location to round.
* @return the rounded location. * @return the rounded location.
*/ */

View file

@ -27,6 +27,7 @@ public enum ServerPublicKey {
/** /**
* Returns a flowable for the serialized public key. * Returns a flowable for the serialized public key.
*
* @param context the context to use for accessing the preferences datastore. * @param context the context to use for accessing the preferences datastore.
* @return a flowable for the serialized public key. * @return a flowable for the serialized public key.
*/ */

View file

@ -14,6 +14,7 @@ import java.io.IOException;
public interface BitmapStorage { public interface BitmapStorage {
/** /**
* Creates an instance of {@link BitmapStorage} optimized for the current android version. * Creates an instance of {@link BitmapStorage} optimized for the current android version.
*
* @param context The application context. * @param context The application context.
*/ */
@NonNull @NonNull
@ -27,6 +28,7 @@ public interface BitmapStorage {
/** /**
* Saves the bitmap to the storage. * Saves the bitmap to the storage.
*
* @param bitmap The bitmap to save. * @param bitmap The bitmap to save.
* @param fileName The name of the file to save the bitmap to. * @param fileName The name of the file to save the bitmap to.
* @throws IOException If the bitmap cannot be saved. * @throws IOException If the bitmap cannot be saved.

View file

@ -26,6 +26,7 @@ public class BitmapStorageQ implements BitmapStorage {
/** /**
* Creates a new instance of {@link BitmapStorageQ}. * Creates a new instance of {@link BitmapStorageQ}.
*
* @param context the context to use. * @param context the context to use.
*/ */
public BitmapStorageQ(@NonNull Context context) { public BitmapStorageQ(@NonNull Context context) {

View file

@ -17,6 +17,7 @@ public enum BitmapUtils {
/** /**
* Saves the bitmap to file * Saves the bitmap to file
*
* @param bitmap The bitmap to save * @param bitmap The bitmap to save
* @param file The file to save to * @param file The file to save to
* @throws IOException If the file cannot be saved * @throws IOException If the file cannot be saved
@ -28,6 +29,7 @@ public enum BitmapUtils {
/** /**
* Saves the bitmap to an output stream * Saves the bitmap to an output stream
*
* @param bitmap The bitmap to save * @param bitmap The bitmap to save
* @param os The output stream to save to * @param os The output stream to save to
* @throws IOException If the file cannot be saved * @throws IOException If the file cannot be saved

View file

@ -1,47 +0,0 @@
package rs.chir.invtracker.utils;
import java.io.IOException;
import java.util.List;
import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.core.SingleObserver;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;
/**
* Adapts a {@link Call} to a {@link Single}.
*/
public class CallAdapter extends Single<Response> implements Callback {
/**
* Subscribers to the call.
*/
private final List<SingleObserver<? super Response>> subscribers = new java.util.ArrayList<>(1);
/**
* Creates a new adapter.
*/
public CallAdapter(@androidx.annotation.NonNull Call call) {
call.enqueue(this);
}
@Override
protected void subscribeActual(@NonNull SingleObserver<? super Response> observer) {
subscribers.add(observer);
}
@Override
public void onFailure(@androidx.annotation.NonNull Call call, @androidx.annotation.NonNull IOException e) {
for (var subscriber : subscribers) {
subscriber.onError(e);
}
}
@Override
public void onResponse(@androidx.annotation.NonNull Call call, @androidx.annotation.NonNull Response response) throws IOException {
for (var subscriber : subscribers) {
subscriber.onSuccess(response);
}
}
}

View file

@ -15,6 +15,7 @@ public enum ConnectionScorer {
/** /**
* Check if the data saver is enabled * Check if the data saver is enabled
*
* @param cm the connectivity manager * @param cm the connectivity manager
* @return true if the data saver is enabled * @return true if the data saver is enabled
*/ */
@ -27,6 +28,7 @@ public enum ConnectionScorer {
/** /**
* Scores the connection quality. * Scores the connection quality.
*
* @param context * @param context
* @return the connection quality score * @return the connection quality score
*/ */

View file

@ -1,84 +0,0 @@
package rs.chir.invtracker.utils;
import org.reactivestreams.Subscriber;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
import rs.chir.compat.java.util.Optional;
import rs.chir.compat.java.util.function.Function;
import rs.chir.invtracker.model.Cursor;
import rs.chir.utils.xml.XMLSerializable;
/**
* Adapts a {@link Cursor} to a {@link Flowable}.
*/
public class CursorStreamable<T extends XMLSerializable> extends Flowable<T> {
/**
* Subscribers
*/
private final List<Subscriber<? super T>> subscribers = new ArrayList<>(1);
/**
* The cursor supplier
*/
private final Function<Optional<String>, Single<Cursor<T>>> cursorSupplier;
/**
* Whether the loading has started. We dont start streaming data until the first subscriber is
* added.
*/
private final AtomicBoolean isLoading = new AtomicBoolean(false);
/**
* Creates a new streamable.
* @param cursorSupplier the cursor supplier
*/
public CursorStreamable(@NonNull Function<Optional<String>, Single<Cursor<T>>> cursorSupplier) {
this.cursorSupplier = cursorSupplier;
}
@Override
protected void subscribeActual(@NonNull Subscriber<? super T> subscriber) {
subscribers.add(subscriber);
if (this.isLoading.compareAndSet(false, true)) {
this.loadNextChunk(Optional.empty());
}
}
/**
* Loads the next chunk of data.
* @param lastId the ID of the last item in the previous chunk
*/
private void loadNextChunk(Optional<String> lastId) {
this.cursorSupplier.apply(lastId)
.subscribeOn(Schedulers.io())
.subscribe(cursor -> {
for (var item : cursor.items()) {
// send all items to all subscribers
for (var subscriber : subscribers) {
subscriber.onNext(item);
}
}
// While the nextId should be empty if there are no more items,
// some places may still return it, so we check for how many items are in the cursor.
if (cursor.nextId().isEmpty() || cursor.items().isEmpty()) {
// no more items to load, tell that to all subscribers
for (var subscriber : subscribers) {
subscriber.onComplete();
}
} else {
// otherwise, load the next chunk
this.loadNextChunk(cursor.nextId());
}
}, t -> {
// notify all subscribers of the error
for (var subscriber : subscribers) {
subscriber.onError(t);
}
});
}
}

View file

@ -1,42 +0,0 @@
package rs.chir.invtracker.utils;
import androidx.annotation.NonNull;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.core.SingleObserver;
/**
* Adapts a {@link ListenableFuture} to a {@link Single}.
*/
public class ListenableFutureAdapter<T> extends Single<T> {
/**
* The subscribers that are waiting for the future to complete.
*/
private final List<SingleObserver<? super T>> subscribers = new java.util.ArrayList<>(1);
public ListenableFutureAdapter(@NonNull ListenableFuture<T> future, @NonNull Executor executor) {
future.addListener(() -> {
try {
var result = future.get();
for (var subscriber : subscribers) {
subscriber.onSuccess(result);
}
} catch (InterruptedException | ExecutionException e) {
for (var subscriber : subscribers) {
subscriber.onError(e);
}
}
}, executor);
}
@Override
protected void subscribeActual(@NonNull SingleObserver<? super T> observer) {
subscribers.add(observer);
}
}

View file

@ -0,0 +1,105 @@
package rs.chir.invtracker.utils;
import androidx.annotation.NonNull;
import com.google.android.gms.tasks.Task;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.FlowableEmitter;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;
import rs.chir.compat.java.util.Optional;
import rs.chir.compat.java.util.function.Function;
import rs.chir.invtracker.model.Cursor;
import rs.chir.utils.xml.XMLSerializable;
/**
* Various adapters to convert to {@link Single}s or {@link Flowable}s.
*/
public enum RXJavaAdapters {
;
/**
* Adapts a {@link Call} to a {@link Single}.
*
* @param call the call to adapt
* @return the single
*/
@NonNull
public static Single<Response> fromCall(@NonNull Call call) {
return Single.create(subscriber -> call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
subscriber.onError(e);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
subscriber.onSuccess(response);
}
}));
}
private static <T extends XMLSerializable> void loadNextChunk(@NonNull Function<Optional<String>, Single<Cursor<T>>> cursorSupplier, @NonNull FlowableEmitter<T> subscriber, Optional<String> lastId) {
var d = cursorSupplier.apply(lastId)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(cursor -> {
for (var item : cursor.items()) {
subscriber.onNext(item);
}
if (cursor.nextId().isEmpty() || cursor.items().isEmpty()) {
subscriber.onComplete();
} else {
RXJavaAdapters.loadNextChunk(cursorSupplier, subscriber, cursor.nextId());
}
}, subscriber::onError);
subscriber.setDisposable(d);
}
@NonNull
public static <T extends XMLSerializable> Flowable<T> fromCursor(@NonNull Function<Optional<String>, Single<Cursor<T>>> cursorSupplier) {
return Flowable.create(subscriber -> {
RXJavaAdapters.loadNextChunk(cursorSupplier, subscriber, Optional.empty());
}, BackpressureStrategy.BUFFER);
}
@NonNull
public static <T> Single<T> fromListenableFuture(@NonNull ListenableFuture<T> future, @NonNull Executor executor) {
return Single.create(subscriber -> {
future.addListener(() -> {
try {
var result = future.get();
subscriber.onSuccess(result);
} catch (InterruptedException | ExecutionException e) {
subscriber.onError(e);
}
}, executor);
});
}
@NonNull
public static <T> Single<Optional<T>> fromTask(@NonNull Task<T> task) {
if (task.isComplete()) {
if (task.isSuccessful()) {
return Single.just(Optional.of(task.getResult()));
} else {
return Single.error(task.getException());
}
} else {
return Single.create(subscriber -> {
task.addOnSuccessListener(v -> subscriber.onSuccess(Optional.ofNullable(v)));
task.addOnFailureListener(subscriber::onError);
});
}
}
}

View file

@ -87,6 +87,7 @@ public class SingleBackoff<T> extends Single<T> {
/** /**
* Reports the success of the single * Reports the success of the single
*
* @param result the result of the single * @param result the result of the single
*/ */
private void onSuccess(T result) { private void onSuccess(T result) {
@ -98,6 +99,7 @@ public class SingleBackoff<T> extends Single<T> {
/** /**
* Reports the error of the single * Reports the error of the single
*
* @param error the error of the single * @param error the error of the single
*/ */
private void onError(Throwable error) { private void onError(Throwable error) {

View file

@ -17,11 +17,8 @@ import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.location.Priority; import com.google.android.gms.location.Priority;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.core.SingleObserver;
import rs.chir.compat.java.util.OptionalDouble; import rs.chir.compat.java.util.OptionalDouble;
import rs.chir.invtracker.client.model.LocationRounder; import rs.chir.invtracker.client.model.LocationRounder;
import rs.chir.invtracker.model.GeoLocation; import rs.chir.invtracker.model.GeoLocation;
@ -29,38 +26,8 @@ import rs.chir.invtracker.model.GeoLocation;
/** /**
* {@link Single} implementation that returns a location * {@link Single} implementation that returns a location
*/ */
public class SingleLocation extends Single<GeoLocation> { public enum SingleLocation {
/** ;
* List of subscribers to this single.
*/
final List<SingleObserver<? super GeoLocation>> observers = new ArrayList<>(1);
/**
* Creates a new Location Single
*
* @param client the location client to use for getting the location
* @param locationRequest the location request to use for getting the location
* @param context the context to use for rounding the location
*/
@SuppressLint("MissingPermission")
private SingleLocation(@NonNull FusedLocationProviderClient client, LocationRequest request, Context context) {
// Request a location update
client.requestLocationUpdates(request, new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
var location = locationResult.getLastLocation();
if (location != null) {
for (var observer : observers) {
observer.onSuccess(SingleLocation.fromAndroidLocation(location, context));
}
} else {
for (var observer : observers) {
observer.onError(new AndroidException("No location available"));
}
}
}
}, Looper.getMainLooper());
}
/** /**
* Converts an Android location to a GeoLocation * Converts an Android location to a GeoLocation
@ -85,6 +52,7 @@ public class SingleLocation extends Single<GeoLocation> {
/** /**
* Retrieve the most recent location, requesting a location update if necessary. * Retrieve the most recent location, requesting a location update if necessary.
*
* @param client the location client to use for getting the location * @param client the location client to use for getting the location
* @param context the context to use for rounding the location * @param context the context to use for rounding the location
* @return the most recent location * @return the most recent location
@ -93,7 +61,7 @@ public class SingleLocation extends Single<GeoLocation> {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
public static Single<GeoLocation> getSingleLocation(@NonNull FusedLocationProviderClient client, @NonNull Context context) { public static Single<GeoLocation> getSingleLocation(@NonNull FusedLocationProviderClient client, @NonNull Context context) {
// Try accessing the last location // Try accessing the last location
return TaskAdapter.fromTask(client.getLastLocation()) return RXJavaAdapters.fromTask(client.getLastLocation())
.flatMap(location -> { .flatMap(location -> {
if (location.isPresent()) { if (location.isPresent()) {
return Single.just(SingleLocation.fromAndroidLocation(location.get(), context)); return Single.just(SingleLocation.fromAndroidLocation(location.get(), context));
@ -106,10 +74,12 @@ public class SingleLocation extends Single<GeoLocation> {
/** /**
* Retrieves a location update, requesting a location update if necessary. * Retrieves a location update, requesting a location update if necessary.
*
* @param client the location client to use for getting the location * @param client the location client to use for getting the location
* @param context the context to use for rounding the location * @param context the context to use for rounding the location
* @return the next location * @return the next location
*/ */
@SuppressLint("MissingPermission")
@NonNull @NonNull
public static Single<GeoLocation> getNextLocation(@NonNull FusedLocationProviderClient client, @NonNull Context context) { public static Single<GeoLocation> getNextLocation(@NonNull FusedLocationProviderClient client, @NonNull Context context) {
var request = LocationRequest.create(); var request = LocationRequest.create();
@ -122,12 +92,19 @@ public class SingleLocation extends Single<GeoLocation> {
.addLocationRequest(request); .addLocationRequest(request);
// TODO: you could probably cache the settings client? // TODO: you could probably cache the settings client?
var settingsClient = LocationServices.getSettingsClient(context); var settingsClient = LocationServices.getSettingsClient(context);
return TaskAdapter.fromTask(settingsClient.checkLocationSettings(builder.build())) return RXJavaAdapters.fromTask(settingsClient.checkLocationSettings(builder.build()))
.flatMap(resp -> new SingleLocation(client, request, context)); .flatMap(resp -> Single.create(subscriber -> {
} client.requestLocationUpdates(request, new LocationCallback() {
@Override @Override
protected void subscribeActual(@io.reactivex.rxjava3.annotations.NonNull SingleObserver<? super GeoLocation> observer) { public void onLocationResult(@NonNull LocationResult locationResult) {
observers.add(observer); var location = locationResult.getLastLocation();
if (location != null) {
subscriber.onSuccess(SingleLocation.fromAndroidLocation(location, context));
} else {
subscriber.onError(new AndroidException("No location available"));
}
}
}, Looper.getMainLooper());
}));
} }
} }

View file

@ -1,73 +0,0 @@
package rs.chir.invtracker.utils;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.android.gms.tasks.Task;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.core.SingleObserver;
import rs.chir.compat.java.util.Optional;
/**
* Single adapter for a GMS {@link Task};
* @param <T> the type of the result of the task.
*/
public class TaskAdapter<T> extends Single<Optional<T>> {
/**
* The subscribers to the task.
*/
private final List<SingleObserver<? super Optional<T>>> observers = new ArrayList<>(1);
/**
* Creates a new adapter.
* @param task the task to wrap.
*/
private TaskAdapter(@NonNull Task<T> task) {
task.addOnCompleteListener(result -> {
Log.i("TaskAdapter", "Task completed: " + result);
if (result.isSuccessful()) {
// rxjava does not like nulls, so we use Optional.empty() instead.
var out = Optional.ofNullable(result.getResult());
for (var observer : observers) {
observer.onSuccess(out);
}
} else {
for (var observer : observers) {
observer.onError(result.getException());
}
}
});
}
/**
* Creates a new adapter from a task
*
* @param task the task to wrap.
* @param <T> the type of the result of the task.
* @return the adapter.
*/
@NonNull
public static <T> Single<Optional<T>> fromTask(@NonNull Task<T> task) {
if (task.isComplete()) {
// if the task is successful, we dont need to wrap it in an adapter.
if (task.isSuccessful()) {
return Single.just(Optional.ofNullable(task.getResult()));
} else {
return Single.error(Objects.requireNonNull(task.getException()));
}
} else {
return new TaskAdapter<>(task);
}
}
@Override
protected void subscribeActual(@io.reactivex.rxjava3.annotations.NonNull SingleObserver<? super Optional<T>> observer) {
observers.add(observer);
}
}

View file

@ -84,7 +84,7 @@
<fragment <fragment
android:id="@+id/itemDetailFragment" android:id="@+id/itemDetailFragment"
android:name="rs.chir.invtracker.client.ItemDetailFragment" android:name="rs.chir.invtracker.client.ItemDetailFragment"
android:label="fragment_item_detail" android:label="@string/fragment_item_detail"
tools:layout="@layout/fragment_item_detail"> tools:layout="@layout/fragment_item_detail">
<argument <argument
android:name="id" android:name="id"

View file

@ -4,6 +4,7 @@
<string name="action_settings">Einstellungen</string> <string name="action_settings">Einstellungen</string>
<string name="load_fragment_label">Invtracker</string> <string name="load_fragment_label">Invtracker</string>
<string name="nearby_fragment_label">Gegenstandsliste</string> <string name="nearby_fragment_label">Gegenstandsliste</string>
<string name="fragment_item_detail">Objektdetails</string>
<string name="login_fragment_label">Login</string> <string name="login_fragment_label">Login</string>
<string name="map_fragment_label">Karte</string> <string name="map_fragment_label">Karte</string>
<string name="loading_text">Verbindung mit dem Server aufbauen…</string> <string name="loading_text">Verbindung mit dem Server aufbauen…</string>

View file

@ -84,4 +84,5 @@
<string name="qr_scan_fragment_label">QR Code Scanner</string> <string name="qr_scan_fragment_label">QR Code Scanner</string>
<string name="qr_code_fragment">QR Code</string> <string name="qr_code_fragment">QR Code</string>
<string name="edit_item_fragment">Edit Item</string> <string name="edit_item_fragment">Edit Item</string>
<string name="fragment_item_detail">Item Detail</string>
</resources> </resources>

View file

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<paths> <paths>
<cache-path name="cache_files" path="."/> <cache-path
name="cache_files"
path="." />
</paths> </paths>

View file

@ -1,4 +1,4 @@
#Wed Aug 10 11:16:39 GMT 2022 #Mon Aug 15 10:52:08 GMT 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists