Compare commits

...

3 commits

Author SHA1 Message Date
Morten Delenk
98c61e7b01
Remove SinglePermission 2022-08-15 10:17:17 +01:00
Morten Delenk
d864000235
Fix default value of the location privacy setting 2022-08-15 09:15:55 +01:00
Morten Delenk
fff0f56d14
Initialize settings in MainActivity 2022-08-15 09:05:22 +01:00
9 changed files with 125 additions and 112 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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