This commit is contained in:
oupson 2021-12-25 21:29:29 +01:00
parent 163ef6197b
commit 5802c52b97
15 changed files with 214 additions and 97 deletions

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -4,13 +4,13 @@ plugins {
}
android {
compileSdkVersion 30
compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "fr.oupson.taotoolbox"
minSdkVersion 21
targetSdkVersion 30
targetSdkVersion 31
versionCode 1
versionName "0.0.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -43,19 +43,17 @@ android {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'androidx.preference:preference-ktx:1.1.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
implementation project(":common")
implementation 'org.osmdroid:osmdroid-android:6.1.10'

View File

@ -2,9 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.oupson.taotoolbox">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
@ -15,13 +16,17 @@
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.TaoToolbox">
<activity android:name=".activities.TaoWidgetConfigurationActivity">
<activity
android:name=".activities.TaoWidgetConfigurationActivity"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver android:name=".receivers.TaoWidget">
<receiver
android:name=".receivers.TaoWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@ -31,7 +36,9 @@
android:resource="@xml/widget_tao_info" />
</receiver>
<activity android:name=".activities.MainActivity">
<activity
android:name=".activities.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -12,6 +12,7 @@ import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import fr.oupson.common.api.TaoRestApi
import fr.oupson.common.db.TaoDatabaseHelper
@ -20,6 +21,7 @@ import fr.oupson.common.db.TaoDatabaseHelper.TaoDatabase.StopEntry
import fr.oupson.taotoolbox.BuildConfig
import fr.oupson.taotoolbox.R
import fr.oupson.taotoolbox.databinding.ActivityMainBinding
import fr.oupson.taotoolbox.utils.PolylineDecoder
import fr.oupson.taotoolbox.windows.StopInfoWindow
import kotlinx.coroutines.*
import org.json.JSONArray
@ -98,7 +100,7 @@ class MainActivity : AppCompatActivity() {
requestPermissionsIfNecessary()
GlobalScope.launch(scope) {
lifecycleScope.launch(scope) {
val helper = TaoDatabaseHelper(this@MainActivity).apply {
try {
this.checkUpdate()
@ -241,12 +243,7 @@ class MainActivity : AppCompatActivity() {
)
}
val geo = route.getJSONArray("geojson") // TODO DECODE ENCODED ?
for (i in 0 until geo.length()) {
val value = geo.getJSONArray(i)
routeLine.addPoint(GeoPoint(value.getDouble(1), value.getDouble(0)))
}
PolylineDecoder.decodeInto(routeLine, route.getString("geojsonEncoded"), 1)
binding.map.overlays.add(routeLine)
}

View File

@ -12,6 +12,7 @@ import android.view.MenuItem
import android.view.View
import android.widget.AutoCompleteTextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import fr.oupson.common.api.TaoRestApi
import fr.oupson.common.db.TaoDatabaseHelper
import fr.oupson.common.db.TaoDatabaseHelper.TaoDatabase.*
@ -69,7 +70,7 @@ class TaoWidgetConfigurationActivity : AppCompatActivity() {
val appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(this)
val ids =
appWidgetManager.getAppWidgetIds(ComponentName(this, TaoWidget::class.java))
GlobalScope.launch(Dispatchers.IO) {
lifecycleScope.launch(Dispatchers.IO) {
for (id in ids) {
TaoWidget.updateAppWidget(
this@TaoWidgetConfigurationActivity,
@ -88,7 +89,7 @@ class TaoWidgetConfigurationActivity : AppCompatActivity() {
(binding.configSelectLine.editText as? AutoCompleteTextView)?.also { autoCompleteTextView ->
autoCompleteTextView.setAdapter(lineAdapter)
autoCompleteTextView.setOnItemClickListener { _, _, position, _ ->
GlobalScope.launch(Dispatchers.IO) {
lifecycleScope.launch(Dispatchers.IO) {
linePosition = position
directionPosition = -1
loadDirectionList(position)
@ -99,7 +100,7 @@ class TaoWidgetConfigurationActivity : AppCompatActivity() {
(binding.configSelectDirection.editText as? AutoCompleteTextView)?.also { directionAutoComplete ->
directionAutoComplete.setAdapter(directionAdapter)
directionAutoComplete.setOnItemClickListener { _, _, position, _ ->
GlobalScope.launch(Dispatchers.IO) {
lifecycleScope.launch(Dispatchers.IO) {
directionPosition = position
stopPosition = -1
@ -120,7 +121,7 @@ class TaoWidgetConfigurationActivity : AppCompatActivity() {
save()
}
GlobalScope.launch(Dispatchers.IO) {
lifecycleScope.launch(Dispatchers.IO) {
db = TaoDatabaseHelper(this@TaoWidgetConfigurationActivity).apply {
checkUpdate()
withContext(Dispatchers.Main) {

View File

@ -7,18 +7,19 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import fr.oupson.taotoolbox.R
import fr.oupson.common.api.LineColors
import fr.oupson.common.api.Schedule
import fr.oupson.common.api.RealtimeStopArea
import fr.oupson.taotoolbox.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ScheduleAdapter(
private val scheduleList: SimplePtr<Array<Schedule>>
// TODO ADAPT
class RealTimeAdapter(
private val realTimeList: SimplePtr<Array<RealtimeStopArea>>
) :
RecyclerView.Adapter<ScheduleAdapter.ScheduleViewHolder>() {
RecyclerView.Adapter<RealTimeAdapter.RealTimeViewHolder>() {
class SimplePtr<T>(private var value: T? = null) {
fun get(): T? = value
fun set(value: T?) {
@ -26,21 +27,23 @@ class ScheduleAdapter(
}
}
class ScheduleViewHolder(
class RealTimeViewHolder(
private val view: View
) : RecyclerView.ViewHolder(view) {
private val scheduleDirectionTextView =
view.findViewById<TextView>(R.id.item_schedule_direction_text_view)
private val scheduleImageView = view.findViewById<ImageView>(R.id.item_schedule_line_image_view)
private val scheduleNextTextView = view.findViewById<TextView>(R.id.item_schedule_next_text_view)
private val scheduleImageView =
view.findViewById<ImageView>(R.id.item_schedule_line_image_view)
private val scheduleNextTextView =
view.findViewById<TextView>(R.id.item_schedule_next_text_view)
private val scheduleAfterTextView =
view.findViewById<TextView>(R.id.item_schedule_after_text_view)
fun bind(schedule: Schedule) {
fun bind(schedule: RealtimeStopArea) {
scheduleDirectionTextView.text = view.context.getString(
R.string.line_and_direction_name,
schedule.lineColors.lineCode,
schedule.lineDirectionName
schedule.routeName
)
scheduleNextTextView.text =
@ -50,7 +53,6 @@ class ScheduleAdapter(
schedule.timeRemainingAfter() ?: ""
)
GlobalScope.launch(Dispatchers.Default) {
val btm = LineColors.getLineBtm(
view.context,
@ -67,16 +69,16 @@ class ScheduleAdapter(
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduleViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RealTimeViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ScheduleViewHolder(inflater.inflate(R.layout.item_schedule, parent, false))
return RealTimeViewHolder(inflater.inflate(R.layout.item_schedule, parent, false))
}
override fun onBindViewHolder(holder: ScheduleViewHolder, position: Int) {
val ref = scheduleList.get()
override fun onBindViewHolder(holder: RealTimeViewHolder, position: Int) {
val ref = realTimeList.get()
if (ref != null)
holder.bind(ref[position])
}
override fun getItemCount(): Int = scheduleList.get()?.size ?: 0
override fun getItemCount(): Int = realTimeList.get()?.size ?: 0
}

View File

@ -7,6 +7,7 @@ import android.content.Context
import android.content.Intent
import android.graphics.*
import android.net.Uri
import android.os.Build
import android.util.Log
import android.widget.RemoteViews
import fr.oupson.common.api.LineColors
@ -37,12 +38,18 @@ class TaoWidget : AppWidgetProvider() {
serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
serviceIntent.data = Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))
val pending = PendingIntent.getBroadcast(
context,
appWidgetId,
serviceIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
val pending =
PendingIntent.getBroadcast(
context,
appWidgetId,
serviceIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
views.setOnClickPendingIntent(R.id.tao_widget_root, pending)
try {

View File

@ -0,0 +1,37 @@
package fr.oupson.taotoolbox.utils;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.overlay.Polyline;
public class PolylineDecoder {
public static void decodeInto(Polyline line, String encodedString, int precision) {
int index = 0;
int len = encodedString.length();
int lat = 0, lng = 0;
while (index < len) {
int b, shift, result;
shift = result = 0;
do {
b = encodedString.charAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = result = 0;
do {
b = encodedString.charAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lng += dlng;
GeoPoint p = new GeoPoint(((double) (lng * precision)) / 1.0e6, ((double) (lat * precision)) / 1.0e6, 0);
line.addPoint(p);
}
}
}

View File

@ -3,10 +3,10 @@ package fr.oupson.taotoolbox.windows
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.oupson.taotoolbox.R
import fr.oupson.taotoolbox.adapters.ScheduleAdapter
import fr.oupson.common.api.Schedule
import fr.oupson.common.api.RealtimeStopArea
import fr.oupson.common.api.TaoRestApi
import fr.oupson.taotoolbox.R
import fr.oupson.taotoolbox.adapters.RealTimeAdapter
import kotlinx.coroutines.*
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
@ -21,13 +21,13 @@ class StopInfoWindow(
private val titleTextView: TextView by lazy { view.findViewById(R.id.window_stop_info_title_text_view) }
private val scheduleList: ScheduleAdapter.SimplePtr<Array<Schedule>> by lazy {
ScheduleAdapter.SimplePtr(
private val realtimeList: RealTimeAdapter.SimplePtr<Array<RealtimeStopArea>> by lazy {
RealTimeAdapter.SimplePtr(
null
)
}
private val adapter: ScheduleAdapter by lazy { ScheduleAdapter(scheduleList) }
private val adapter: RealTimeAdapter by lazy { RealTimeAdapter(realtimeList) }
private val recyclerView: RecyclerView by lazy {
view.findViewById<RecyclerView>(R.id.window_stop_info_schedule_recycler_view).also {
it.adapter = adapter
@ -36,7 +36,7 @@ class StopInfoWindow(
}
override fun onOpen(item: Any?) {
scheduleList.set(null)
realtimeList.set(null)
recyclerView.adapter?.notifyDataSetChanged()
if (item is Marker) {
@ -44,10 +44,10 @@ class StopInfoWindow(
titleTextView.text = item.title
GlobalScope.launch(windowsContext) {
val schedule = taoRestApi.getSchedule(id)
val realtime = taoRestApi.getRealtimeByStopArea(id)
withContext(Dispatchers.Main) {
scheduleList.set(schedule)
realtimeList.set(realtime)
adapter.notifyDataSetChanged()
}
}

View File

@ -1,13 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.5.0"
ext.ktor_version = "1.5.3"
ext.kotlin_version = "1.6.10"
ext.ktor_version = "1.6.3"
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.0'
classpath 'com.android.tools.build:gradle:7.0.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@ -1,18 +1,16 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.30'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.5.30'
}
android {
compileSdkVersion 30
compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@ -34,14 +32,13 @@ android {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
implementation "io.ktor:ktor-client-serialization:$ktor_version"
implementation "io.ktor:ktor-client-core:$ktor_version"

View File

@ -130,7 +130,10 @@ object DateAsStringSerializer : KSerializer<Date> {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX", Locale.getDefault()).parse(string)!!
} else { // Z format is not supported :/
val newDateStr = if (string.endsWith("Z")) {
string.substring(0, string.length - 1) + "+0000" // Little trick to get time in correct time zone
string.substring(
0,
string.length - 1
) + "+0000" // Little trick to get time in correct time zone
} else {
throw Exception("$string is not supported") // TODO ?
}
@ -164,4 +167,65 @@ data class Schedule(
nextScheduleAfter?.let {
(it.time - Date().time) / (1000 * 60)
}
}
}
@Serializable
data class RealtimeStopArea(
val lineId: String,
val lineCode: String,
val lineColors: LineColors,
val routeId: String,
val routeName: String,
val lineIsTAD: Boolean,
val nextPassages: Array<NextPassage>,
val summaryStatus: String?
) {
fun timeRemaining(): Long? =
nextPassages.getOrNull(0)?.let {
(it.date!!.time - Date().time) / (1000 * 60)
}
fun timeRemainingAfter(): Long? =
nextPassages.getOrNull(1)?.let {
(it.date!!.time - Date().time) / (1000 * 60)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as RealtimeStopArea
if (lineId != other.lineId) return false
if (lineCode != other.lineCode) return false
if (lineColors != other.lineColors) return false
if (routeId != other.routeId) return false
if (routeName != other.routeName) return false
if (lineIsTAD != other.lineIsTAD) return false
if (!nextPassages.contentEquals(other.nextPassages)) return false
if (summaryStatus != other.summaryStatus) return false
return true
}
override fun hashCode(): Int {
var result = lineId.hashCode()
result = 31 * result + lineCode.hashCode()
result = 31 * result + lineColors.hashCode()
result = 31 * result + routeId.hashCode()
result = 31 * result + routeName.hashCode()
result = 31 * result + lineIsTAD.hashCode()
result = 31 * result + nextPassages.contentHashCode()
result = 31 * result + (summaryStatus?.hashCode() ?: 0)
return result
}
}
@Serializable
data class NextPassage(
@Serializable(with = DateAsStringSerializer::class)
val date: Date?,
val vehicleId: String,
val loadPrediction: String?, // TODO
val isLiveData: Boolean
)

View File

@ -3,6 +3,7 @@ package fr.oupson.common.api
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
@ -12,7 +13,9 @@ import kotlin.coroutines.CoroutineContext
class TaoRestApi(private val httpClient: HttpClient) {
constructor() : this(HttpClient(Android) {
install(JsonFeature)
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json { ignoreUnknownKeys = true })
}
engine {
connectTimeout = 10_1000
socketTimeout = 10_000
@ -69,9 +72,24 @@ class TaoRestApi(private val httpClient: HttpClient) {
}, true) {}
}
suspend fun getRealtimeByStopArea(stopAreaId: String, limit: Int? = null, includeLoadPrediction: Boolean? = null) : Array<RealtimeStopArea> = withContext(Dispatchers.IO) {
httpClient.submitForm("https://navigorleans.c-t.io/api/3.0/realtime/byStopArea", Parameters.build {
append(
"stopAreaId", stopAreaId
)
if (limit != null) {
append("limit", limit.toString())
}
if (includeLoadPrediction != null) {
append("includeLoadPrediction", includeLoadPrediction.toString())
}
}, true) {}
}
suspend fun getTaoGeoJson(
lineId: String
): String = withContext(requestContext) {
httpClient.get("$baseUrl/2.0/lines/$lineId/geojson")
httpClient.get("$baseUrl/2.0/lines/$lineId/geojson/encoded")
}
}

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip

View File

@ -33,16 +33,15 @@ android {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.4.2'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'androidx.wear:wear:1.1.0'
implementation "androidx.wear:wear-tiles:1.0.0-alpha01"
debugImplementation "androidx.wear:wear-tiles-renderer:1.0.0-alpha01"