Compare commits
4 commits
98c61e7b01
...
a146ae77f1
Author | SHA1 | Date | |
---|---|---|---|
|
a146ae77f1 | ||
|
eefd8134c3 | ||
|
df839b4c03 | ||
|
9043b99914 |
45 changed files with 370 additions and 432 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,6 +88,7 @@ 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.
|
||||
|
@ -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,6 +53,7 @@ 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
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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,6 +137,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Updates the location of the given item.
|
||||
*
|
||||
* @param item The item
|
||||
* @param location The location
|
||||
* @return A single that emits true on success
|
||||
|
@ -140,6 +150,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Updates the location of the given item.
|
||||
*
|
||||
* @param itemId The item ID
|
||||
* @param location The location
|
||||
* @return A single that emits true on success
|
||||
|
@ -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,6 +170,7 @@ public abstract class Client {
|
|||
|
||||
/**
|
||||
* Retrieves a page of locations for the given item.
|
||||
*
|
||||
* @param itemId The item ID
|
||||
* @param cont An optional continuation token
|
||||
* @return A single that emits a page of locations
|
||||
|
@ -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,6 +72,7 @@ public class ClientImpl extends Client {
|
|||
|
||||
/**
|
||||
* Creates a new client.
|
||||
*
|
||||
* @param context the context used to retrieve the API token
|
||||
* @param cronetEngine the Cronet engine
|
||||
*/
|
||||
|
@ -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,6 +106,7 @@ 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
|
||||
|
@ -129,6 +130,7 @@ public class ClientImpl extends Client {
|
|||
|
||||
/**
|
||||
* Encodes a request body.
|
||||
*
|
||||
* @param body the body to encode
|
||||
* @param <T> the type of the body
|
||||
* @return the encoded body
|
||||
|
@ -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,7 +291,7 @@ 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)))
|
||||
.flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.method("PATCH", RequestBody.create(this.encodeBody(item)))
|
||||
.build())))),
|
||||
TrackedItem.class);
|
||||
}
|
||||
|
@ -297,7 +300,7 @@ public class ClientImpl extends Client {
|
|||
@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)))
|
||||
.flatMap(request -> RXJavaAdapters.fromCall(this.client.newCall(request.method("POST", RequestBody.create(this.encodeBody(item)))
|
||||
.build())))),
|
||||
TrackedItem.class);
|
||||
}
|
||||
|
@ -306,7 +309,7 @@ public class ClientImpl extends Client {
|
|||
@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;
|
|
@ -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;
|
||||
|
@ -187,6 +184,7 @@ 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) {
|
||||
|
@ -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,6 +94,7 @@ 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 savedInstanceState the saved instance state
|
||||
|
@ -92,6 +108,7 @@ 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 savedInstanceState the saved instance state
|
||||
|
@ -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,6 +145,7 @@ 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 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}
|
||||
*
|
||||
* @param value the single to subscribe to
|
||||
* @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}
|
||||
*
|
||||
* @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
|
||||
|
@ -155,6 +176,7 @@ 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 consumer the consumer to call when the observable emits an item
|
||||
* @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.
|
||||
*
|
||||
* @param view the view
|
||||
* @param savedInstanceState the saved instance state
|
||||
*/
|
||||
|
@ -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,35 +309,24 @@ 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) {
|
||||
|
@ -316,6 +335,7 @@ public class FragmentBase<FragmentBinding extends ViewBinding> extends Fragment
|
|||
|
||||
/**
|
||||
* Requests the given permission
|
||||
*
|
||||
* @param permission the permission to request
|
||||
*/
|
||||
protected void requestPermission(@NonNull String 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,6 +72,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
|
||||
/**
|
||||
* Called when the fragment is attached to the activity.
|
||||
*
|
||||
* @param view The view.
|
||||
* @param savedInstanceState The saved instance state.
|
||||
*/
|
||||
|
@ -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,6 +145,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
|
||||
/**
|
||||
* Called when the URL is loaded
|
||||
*
|
||||
* @param image The image URI
|
||||
* @param inputStream the response body
|
||||
*/
|
||||
|
@ -151,7 +154,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
var fileName = image.getPath().substring(image.getPath().lastIndexOf('/'));
|
||||
var path = this.requireActivity().getCacheDir().getAbsolutePath();
|
||||
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
|
||||
// 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
|
||||
*
|
||||
* @param image The image URI
|
||||
* @return The image if present
|
||||
*/
|
||||
private Optional<Bitmap> getImageIfPresent(@NonNull URI uri) {
|
||||
var fileName = uri.getPath().substring(uri.getPath().lastIndexOf('/'));
|
||||
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
|
||||
if (file.exists()) {
|
||||
try {
|
||||
|
@ -194,6 +199,7 @@ public class ItemDetailFragment extends FragmentBase<FragmentItemDetailBinding>
|
|||
|
||||
/**
|
||||
* Returns the item id
|
||||
*
|
||||
* @return The item id
|
||||
*/
|
||||
long getmId() {
|
||||
|
@ -207,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,6 +27,7 @@ class ItemDetailMenuProvider implements MenuProvider {
|
|||
|
||||
/**
|
||||
* Called when the menu is created
|
||||
*
|
||||
* @param menu the menu
|
||||
* @param inflater the menu inflater
|
||||
*/
|
||||
|
@ -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,7 +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()),
|
||||
.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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -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,6 +177,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> implemen
|
|||
|
||||
/**
|
||||
* Update the location of the object
|
||||
*
|
||||
* @param id the object id
|
||||
* @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
|
||||
*
|
||||
* @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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -83,6 +80,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
|
|||
|
||||
/**
|
||||
* Creates a new view holder.
|
||||
*
|
||||
* @param parent the parent view group.
|
||||
* @param viewType the view type.
|
||||
*/
|
||||
|
@ -97,6 +95,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
|
|||
|
||||
/**
|
||||
* Binds the view holder to the data.
|
||||
*
|
||||
* @param holder the view holder.
|
||||
* @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.
|
||||
*
|
||||
* @return the number of items in the list.
|
||||
*/
|
||||
@Override
|
||||
|
@ -151,6 +151,7 @@ public class LocationListAdapter extends RecyclerView.Adapter<LocationListAdapte
|
|||
|
||||
/**
|
||||
* Creates a new view holder.
|
||||
*
|
||||
* @param view the view to use.
|
||||
* @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.
|
||||
*
|
||||
* @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,6 +28,7 @@ public interface BitmapStorage {
|
|||
|
||||
/**
|
||||
* Saves the bitmap to the storage.
|
||||
*
|
||||
* @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,6 +17,7 @@ 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
|
||||
|
@ -28,6 +29,7 @@ public enum BitmapUtils {
|
|||
|
||||
/**
|
||||
* Saves the bitmap to an output stream
|
||||
*
|
||||
* @param bitmap The bitmap to save
|
||||
* @param os The output stream to save to
|
||||
* @throws IOException If the file cannot be saved
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,38 +26,8 @@ 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
|
||||
|
@ -85,6 +52,7 @@ 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 context the context to use for rounding the location
|
||||
* @return the most recent location
|
||||
|
@ -93,7 +61,7 @@ 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));
|
||||
|
@ -106,10 +74,12 @@ public class SingleLocation extends Single<GeoLocation> {
|
|||
|
||||
/**
|
||||
* Retrieves a location update, requesting a location update if necessary.
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
|
||||
return RXJavaAdapters.fromTask(settingsClient.checkLocationSettings(builder.build()))
|
||||
.flatMap(resp -> Single.create(subscriber -> {
|
||||
client.requestLocationUpdates(request, new LocationCallback() {
|
||||
@Override
|
||||
protected void subscribeActual(@io.reactivex.rxjava3.annotations.NonNull SingleObserver<? super GeoLocation> observer) {
|
||||
observers.add(observer);
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -84,7 +84,7 @@
|
|||
<fragment
|
||||
android:id="@+id/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">
|
||||
<argument
|
||||
android:name="id"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<string name="action_settings">Einstellungen</string>
|
||||
<string name="load_fragment_label">Invtracker</string>
|
||||
<string name="nearby_fragment_label">Gegenstandsliste</string>
|
||||
<string name="fragment_item_detail">Objektdetails</string>
|
||||
<string name="login_fragment_label">Login</string>
|
||||
<string name="map_fragment_label">Karte</string>
|
||||
<string name="loading_text">Verbindung mit dem Server aufbauen…</string>
|
||||
|
|
|
@ -84,4 +84,5 @@
|
|||
<string name="qr_scan_fragment_label">QR Code Scanner</string>
|
||||
<string name="qr_code_fragment">QR Code</string>
|
||||
<string name="edit_item_fragment">Edit Item</string>
|
||||
<string name="fragment_item_detail">Item Detail</string>
|
||||
</resources>
|
||||
|
|
|
@ -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