Compare commits
3 commits
824c552738
...
98c61e7b01
Author | SHA1 | Date | |
---|---|---|---|
|
98c61e7b01 | ||
|
d864000235 | ||
|
fff0f56d14 |
9 changed files with 125 additions and 112 deletions
|
@ -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<FragmentEditItemBinding> {
|
|||
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<FragmentEditItemBinding> {
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -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<FragmentBinding extends ViewBinding> extends Fragment
|
|||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.ui.AppBarConfiguration;
|
||||
import androidx.navigation.ui.NavigationUI;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import rs.chir.invtracker.client.databinding.ActivityMainBinding;
|
||||
|
||||
|
@ -37,6 +38,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
this.appBarConfiguration = new AppBarConfiguration.Builder(R.id.loadFragment, R.id.loginFragment, R.id.nearbyFragment).build();
|
||||
NavigationUI.setupActionBarWithNavController(this, navController, this.appBarConfiguration);
|
||||
|
||||
// Initialize settings
|
||||
PreferenceManager.setDefaultValues(this, R.xml.root_preferences, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<FragmentNearbyBinding> {
|
||||
/**
|
||||
* The filter mode
|
||||
* The filter mode
|
||||
*/
|
||||
private ListFilterMode filterMode = ListFilterMode.NEARBY;
|
||||
|
||||
|
@ -33,19 +32,18 @@ public class NearbyFragment extends FragmentBase<FragmentNearbyBinding> {
|
|||
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() {
|
||||
|
|
|
@ -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<FragmentQRCodeBinding> {
|
|||
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<FragmentQRCodeBinding> {
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<FragmentQrScanBinding> implemen
|
|||
*/
|
||||
private boolean hasPaused = false;
|
||||
|
||||
private long id;
|
||||
|
||||
/**
|
||||
* Initializes the cosmetic alignment box
|
||||
*/
|
||||
|
@ -71,7 +72,7 @@ public class QRScanFragment extends FragmentBase<FragmentQrScanBinding> 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<FragmentQrScanBinding> 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<FragmentQrScanBinding> 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);
|
||||
|
|
|
@ -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<String> {
|
||||
/**
|
||||
* The subscribers to this {@link Single}.
|
||||
*/
|
||||
private final List<SingleObserver<? super String>> subscribers = new java.util.ArrayList<>(1);
|
||||
/**
|
||||
* Permission that is requested.
|
||||
*/
|
||||
private final String permission;
|
||||
/**
|
||||
* The {@link ActivityResultLauncher} for requesting the permission.
|
||||
*/
|
||||
private final ActivityResultLauncher<String> launcher;
|
||||
|
||||
/**
|
||||
* Creates a new {@link SinglePermission} for the given permission.
|
||||
*
|
||||
* <br>
|
||||
* 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<? super String> observer) {
|
||||
subscribers.add(observer);
|
||||
if (subscribers.size() == 1) {
|
||||
launcher.launch(permission);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -84,4 +84,4 @@
|
|||
<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>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<ListPreference
|
||||
android:id="@+id/pref_location_privacy"
|
||||
android:defaultValue="location_round"
|
||||
android:defaultValue="privacy_round"
|
||||
android:entries="@array/demo_privacy_type_lbl"
|
||||
android:entryValues="@array/demo_privacy_type"
|
||||
android:key="location_privacy"
|
||||
|
@ -18,4 +18,4 @@
|
|||
android:title="@string/preference_location_accuracy"
|
||||
app:min="0" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
|
Loading…
Reference in a new issue