Replace the rxjava adapters with better alternatives
This commit is contained in:
parent
eefd8134c3
commit
a146ae77f1
42 changed files with 362 additions and 428 deletions
|
@ -51,6 +51,7 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
|
|||
|
||||
/**
|
||||
* Converts an RGB value into a luminance value.
|
||||
*
|
||||
* @param r The red 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].
|
||||
|
@ -62,6 +63,7 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
|
|||
|
||||
/**
|
||||
* Converts an RGB value into a luminance value.
|
||||
*
|
||||
* @param r The red 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].
|
||||
|
@ -76,6 +78,7 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
|
|||
|
||||
/**
|
||||
* Retrieves a plane proxy from the image proxy.
|
||||
*
|
||||
* @param planeIndex the plane index to retrieve.
|
||||
* @return the plane proxy.
|
||||
*/
|
||||
|
@ -85,9 +88,10 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
|
|||
|
||||
/**
|
||||
* Retrieves a single pixel on a plane from the image.
|
||||
*
|
||||
* @param planeIndex the plane index to retrieve the pixel from.
|
||||
* @param x the x coordinate of the pixel.
|
||||
* @param y the y coordinate of the pixel.
|
||||
* @param x the x coordinate of the pixel.
|
||||
* @param y the y coordinate of the pixel.
|
||||
* @return the pixel.
|
||||
*/
|
||||
private byte getOnPlane(int planeIndex, int x, int y) {
|
||||
|
@ -101,6 +105,7 @@ public class ImageProxyLuminanceSource extends LuminanceSource {
|
|||
|
||||
/**
|
||||
* Retrieves the luminance value of a single pixel from the image.
|
||||
*
|
||||
* @param x the x coordinate of the pixel.
|
||||
* @param y the y coordinate of the pixel.
|
||||
* @return the pixel.
|
||||
|
|
|
@ -13,7 +13,7 @@ import rs.chir.compat.java.util.function.Function;
|
|||
|
||||
/**
|
||||
* {@link ImageAnalysis.Analyzer} for QR codes.
|
||||
*
|
||||
* <p>
|
||||
* When a QR code is found, the analyzer will call the {@link QRAnalyzerListener} callback.
|
||||
*
|
||||
* @author Morten Delenk
|
||||
|
@ -31,6 +31,7 @@ public class QRAnalyzer implements ImageAnalysis.Analyzer {
|
|||
|
||||
/**
|
||||
* Creates a new QR analyzer.
|
||||
*
|
||||
* @param listener The listener to call when a QR code is found.
|
||||
*/
|
||||
public QRAnalyzer(@NonNull QRAnalyzerListener listener) {
|
||||
|
@ -52,6 +53,7 @@ public class QRAnalyzer implements ImageAnalysis.Analyzer {
|
|||
|
||||
/**
|
||||
* Analyzes the given {@link ImageProxy}.
|
||||
*
|
||||
* @param image The image to analyze.
|
||||
*/
|
||||
@Override
|
||||
|
@ -67,6 +69,7 @@ public class QRAnalyzer implements ImageAnalysis.Analyzer {
|
|||
public interface QRAnalyzerListener {
|
||||
/**
|
||||
* Called when a QR code is found.
|
||||
*
|
||||
* @param qrCode The contents of the QR code.
|
||||
*/
|
||||
void onQRFound(@NonNull String qrCode);
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
/**
|
||||
* Module for computer vision on Android.
|
||||
*
|
||||
* <p>
|
||||
* Currently supports:
|
||||
*
|
||||
* <ul>
|
||||
* <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>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
*/
|
||||
package rs.chir.cv;
|
|
@ -36,11 +36,13 @@ public class CachedClientImpl extends Client {
|
|||
private final Context context;
|
||||
/**
|
||||
* Cached objects.
|
||||
*
|
||||
* @see TrackedItem
|
||||
*/
|
||||
private final Map<Long, TrackedItem> objects = new java.util.HashMap<>(10);
|
||||
/**
|
||||
* Cached public key.
|
||||
*
|
||||
* @see PublicKey
|
||||
*/
|
||||
private Optional<PublicKey> publicKey = Optional.empty();
|
||||
|
@ -51,8 +53,9 @@ public class CachedClientImpl extends Client {
|
|||
|
||||
/**
|
||||
* Creates a new cached client.
|
||||
*
|
||||
* @param context the context used to retrieve network connectivity information
|
||||
* @param client the underlying client
|
||||
* @param client the underlying client
|
||||
*/
|
||||
public CachedClientImpl(@NonNull Context context, @NonNull Client client) {
|
||||
this.client = client;
|
||||
|
@ -61,6 +64,7 @@ public class CachedClientImpl extends Client {
|
|||
|
||||
/**
|
||||
* Returns whether aggressive caching is desired.
|
||||
*
|
||||
* @return true if network connectivity is limited
|
||||
*/
|
||||
private boolean aggressivelyCache() {
|
||||
|
@ -96,6 +100,7 @@ public class CachedClientImpl extends Client {
|
|||
|
||||
/**
|
||||
* Caches the given object.
|
||||
*
|
||||
* @param trackedItem the object to cache
|
||||
*/
|
||||
private void cacheObject(TrackedItem trackedItem) {
|
||||
|
@ -148,7 +153,7 @@ public class CachedClientImpl extends Client {
|
|||
});
|
||||
}
|
||||
Optional<String> next = Optional.empty();
|
||||
if(items.size() != 0) {
|
||||
if (items.size() != 0) {
|
||||
next = Optional.of(String.valueOf(items.get(items.size() - 1).id()));
|
||||
}
|
||||
return Single.just(new Cursor<>(items, next));
|
||||
|
|
|
@ -15,7 +15,7 @@ import rs.chir.invtracker.model.GeoRect;
|
|||
import rs.chir.invtracker.model.PasetoToken;
|
||||
import rs.chir.invtracker.model.PublicKey;
|
||||
import rs.chir.invtracker.model.TrackedItem;
|
||||
import rs.chir.invtracker.utils.CursorStreamable;
|
||||
import rs.chir.invtracker.utils.RXJavaAdapters;
|
||||
|
||||
/**
|
||||
* The API Client abstract base class.
|
||||
|
@ -29,6 +29,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Gets the public key.
|
||||
*
|
||||
* @return A single that emits the public key
|
||||
* @see PublicKey
|
||||
* @see Single
|
||||
|
@ -38,6 +39,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Attempts to log in with the given credentials.
|
||||
*
|
||||
* @param username the username
|
||||
* @param password the password
|
||||
* @return A single that emits the API token
|
||||
|
@ -49,6 +51,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Retrieves an item with the given ID.
|
||||
*
|
||||
* @param id the item ID
|
||||
* @return A single that emits the item
|
||||
* @see TrackedItem
|
||||
|
@ -59,6 +62,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Retrieves a page of items.
|
||||
*
|
||||
* @param cont An optional continuation token
|
||||
* @return A single that emits a page of items
|
||||
* @see TrackedItem
|
||||
|
@ -70,17 +74,19 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Retrieve all items
|
||||
*
|
||||
* @return A flowable that emits all items
|
||||
* @see TrackedItem
|
||||
* @see Flowable
|
||||
*/
|
||||
@NonNull
|
||||
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.
|
||||
*
|
||||
* @param rect The rectangle
|
||||
* @param cont An optional continuation token
|
||||
* @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.
|
||||
*
|
||||
* @param rect The rectangle
|
||||
* @return A flowable that emits all items
|
||||
* @see TrackedItem
|
||||
|
@ -100,11 +107,12 @@ public abstract class Client {
|
|||
*/
|
||||
@NonNull
|
||||
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.
|
||||
*
|
||||
* @param rect The rectangle
|
||||
* @return A flowable that emits all matching items
|
||||
* @see TrackedItem
|
||||
|
@ -117,6 +125,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Retrieves a page of items that are within the given rectangle.
|
||||
*
|
||||
* @param rect The rectangle
|
||||
* @param cont An optional continuation token
|
||||
* @return A single that emits a page of items
|
||||
|
@ -128,7 +137,8 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Updates the location of the given item.
|
||||
* @param item The item
|
||||
*
|
||||
* @param item The item
|
||||
* @param location The location
|
||||
* @return A single that emits true on success
|
||||
* @see Single
|
||||
|
@ -140,7 +150,8 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Updates the location of the given item.
|
||||
* @param itemId The item ID
|
||||
*
|
||||
* @param itemId The item ID
|
||||
* @param location The location
|
||||
* @return A single that emits true on success
|
||||
* @see Single
|
||||
|
@ -150,6 +161,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Starts a download for an image.
|
||||
*
|
||||
* @param imageUrl The image URL
|
||||
* @return A single that emits the image input stream
|
||||
*/
|
||||
|
@ -158,8 +170,9 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Retrieves a page of locations for the given item.
|
||||
*
|
||||
* @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
|
||||
* @see GeoLocation
|
||||
* @see Cursor
|
||||
|
@ -169,6 +182,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Retrieves all locations for the given item.
|
||||
*
|
||||
* @param itemId The item ID
|
||||
* @return A flowable that emits all locations
|
||||
* @see GeoLocation
|
||||
|
@ -176,11 +190,12 @@ public abstract class Client {
|
|||
*/
|
||||
@NonNull
|
||||
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.
|
||||
*
|
||||
* @param uri The source URI
|
||||
* @return The URL of the uploaded image
|
||||
* @see Single
|
||||
|
@ -191,6 +206,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Updates an item.
|
||||
*
|
||||
* @param item The item
|
||||
* @return A single that emits the updated item
|
||||
* @see Single
|
||||
|
@ -201,6 +217,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Creates a new item.
|
||||
*
|
||||
* @param item The item
|
||||
* @return A single that emits the created item
|
||||
* @see Single
|
||||
|
@ -211,6 +228,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Deletes an item.
|
||||
*
|
||||
* @param id The item id
|
||||
* @return A single that emits true on success
|
||||
* @see Single
|
||||
|
|
|
@ -8,11 +8,13 @@ import rs.chir.invtracker.utils.SingleBackoff;
|
|||
|
||||
/**
|
||||
* An extended version of {@link SingleBackoff} that errors out on {@link UnauthorizedException}.
|
||||
*
|
||||
* @param <T> the type of the result
|
||||
*/
|
||||
class ClientBackoff<T> extends SingleBackoff<T> {
|
||||
/**
|
||||
* Creates a new client backoff.
|
||||
*
|
||||
* @param singleSupplier the supplier of the result
|
||||
*/
|
||||
ClientBackoff(@NonNull Supplier<Single<T>> singleSupplier) {
|
||||
|
|
|
@ -16,8 +16,6 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.xml.transform.TransformerException;
|
||||
|
||||
|
@ -32,7 +30,6 @@ import okio.BufferedSink;
|
|||
import okio.Okio;
|
||||
import rs.chir.compat.java.util.Base64;
|
||||
import rs.chir.compat.java.util.Optional;
|
||||
import rs.chir.invtracker.client.BuildConfig;
|
||||
import rs.chir.invtracker.client.model.APIKey;
|
||||
import rs.chir.invtracker.model.Cursor;
|
||||
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.PublicKey;
|
||||
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.XMLSerializable;
|
||||
|
||||
|
@ -75,7 +72,8 @@ public class ClientImpl extends 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
|
||||
*/
|
||||
private ClientImpl(Context context, CronetEngine cronetEngine) {
|
||||
|
@ -87,6 +85,7 @@ public class ClientImpl extends Client {
|
|||
|
||||
/**
|
||||
* Creates a new client.
|
||||
*
|
||||
* @param context the context used
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -97,6 +96,7 @@ public class ClientImpl extends Client {
|
|||
|
||||
/**
|
||||
* Returns the HTTP client.
|
||||
*
|
||||
* @return the HTTP client
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -106,9 +106,10 @@ public class ClientImpl extends Client {
|
|||
|
||||
/**
|
||||
* Parses a response
|
||||
*
|
||||
* @param response A single emitting the response
|
||||
* @param clazz The expected class of the response
|
||||
* @param <T> The expected class of the response
|
||||
* @param clazz The expected class of the response
|
||||
* @param <T> The expected class of the response
|
||||
* @return A single emitting the parsed response
|
||||
* @see SimpleXML#parseDocument(InputStream)
|
||||
* @see SimpleXML#deserialize(Node, Class)
|
||||
|
@ -129,10 +130,11 @@ public class ClientImpl extends Client {
|
|||
|
||||
/**
|
||||
* Encodes a request body.
|
||||
*
|
||||
* @param body the body to encode
|
||||
* @param <T> the type of the body
|
||||
* @param <T> the type of the body
|
||||
* @return the encoded body
|
||||
* @throws IOException If the serializer could not write to a {@link ByteArrayOutputStream}
|
||||
* @throws IOException If the serializer could not write to a {@link ByteArrayOutputStream}
|
||||
* @throws TransformerException If the serializer could not transform the body to XML
|
||||
* @see SimpleXML#serialize(XMLSerializable)
|
||||
* @see SimpleXML#writeTo(Document, OutputStream)
|
||||
|
@ -156,12 +158,13 @@ public class ClientImpl extends Client {
|
|||
Request request = new Request.Builder()
|
||||
.url(API_URL + "/public-key")
|
||||
.build();
|
||||
return new CallAdapter(this.client.newCall(request));
|
||||
return RXJavaAdapters.fromCall(this.client.newCall(request));
|
||||
}), PublicKey.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Request.Builder} for the given API route.
|
||||
*
|
||||
* @param route the API route
|
||||
* @return A single emitting the {@link Request.Builder}
|
||||
*/
|
||||
|
@ -188,7 +191,7 @@ public class ClientImpl extends Client {
|
|||
.header("Authorization", authHeader)
|
||||
.post(RequestBody.create(EMPTY_BODY))
|
||||
.build();
|
||||
return new CallAdapter(this.client.newCall(request));
|
||||
return RXJavaAdapters.fromCall(this.client.newCall(request));
|
||||
}), PasetoToken.class);
|
||||
}
|
||||
|
||||
|
@ -197,14 +200,14 @@ public class ClientImpl extends Client {
|
|||
public Single<TrackedItem> getObject(long id) {
|
||||
return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects/" + id).map(Request.Builder::build)
|
||||
.map(this.client::newCall)
|
||||
.flatMap(CallAdapter::new)), TrackedItem.class);
|
||||
.flatMap(RXJavaAdapters::fromCall)), TrackedItem.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Single<Cursor<TrackedItem>> getObjects(@NonNull Optional<String> cont) {
|
||||
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)
|
||||
.map(cursor -> (Cursor<TrackedItem>) cursor);
|
||||
}
|
||||
|
@ -216,7 +219,7 @@ public class ClientImpl extends Client {
|
|||
.flatMap(request -> {
|
||||
var req = request.method("POST", RequestBody.create(this.encodeBody(rect)))
|
||||
.build();
|
||||
return new CallAdapter(this.client.newCall(req));
|
||||
return RXJavaAdapters.fromCall(this.client.newCall(req));
|
||||
})),
|
||||
Cursor.class)
|
||||
.map(cursor -> (Cursor<TrackedItem>) cursor);
|
||||
|
@ -229,7 +232,7 @@ public class ClientImpl extends Client {
|
|||
.flatMap(builder -> {
|
||||
var req = builder.method("POST", RequestBody.create(this.encodeBody(location)))
|
||||
.build();
|
||||
return new CallAdapter(this.client.newCall(req));
|
||||
return RXJavaAdapters.fromCall(this.client.newCall(req));
|
||||
}).map(__ -> true);
|
||||
}
|
||||
|
||||
|
@ -246,7 +249,7 @@ public class ClientImpl extends Client {
|
|||
rb = Single.just(new Request.Builder()
|
||||
.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);
|
||||
}
|
||||
|
||||
|
@ -254,7 +257,7 @@ public class ClientImpl extends Client {
|
|||
@NonNull
|
||||
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(""))
|
||||
.flatMap(request -> new CallAdapter(this.client.newCall(request.get().build())))),
|
||||
.flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.get().build())))),
|
||||
Cursor.class)
|
||||
.map(cursor -> (Cursor<GeoLocation>) cursor);
|
||||
}
|
||||
|
@ -280,7 +283,7 @@ public class ClientImpl extends Client {
|
|||
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);
|
||||
}
|
||||
|
||||
|
@ -288,25 +291,25 @@ public class ClientImpl extends Client {
|
|||
@Override
|
||||
public Single<TrackedItem> updateObject(@NonNull TrackedItem item) {
|
||||
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)))
|
||||
.build())))),
|
||||
TrackedItem.class);
|
||||
.flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.method("PATCH", RequestBody.create(this.encodeBody(item)))
|
||||
.build())))),
|
||||
TrackedItem.class);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Single<TrackedItem> createObject(@NonNull TrackedItem item) {
|
||||
return this.parseResponse(new ClientBackoff<>(() -> this.createRequest("/objects")
|
||||
.flatMap(request -> new CallAdapter(this.client.newCall(request.method("POST", RequestBody.create(this.encodeBody(item)))
|
||||
.build())))),
|
||||
TrackedItem.class);
|
||||
.flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.method("POST", RequestBody.create(this.encodeBody(item)))
|
||||
.build())))),
|
||||
TrackedItem.class);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Single<Boolean> deleteObject(long 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import java.io.File;
|
|||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
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.
|
||||
|
@ -36,6 +36,7 @@ public enum CronetEngineProvider {
|
|||
|
||||
/**
|
||||
* Creates a new {@link CronetEngine} instance.
|
||||
*
|
||||
* @param context The context used for installing cronet
|
||||
* @return A single that emits the {@link CronetEngine} instance
|
||||
*/
|
||||
|
@ -43,7 +44,7 @@ public enum CronetEngineProvider {
|
|||
public static Single<CronetEngine> getInstance(@NonNull Context context) {
|
||||
if (!ENGINE.isPresent()) {
|
||||
var installTask = CronetProviderInstaller.installProvider(context);
|
||||
return TaskAdapter.fromTask(installTask)
|
||||
return RXJavaAdapters.fromTask(installTask)
|
||||
.map(_void -> new CronetEngine.Builder(context))
|
||||
.onErrorResumeNext(throwable -> {
|
||||
Log.e("CronetEngineProvider", "Failed to install Cronet provider " + throwable);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* API client tools.
|
||||
*
|
||||
* <p>
|
||||
* You can retrieve an API client using {@link rs.chir.invtracker.client.Application#getClient()}.
|
||||
*/
|
||||
package rs.chir.invtracker.api;
|
|
@ -25,7 +25,7 @@ import rs.chir.invtracker.api.ClientImpl;
|
|||
* @see android.app.Application
|
||||
*/
|
||||
public class Application extends android.app.Application {
|
||||
/**
|
||||
/**
|
||||
* The instance of the API client.
|
||||
*/
|
||||
private Optional<Client> client = Optional.empty();
|
||||
|
@ -36,6 +36,7 @@ public class Application extends android.app.Application {
|
|||
|
||||
/**
|
||||
* Returns the Application instance for a context
|
||||
*
|
||||
* @param context the context to get the Application instance for
|
||||
* @return the Application instance for the context
|
||||
*/
|
||||
|
@ -64,6 +65,7 @@ public class Application extends android.app.Application {
|
|||
|
||||
/**
|
||||
* Returns the preferences datastore.
|
||||
*
|
||||
* @return the preferences datastore
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -73,6 +75,7 @@ public class Application extends android.app.Application {
|
|||
|
||||
/**
|
||||
* Returns the API client.
|
||||
*
|
||||
* @return A single that completes with the API client.
|
||||
*/
|
||||
@NonNull
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package rs.chir.invtracker.client;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.provider.MediaStore.ACTION_PICK_IMAGES;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
@ -20,7 +18,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
@ -112,7 +109,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
|
|||
* Use this factory method to create a new instance of
|
||||
* this fragment using the provided parameters.
|
||||
*
|
||||
* @param itemId the item ID to edit.
|
||||
* @param itemId the item ID to edit.
|
||||
* @param editMode <code>true</code> if you want to edit, create otherwise.
|
||||
* @return A new instance of fragment EditItemFragment.
|
||||
*/
|
||||
|
@ -145,7 +142,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
|
|||
this.subscribe(this.getClient().flatMap(client -> client.getObject(mId)), this::prefillItem);
|
||||
}
|
||||
this.getBinding().buttonTakePicture.setOnClickListener(v -> {
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
this.requestPermission(Manifest.permission.CAMERA);
|
||||
return;
|
||||
}
|
||||
|
@ -178,7 +175,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
|
|||
var description = this.getBinding().editTextDescription.getText().toString();
|
||||
var item = new TrackedItem(mId, name, description, this.image, Optional.empty());
|
||||
// Update or create the item
|
||||
if(mEditMode) {
|
||||
if (mEditMode) {
|
||||
this.subscribe(this.getClient().flatMap(client -> client.updateObject(item)), this::onActionSuccess);
|
||||
} else {
|
||||
this.subscribe(this.getClient().flatMap(client -> client.createObject(item)), this::onActionSuccess);
|
||||
|
@ -187,10 +184,11 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
|
|||
|
||||
/**
|
||||
* Handler for when the saving of the item is successful.
|
||||
*
|
||||
* @param item the item that was saved.
|
||||
*/
|
||||
private void onActionSuccess(TrackedItem trackedItem) {
|
||||
if(mEditMode) {
|
||||
if (mEditMode) {
|
||||
// return if we were editing
|
||||
this.getNavController().navigateUp();
|
||||
} else {
|
||||
|
@ -201,6 +199,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
|
|||
|
||||
/**
|
||||
* Pick an @{link ActivityResultLauncher} to use for picking an image.
|
||||
*
|
||||
* @return the @{link ActivityResultLauncher} to use for picking an image.
|
||||
*/
|
||||
private ActivityResultLauncher<String> getImagePickerLauncher() {
|
||||
|
@ -220,6 +219,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
|
|||
|
||||
/**
|
||||
* Callback for when an image was taken.
|
||||
*
|
||||
* @param success <code>true</code> if the image was taken successfully.
|
||||
*/
|
||||
private void onPictureTaken(@NonNull Boolean success) {
|
||||
|
@ -234,6 +234,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
|
|||
|
||||
/**
|
||||
* Upload an image to the server.
|
||||
*
|
||||
* @param uri the source image URI.
|
||||
*/
|
||||
private void uploadImage(Uri uri) {
|
||||
|
@ -255,6 +256,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
|
|||
|
||||
/**
|
||||
* Attempts to take a picture with the camera.
|
||||
*
|
||||
* @throws IOException if the camera cannot be accessed.
|
||||
*/
|
||||
private void takePicture() throws IOException {
|
||||
|
@ -277,6 +279,7 @@ public class EditItemFragment extends FragmentBase<FragmentEditItemBinding> {
|
|||
|
||||
/**
|
||||
* Prefill the fragment with the given item.
|
||||
*
|
||||
* @param item the item to prefill with.
|
||||
*/
|
||||
private void prefillItem(@NonNull TrackedItem trackedItem) {
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.activity.result.ActivityResultCaller;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.IdRes;
|
||||
|
@ -22,8 +21,8 @@ import androidx.viewbinding.ViewBinding;
|
|||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.ConcurrentModificationException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ConcurrentModificationException;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
|
@ -38,6 +37,7 @@ import rs.chir.utils.TypeParameterGetter;
|
|||
|
||||
/**
|
||||
* Base class for all fragments.
|
||||
*
|
||||
* @param <FragmentBinding> the type of the view binding.
|
||||
*/
|
||||
public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment implements TypeParameterGetter<FragmentBinding> {
|
||||
|
@ -56,9 +56,23 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
*/
|
||||
@Nullable
|
||||
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
|
||||
*
|
||||
* @return the fragment binding
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -71,6 +85,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Sets the fragment binding
|
||||
*
|
||||
* @param binding the fragment binding
|
||||
*/
|
||||
protected void setBinding(@NonNull FragmentBinding binding) {
|
||||
|
@ -79,8 +94,9 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Called when the view is created.
|
||||
* @param inflater the layout inflater
|
||||
* @param container the container view
|
||||
*
|
||||
* @param inflater the layout inflater
|
||||
* @param container the container view
|
||||
* @param savedInstanceState the saved instance state
|
||||
* @return the inflated view
|
||||
*/
|
||||
|
@ -92,10 +108,11 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Called when the view is created.
|
||||
* @param inflater the layout inflater
|
||||
* @param container the container view
|
||||
*
|
||||
* @param inflater the layout inflater
|
||||
* @param container the container view
|
||||
* @param savedInstanceState the saved instance state
|
||||
* @param clazz the class of the fragment binding
|
||||
* @param clazz the class of the fragment binding
|
||||
* @return the inflated view
|
||||
*/
|
||||
protected View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
|
@ -115,6 +132,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Adds a disposable to the {@link CompositeDisposable}
|
||||
*
|
||||
* @param disposable the disposable to add
|
||||
*/
|
||||
protected void setAction(@NonNull Disposable action) {
|
||||
|
@ -127,7 +145,8 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected <T> void subscribe(@NonNull Observable<T> value, @NonNull Consumer<? super T> consumer) {
|
||||
|
@ -136,7 +155,8 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected <T> void subscribe(@NonNull Single<T> value, @NonNull Consumer<T> consumer) {
|
||||
|
@ -145,9 +165,10 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* 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 error the consumer to call when an error occurs
|
||||
* @param error the consumer to call when an error occurs
|
||||
*/
|
||||
protected <T> void subscribe(@NonNull Single<? extends T> value, @NonNull Consumer<? super T> consumer, @NonNull Consumer<? super Throwable> error) {
|
||||
this.subscribe(value.toObservable(), consumer, error);
|
||||
|
@ -155,9 +176,10 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* 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 error the consumer to call when an error occurs
|
||||
* @param error the consumer to call when an error occurs
|
||||
*/
|
||||
protected <T> void subscribe(@NonNull Observable<T> value, @NonNull Consumer<? super T> consumer, @NonNull Consumer<? super Throwable> error) {
|
||||
this.setAction(value.subscribeOn(Schedulers.io())
|
||||
|
@ -167,7 +189,8 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Called when the fragment is attached to the view.
|
||||
* @param view the view
|
||||
*
|
||||
* @param view the view
|
||||
* @param savedInstanceState the saved instance state
|
||||
*/
|
||||
@Override
|
||||
|
@ -181,6 +204,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Creates this fragment’s menu provider.
|
||||
*
|
||||
* @return the menu provider, or null if no menu provider is needed
|
||||
*/
|
||||
@Nullable
|
||||
|
@ -213,6 +237,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Returns the menu host
|
||||
*
|
||||
* @return the menu host
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -223,6 +248,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Returns the application
|
||||
*
|
||||
* @return the application
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -232,6 +258,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Returns the API client
|
||||
*
|
||||
* @return the API client
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -241,6 +268,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Returns the navigation controller
|
||||
*
|
||||
* @return the navigation controller
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -250,15 +278,16 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Navigate to the given fragment
|
||||
*
|
||||
* @param id the action id
|
||||
*/
|
||||
void navigate(@IdRes int id) {
|
||||
this.getNavController().navigate(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the app has the given permission
|
||||
*
|
||||
* @param permission the permission to check
|
||||
* @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
|
||||
*
|
||||
* @param throwable the throwable
|
||||
*/
|
||||
protected void onActionError(@NonNull Throwable throwable) {
|
||||
|
@ -279,51 +309,41 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Navigates to the given fragment
|
||||
*
|
||||
* @param fragmentDirection the fragment direction
|
||||
*/
|
||||
void navigate(NavDirections 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
|
||||
*
|
||||
* @param permission the permission
|
||||
*/
|
||||
protected void onPermissionGranted(@NonNull String permission) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the requested permission is denied
|
||||
*
|
||||
* @param permission the permission
|
||||
*/
|
||||
protected void onPermissionDenied(@NonNull String permission) {
|
||||
this.onActionError(new RuntimeException("Permission denied: " + permission));
|
||||
this.onActionError(new RuntimeException("Permission denied: " + permission));
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the given permission
|
||||
*
|
||||
* @param permission the permission to request
|
||||
*/
|
||||
protected void requestPermission(@NonNull String permission) {
|
||||
if(this.hasPermission(permission)) {
|
||||
if (this.hasPermission(permission)) {
|
||||
this.onPermissionGranted(permission);
|
||||
return;
|
||||
}
|
||||
if(this.requestedPermission != null) {
|
||||
if (this.requestedPermission != null) {
|
||||
throw new ConcurrentModificationException("Only one permission can be requested at a time");
|
||||
}
|
||||
this.requestedPermission = permission;
|
||||
|
|
|
@ -11,7 +11,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.view.MenuProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -60,6 +59,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
|
||||
/**
|
||||
* Called when the fragment is created.
|
||||
*
|
||||
* @param savedInstanceState The saved instance state.
|
||||
*/
|
||||
@Override
|
||||
|
@ -72,7 +72,8 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
|
||||
/**
|
||||
* Called when the fragment is attached to the activity.
|
||||
* @param view The view.
|
||||
*
|
||||
* @param view The view.
|
||||
* @param savedInstanceState The saved instance state.
|
||||
*/
|
||||
@Override
|
||||
|
@ -99,7 +100,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
// Reload the item
|
||||
this.subscribe(this.getClient().flatMap(client -> client.getObject(this.mId)), this::itemLoaded);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onActionError(@NonNull Throwable throwable) {
|
||||
Log.e("ItemDetailFragment", "Error loading item", throwable);
|
||||
|
@ -134,6 +135,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
|
||||
/**
|
||||
* Fetches the image from the server
|
||||
*
|
||||
* @param image The image URI
|
||||
*/
|
||||
private void fetchImage(@NonNull URI image) {
|
||||
|
@ -143,7 +145,8 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
|
||||
/**
|
||||
* Called when the URL is loaded
|
||||
* @param image The image URI
|
||||
*
|
||||
* @param image The image URI
|
||||
* @param inputStream the response body
|
||||
*/
|
||||
private void imageLoaded(@NonNull URI image, @NonNull InputStream inputStream) {
|
||||
|
@ -171,6 +174,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
|
||||
/**
|
||||
* Loads and returns the image if present
|
||||
*
|
||||
* @param image The image URI
|
||||
* @return The image if present
|
||||
*/
|
||||
|
@ -195,6 +199,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
|
||||
/**
|
||||
* Returns the item id
|
||||
*
|
||||
* @return The item id
|
||||
*/
|
||||
long getmId() {
|
||||
|
@ -208,7 +213,8 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
var builder = new AlertDialog.Builder(this.requireContext());
|
||||
builder.setMessage(R.string.delete_warning)
|
||||
.setPositiveButton(R.string.yes, (__1, __2) -> this.reallyDelete())
|
||||
.setNegativeButton(R.string.no, (__1, __2) -> {});
|
||||
.setNegativeButton(R.string.no, (__1, __2) -> {
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ class ItemDetailMenuProvider implements MenuProvider {
|
|||
|
||||
/**
|
||||
* Creates a new instance of {@link ItemDetailMenuProvider}
|
||||
*
|
||||
* @param itemDetailFragment the fragment
|
||||
*/
|
||||
ItemDetailMenuProvider(ItemDetailFragment itemDetailFragment) {
|
||||
|
@ -26,7 +27,8 @@ class ItemDetailMenuProvider implements MenuProvider {
|
|||
|
||||
/**
|
||||
* Called when the menu is created
|
||||
* @param menu the menu
|
||||
*
|
||||
* @param menu the menu
|
||||
* @param inflater the menu inflater
|
||||
*/
|
||||
@Override
|
||||
|
@ -36,6 +38,7 @@ class ItemDetailMenuProvider implements MenuProvider {
|
|||
|
||||
/**
|
||||
* Called when an item is selected
|
||||
*
|
||||
* @param item the item
|
||||
* @return true if the item was handled, false otherwise
|
||||
*/
|
||||
|
|
|
@ -19,6 +19,7 @@ public class LoadFragment extends FragmentBase<FragmentLoadBinding> {
|
|||
|
||||
/**
|
||||
* Called when the API key is set.
|
||||
*
|
||||
* @param hasToken {@code true} if the API key is set, {@code false} otherwise.
|
||||
*/
|
||||
private void tokenResponse(boolean hasToken) {
|
||||
|
|
|
@ -40,6 +40,7 @@ public class LoginFragment extends FragmentBase<FragmentLoginBinding> {
|
|||
|
||||
/**
|
||||
* Called when the login button is clicked.
|
||||
*
|
||||
* @param view The view.
|
||||
*/
|
||||
private void login(View view) {
|
||||
|
@ -54,6 +55,7 @@ public class LoginFragment extends FragmentBase<FragmentLoginBinding> {
|
|||
|
||||
/**
|
||||
* Called when the login response is received.
|
||||
*
|
||||
* @param token The token.
|
||||
*/
|
||||
private void loginResponse(@NonNull PasetoToken pasetoToken) {
|
||||
|
|
|
@ -45,6 +45,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
/**
|
||||
* Returns the activity binding.
|
||||
*
|
||||
* @return The activity binding.
|
||||
*/
|
||||
@NonNull
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package rs.chir.invtracker.client;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -18,9 +17,6 @@ import org.osmdroid.config.Configuration;
|
|||
import org.osmdroid.util.GeoPoint;
|
||||
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.model.TrackedItem;
|
||||
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
|
||||
*
|
||||
* @param item the tracked item to add a marker for
|
||||
*/
|
||||
private void addMarker(@NonNull TrackedItem item) {
|
||||
|
@ -103,8 +100,8 @@ public class MapFragment extends FragmentBase<FragmentMapBinding> {
|
|||
var map = this.getBinding().map;
|
||||
map.getOverlays().clear();
|
||||
this.subscribe(this.getClient().toObservable()
|
||||
.flatMap(client -> client.streamObjects().toObservable())
|
||||
.filter(item -> item.lastKnownLocation().isPresent()),
|
||||
.flatMap(client -> client.streamObjects().toObservable())
|
||||
.filter(item -> item.lastKnownLocation().isPresent()),
|
||||
this::addMarker);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ public class MapMenuProvider implements MenuProvider {
|
|||
|
||||
/**
|
||||
* Creates a new instance of {@link MapMenuProvider}
|
||||
*
|
||||
* @param mapFragment the fragment
|
||||
*/
|
||||
public MapMenuProvider(MapFragment mapFragment) {
|
||||
|
|
|
@ -58,6 +58,7 @@ public class NearbyFragment extends FragmentBase<FragmentNearbyBinding> {
|
|||
|
||||
/**
|
||||
* Change the filter mode.
|
||||
*
|
||||
* @param filterMode The new filter mode.
|
||||
*/
|
||||
void switchFilter(ListFilterMode filterMode) {
|
||||
|
@ -87,6 +88,7 @@ public class NearbyFragment extends FragmentBase<FragmentNearbyBinding> {
|
|||
|
||||
/**
|
||||
* Called when the location response is received.
|
||||
*
|
||||
* @param location The location.
|
||||
*/
|
||||
private void onLocationResponse(@NonNull GeoLocation location) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.view.MenuItem;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.MenuProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
/**
|
||||
* The menu provider for {@link NearbyFragment}
|
||||
|
@ -19,6 +18,7 @@ class NearbyMenuProvider implements MenuProvider {
|
|||
|
||||
/**
|
||||
* Creates a new instance of {@link NearbyMenuProvider}
|
||||
*
|
||||
* @param nearbyFragment the fragment
|
||||
*/
|
||||
public NearbyMenuProvider(NearbyFragment nearbyFragment) {
|
||||
|
|
|
@ -18,6 +18,7 @@ class NearbyOnTouchListener implements RecyclerView.OnItemTouchListener {
|
|||
|
||||
/**
|
||||
* Creates a new instance of {@link NearbyOnTouchListener}
|
||||
*
|
||||
* @param nearbyFragment the fragment
|
||||
*/
|
||||
public NearbyOnTouchListener(NearbyFragment nearbyFragment) {
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.view.View;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.print.PrintHelper;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
@ -130,6 +129,6 @@ public class QRCodeFragment extends FragmentBase<FragmentQRCodeBinding> {
|
|||
|
||||
@Override
|
||||
protected void onPermissionGranted(@NonNull String permission) {
|
||||
this.storeBitmap();
|
||||
this.storeBitmap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import java.util.regex.Pattern;
|
|||
import rs.chir.cv.QRAnalyzer;
|
||||
import rs.chir.invtracker.client.databinding.FragmentQrScanBinding;
|
||||
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.utils.math.Vec2;
|
||||
|
||||
|
@ -84,20 +84,20 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
|
|||
|
||||
@Override
|
||||
protected void onPermissionGranted(String permission) {
|
||||
if(permission.equals(Manifest.permission.CAMERA)) {
|
||||
this.startCamera();
|
||||
} else if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||
var fusedLocationClient = LocationServices.getFusedLocationProviderClient(this.requireActivity());
|
||||
this.subscribe(SingleLocation.getNextLocation(fusedLocationClient, this.requireContext()), location -> this.updateLocation(id, location), err -> {
|
||||
err.printStackTrace();
|
||||
this.showDetailView(id);
|
||||
});
|
||||
}
|
||||
if (permission.equals(Manifest.permission.CAMERA)) {
|
||||
this.startCamera();
|
||||
} else if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||
var fusedLocationClient = LocationServices.getFusedLocationProviderClient(this.requireActivity());
|
||||
this.subscribe(SingleLocation.getNextLocation(fusedLocationClient, this.requireContext()), location -> this.updateLocation(id, location), err -> {
|
||||
err.printStackTrace();
|
||||
this.showDetailView(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPermissionDenied(String permission) {
|
||||
if(permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||
if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||
this.showDetailView(id);
|
||||
} else {
|
||||
super.onPermissionDenied(permission);
|
||||
|
@ -121,6 +121,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
|
|||
|
||||
/**
|
||||
* Returns the size of the application window in pixels
|
||||
*
|
||||
* @return the size of the application window in pixels
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -139,7 +140,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
|
|||
|
||||
private void startCamera() {
|
||||
// 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
|
||||
|
@ -164,6 +165,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
|
|||
|
||||
/**
|
||||
* Show or hide the viewfinder
|
||||
*
|
||||
* @param hide true to hide the viewfinder, false to show it
|
||||
*/
|
||||
private void hideUI(boolean b) {
|
||||
|
@ -175,7 +177,8 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
|
|||
|
||||
/**
|
||||
* Update the location of the object
|
||||
* @param id the object id
|
||||
*
|
||||
* @param id the object id
|
||||
* @param location the location of the object
|
||||
*/
|
||||
private void updateLocation(long id, GeoLocation location) {
|
||||
|
@ -193,6 +196,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
|
|||
|
||||
/**
|
||||
* Show the detail view of the object
|
||||
*
|
||||
* @param id the object id
|
||||
*/
|
||||
private void showDetailView(long id) {
|
||||
|
@ -201,6 +205,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
|
|||
|
||||
/**
|
||||
* Initializes the camera
|
||||
*
|
||||
* @param cameraProvider the camera provider
|
||||
*/
|
||||
private void openedCamera(@NonNull ProcessCameraProvider cameraProvider) {
|
||||
|
|
|
@ -7,7 +7,6 @@ import androidx.datastore.preferences.core.Preferences;
|
|||
import androidx.datastore.preferences.core.PreferencesKeys;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import rs.chir.compat.java.util.Objects;
|
||||
import rs.chir.compat.java.util.Optional;
|
||||
import rs.chir.invtracker.client.Application;
|
||||
|
||||
|
@ -25,7 +24,7 @@ public enum APIKey {
|
|||
* Sets the API key in the preferences datastore.
|
||||
*
|
||||
* @param context the context to use for accessing the preferences datastore.
|
||||
* @param key the API key to set.
|
||||
* @param key the API key to set.
|
||||
* @return a {@link Single} that completes when the API key is set.
|
||||
*/
|
||||
@NonNull
|
||||
|
|
|
@ -14,13 +14,10 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import rs.chir.compat.java.util.Optional;
|
||||
import rs.chir.invtracker.api.Client;
|
||||
import rs.chir.invtracker.client.Application;
|
||||
import rs.chir.invtracker.client.R;
|
||||
import rs.chir.invtracker.model.GeoRect;
|
||||
|
@ -56,7 +53,7 @@ public class ItemListAdapter extends RecyclerView.Adapter<ItemListAdapter.ViewHo
|
|||
* Creates a new adapter with a bound.
|
||||
*
|
||||
* @param context the context to use for accessing the client.
|
||||
* @param bound the geo bound to use for filtering the items.
|
||||
* @param bound the geo bound to use for filtering the items.
|
||||
*/
|
||||
public ItemListAdapter(@NonNull Context context, @NonNull Optional<GeoRect> bounds) {
|
||||
Application.getInstance(context)
|
||||
|
@ -88,7 +85,7 @@ public class ItemListAdapter extends RecyclerView.Adapter<ItemListAdapter.ViewHo
|
|||
/**
|
||||
* Called when a new view holder is needed.
|
||||
*
|
||||
* @param parent the parent view group.
|
||||
* @param parent the parent view group.
|
||||
* @param viewType the view type.
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -103,7 +100,7 @@ public class ItemListAdapter extends RecyclerView.Adapter<ItemListAdapter.ViewHo
|
|||
/**
|
||||
* Called when a view holder is bound.
|
||||
*
|
||||
* @param holder the view holder.
|
||||
* @param holder the view holder.
|
||||
* @param position the position of the item.
|
||||
*/
|
||||
@Override
|
||||
|
@ -166,6 +163,7 @@ public class ItemListAdapter extends RecyclerView.Adapter<ItemListAdapter.ViewHo
|
|||
|
||||
/**
|
||||
* Creates a new view holder.
|
||||
*
|
||||
* @param itemView the view to use.
|
||||
*/
|
||||
ViewHolder(@NonNull View view) {
|
||||
|
@ -178,6 +176,7 @@ public class ItemListAdapter extends RecyclerView.Adapter<ItemListAdapter.ViewHo
|
|||
|
||||
/**
|
||||
* Binds the data to the view.
|
||||
*
|
||||
* @param item the item to bind.
|
||||
*/
|
||||
void bind(@NonNull TrackedItem item) {
|
||||
|
|
|
@ -22,12 +22,9 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import rs.chir.invtracker.api.Client;
|
||||
import rs.chir.invtracker.client.Application;
|
||||
import rs.chir.invtracker.client.R;
|
||||
import rs.chir.invtracker.model.GeoLocation;
|
||||
|
@ -54,12 +51,12 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
|
|||
* Creates a new adapter.
|
||||
*
|
||||
* @param context the context to use for accessing the client and map resources.
|
||||
* @param itemId the ID of the item to load the locations for.
|
||||
* @param itemId the ID of the item to load the locations for.
|
||||
*/
|
||||
public LocationListAdapter(@NonNull Context context, long itemId) {
|
||||
this.context = context;
|
||||
Application.getInstance(context)
|
||||
.getClient().toObservable().flatMap(client -> client.streamLocations(itemId).toObservable())
|
||||
.getClient().toObservable().flatMap(client -> client.streamLocations(itemId).toObservable())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(item -> {
|
||||
|
@ -83,7 +80,8 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
|
|||
|
||||
/**
|
||||
* Creates a new view holder.
|
||||
* @param parent the parent view group.
|
||||
*
|
||||
* @param parent the parent view group.
|
||||
* @param viewType the view type.
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -97,7 +95,8 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
|
|||
|
||||
/**
|
||||
* Binds the view holder to the data.
|
||||
* @param holder the view holder.
|
||||
*
|
||||
* @param holder the view holder.
|
||||
* @param position the position in the list.
|
||||
*/
|
||||
@Override
|
||||
|
@ -120,6 +119,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
|
|||
|
||||
/**
|
||||
* Returns the number of items in the list.
|
||||
*
|
||||
* @return the number of items in the list.
|
||||
*/
|
||||
@Override
|
||||
|
@ -151,7 +151,8 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
|
|||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
ViewHolder(@NonNull View view, @NonNull Context context) {
|
||||
|
@ -170,6 +171,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
|
|||
|
||||
/**
|
||||
* Binds the view holder to the data.
|
||||
*
|
||||
* @param location the location to bind to.
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
|
|
|
@ -29,6 +29,7 @@ public class LocationRounder {
|
|||
|
||||
/**
|
||||
* Constructs a new {@link LocationRounder}.
|
||||
*
|
||||
* @param digits the number of decimal places to round to.
|
||||
* @param method the method to use for rounding.
|
||||
*/
|
||||
|
@ -40,6 +41,7 @@ public class LocationRounder {
|
|||
|
||||
/**
|
||||
* Constructs a new location rounder from the application preferences.
|
||||
*
|
||||
* @param context the context to use for accessing the preferences datastore.
|
||||
* @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.
|
||||
*
|
||||
* @param value the value to round.
|
||||
* @return the rounded value.
|
||||
*/
|
||||
|
@ -63,6 +66,7 @@ public class LocationRounder {
|
|||
|
||||
/**
|
||||
* Truncates double to a multiple of 10^-digits.
|
||||
*
|
||||
* @param value the value to truncate.
|
||||
* @return the truncated value.
|
||||
*/
|
||||
|
@ -75,6 +79,7 @@ public class LocationRounder {
|
|||
|
||||
/**
|
||||
* Randomizes double into the range <code>[trunc(val), trunc(val) + 1)</code>.
|
||||
*
|
||||
* @param val the value to randomize.
|
||||
* @return the randomized value.
|
||||
*/
|
||||
|
@ -86,6 +91,7 @@ public class LocationRounder {
|
|||
|
||||
/**
|
||||
* Performs the rounding operation.
|
||||
*
|
||||
* @param val the value to round.
|
||||
* @return the rounded value.
|
||||
*/
|
||||
|
@ -100,6 +106,7 @@ public class LocationRounder {
|
|||
|
||||
/**
|
||||
* Rounds a {@link GeoLocation}.
|
||||
*
|
||||
* @param location the location to round.
|
||||
* @return the rounded location.
|
||||
*/
|
||||
|
|
|
@ -27,6 +27,7 @@ public enum ServerPublicKey {
|
|||
|
||||
/**
|
||||
* Returns a flowable for the serialized public key.
|
||||
*
|
||||
* @param context the context to use for accessing the preferences datastore.
|
||||
* @return a flowable for the serialized public key.
|
||||
*/
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.io.IOException;
|
|||
public interface BitmapStorage {
|
||||
/**
|
||||
* Creates an instance of {@link BitmapStorage} optimized for the current android version.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -27,7 +28,8 @@ public interface BitmapStorage {
|
|||
|
||||
/**
|
||||
* 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.
|
||||
* @throws IOException If the bitmap cannot be saved.
|
||||
*/
|
||||
|
|
|
@ -26,6 +26,7 @@ public class BitmapStorageQ implements BitmapStorage {
|
|||
|
||||
/**
|
||||
* Creates a new instance of {@link BitmapStorageQ}.
|
||||
*
|
||||
* @param context the context to use.
|
||||
*/
|
||||
public BitmapStorageQ(@NonNull Context context) {
|
||||
|
|
|
@ -17,9 +17,10 @@ public enum BitmapUtils {
|
|||
|
||||
/**
|
||||
* Saves the bitmap to file
|
||||
*
|
||||
* @param bitmap The bitmap to save
|
||||
* @param file The file to save to
|
||||
* @throws IOException If the file cannot be saved
|
||||
* @param file The file to save to
|
||||
* @throws IOException If the file cannot be saved
|
||||
* @throws FileNotFoundException If the file cannot be found
|
||||
*/
|
||||
public static void saveBitmap(@NonNull Bitmap bitmap, @NonNull String fileName) throws FileNotFoundException, IOException {
|
||||
|
@ -28,8 +29,9 @@ public enum BitmapUtils {
|
|||
|
||||
/**
|
||||
* Saves the bitmap to an output stream
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public static void saveBitmap(@NonNull Bitmap bitmap, @NonNull OutputStream os) throws IOException {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ public enum ConnectionScorer {
|
|||
|
||||
/**
|
||||
* Check if the data saver is enabled
|
||||
*
|
||||
* @param cm the connectivity manager
|
||||
* @return true if the data saver is enabled
|
||||
*/
|
||||
|
@ -27,6 +28,7 @@ public enum ConnectionScorer {
|
|||
|
||||
/**
|
||||
* Scores the connection quality.
|
||||
*
|
||||
* @param context
|
||||
* @return the connection quality score
|
||||
*/
|
||||
|
|
|
@ -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 don’t 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
105
app/src/main/java/rs/chir/invtracker/utils/RXJavaAdapters.java
Normal file
105
app/src/main/java/rs/chir/invtracker/utils/RXJavaAdapters.java
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,7 +55,7 @@ public class SingleBackoff<T> extends Single<T> {
|
|||
/**
|
||||
* Creates a new single backoff.
|
||||
*
|
||||
* @param singleSupplier the supplier of the single to retry
|
||||
* @param singleSupplier the supplier of the single to retry
|
||||
* @param exceptionFilter the exception filter for the single. If it returns true, the single is retried.
|
||||
*/
|
||||
public SingleBackoff(@androidx.annotation.NonNull Supplier<Single<T>> singleSupplier, @NonNull Predicate<? super Throwable> exceptionFilter) {
|
||||
|
@ -87,6 +87,7 @@ public class SingleBackoff<T> extends Single<T> {
|
|||
|
||||
/**
|
||||
* Reports the success of the single
|
||||
*
|
||||
* @param result the result of the single
|
||||
*/
|
||||
private void onSuccess(T result) {
|
||||
|
@ -98,6 +99,7 @@ public class SingleBackoff<T> extends Single<T> {
|
|||
|
||||
/**
|
||||
* Reports the error of the single
|
||||
*
|
||||
* @param error the error of the single
|
||||
*/
|
||||
private void onError(Throwable error) {
|
||||
|
|
|
@ -17,11 +17,8 @@ import com.google.android.gms.location.LocationSettingsRequest;
|
|||
import com.google.android.gms.location.Priority;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.core.SingleObserver;
|
||||
import rs.chir.compat.java.util.OptionalDouble;
|
||||
import rs.chir.invtracker.client.model.LocationRounder;
|
||||
import rs.chir.invtracker.model.GeoLocation;
|
||||
|
@ -29,44 +26,14 @@ import rs.chir.invtracker.model.GeoLocation;
|
|||
/**
|
||||
* {@link Single} implementation that returns a location
|
||||
*/
|
||||
public class SingleLocation extends Single<GeoLocation> {
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
public enum SingleLocation {
|
||||
;
|
||||
|
||||
/**
|
||||
* Converts an Android location to a GeoLocation
|
||||
*
|
||||
* @param location the location to convert
|
||||
* @param context the context to use for rounding the location
|
||||
* @param context the context to use for rounding the location
|
||||
* @return the converted location
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -85,7 +52,8 @@ public class SingleLocation extends Single<GeoLocation> {
|
|||
|
||||
/**
|
||||
* 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
|
||||
* @return the most recent location
|
||||
*/
|
||||
|
@ -93,23 +61,25 @@ public class SingleLocation extends Single<GeoLocation> {
|
|||
@SuppressLint("MissingPermission")
|
||||
public static Single<GeoLocation> getSingleLocation(@NonNull FusedLocationProviderClient client, @NonNull Context context) {
|
||||
// Try accessing the last location
|
||||
return TaskAdapter.fromTask(client.getLastLocation())
|
||||
return RXJavaAdapters.fromTask(client.getLastLocation())
|
||||
.flatMap(location -> {
|
||||
if (location.isPresent()) {
|
||||
return Single.just(SingleLocation.fromAndroidLocation(location.get(), context));
|
||||
} else {
|
||||
// if there is no last location, request a location update
|
||||
return SingleLocation.getNextLocation(client, context);
|
||||
return SingleLocation.getNextLocation(client, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return the next location
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
@NonNull
|
||||
public static Single<GeoLocation> getNextLocation(@NonNull FusedLocationProviderClient client, @NonNull Context context) {
|
||||
var request = LocationRequest.create();
|
||||
|
@ -122,12 +92,19 @@ public class SingleLocation extends Single<GeoLocation> {
|
|||
.addLocationRequest(request);
|
||||
// TODO: you could probably cache the settings client?
|
||||
var settingsClient = LocationServices.getSettingsClient(context);
|
||||
return TaskAdapter.fromTask(settingsClient.checkLocationSettings(builder.build()))
|
||||
.flatMap(resp -> new SingleLocation(client, request, context));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void subscribeActual(@io.reactivex.rxjava3.annotations.NonNull SingleObserver<? super GeoLocation> observer) {
|
||||
observers.add(observer);
|
||||
return RXJavaAdapters.fromTask(settingsClient.checkLocationSettings(builder.build()))
|
||||
.flatMap(resp -> Single.create(subscriber -> {
|
||||
client.requestLocationUpdates(request, new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(@NonNull LocationResult locationResult) {
|
||||
var location = locationResult.getLastLocation();
|
||||
if (location != null) {
|
||||
subscriber.onSuccess(SingleLocation.fromAndroidLocation(location, context));
|
||||
} else {
|
||||
subscriber.onError(new AndroidException("No location available"));
|
||||
}
|
||||
}
|
||||
}, Looper.getMainLooper());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 don’t 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);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="cache_files" path="."/>
|
||||
<cache-path
|
||||
name="cache_files"
|
||||
path="." />
|
||||
</paths>
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,4 +1,4 @@
|
|||
#Wed Aug 10 11:16:39 GMT 2022
|
||||
#Mon Aug 15 10:52:08 GMT 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
|
|
Loading…
Reference in a new issue