Replace the rxjava adapters with better alternatives

This commit is contained in:
Morten Delenk 2022-08-15 14:20:33 +01:00
parent eefd8134c3
commit a146ae77f1
No known key found for this signature in database
GPG key ID: 5130416C797067B6
42 changed files with 362 additions and 428 deletions

View file

@ -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.

View file

@ -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);

View file

@ -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;

View file

@ -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));

View file

@ -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

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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

View file

@ -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) {

View file

@ -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 fragments 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;

View file

@ -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();
}

View file

@ -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
*/

View file

@ -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) {

View file

@ -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) {

View file

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

View file

@ -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);
}

View file

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

View file

@ -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) {

View file

@ -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) {

View file

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

View file

@ -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();
}
}

View file

@ -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) {

View file

@ -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

View file

@ -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) {

View file

@ -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")

View file

@ -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.
*/

View file

@ -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.
*/

View file

@ -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.
*/

View file

@ -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) {

View file

@ -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 {

View file

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

View file

@ -15,6 +15,7 @@ public enum ConnectionScorer {
/**
* Check if the data saver is enabled
*
* @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
*/

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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());
}));
}
}

View file

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

View file

@ -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>

View file

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