Compare commits

...

14 Commits
0.0.5 ... 0.1.0

Author SHA1 Message Date
Johan von Forstner
2bd5f746ed Release 0.1.0 2020-05-21 16:46:36 +02:00
Johan von Forstner
fbc15f2925 sort donations by price 2020-05-21 16:45:54 +02:00
Johan von Forstner
11f492df1d Release 0.0.7 2020-05-21 15:11:22 +02:00
Johan von Forstner
629fbb0f1b reduce clusterDistance to 40 2020-05-21 14:58:02 +02:00
Johan von Forstner
d00840c3bd implement donation view 2020-05-21 14:53:30 +02:00
Johan von Forstner
084084c26c fix highlighted charger after moving map 2020-05-19 20:50:54 +02:00
Johan von Forstner
f4b174efe1 bounce marker when selected 2020-05-19 20:46:34 +02:00
Johan von Forstner
81d3ba115a change package name and launcher name for debug version of the app 2020-05-19 20:42:42 +02:00
Johan von Forstner
a35a5f7050 re-add errorneously removed imports 2020-05-19 20:32:45 +02:00
Johan von Forstner
c1cec8781b highlight currently selected chharger (fixes #15) 2020-05-19 20:23:59 +02:00
Johan von Forstner
be98e7e266 Release 0.0.6 2020-05-17 22:40:02 +02:00
Johan von Forstner
49ef661ac1 dark mode: set text color for marker clustering to white 2020-05-17 22:39:12 +02:00
Johan von Forstner
1d98264437 fix color of location button in dark mode 2020-05-17 22:34:29 +02:00
Johan von Forstner
4d137614d5 fix broken database transactions 2020-05-17 22:33:04 +02:00
20 changed files with 409 additions and 68 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 5
versionName "0.0.5"
versionCode 8
versionName "0.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -29,6 +29,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
applicationIdSuffix ".debug"
debuggable true
}
}
def isRunningOnTravis = System.getenv("CI") == "true"
@@ -95,6 +99,7 @@ dependencies {
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:3.4.0'
implementation 'io.michaelrocks:bimap:1.0.2'
// navigation library
def nav_version = "2.3.0-alpha06"

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EV Map (debug)</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EV Map (debug)</string>
</resources>

View File

@@ -298,4 +298,8 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
}
return value
}
}
class DonationAdapter() : DataBindingAdapter<DonationItem>() {
override fun getItemViewType(position: Int): Int = R.layout.item_donation
}

View File

@@ -29,9 +29,6 @@ class AboutFragment : PreferenceFragmentCompat() {
setPreferencesFromResource(R.xml.about, rootKey)
findPreference<Preference>("version")?.summary = BuildConfig.VERSION_NAME
//TODO: disable donations until fully implemented
findPreference<Preference>("donate")?.isVisible = false
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {

View File

@@ -9,10 +9,14 @@ import androidx.appcompat.widget.Toolbar
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DonationAdapter
import net.vonforst.evmap.databinding.FragmentDonateBinding
import net.vonforst.evmap.viewmodel.DonateViewModel
@@ -40,5 +44,21 @@ class DonateFragment : Fragment() {
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
binding.productsList.apply {
adapter = DonationAdapter().apply {
onClickListener = {
vm.startPurchase(it, requireActivity())
}
}
layoutManager = LinearLayoutManager(context)
}
vm.purchaseSuccessful.observe(viewLifecycleOwner, Observer {
Snackbar.make(view, R.string.donation_successful, Snackbar.LENGTH_LONG).show()
})
vm.purchaseFailed.observe(viewLifecycleOwner, Observer {
Snackbar.make(view, R.string.donation_failed, Snackbar.LENGTH_LONG).show()
})
}
}

View File

@@ -43,6 +43,8 @@ import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
import io.michaelrocks.bimap.HashBiMap
import io.michaelrocks.bimap.MutableBiMap
import kotlinx.android.synthetic.main.fragment_map.*
import net.vonforst.evmap.*
import net.vonforst.evmap.adapter.ConnectorAdapter
@@ -78,7 +80,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
private var markers: Map<Marker, ChargeLocation> = emptyMap()
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
private var clusterMarkers: List<Marker> = emptyList()
private lateinit var clusterIconGenerator: ClusterIconGenerator
@@ -251,8 +253,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.fabDirections.show()
detailAppBarBehavior.setToolbarTitle(it.name)
updateFavoriteToggle()
highlightMarker(it)
} else {
bottomSheetBehavior.state = STATE_HIDDEN
unhighlightAllMarkers()
}
})
vm.chargepoints.observe(viewLifecycleOwner, Observer {
@@ -264,6 +268,38 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
})
}
private fun unhighlightAllMarkers() {
markers.forEach { (m, c) ->
m.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(c)
)
)
}
}
private fun highlightMarker(charger: ChargeLocation) {
val marker = markers.inverse[charger] ?: return
// highlight this marker
marker.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(charger), highlight = true
)
)
animator.animateMarkerBounce(marker)
// un-highlight all other markers
markers.forEach { (m, c) ->
if (m != marker) {
m.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(c)
)
)
}
}
}
private fun updateFavoriteToggle() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
@@ -457,34 +493,35 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
val chargepointIds = chargers.map { it.id }.toSet()
markers = markers.filter {
// remove markers that disappeared
markers.entries.toList().forEach {
if (!chargepointIds.contains(it.value.id)) {
val tint = getMarkerTint(it.value)
if (it.key.isVisible) {
animator.animateMarkerDisappear(it.key, tint)
val tint = getMarkerTint(it.value)
val highlight = it.value == vm.chargerSparse.value
animator.animateMarkerDisappear(it.key, tint, highlight)
} else {
it.key.remove()
}
false
} else {
true
markers.remove(it.key)
}
}
markers = markers + chargers.filter {
// add new markers
chargers.filter {
!markers.containsValue(it)
}.map { charger ->
}.forEach { charger ->
val tint = getMarkerTint(charger)
val highlight = charger == vm.chargerSparse.value
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
.icon(
chargerIconGenerator.getBitmapDescriptor(tint)
chargerIconGenerator.getBitmapDescriptor(tint, highlight = highlight)
)
)
animator.animateMarkerAppear(marker, tint)
marker to charger
}.toMap()
animator.animateMarkerAppear(marker, tint, highlight)
markers[marker] = charger
}
clusterMarkers = clusters.map { cluster ->
map.addMarker(
MarkerOptions()

View File

@@ -53,11 +53,15 @@ abstract class AppDatabase : RoomDatabase() {
override fun migrate(db: SupportSQLiteDatabase) {
// recreate ChargeLocation table to make postcode nullable
db.beginTransaction()
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT NOT NULL, `country` TEXT NOT NULL, `postcode` TEXT, `street` TEXT NOT NULL, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
db.execSQL("DROP TABLE `ChargeLocation`")
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
db.endTransaction()
try {
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT NOT NULL, `country` TEXT NOT NULL, `postcode` TEXT, `street` TEXT NOT NULL, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
db.execSQL("DROP TABLE `ChargeLocation`")
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}
@@ -71,11 +75,15 @@ abstract class AppDatabase : RoomDatabase() {
override fun migrate(db: SupportSQLiteDatabase) {
// recreate ChargeLocation table to make other address fields nullable
db.beginTransaction()
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
db.execSQL("DROP TABLE `ChargeLocation`")
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
db.endTransaction()
try {
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
db.execSQL("DROP TABLE `ChargeLocation`")
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}
}

View File

@@ -10,6 +10,7 @@ import android.view.ViewGroup
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.widget.TextViewCompat
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.maps.android.ui.IconGenerator
@@ -20,7 +21,6 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
init {
setBackground(context.getDrawable(R.drawable.marker_cluster_bg))
setContentView(makeSquareTextView(context))
setTextAppearance(R.style.TextAppearance_AppCompat_Inverse)
}
private fun makeSquareTextView(context: Context): SquareTextView? {
@@ -34,13 +34,15 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
)
id = com.google.maps.android.R.id.amu_text
setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi)
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_AppCompat)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
}
}
}
class ChargerIconGenerator(val context: Context) {
data class BitmapData(val tint: Int, val scale: Int, val alpha: Int)
data class BitmapData(val tint: Int, val scale: Int, val alpha: Int, val highlight: Boolean)
val cacheSize = 4 * 1024 * 1024; // 4MiB
val cache = object : LruCache<BitmapData, Bitmap>(cacheSize) {
@@ -50,6 +52,7 @@ class ChargerIconGenerator(val context: Context) {
}
val oversize = 1f // increase to add padding for overshoot scale animation
val icon = R.drawable.ic_map_marker_charging
val highlightIcon = R.drawable.ic_map_marker_highlight
init {
preloadCache()
@@ -64,10 +67,12 @@ class ChargerIconGenerator(val context: Context) {
R.color.charger_11kw,
R.color.charger_low
)
for (tint in tints) {
for (scale in 0..20) {
val data = BitmapData(tint, scale, 255)
cache.put(data, generateBitmap(data))
for (highlight in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..20) {
val data = BitmapData(tint, scale, 255, highlight)
cache.put(data, generateBitmap(data))
}
}
}
}
@@ -75,9 +80,10 @@ class ChargerIconGenerator(val context: Context) {
fun getBitmapDescriptor(
@ColorRes tint: Int,
scale: Int = 20,
alpha: Int = 255
alpha: Int = 255,
highlight: Boolean = false
): BitmapDescriptor? {
val data = BitmapData(tint, scale, alpha)
val data = BitmapData(tint, scale, alpha, highlight)
val cachedImg = cache[data]
return if (cachedImg != null) {
BitmapDescriptorFactory.fromBitmap(cachedImg)
@@ -118,6 +124,18 @@ class ChargerIconGenerator(val context: Context) {
)
vd.draw(canvas)
if (data.highlight) {
val highlightDrawable = context.getDrawable(highlightIcon)!!
highlightDrawable.setBounds(
leftPadding.toInt(), topPadding.toInt(),
leftPadding.toInt() + vd.intrinsicWidth,
topPadding.toInt() + vd.intrinsicHeight
)
highlightDrawable.alpha = data.alpha
highlightDrawable.draw(canvas)
}
return bm
}
}

View File

@@ -1,12 +1,14 @@
package net.vonforst.evmap.ui
import android.animation.ValueAnimator
import android.view.animation.BounceInterpolator
import androidx.core.animation.addListener
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import com.google.android.gms.maps.model.Marker
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import kotlin.math.max
fun getMarkerTint(charger: ChargeLocation): Int = when {
charger.maxPower >= 100 -> R.color.charger_100kw
@@ -21,7 +23,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
fun animateMarkerAppear(
marker: Marker,
tint: Int
tint: Int,
highlight: Boolean
) {
animatingMarkers[marker]?.cancel()
animatingMarkers.remove(marker)
@@ -37,7 +40,7 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
}
val scale = animationState.animatedValue as Int
marker.setIcon(
gen.getBitmapDescriptor(tint, scale = scale)
gen.getBitmapDescriptor(tint, scale = scale, highlight = highlight)
)
}
addListener(onEnd = {
@@ -50,7 +53,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
fun animateMarkerDisappear(
marker: Marker,
tint: Int
tint: Int,
highlight: Boolean
) {
animatingMarkers[marker]?.cancel()
animatingMarkers.remove(marker)
@@ -66,7 +70,7 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
}
val scale = animationState.animatedValue as Int
marker.setIcon(
gen.getBitmapDescriptor(tint, scale = scale)
gen.getBitmapDescriptor(tint, scale = scale, highlight = highlight)
)
}
addListener(onEnd = {
@@ -77,4 +81,25 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
animatingMarkers[marker] = anim
anim.start()
}
fun animateMarkerBounce(marker: Marker) {
animatingMarkers[marker]?.cancel()
animatingMarkers.remove(marker)
val anim = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 700
interpolator = BounceInterpolator()
addUpdateListener { state ->
if (!marker.isVisible) {
cancel()
animatingMarkers.remove(marker)
return@addUpdateListener
}
val t = max(1f - state.animatedValue as Float, 0f) / 2
marker.setAnchor(0.5f, 1.0f + t)
}
}
animatingMarkers[marker] = anim
anim.start()
}
}

View File

@@ -1,9 +1,12 @@
package net.vonforst.evmap.viewmodel
import android.app.Activity
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import com.android.billingclient.api.*
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.adapter.Equatable
class DonateViewModel(application: Application) : AndroidViewModel(application),
PurchasesUpdatedListener {
@@ -12,18 +15,99 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
.enablePendingPurchases()
.build()
val products: MutableLiveData<List<SkuDetails>> by lazy {
MutableLiveData<List<SkuDetails>>().apply {
value = null
init {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
}
val params = SkuDetailsParams.newBuilder().setType(BillingClient.SkuType.INAPP).build()
billingClient.querySkuDetailsAsync(params) { result, details ->
value = details
override fun onBillingSetupFinished(p0: BillingResult?) {
loadProducts()
// consume pending purchases
val purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
purchases.purchasesList.forEach {
if (!it.isAcknowledged) {
consumePurchase(it.purchaseToken, false)
}
}
}
})
}
private fun loadProducts() {
val params = SkuDetailsParams.newBuilder()
.setType(BillingClient.SkuType.INAPP)
.setSkusList(
listOf(
"donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur"
) +
if (BuildConfig.DEBUG) {
listOf(
"android.test.purchased", "android.test.canceled",
"android.test.item_unavailable"
)
} else {
emptyList()
}
)
.build()
billingClient.querySkuDetailsAsync(params) { result, details ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
products.value = Resource.success(details
.sortedBy { it.priceAmountMicros }
.map { DonationItem(it) }
)
} else {
products.value = Resource.error(result.debugMessage, null)
}
}
}
override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
TODO("Not yet implemented")
val products: MutableLiveData<Resource<List<DonationItem>>> by lazy {
MutableLiveData<Resource<List<DonationItem>>>().apply {
value = Resource.loading(null)
}
}
}
val purchaseSuccessful = SingleLiveEvent<Nothing>()
val purchaseFailed = SingleLiveEvent<Nothing>()
override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
val purchaseToken = purchase.purchaseToken
consumePurchase(purchaseToken)
}
} else if (result.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
purchaseFailed.call()
}
}
private fun consumePurchase(purchaseToken: String, showSuccess: Boolean = true) {
val params = ConsumeParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build()
billingClient.consumeAsync(params) { _, _ ->
if (showSuccess) purchaseSuccessful.call()
}
}
fun startPurchase(it: DonationItem, activity: Activity) {
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(it.sku)
.build()
val response = billingClient.launchBillingFlow(activity, flowParams)
if (response.responseCode != BillingClient.BillingResponseCode.OK) {
purchaseFailed.call()
}
}
override fun onCleared() {
billingClient.endConnection()
}
}
data class DonationItem(val sku: SkuDetails) : Equatable

View File

@@ -172,7 +172,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude,
clustering = zoom < 13 && useClustering, zoom = zoom,
clusterDistance = 70, freecharging = freecharging, minPower = minPower,
clusterDistance = 40, freecharging = freecharging, minPower = minPower,
freeparking = freeparking, plugs = connectors
)

View File

@@ -1,7 +1,10 @@
package net.vonforst.evmap.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.*
import java.util.concurrent.atomic.AtomicBoolean
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
object : ViewModelProvider.Factory {
@@ -32,4 +35,31 @@ data class Resource<out T>(val status: Status, val data: T?, val message: String
return Resource(Status.LOADING, data, null)
}
}
}
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending: AtomicBoolean = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(it)
}
})
}
@MainThread
override fun setValue(@Nullable t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
}

View File

@@ -6,9 +6,7 @@
<path
android:fillColor="#FFFFFF"
android:pathData="M109.8,0h13.6c33.9,1.9 67.1,18.5 87.7,45.8c13.5,17.2 21,38.6 22.7,60.3v8.1c-0.8,42.1 -27.7,76.6 -51,109.4c-26.2,37 -50.4,77.3 -57.1,122.9c-1.8,7.7 0.4,18.5 -8.9,22c-2.2,-1.7 -4.7,-3.1 -6.2,-5.4c-2.7,-25.5 -9.1,-50.7 -20,-73.9c-12.3,-27.1 -29.5,-51.6 -47,-75.6C33,199 23,184.2 14.7,168.3c-13,-23.8 -17.9,-51.9 -12.5,-78.6c4.4,-21.1 15.4,-40.6 30.6,-55.7C53.3,14 81.1,1.8 109.8,0z" />
<!--<path android:fillColor="#802C27" android:pathData="M107.2,74.1c18.9,-4.8 40.4,5.5 47.7,23.7c6.1,14.5 1.9,32.5 -9.9,42.9c-12.6,11.5 -32.4,14 -47.5,6c-13.9,-6.8 -23,-22.6 -21.3,-38.1C77.6,92 91.1,77.7 107.2,74.1z"/>-->
<path
android:fillColor="#808080"
android:pathData="M90.9,57.3v68.2h18.6v55.8l43.4,-74.4h-24.8l24.8,-49.6H90.9z" />
<!--<path android:fillColor="#802C27" android:pathData="M159,85.3L159,85.3l-20.8,-20.9l-5.9,5.9l11.8,11.8c-5.3,2 -9,7.1 -9,13.1c0,7.7 6.3,14 14,14c2,0 3.9,-0.4 5.6,-1.2v40.4c0,3.1 -2.5,5.6 -5.6,5.6s-5.6,-2.5 -5.6,-5.6v-25.2c0,-6.2 -5,-11.2 -11.2,-11.2h-5.6V72.8c0,-6.2 -5,-11.2 -11.2,-11.2H81.8c-6.2,0 -11.2,5 -11.2,11.2v89.7h56.1v-42.1h8.4v28c0,7.7 6.3,14 14,14s14,-6.3 14,-14V95.2C163.1,91.3 161.6,87.8 159,85.3M149.1,100.8c-3.1,0 -5.6,-2.5 -5.6,-5.6c0,-3.1 2.5,-5.6 5.6,-5.6s5.6,2.5 5.6,5.6C154.7,98.3 152.2,100.8 149.1,100.8M93.1,145.6v-25.2H81.8l22.4,-42.1v28h11.2L93.1,145.6z"/>-->
</vector>

View File

@@ -0,0 +1,12 @@
<vector android:height="44.11976dp"
android:viewportHeight="368.4"
android:viewportWidth="233.8"
android:width="28dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<!--<path
android:fillColor="#FFFFFF"
android:pathData="M109.8,0h13.6c33.9,1.9 67.1,18.5 87.7,45.8c13.5,17.2 21,38.6 22.7,60.3v8.1c-0.8,42.1 -27.7,76.6 -51,109.4c-26.2,37 -50.4,77.3 -57.1,122.9c-1.8,7.7 0.4,18.5 -8.9,22c-2.2,-1.7 -4.7,-3.1 -6.2,-5.4c-2.7,-25.5 -9.1,-50.7 -20,-73.9c-12.3,-27.1 -29.5,-51.6 -47,-75.6C33,199 23,184.2 14.7,168.3c-13,-23.8 -17.9,-51.9 -12.5,-78.6c4.4,-21.1 15.4,-40.6 30.6,-55.7C53.3,14 81.1,1.8 109.8,0z" />-->
<path
android:fillColor="#FFFFFF"
android:pathData="M90.9,57.3v68.2h18.6v55.8l43.4,-74.4h-24.8l24.8,-49.6H90.9z" />
</vector>

View File

@@ -1,38 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.viewmodel.DonateViewModel" />
<import type="net.vonforst.evmap.viewmodel.Status" />
<variable
name="vm"
type="DonateViewModel" />
</data>
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:actionBarSize" />
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<TextView
android:id="@+id/textView20"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/donations_info"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/products_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:data="@{vm.products}" />
</LinearLayout>
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:data="@{vm.products.data}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView20"
tools:listitem="@layout/item_donation" />
<ProgressBar
android:id="@+id/progressBar3"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/products_list"
app:goneUnless="@{vm.products.status == Status.LOADING}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -103,10 +103,10 @@
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_location"
app:backgroundTint="@android:color/white"
app:backgroundTint="?android:colorBackground"
app:borderWidth="0dp"
app:isFabActive="@{ vm.myLocationEnabled }"
app:layout_behavior=".ui.HideOnScrollFabBehavior" />
app:layout_behavior="net.vonforst.evmap.ui.HideOnScrollFabBehavior" />
<androidx.core.widget.NestedScrollView
android:id="@+id/bottom_sheet"

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="net.vonforst.evmap.viewmodel.DonationItem" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="16dp">
<TextView
android:id="@+id/textView15"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.sku.title}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/textView21"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Spende" />
<TextView
android:id="@+id/textView21"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.sku.price}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="1,00 €" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>

View File

@@ -64,4 +64,7 @@
<string name="show_less">weniger…</string>
<string name="favorites_empty_state">Wenn du Ladestationen als Favorit markierst, tauchen sie hier auf.</string>
<string name="donate">Spenden</string>
<string name="donations_info">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
<string name="donation_successful">Vielen Dank! ❤️</string>
<string name="donation_failed">Etwas ist schiefgelaufen. 😕</string>
</resources>

View File

@@ -63,4 +63,7 @@
<string name="show_less">less…</string>
<string name="favorites_empty_state">If you add chargers as favorites, they will show up here.</string>
<string name="donate">Donate</string>
<string name="donations_info">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.</string>
<string name="donation_successful">Thank you! ❤️</string>
<string name="donation_failed">Something went wrong. 😕</string>
</resources>