Convert LocationControlSet to compose

pull/1924/head
Alex Baker 3 years ago
parent 08069d0a3d
commit 8f4db8374d

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

@ -3,10 +3,15 @@ package org.tasks.preferences;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastQ; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastQ;
import static java.util.Arrays.asList;
import android.Manifest.permission; import android.Manifest.permission;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import dagger.hilt.android.qualifiers.ApplicationContext; import dagger.hilt.android.qualifiers.ApplicationContext;
@ -34,9 +39,7 @@ public class PermissionChecker {
} }
public boolean canAccessBackgroundLocation() { public boolean canAccessBackgroundLocation() {
return atLeastQ() return checkPermissions(backgroundPermissions().toArray(new String[0]));
? canAccessForegroundLocation() && checkPermissions(permission.ACCESS_BACKGROUND_LOCATION)
: canAccessForegroundLocation();
} }
private boolean checkPermissions(String... permissions) { private boolean checkPermissions(String... permissions) {
@ -48,4 +51,10 @@ public class PermissionChecker {
} }
return true; return true;
} }
public static List<String> backgroundPermissions() {
return atLeastQ()
? asList(permission.ACCESS_FINE_LOCATION, permission.ACCESS_BACKGROUND_LOCATION)
: Collections.singletonList(permission.ACCESS_FINE_LOCATION);
}
} }

@ -4,21 +4,30 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import android.text.SpannableString import androidx.compose.foundation.layout.Column
import android.text.Spanned import androidx.compose.foundation.layout.Row
import android.text.style.ClickableSpan import androidx.compose.foundation.layout.padding
import android.view.View import androidx.compose.material.Icon
import android.view.ViewGroup import androidx.compose.material.IconButton
import android.widget.ImageView import androidx.compose.material.Text
import android.widget.TextView 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 androidx.core.util.Pair
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.compose.DisabledText
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.data.Geofence import org.tasks.data.Geofence
import org.tasks.data.Location import org.tasks.data.Location
import org.tasks.data.Place import org.tasks.data.Place
import org.tasks.databinding.LocationRowBinding
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.GeofenceDialog import org.tasks.dialogs.GeofenceDialog
import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.openUri
@ -27,78 +36,37 @@ import org.tasks.location.LocationPickerActivity
import org.tasks.preferences.Device import org.tasks.preferences.Device
import org.tasks.preferences.FragmentPermissionRequestor import org.tasks.preferences.FragmentPermissionRequestor
import org.tasks.preferences.PermissionChecker import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.PermissionChecker.backgroundPermissions
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class LocationControlSet : TaskEditControlFragment() { class LocationControlSet : TaskEditControlComposeFragment() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var device: Device @Inject lateinit var device: Device
@Inject lateinit var permissionRequestor: FragmentPermissionRequestor @Inject lateinit var permissionRequestor: FragmentPermissionRequestor
@Inject lateinit var permissionChecker: PermissionChecker @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?) { private fun setLocation(location: Location?) {
viewModel.selectedLocation = location viewModel.selectedLocation.value = 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
}
} }
override fun onRowClick() { override fun onRowClick() {
val location = viewModel.selectedLocation val location = viewModel.selectedLocation.value
if (location == null) { if (location == null) {
chooseLocation() chooseLocation()
} else { } else {
val options: MutableList<Pair<Int, () -> Unit>> = ArrayList() val options: MutableList<Pair<Int, () -> 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)) { 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)) { 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.choose_new_location) { chooseLocation() })
options.add(Pair.create(R.string.delete, { setLocation(null) })) options.add(Pair.create(R.string.delete) { setLocation(null) })
val items = options.map { requireContext().getString(it.first!!) } val items = options.map { requireContext().getString(it.first!!) }
dialogBuilder dialogBuilder
.newDialog(location.displayName) .newDialog(location.displayName)
@ -111,36 +79,46 @@ class LocationControlSet : TaskEditControlFragment() {
private fun chooseLocation() { private fun chooseLocation() {
val intent = Intent(activity, LocationPickerActivity::class.java) val intent = Intent(activity, LocationPickerActivity::class.java)
viewModel.selectedLocation?.let { viewModel.selectedLocation.value?.let {
intent.putExtra(LocationPickerActivity.EXTRA_PLACE, it.place as Parcelable) intent.putExtra(LocationPickerActivity.EXTRA_PLACE, it.place as Parcelable)
} }
startActivityForResult(intent, REQUEST_LOCATION_REMINDER) 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() { private fun showGeofenceOptions() {
val dialog = GeofenceDialog.newGeofenceDialog(viewModel.selectedLocation) val dialog = GeofenceDialog.newGeofenceDialog(viewModel.selectedLocation.value)
dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS) dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS)
dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG) dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG)
} }
override fun bind(parent: ViewGroup?) = @OptIn(ExperimentalPermissionsApi::class)
LocationRowBinding.inflate(layoutInflater, parent, true).let { @Composable
locationName = it.locationName override fun Body() {
locationAddress = it.locationAddress val location = viewModel.selectedLocation.collectAsStateLifecycleAware().value
geofenceOptions = it.geofenceOptions.apply { val hasPermissions =
setOnClickListener { geofenceOptions() } rememberMultiplePermissionsState(permissions = backgroundPermissions())
} .allPermissionsGranted
it.root 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 override val icon = R.drawable.ic_outline_place_24px
@ -149,11 +127,11 @@ class LocationControlSet : TaskEditControlFragment() {
override val isClickable = true override val isClickable = true
private fun openWebsite() { private fun openWebsite() {
viewModel.selectedLocation?.let { context?.openUri(it.url) } viewModel.selectedLocation.value?.let { context?.openUri(it.url) }
} }
private fun call() { private fun call() {
viewModel.selectedLocation?.let { viewModel.selectedLocation.value?.let {
startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + it.phone))) startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + it.phone)))
} }
} }
@ -166,7 +144,7 @@ class LocationControlSet : TaskEditControlFragment() {
} else if (requestCode == REQUEST_LOCATION_REMINDER) { } else if (requestCode == REQUEST_LOCATION_REMINDER) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
val place: Place = data!!.getParcelableExtra(LocationPickerActivity.EXTRA_PLACE)!! val place: Place = data!!.getParcelableExtra(LocationPickerActivity.EXTRA_PLACE)!!
val location = viewModel.selectedLocation val location = viewModel.selectedLocation.value
val geofence = if (location == null) { val geofence = if (location == null) {
Geofence(place.uid, preferences) Geofence(place.uid, preferences)
} else { } else {
@ -183,7 +161,7 @@ class LocationControlSet : TaskEditControlFragment() {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
setLocation(Location( setLocation(Location(
data?.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE) ?: return, data?.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE) ?: return,
viewModel.selectedLocation?.place ?: return viewModel.selectedLocation.value?.place ?: return
)) ))
} }
} else { } else {
@ -199,4 +177,39 @@ class LocationControlSet : TaskEditControlFragment() {
private const val FRAG_TAG_LOCATION_DIALOG = "location_dialog" private const val FRAG_TAG_LOCATION_DIALOG = "location_dialog"
private const val FRAG_TAG_REQUEST_LOCATION = "request_location" private const val FRAG_TAG_REQUEST_LOCATION = "request_location"
} }
} }
@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
)
}
}
}

@ -251,10 +251,10 @@ class TaskEditViewModel @Inject constructor(
var originalLocation: Location? = null var originalLocation: Location? = null
private set(value) { private set(value) {
field = value field = value
selectedLocation = value selectedLocation.value = value
} }
var selectedLocation: Location? = null var selectedLocation = MutableStateFlow<Location?>(null)
private lateinit var originalTags: List<TagData> private lateinit var originalTags: List<TagData>
@ -311,7 +311,7 @@ class TaskEditViewModel @Inject constructor(
task.elapsedSeconds != elapsedSeconds || task.elapsedSeconds != elapsedSeconds ||
task.estimatedSeconds != estimatedSeconds || task.estimatedSeconds != estimatedSeconds ||
originalList != selectedList.value || originalList != selectedList.value ||
originalLocation != selectedLocation || originalLocation != selectedLocation.value ||
originalTags.toHashSet() != selectedTags.value.toHashSet() || originalTags.toHashSet() != selectedTags.value.toHashSet() ||
newSubtasks.isNotEmpty() || newSubtasks.isNotEmpty() ||
getRingFlags() != when { getRingFlags() != when {
@ -355,14 +355,14 @@ class TaskEditViewModel @Inject constructor(
taskMover.move(listOf(task.id), selectedList.value!!) taskMover.move(listOf(task.id), selectedList.value!!)
} }
if ((isNew && selectedLocation != null) || originalLocation != selectedLocation) { if ((isNew && selectedLocation.value != null) || originalLocation != selectedLocation.value) {
originalLocation?.let { location -> originalLocation?.let { location ->
if (location.geofence.id > 0) { if (location.geofence.id > 0) {
locationDao.delete(location.geofence) locationDao.delete(location.geofence)
geofenceApi.update(location.place) geofenceApi.update(location.place)
} }
} }
selectedLocation?.let { location -> selectedLocation.value?.let { location ->
val place = location.place val place = location.place
val geofence = location.geofence val geofence = location.geofence
geofence.task = task.id geofence.task = task.id

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false">
<ImageView
android:id="@+id/geofence_options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="top|center"
android:alpha="@dimen/alpha_secondary"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_outline_notifications_off_24px"
app:tint="@color/icon_tint"
android:visibility="gone"/>
<TextView
android:id="@+id/location_name"
style="@style/TaskEditTextPrimary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/geofence_options"
android:gravity="start"
android:hint="@string/add_location"
android:textAlignment="viewStart"/>
<TextView
android:id="@+id/location_address"
style="@style/TaskEditTextPrimary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/location_name"
android:layout_toStartOf="@id/geofence_options"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?android:textColorSecondary"
android:visibility="gone"/>
</RelativeLayout>
Loading…
Cancel
Save