diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 906100c7..fa5110f7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -289,6 +289,10 @@
android:resource="@xml/shortcuts" />
+
+
+
+
(R.id.fragment_container_view, args = intent.extras)
+ }
+ }
+
+ LocalBroadcastManager.getInstance(this).registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(ctx: Context, intent: Intent) {
+ finish()
+ }
+ }, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt b/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt
index bc02cc61..55d7c83b 100644
--- a/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt
+++ b/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt
@@ -1,10 +1,15 @@
package net.vonforst.evmap.auto
+import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
import android.content.pm.PackageManager.NameNotFoundException
import android.hardware.Sensor
import android.hardware.SensorManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.IInterface
import android.text.Html
import androidx.annotation.StringRes
import androidx.car.app.CarContext
@@ -13,16 +18,27 @@ import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
+import androidx.core.content.IntentCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import net.vonforst.evmap.*
+import net.vonforst.evmap.api.availability.TeslaAuthenticationApi
+import net.vonforst.evmap.api.availability.TeslaOwnerApi
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
+import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
+import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
import net.vonforst.evmap.storage.AppDatabase
+import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
import net.vonforst.evmap.storage.PreferenceDataSource
+import okhttp3.OkHttpClient
+import java.io.IOException
+import java.time.Instant
import kotlin.math.max
import kotlin.math.min
@@ -125,6 +141,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
+ val encryptedPrefs = EncryptedPreferenceDataStore(ctx)
val db = AppDatabase.getInstance(ctx)
val dataSourceNames = carContext.resources.getStringArray(R.array.pref_data_source_names)
@@ -134,6 +151,8 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
val searchProviderValues =
carContext.resources.getStringArray(R.array.pref_search_provider_values)
+ var teslaLoggingIn = false
+
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(R.string.settings_data_sources))
@@ -183,9 +202,122 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
}
}
}.build())
+ addItem(
+ Row.Builder()
+ .setTitle(carContext.getString(R.string.pref_prediction_enabled))
+ .addText(carContext.getString(R.string.pref_prediction_enabled_summary))
+ .setToggle(Toggle.Builder {
+ prefs.predictionEnabled = it
+ }.setChecked(prefs.predictionEnabled).build())
+ .build()
+ )
+ addItem(Row.Builder().apply {
+ setTitle(carContext.getString(R.string.pref_tesla_account))
+ addText(
+ if (encryptedPrefs.teslaRefreshToken != null) {
+ carContext.getString(
+ R.string.pref_tesla_account_enabled,
+ encryptedPrefs.teslaEmail
+ )
+ } else if (teslaLoggingIn) {
+ carContext.getString(R.string.logging_in)
+ } else {
+ carContext.getString(R.string.pref_tesla_account_disabled)
+ }
+ )
+ if (encryptedPrefs.teslaRefreshToken != null) {
+ setOnClickListener {
+ teslaLogout()
+ }
+ } else {
+ setOnClickListener(ParkedOnlyOnClickListener.create {
+ teslaLogin()
+ })
+ }
+ }.build())
}.build())
}.build()
}
+
+ private fun teslaLogin() {
+ val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
+ val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
+ val uri = TeslaAuthenticationApi.buildSignInUri(codeChallenge)
+
+ val args = OAuthLoginFragmentArgs(
+ uri.toString(),
+ TeslaAuthenticationApi.resultUrlPrefix,
+ "#000000"
+ ).toBundle()
+ val intent = Intent(carContext, OAuthLoginActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtras(args)
+
+ LocalBroadcastManager.getInstance(carContext)
+ .registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(ctx: Context, intent: Intent) {
+ val url = IntentCompat.getParcelableExtra(
+ intent,
+ OAuthLoginFragment.EXTRA_URL,
+ Uri::class.java
+ )
+ teslaGetAccessToken(url!!, codeVerifier)
+ }
+ }, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
+
+ carContext.startActivity(intent)
+
+ if (BuildConfig.FLAVOR_automotive != "automotive") {
+ CarToast.makeText(
+ carContext,
+ R.string.opened_on_phone,
+ CarToast.LENGTH_LONG
+ ).show()
+ }
+ }
+
+ private fun teslaGetAccessToken(url: Uri, codeVerifier: String) {
+ teslaLoggingIn = true
+ invalidate()
+
+ val code = url.getQueryParameter("code") ?: return
+ val okhttp = OkHttpClient.Builder().addDebugInterceptors().build()
+ val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier)
+ lifecycleScope.launch {
+ try {
+ val time = Instant.now().epochSecond
+ val response =
+ TeslaAuthenticationApi.create(okhttp).getToken(request)
+ val userResponse =
+ TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo()
+
+ encryptedPrefs.teslaEmail = userResponse.response.email
+ encryptedPrefs.teslaAccessToken = response.accessToken
+ encryptedPrefs.teslaAccessTokenExpiry = time + response.expiresIn
+ encryptedPrefs.teslaRefreshToken = response.refreshToken
+ } catch (e: IOException) {
+ CarToast.makeText(
+ carContext,
+ R.string.generic_connection_error,
+ CarToast.LENGTH_SHORT
+ ).show()
+ } finally {
+ teslaLoggingIn = false
+ }
+ invalidate()
+ }
+ }
+
+ private fun teslaLogout() {
+ // sign out
+ encryptedPrefs.teslaRefreshToken = null
+ encryptedPrefs.teslaAccessToken = null
+ encryptedPrefs.teslaAccessTokenExpiry = -1
+ encryptedPrefs.teslaEmail = null
+ CarToast.makeText(carContext, R.string.logged_out, CarToast.LENGTH_SHORT).show()
+
+ invalidate()
+ }
}
class ChooseDataSourceScreen(
diff --git a/app/src/main/java/net/vonforst/evmap/fragment/oauth/OAuthLoginFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/oauth/OAuthLoginFragment.kt
index 3d844c3f..6e2e94ff 100644
--- a/app/src/main/java/net/vonforst/evmap/fragment/oauth/OAuthLoginFragment.kt
+++ b/app/src/main/java/net/vonforst/evmap/fragment/oauth/OAuthLoginFragment.kt
@@ -1,6 +1,7 @@
package net.vonforst.evmap.fragment.oauth
import android.annotation.SuppressLint
+import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
@@ -13,17 +14,25 @@ import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
+import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.transition.MaterialSharedAxis
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
+import java.lang.IllegalStateException
class OAuthLoginFragment : Fragment() {
+ companion object {
+ val ACTION_OAUTH_RESULT = "oauth_result"
+ val EXTRA_URL = "url"
+ }
+
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
@@ -43,10 +52,24 @@ class OAuthLoginFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById(R.id.toolbar)
- toolbar.setupWithNavController(
- findNavController(),
- (requireActivity() as MapsActivity).appBarConfiguration
- )
+ val navController = try {
+ findNavController()
+ } catch (e: IllegalStateException) {
+ null
+ // standalone in OAuthLoginActivity
+ }
+
+ if (navController != null) {
+ toolbar.setupWithNavController(
+ navController,
+ (requireActivity() as MapsActivity).appBarConfiguration
+ )
+ } else {
+ toolbar.title = getString(R.string.login)
+ toolbar.navigationIcon =
+ AppCompatResources.getDrawable(requireContext(), R.drawable.ic_arrow_back)
+ toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() }
+ }
val args = OAuthLoginFragmentArgs.fromBundle(requireArguments())
val uri = Uri.parse(args.url)
@@ -68,7 +91,12 @@ class OAuthLoginFragment : Fragment() {
val result = Bundle()
result.putString("url", url.toString())
setFragmentResult(args.url, result)
- findNavController().popBackStack()
+ context?.let {
+ LocalBroadcastManager.getInstance(it).sendBroadcast(
+ Intent(ACTION_OAUTH_RESULT).putExtra(EXTRA_URL, url)
+ )
+ }
+ navController?.popBackStack()
}
return url.host != uri.host
diff --git a/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt
index 5d18f930..311d75b7 100644
--- a/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt
+++ b/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt
@@ -141,18 +141,11 @@ class DataSettingsFragment : BaseSettingsFragment() {
private fun teslaLogin() {
val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
- val uri = Uri.parse("https://auth.tesla.com/oauth2/v3/authorize").buildUpon()
- .appendQueryParameter("client_id", "ownerapi")
- .appendQueryParameter("code_challenge", codeChallenge)
- .appendQueryParameter("code_challenge_method", "S256")
- .appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
- .appendQueryParameter("response_type", "code")
- .appendQueryParameter("scope", "openid email offline_access")
- .appendQueryParameter("state", "123").build()
+ val uri = TeslaAuthenticationApi.buildSignInUri(codeChallenge)
val args = OAuthLoginFragmentArgs(
uri.toString(),
- "https://auth.tesla.com/void/callback",
+ TeslaAuthenticationApi.resultUrlPrefix,
"#000000"
).toBundle()
@@ -184,7 +177,8 @@ class DataSettingsFragment : BaseSettingsFragment() {
encryptedPrefs.teslaRefreshToken = response.refreshToken
} catch (e: IOException) {
view?.let {
- Snackbar.make(it, R.string.connection_error, Snackbar.LENGTH_SHORT).show()
+ Snackbar.make(it, R.string.generic_connection_error, Snackbar.LENGTH_SHORT)
+ .show()
}
}
refreshTeslaAccountStatus()
diff --git a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt
index 98e0bf41..adf18e92 100644
--- a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt
+++ b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt
@@ -261,8 +261,11 @@ class PreferenceDataSource(val context: Context) {
sp.edit().putBoolean("show_chargers_ahead_android_auto", value).apply()
}
- val predictionEnabled: Boolean
+ var predictionEnabled: Boolean
get() = sp.getBoolean("prediction_enabled", true)
+ set(value) {
+ sp.edit().putBoolean("prediction_enabled", value).apply()
+ }
var developerModeEnabled: Boolean
get() = sp.getBoolean("dev_mode_enabled", false)
diff --git a/app/src/main/res/layout/activity_oauth_login.xml b/app/src/main/res/layout/activity_oauth_login.xml
new file mode 100644
index 00000000..8b679c80
--- /dev/null
+++ b/app/src/main/res/layout/activity_oauth_login.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file