diff --git a/app/src/main/java/rs/chir/invtracker/client/EditItemFragment.java b/app/src/main/java/rs/chir/invtracker/client/EditItemFragment.java index 98eebe7..1d3be3f 100644 --- a/app/src/main/java/rs/chir/invtracker/client/EditItemFragment.java +++ b/app/src/main/java/rs/chir/invtracker/client/EditItemFragment.java @@ -29,7 +29,6 @@ import java.net.URI; import rs.chir.compat.java.util.Optional; import rs.chir.invtracker.client.databinding.FragmentEditItemBinding; import rs.chir.invtracker.model.TrackedItem; -import rs.chir.invtracker.utils.SinglePermission; /** * The Item editing and creation interface. @@ -145,14 +144,11 @@ public class EditItemFragment extends FragmentBase { this.getBinding().progressBar.setVisibility(View.VISIBLE); this.subscribe(this.getClient().flatMap(client -> client.getObject(mId)), this::prefillItem); } - // On older versions of android, we need to request the camera permission to ask the camera for a picture - // Hide the camera button if we don't have the permission - if (!this.hasPermission(Manifest.permission.CAMERA) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - this.getBinding().buttonTakePicture.setVisibility(View.INVISIBLE); - this.subscribe(new SinglePermission(this, Manifest.permission.CAMERA), - __ -> this.getBinding().buttonTakePicture.setVisibility(View.VISIBLE)); - } this.getBinding().buttonTakePicture.setOnClickListener(v -> { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + this.requestPermission(Manifest.permission.CAMERA); + return; + } try { this.takePicture(); } catch (IOException e) { @@ -163,6 +159,17 @@ public class EditItemFragment extends FragmentBase { this.getBinding().fab.setOnClickListener(__ -> this.saveItem()); } + @Override + protected void onPermissionGranted(@NonNull String permission) { + if (permission.equals(Manifest.permission.CAMERA)) { + try { + this.takePicture(); + } catch (IOException e) { + this.onActionError(e); + } + } + } + /** * Saves the item to the server. */ diff --git a/app/src/main/java/rs/chir/invtracker/client/FragmentBase.java b/app/src/main/java/rs/chir/invtracker/client/FragmentBase.java index b0cf0e8..d2cd179 100644 --- a/app/src/main/java/rs/chir/invtracker/client/FragmentBase.java +++ b/app/src/main/java/rs/chir/invtracker/client/FragmentBase.java @@ -5,6 +5,9 @@ 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; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,6 +22,7 @@ import androidx.viewbinding.ViewBinding; import com.google.android.material.snackbar.Snackbar; +import java.util.ConcurrentModificationException; import java.lang.reflect.InvocationTargetException; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; @@ -280,4 +284,49 @@ public class FragmentBase extends Fragment void navigate(NavDirections fragmentDirection) { this.getNavController().navigate(fragmentDirection); } + + private String requestedPermission; + + /** + * Permissions request launcher + */ + private final ActivityResultLauncher 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)); + } + + /** + * Requests the given permission + * @param permission the permission to request + */ + protected void requestPermission(@NonNull String permission) { + if(this.hasPermission(permission)) { + this.onPermissionGranted(permission); + return; + } + if(this.requestedPermission != null) { + throw new ConcurrentModificationException("Only one permission can be requested at a time"); + } + this.requestedPermission = permission; + this.permissionLauncher.launch(permission); + } } diff --git a/app/src/main/java/rs/chir/invtracker/client/NearbyFragment.java b/app/src/main/java/rs/chir/invtracker/client/NearbyFragment.java index 13eeca6..e09585a 100644 --- a/app/src/main/java/rs/chir/invtracker/client/NearbyFragment.java +++ b/app/src/main/java/rs/chir/invtracker/client/NearbyFragment.java @@ -18,14 +18,13 @@ import rs.chir.invtracker.client.model.ItemListAdapter; import rs.chir.invtracker.model.GeoLocation; import rs.chir.invtracker.model.GeoRect; import rs.chir.invtracker.utils.SingleLocation; -import rs.chir.invtracker.utils.SinglePermission; /** * Fragment that displays a list of items. */ public class NearbyFragment extends FragmentBase { /** - * The filter mode + * The filter mode */ private ListFilterMode filterMode = ListFilterMode.NEARBY; @@ -33,19 +32,18 @@ public class NearbyFragment extends FragmentBase { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // Check permissions - if (this.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) { - this.fetchData(); - } else { - // Request permission before fetching data - this.subscribe(new SinglePermission(this, Manifest.permission.ACCESS_FINE_LOCATION), __ -> this.fetchData()); - } + this.requestPermission(Manifest.permission.ACCESS_FINE_LOCATION); // Initialize recyclerview this.getBinding().nearbyRecyclerView.setLayoutManager(new LinearLayoutManager(this.getContext())); this.getBinding().nearbyRecyclerView.addOnItemTouchListener(new NearbyOnTouchListener(this)); this.getBinding().fab.setOnClickListener(__ -> this.navigate(R.id.action_nearbyFragment_to_QRScanFragment)); } + @Override + protected void onPermissionGranted(@NonNull String permission) { + this.fetchData(); + } + @Nullable @Override protected MenuProvider getMenuProvider() { diff --git a/app/src/main/java/rs/chir/invtracker/client/QRCodeFragment.java b/app/src/main/java/rs/chir/invtracker/client/QRCodeFragment.java index 82e70db..e1b6098 100644 --- a/app/src/main/java/rs/chir/invtracker/client/QRCodeFragment.java +++ b/app/src/main/java/rs/chir/invtracker/client/QRCodeFragment.java @@ -22,7 +22,6 @@ import java.util.Map; import rs.chir.invtracker.client.databinding.FragmentQRCodeBinding; import rs.chir.invtracker.utils.BitmapStorage; -import rs.chir.invtracker.utils.SinglePermission; /** * Fragment that displays a QR code for the item. @@ -99,26 +98,16 @@ public class QRCodeFragment extends FragmentBase { this.onActionError(e); } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - // pre android 10, we need external storage permission to save the QR code to the gallery - // so we ask for it here and disable the save button until we have it - this.getBinding().saveToGalleryButton.setEnabled(false); - this.subscribe(new SinglePermission(this.requireActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE), - __ -> this.getBinding().saveToGalleryButton.setEnabled(true), - __ -> this.getBinding().saveToGalleryButton.setEnabled(false)); // may be extraneous - } - this.getBinding().imageView.setImageBitmap(this.bitmap); this.getBinding().imageView.setContentDescription("https://invtracker.chir.rs/objects/" + mId); this.getBinding().saveToGalleryButton.setOnClickListener(__ -> { - try { - // save the QR code to the gallery - BitmapStorage.create(this.requireContext()).storeBitmap(this.bitmap, "qr_code_" + mId); - } catch (IOException e) { - this.onActionError(e); + // pre android 10, we need external storage permission to save the QR code to the gallery + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + this.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); + } else { + this.storeBitmap(); } - this.getNavController().navigateUp(); // we are done here }); this.getBinding().print.setOnClickListener(__ -> { @@ -128,4 +117,19 @@ public class QRCodeFragment extends FragmentBase { this.getNavController().navigateUp(); }); } + + private void storeBitmap() { + try { + // save the QR code to the gallery + BitmapStorage.create(this.requireContext()).storeBitmap(this.bitmap, "qr_code_" + mId); + } catch (IOException e) { + this.onActionError(e); + } + this.getNavController().navigateUp(); // we are done here + } + + @Override + protected void onPermissionGranted(@NonNull String permission) { + this.storeBitmap(); + } } diff --git a/app/src/main/java/rs/chir/invtracker/client/QRScanFragment.java b/app/src/main/java/rs/chir/invtracker/client/QRScanFragment.java index 0de6159..53be539 100644 --- a/app/src/main/java/rs/chir/invtracker/client/QRScanFragment.java +++ b/app/src/main/java/rs/chir/invtracker/client/QRScanFragment.java @@ -31,7 +31,6 @@ import rs.chir.invtracker.client.databinding.FragmentQrScanBinding; import rs.chir.invtracker.model.GeoLocation; import rs.chir.invtracker.utils.ListenableFutureAdapter; import rs.chir.invtracker.utils.SingleLocation; -import rs.chir.invtracker.utils.SinglePermission; import rs.chir.utils.math.Vec2; /** @@ -52,6 +51,8 @@ public class QRScanFragment extends FragmentBase implemen */ private boolean hasPaused = false; + private long id; + /** * Initializes the cosmetic alignment box */ @@ -71,7 +72,7 @@ public class QRScanFragment extends FragmentBase implemen this.initViewfinderBox(); // Make sure we have the needed permissions - this.subscribe(new SinglePermission(this, Manifest.permission.CAMERA), __ -> this.startCamera()); + this.requestPermission(Manifest.permission.CAMERA); } @Override @@ -81,6 +82,28 @@ public class QRScanFragment extends FragmentBase implemen this.getNavController().popBackStack(); } + @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); + }); + } + } + + @Override + protected void onPermissionDenied(String permission) { + if(permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)) { + this.showDetailView(id); + } else { + super.onPermissionDenied(permission); + } + } + @Override public void onPause() { super.onPause(); @@ -131,17 +154,8 @@ public class QRScanFragment extends FragmentBase implemen return; // should not happen // while we are determining the location, we don’t want to show the user the viewfinder this.hideUI(true); - var id = Long.parseLong(group); - // TODO: request location permission - if (this.hasPermission(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); - }); - } else { - this.showDetailView(id); - } + this.id = Long.parseLong(group); + this.requestPermission(Manifest.permission.ACCESS_FINE_LOCATION); } else { Snackbar.make(this.getBinding().getRoot(), "Invalid QR code", BaseTransientBottomBar.LENGTH_LONG).show(); this.hasScanned.set(false); diff --git a/app/src/main/java/rs/chir/invtracker/utils/SinglePermission.java b/app/src/main/java/rs/chir/invtracker/utils/SinglePermission.java deleted file mode 100644 index 3bd0d2a..0000000 --- a/app/src/main/java/rs/chir/invtracker/utils/SinglePermission.java +++ /dev/null @@ -1,63 +0,0 @@ -package rs.chir.invtracker.utils; - -import android.util.AndroidException; - -import androidx.activity.result.ActivityResultCaller; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; - -import java.util.List; - -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.core.SingleObserver; - -/** - * {@link Single} that completes when the user grants the requested permissions and errors otherwise. - */ -public class SinglePermission extends Single { - /** - * The subscribers to this {@link Single}. - */ - private final List> subscribers = new java.util.ArrayList<>(1); - /** - * Permission that is requested. - */ - private final String permission; - /** - * The {@link ActivityResultLauncher} for requesting the permission. - */ - private final ActivityResultLauncher launcher; - - /** - * Creates a new {@link SinglePermission} for the given permission. - * - *
- * This function needs to be called before the {@link android.app.Activity} or {@link android.app.Fragment} is started. - * - * @param activity An activity or fragment. - * @param permission The permission to request. - */ - public SinglePermission(@NonNull ActivityResultCaller activity, String permission) { - this.launcher = activity.registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - for (var subscriber : subscribers) { - subscriber.onSuccess(permission); - } - } else { - for (var subscriber : subscribers) { - subscriber.onError(new AndroidException("Permission denied")); - } - } - }); - this.permission = permission; - } - - @Override - protected void subscribeActual(@io.reactivex.rxjava3.annotations.NonNull SingleObserver observer) { - subscribers.add(observer); - if (subscribers.size() == 1) { - launcher.launch(permission); - } - } -}