From 8f4db8374d309ad4343c18c9ee422179e2523030 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 6 Jul 2022 00:39:31 -0500 Subject: [PATCH] Convert LocationControlSet to compose --- .../java/org/tasks/compose/DisabledText.kt | 20 ++ .../tasks/preferences/PermissionChecker.java | 15 +- .../java/org/tasks/ui/LocationControlSet.kt | 177 ++++++++++-------- .../java/org/tasks/ui/TaskEditViewModel.kt | 10 +- app/src/main/res/layout/location_row.xml | 47 ----- 5 files changed, 132 insertions(+), 137 deletions(-) create mode 100644 app/src/main/java/org/tasks/compose/DisabledText.kt delete mode 100644 app/src/main/res/layout/location_row.xml diff --git a/app/src/main/java/org/tasks/compose/DisabledText.kt b/app/src/main/java/org/tasks/compose/DisabledText.kt new file mode 100644 index 000000000..925307bc3 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/DisabledText.kt @@ -0,0 +1,20 @@ +package org.tasks.compose + +import androidx.compose.material.ContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha + +@Composable +fun DisabledText( + text: String, + modifier: Modifier = Modifier +) { + Text( + text = text, + style = MaterialTheme.typography.body1, + modifier = modifier.alpha(alpha = ContentAlpha.disabled) + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/preferences/PermissionChecker.java b/app/src/main/java/org/tasks/preferences/PermissionChecker.java index a4b6a5a0f..ce738427c 100644 --- a/app/src/main/java/org/tasks/preferences/PermissionChecker.java +++ b/app/src/main/java/org/tasks/preferences/PermissionChecker.java @@ -3,10 +3,15 @@ package org.tasks.preferences; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastQ; +import static java.util.Arrays.asList; + import android.Manifest.permission; import android.content.Context; import android.content.pm.PackageManager; +import java.util.Collections; +import java.util.List; + import javax.inject.Inject; import dagger.hilt.android.qualifiers.ApplicationContext; @@ -34,9 +39,7 @@ public class PermissionChecker { } public boolean canAccessBackgroundLocation() { - return atLeastQ() - ? canAccessForegroundLocation() && checkPermissions(permission.ACCESS_BACKGROUND_LOCATION) - : canAccessForegroundLocation(); + return checkPermissions(backgroundPermissions().toArray(new String[0])); } private boolean checkPermissions(String... permissions) { @@ -48,4 +51,10 @@ public class PermissionChecker { } return true; } + + public static List backgroundPermissions() { + return atLeastQ() + ? asList(permission.ACCESS_FINE_LOCATION, permission.ACCESS_BACKGROUND_LOCATION) + : Collections.singletonList(permission.ACCESS_FINE_LOCATION); + } } diff --git a/app/src/main/java/org/tasks/ui/LocationControlSet.kt b/app/src/main/java/org/tasks/ui/LocationControlSet.kt index 10637ebee..d04b2317b 100644 --- a/app/src/main/java/org/tasks/ui/LocationControlSet.kt +++ b/app/src/main/java/org/tasks/ui/LocationControlSet.kt @@ -4,21 +4,30 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Parcelable -import android.text.SpannableString -import android.text.Spanned -import android.text.style.ClickableSpan -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.NotificationsOff +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.core.util.Pair +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState import dagger.hilt.android.AndroidEntryPoint import org.tasks.R import org.tasks.Strings.isNullOrEmpty +import org.tasks.compose.DisabledText +import org.tasks.compose.collectAsStateLifecycleAware import org.tasks.data.Geofence import org.tasks.data.Location import org.tasks.data.Place -import org.tasks.databinding.LocationRowBinding import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.GeofenceDialog import org.tasks.extensions.Context.openUri @@ -27,78 +36,37 @@ import org.tasks.location.LocationPickerActivity import org.tasks.preferences.Device import org.tasks.preferences.FragmentPermissionRequestor import org.tasks.preferences.PermissionChecker +import org.tasks.preferences.PermissionChecker.backgroundPermissions import org.tasks.preferences.Preferences import javax.inject.Inject @AndroidEntryPoint -class LocationControlSet : TaskEditControlFragment() { +class LocationControlSet : TaskEditControlComposeFragment() { @Inject lateinit var preferences: Preferences @Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var device: Device @Inject lateinit var permissionRequestor: FragmentPermissionRequestor @Inject lateinit var permissionChecker: PermissionChecker - private lateinit var locationName: TextView - private lateinit var locationAddress: TextView - private lateinit var geofenceOptions: ImageView - - override fun onResume() { - super.onResume() - - updateUi() - } - private fun setLocation(location: Location?) { - viewModel.selectedLocation = location - updateUi() - } - - private fun updateUi() { - val location = viewModel.selectedLocation - if (location == null) { - locationName.text = "" - geofenceOptions.visibility = View.GONE - locationAddress.visibility = View.GONE - } else { - geofenceOptions.visibility = View.VISIBLE - geofenceOptions.setImageResource( - if (permissionChecker.canAccessBackgroundLocation() - && (location.isArrival || location.isDeparture)) R.drawable.ic_outline_notifications_24px else R.drawable.ic_outline_notifications_off_24px) - val name = location.displayName - val address = location.displayAddress - if (!isNullOrEmpty(address) && address != name) { - locationAddress.text = address - locationAddress.visibility = View.VISIBLE - } else { - locationAddress.visibility = View.GONE - } - val spannableString = SpannableString(name) - spannableString.setSpan( - object : ClickableSpan() { - override fun onClick(view: View) {} - }, - 0, - name.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - locationName.text = spannableString - } + viewModel.selectedLocation.value = location } override fun onRowClick() { - val location = viewModel.selectedLocation + val location = viewModel.selectedLocation.value if (location == null) { chooseLocation() } else { val options: MutableList Unit>> = ArrayList() - options.add(Pair.create(R.string.open_map, { location.open(activity) })) + options.add(Pair.create(R.string.open_map) { location.open(activity) }) if (!isNullOrEmpty(location.phone)) { - options.add(Pair.create(R.string.action_call, { call() })) + options.add(Pair.create(R.string.action_call) { call() }) } if (!isNullOrEmpty(location.url)) { - options.add(Pair.create(R.string.visit_website, { openWebsite() })) + options.add(Pair.create(R.string.visit_website) { openWebsite() }) } - options.add(Pair.create(R.string.choose_new_location, { chooseLocation() })) - options.add(Pair.create(R.string.delete, { setLocation(null) })) + options.add(Pair.create(R.string.choose_new_location) { chooseLocation() }) + options.add(Pair.create(R.string.delete) { setLocation(null) }) val items = options.map { requireContext().getString(it.first!!) } dialogBuilder .newDialog(location.displayName) @@ -111,36 +79,46 @@ class LocationControlSet : TaskEditControlFragment() { private fun chooseLocation() { val intent = Intent(activity, LocationPickerActivity::class.java) - viewModel.selectedLocation?.let { + viewModel.selectedLocation.value?.let { intent.putExtra(LocationPickerActivity.EXTRA_PLACE, it.place as Parcelable) } startActivityForResult(intent, REQUEST_LOCATION_REMINDER) } - private fun geofenceOptions() { - if (permissionChecker.canAccessBackgroundLocation()) { - showGeofenceOptions() - } else { - newLocationPermissionDialog(this, REQUEST_LOCATION_PERMISSIONS) - .show(parentFragmentManager, FRAG_TAG_REQUEST_LOCATION) - } - } - private fun showGeofenceOptions() { - val dialog = GeofenceDialog.newGeofenceDialog(viewModel.selectedLocation) + val dialog = GeofenceDialog.newGeofenceDialog(viewModel.selectedLocation.value) dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS) dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG) } - override fun bind(parent: ViewGroup?) = - LocationRowBinding.inflate(layoutInflater, parent, true).let { - locationName = it.locationName - locationAddress = it.locationAddress - geofenceOptions = it.geofenceOptions.apply { - setOnClickListener { geofenceOptions() } - } - it.root + @OptIn(ExperimentalPermissionsApi::class) + @Composable + override fun Body() { + val location = viewModel.selectedLocation.collectAsStateLifecycleAware().value + val hasPermissions = + rememberMultiplePermissionsState(permissions = backgroundPermissions()) + .allPermissionsGranted + if (location == null) { + DisabledText( + text = stringResource(id = R.string.add_location), + modifier = Modifier.padding(vertical = 20.dp) + ) + } else { + LocationRow( + name = location.displayName, + address = location.displayAddress, + onClick = { + if (hasPermissions) { + showGeofenceOptions() + } else { + newLocationPermissionDialog(this, REQUEST_LOCATION_PERMISSIONS) + .show(parentFragmentManager, FRAG_TAG_REQUEST_LOCATION) + } + }, + geofenceOn = hasPermissions && (location.isArrival || location.isDeparture) + ) } + } override val icon = R.drawable.ic_outline_place_24px @@ -149,11 +127,11 @@ class LocationControlSet : TaskEditControlFragment() { override val isClickable = true private fun openWebsite() { - viewModel.selectedLocation?.let { context?.openUri(it.url) } + viewModel.selectedLocation.value?.let { context?.openUri(it.url) } } private fun call() { - viewModel.selectedLocation?.let { + viewModel.selectedLocation.value?.let { startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + it.phone))) } } @@ -166,7 +144,7 @@ class LocationControlSet : TaskEditControlFragment() { } else if (requestCode == REQUEST_LOCATION_REMINDER) { if (resultCode == Activity.RESULT_OK) { val place: Place = data!!.getParcelableExtra(LocationPickerActivity.EXTRA_PLACE)!! - val location = viewModel.selectedLocation + val location = viewModel.selectedLocation.value val geofence = if (location == null) { Geofence(place.uid, preferences) } else { @@ -183,7 +161,7 @@ class LocationControlSet : TaskEditControlFragment() { if (resultCode == Activity.RESULT_OK) { setLocation(Location( data?.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE) ?: return, - viewModel.selectedLocation?.place ?: return + viewModel.selectedLocation.value?.place ?: return )) } } else { @@ -199,4 +177,39 @@ class LocationControlSet : TaskEditControlFragment() { private const val FRAG_TAG_LOCATION_DIALOG = "location_dialog" private const val FRAG_TAG_REQUEST_LOCATION = "request_location" } -} \ No newline at end of file +} + +@Composable +fun LocationRow( + name: String, + address: String?, + geofenceOn: Boolean, + onClick: () -> Unit, +) { + Row { + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 20.dp) + ) { + Text(text = name) + address?.takeIf { it.isNotBlank() && it != name }?.let { + Text(text = address) + } + } + IconButton( + onClick = onClick, + modifier = Modifier.padding(top = 8.dp /* + 12dp from icon */) + ) { + Icon( + imageVector = if (geofenceOn) { + Icons.Outlined.Notifications + } else { + Icons.Outlined.NotificationsOff + }, + contentDescription = null + ) + } + } +} + diff --git a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt index 63dc3feaf..66b7fedbf 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -251,10 +251,10 @@ class TaskEditViewModel @Inject constructor( var originalLocation: Location? = null private set(value) { field = value - selectedLocation = value + selectedLocation.value = value } - var selectedLocation: Location? = null + var selectedLocation = MutableStateFlow(null) private lateinit var originalTags: List @@ -311,7 +311,7 @@ class TaskEditViewModel @Inject constructor( task.elapsedSeconds != elapsedSeconds || task.estimatedSeconds != estimatedSeconds || originalList != selectedList.value || - originalLocation != selectedLocation || + originalLocation != selectedLocation.value || originalTags.toHashSet() != selectedTags.value.toHashSet() || newSubtasks.isNotEmpty() || getRingFlags() != when { @@ -355,14 +355,14 @@ class TaskEditViewModel @Inject constructor( taskMover.move(listOf(task.id), selectedList.value!!) } - if ((isNew && selectedLocation != null) || originalLocation != selectedLocation) { + if ((isNew && selectedLocation.value != null) || originalLocation != selectedLocation.value) { originalLocation?.let { location -> if (location.geofence.id > 0) { locationDao.delete(location.geofence) geofenceApi.update(location.place) } } - selectedLocation?.let { location -> + selectedLocation.value?.let { location -> val place = location.place val geofence = location.geofence geofence.task = task.id diff --git a/app/src/main/res/layout/location_row.xml b/app/src/main/res/layout/location_row.xml deleted file mode 100644 index 4b9f6a13b..000000000 --- a/app/src/main/res/layout/location_row.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - -