Initial commit

This commit is contained in:
Oupson 2021-05-09 19:23:52 +02:00
commit 730a7edc0e
102 changed files with 4120 additions and 0 deletions

90
.gitignore vendored Normal file
View File

@ -0,0 +1,90 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
output-metadata.json
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
Tao Toolbox

View File

@ -0,0 +1,139 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
</component>
</project>

30
.idea/jarRepositories.xml Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
</component>
</project>

9
.idea/misc.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

63
app/build.gradle Normal file
View File

@ -0,0 +1,63 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 30
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "fr.oupson.taotoolbox"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "0.0.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type.
minifyEnabled true
// Enables resource shrinking, which is performed by the
// Android Gradle plugin.
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
}
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.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'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
implementation project(":common")
implementation 'org.osmdroid:osmdroid-android:6.1.10'
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package fr.oupson.taotoolbox
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.oupson.taotoolbox", appContext.packageName)
}
}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.TaoToolbox">
<activity android:name=".activities.TaoWidgetConfigurationActivity">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver android:name=".receivers.TaoWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_tao_info" />
</receiver>
<activity android:name=".activities.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,324 @@
package fr.oupson.taotoolbox.activities
import android.Manifest
import android.content.pm.PackageManager
import android.database.sqlite.SQLiteDatabase
import android.graphics.*
import android.opengl.Visibility
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import fr.oupson.common.api.TaoRestApi
import fr.oupson.common.db.TaoDatabaseHelper
import fr.oupson.common.db.TaoDatabaseHelper.TaoDatabase.LineEntry
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.windows.StopInfoWindow
import kotlinx.coroutines.*
import org.json.JSONArray
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.XYTileSource
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.overlay.CopyrightOverlay
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.compass.CompassOverlay
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import kotlin.coroutines.coroutineContext
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
private const val REQUEST_PERMISSIONS_REQUEST_CODE = 1
}
private val taoRestApi = TaoRestApi()
private lateinit var binding: ActivityMainBinding
private val job = Job()
private val scope = Dispatchers.IO + job
private val locationOverlay: MyLocationNewOverlay by lazy {
MyLocationNewOverlay(GpsMyLocationProvider(this), binding.map)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.bottomAppBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
window.apply {
clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
decorView.systemUiVisibility =
decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
statusBarColor = Color.TRANSPARENT
}
Configuration.getInstance().also {
it.load(this, PreferenceManager.getDefaultSharedPreferences(this))
it.userAgentValue = BuildConfig.APPLICATION_ID
}
binding.loadingProgressBar.visibility = View.VISIBLE
binding.map.setTileSource(
XYTileSource(
"osm fr", 1, 19, 256, ".png",
arrayOf(
"https://a.tile.openstreetmap.fr/osmfr/",
"https://b.tile.openstreetmap.fr/osmfr/",
"https://c.tile.openstreetmap.fr/osmfr/"
), "© OpenStreetMap contributors"
)
)
buildOtherOverlays()
requestPermissionsIfNecessary()
GlobalScope.launch(scope) {
val helper = TaoDatabaseHelper(this@MainActivity).apply {
try {
this.checkUpdate()
withContext(Dispatchers.Main) {
binding.loadingProgressBar.visibility = View.GONE
}
} catch (e: Exception) {
Log.d(TAG, "while checking update", e)
}
}
val db = helper.readableDatabase
val copyright = CopyrightOverlay(this@MainActivity).apply {
this.setAlignRight(true)
}
binding.map.overlays.add(copyright)
buildLineOverlays(db, helper)
buildMarkerOverlays(db)
binding.map.overlays.remove(copyright)
binding.map.overlays.add(copyright)
db.close()
}
}
private fun buildOtherOverlays() {
binding.map.overlays.add(locationOverlay)
val compassOverlay =
CompassOverlay(this, InternalCompassOrientationProvider(this), binding.map)
compassOverlay.enableCompass()
compassOverlay.setCompassCenter(35.0f, 70f) // TODO
binding.map.overlays.add(compassOverlay)
binding.map.apply {
controller.setZoom(15.0)
controller.animateTo(GeoPoint(47.90250000, 1.90888889))
zoomController.setVisibility(CustomZoomButtonsController.Visibility.SHOW_AND_FADEOUT)
setMultiTouchControls(true)
isFlingEnabled = true
}
}
private suspend fun buildMarkerOverlays(db: SQLiteDatabase) {
val infoWindow = StopInfoWindow(binding.map, taoRestApi)
val cursor = db.query(
StopEntry.TABLE_NAME,
arrayOf(
StopEntry.COLUMN_NAME_STOP_ID,
StopEntry.COLUMN_NAME_STOP_NAME,
StopEntry.COLUMN_NAME_COORD_LONG,
StopEntry.COLUMN_NAME_COORD_LAT
),
null,
null,
null,
null,
null
)
with(cursor) {
val stopIdIndex = cursor.getColumnIndexOrThrow(StopEntry.COLUMN_NAME_STOP_ID)
val stopNameIndex = cursor.getColumnIndexOrThrow(StopEntry.COLUMN_NAME_STOP_NAME)
val stopCoordLongIndex =
cursor.getColumnIndexOrThrow(StopEntry.COLUMN_NAME_COORD_LONG)
val stopCoordLatIndex =
cursor.getColumnIndexOrThrow(StopEntry.COLUMN_NAME_COORD_LAT)
val drawable =
ContextCompat.getDrawable(this@MainActivity, R.drawable.ic_baseline_place_48)
drawable?.colorFilter = PorterDuffColorFilter(
ContextCompat.getColor(this@MainActivity, R.color.tao_blue),
PorterDuff.Mode.MULTIPLY
)
while (moveToNext() && coroutineContext.isActive) {
binding.map.overlays.add(Marker(binding.map).apply {
position = GeoPoint(
getDouble(stopCoordLatIndex), getDouble(stopCoordLongIndex),
)
icon = drawable
title = getString(stopNameIndex)
id = getString(stopIdIndex)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
this.infoWindow = infoWindow
})
}
}
cursor.close()
}
private suspend fun buildLineOverlays(db: SQLiteDatabase, helper: TaoDatabaseHelper) {
val lineCursor = db.query(
LineEntry.TABLE_NAME,
arrayOf(
LineEntry.COLUMN_NAME_LINE_ID,
LineEntry.COLUMN_NAME_LINE_NAME,
LineEntry.COLUMN_NAME_BACKGROUND_COLOR
),
null,
null,
null,
null, null
)
with(lineCursor) {
val lineIdIndex = getColumnIndexOrThrow(LineEntry.COLUMN_NAME_LINE_ID)
val lineNameIndex = getColumnIndexOrThrow(
LineEntry.COLUMN_NAME_LINE_NAME
)
val lineBackgroundColorIndex =
getColumnIndexOrThrow(LineEntry.COLUMN_NAME_BACKGROUND_COLOR)
while (lineCursor.moveToNext() && coroutineContext.isActive) {
val jsonResponse = JSONArray(taoRestApi.getTaoGeoJson(getString(lineIdIndex)))
val color = Color.parseColor(getString(lineBackgroundColorIndex))
for (routeIndex in 0 until jsonResponse.length()) {
val route = jsonResponse.getJSONObject(routeIndex)
val routeLine = Polyline(binding.map).apply {
outlinePaint.color = color
id = route.getString("routeId")
title = getString(lineNameIndex)
subDescription = this@MainActivity.getString(
R.string.line_popup_description,
helper.getRouteName(db, route.getString("routeId"))
)
}
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)))
}
binding.map.overlays.add(routeLine)
}
}
}
lineCursor.close()
}
override fun onResume() {
super.onResume()
binding.map.onResume()
}
override fun onPause() {
super.onPause()
binding.map.onResume()
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_PERMISSIONS_REQUEST_CODE -> {
val res = grantResults.getOrNull(0)
if (res == PackageManager.PERMISSION_GRANTED) {
onAccessLocationPermissionGranted()
}
}
}
}
private fun onAccessLocationPermissionGranted() {
Log.d(TAG, "onAccessLocationPermissionGranted")
locationOverlay.enableMyLocation()
locationOverlay.isOptionsMenuEnabled = true
locationOverlay.enableFollowLocation()
}
private fun requestPermissionsIfNecessary() {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_PERMISSIONS_REQUEST_CODE
)
} else {
onAccessLocationPermissionGranted()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
false
}
else -> super.onOptionsItemSelected(item)
}
}
}

View File

@ -0,0 +1,366 @@
package fr.oupson.taotoolbox.activities
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.database.sqlite.SQLiteDatabase
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.AutoCompleteTextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import fr.oupson.common.api.TaoRestApi
import fr.oupson.common.db.TaoDatabaseHelper
import fr.oupson.common.db.TaoDatabaseHelper.TaoDatabase.*
import fr.oupson.taotoolbox.BuildConfig
import fr.oupson.taotoolbox.R
import fr.oupson.taotoolbox.adapters.DirectionAdapter
import fr.oupson.taotoolbox.adapters.LineAdapter
import fr.oupson.taotoolbox.adapters.StopAdapter
import fr.oupson.taotoolbox.databinding.ActivityTaoWidgetConfigurationBinding
import fr.oupson.taotoolbox.receivers.TaoWidget
import kotlinx.coroutines.*
// TODO FIND A WAY TO AUTO SELECT
class TaoWidgetConfigurationActivity : AppCompatActivity() {
companion object {
private const val LINE_ID = "WIDGET_LINE_ID"
private const val LINE_CODE = "WIDGET_LINE_CODE"
private const val ROUTE_ID = "WIDGET_LINE_ID"
private const val ROUTE_NAME = "ROUTE_NAME"
private const val STOP_ID = "WIDGET_STOP_ID"
private const val STOP_NAME = "WIDGET_STOP_NAME"
private const val BG_COLOR = "WIDGET_LINE_BACKGROUND_COLOR"
private const val TEXT_COLOR = "WIDGET_LINE_TEXT_COLOR"
private const val PREFERENCE_NAME = "WIDGET"
private const val TAG = "TaoWidgetConfiguration"
private fun savePrefs(
context: Context,
widgetId: Int,
lineId: String,
lineCode: String,
routeId: String,
routeName: String,
stopId: String,
stopName: String,
bgColor: Int,
textColor: Int
) {
Log.d(TAG, "savePrefs, widget id : $widgetId")
context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE).edit(true) {
putString("${LINE_ID}_$widgetId", lineId)
putString("${LINE_CODE}_$widgetId", lineCode)
putString("${ROUTE_ID}_$widgetId", routeId)
putString("${ROUTE_NAME}_$widgetId", routeName)
putString("${STOP_ID}_$widgetId", stopId)
putString("${STOP_NAME}_$widgetId", stopName)
putInt("${BG_COLOR}_$widgetId", bgColor)
putInt("${TEXT_COLOR}_$widgetId", textColor)
}
}
data class WidgetPrefs(
val lineId: String,
val lineCode: String,
val routeId: String,
val routeName: String,
val stopId: String,
val stopName: String,
val bgColor: Int,
val textColor: Int
)
fun getPrefs(context: Context, widgetId: Int): WidgetPrefs? =
with(context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)) {
Log.d(TAG, "getPrefs, widget id : $widgetId")
WidgetPrefs(
getString("${LINE_ID}_$widgetId", null) ?: return null,
getString("${LINE_CODE}_$widgetId", null) ?: return null,
getString("${ROUTE_ID}_$widgetId", null) ?: return null,
getString("${ROUTE_NAME}_$widgetId", null) ?: return null,
getString("${STOP_ID}_$widgetId", null) ?: return null,
getString("${STOP_NAME}_$widgetId", null) ?: return null,
getInt("${BG_COLOR}_$widgetId", Color.BLACK),
getInt("${TEXT_COLOR}_$widgetId", Color.WHITE),
)
}
fun destroyPrefs(context: Context, widgetId: Int) {
Log.d(TAG, "destroyPrefs, widget id : $widgetId")
val prefs = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
prefs.edit(commit = true) {
remove("${LINE_ID}_$widgetId")
remove("${LINE_CODE}_$widgetId")
remove("${ROUTE_ID}_$widgetId")
remove("${ROUTE_NAME}_$widgetId")
remove("${STOP_ID}_$widgetId")
remove("${STOP_NAME}_$widgetId")
remove("${BG_COLOR}_$widgetId")
remove("${TEXT_COLOR}_$widgetId")
}
}
}
private var widgetId: Int? = null
private lateinit var binding: ActivityTaoWidgetConfigurationBinding
private var db: SQLiteDatabase? = null
private val lineList = arrayListOf<LineAdapter.Line>()
private val directionList = arrayListOf<DirectionAdapter.Direction>()
private val stopList = arrayListOf<StopAdapter.Stop>()
private val lineAdapter: LineAdapter by lazy { LineAdapter(this, lineList) }
private val directionAdapter: DirectionAdapter by lazy { DirectionAdapter(this, directionList) }
private val stopAdapter: StopAdapter by lazy { StopAdapter(this, stopList) }
private var linePosition: Int = -1
private var directionPosition: Int = -1
private var stopPosition: Int = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTaoWidgetConfigurationBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.bottomAppBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
widgetId = intent.extras?.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
if ((widgetId == AppWidgetManager.INVALID_APPWIDGET_ID || widgetId == null) && !BuildConfig.DEBUG) {
setResult(Activity.RESULT_CANCELED, null)
finish()
}
binding.loadingProgressBar.visibility = View.VISIBLE
(binding.configSelectLine.editText as? AutoCompleteTextView)?.also { autoCompleteTextView ->
autoCompleteTextView.setAdapter(lineAdapter)
autoCompleteTextView.setOnItemClickListener { _, _, position, _ ->
GlobalScope.launch(Dispatchers.IO) {
linePosition = position
directionPosition = -1
loadDirectionList(position)
}
}
}
(binding.configSelectDirection.editText as? AutoCompleteTextView)?.also { directionAutoComplete ->
directionAutoComplete.setAdapter(directionAdapter)
directionAutoComplete.setOnItemClickListener { _, _, position, _ ->
GlobalScope.launch(Dispatchers.IO) {
directionPosition = position
stopPosition = -1
loadStopList(position)
}
}
}
(binding.configSelectStop.editText as? AutoCompleteTextView)?.also {
it.setAdapter(stopAdapter)
it.setOnItemClickListener { _, _, position, _ ->
binding.configSelectStop.isErrorEnabled = false
stopPosition = position
}
}
binding.fabConfirm.setOnClickListener {
save()
}
GlobalScope.launch(Dispatchers.IO) {
db = TaoDatabaseHelper(this@TaoWidgetConfigurationActivity).apply {
checkUpdate()
withContext(Dispatchers.Main) {
binding.loadingProgressBar.visibility = View.GONE
}
}.readableDatabase
loadLineList()
}
}
private fun checkEntryValid(): Boolean {
var res = true
if (linePosition == -1) {
res = false
binding.configSelectLine.error = getString(R.string.activity_tao_config_missing_value)
}
if (directionPosition == -1) {
res = false
binding.configSelectDirection.error =
getString(R.string.activity_tao_config_missing_value)
}
if (stopPosition == -1) {
res = false
binding.configSelectStop.error = getString(R.string.activity_tao_config_missing_value)
}
return res
}
private fun save() {
if (!checkEntryValid())
return
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID || widgetId == null) {
setResult(Activity.RESULT_CANCELED, null)
} else {
savePrefs(
this,
widgetId!!,
lineList[linePosition].lineId,
lineList[linePosition].lineCode,
directionList[directionPosition].routeId,
directionList[directionPosition].directionName,
stopList[stopPosition].stopId,
stopList[stopPosition].stopName,
lineList[linePosition].bgColor,
lineList[linePosition].textColor,
)
val appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(this)
runBlocking {
TaoWidget.updateAppWidget(
this@TaoWidgetConfigurationActivity,
appWidgetManager,
widgetId!!,
TaoRestApi()
)
}
val resultValue = Intent().apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
}
setResult(Activity.RESULT_OK, resultValue)
}
finish()
}
private suspend fun loadLineList() {
if (db != null) {
lineList.clear()
val cursor = db!!.query(
true,
LineEntry.TABLE_NAME,
arrayOf(
LineEntry.COLUMN_NAME_LINE_ID,
LineEntry.COLUMN_NAME_LINE_CODE,
LineEntry.COLUMN_NAME_LINE_NAME,
LineEntry.COLUMN_NAME_BACKGROUND_COLOR,
LineEntry.COLUMN_NAME_TEXT_COLOR
),
null,
null,
null,
null,
LineEntry.COLUMN_NAME_LINE_ORDER,
null
)
while (cursor.moveToNext()) {
lineList.add(
LineAdapter.Line(
cursor.getString(0),
cursor.getString(1),
cursor.getString(2),
Color.parseColor(cursor.getString(3)),
Color.parseColor(cursor.getString(4))
)
)
}
cursor.close()
withContext(Dispatchers.Main) {
lineAdapter.notifyDataSetChanged()
}
}
}
private suspend fun loadStopList(position: Int) {
stopList.clear()
if (db != null) {
val cursor = db!!.rawQuery(
"SELECT ${StopEntry.COLUMN_NAME_STOP_ID}, ${StopEntry.COLUMN_NAME_STOP_NAME} FROM ${StopEntry.TABLE_NAME} NATURAL JOIN ${IsStopEntry.TABLE_NAME} NATURAL JOIN ${DirectionEntry.TABLE_NAME} WHERE ${DirectionEntry.COLUMN_NAME_ROUTE_ID} = ? ORDER BY ${IsStopEntry.COLUMN_NAME_STOP_INDEX}",
arrayOf(directionList[position].routeId)
)
while (cursor.moveToNext()) {
stopList.add(
StopAdapter.Stop(
cursor.getString(0),
cursor.getString(1)
)
)
}
cursor.close()
}
withContext(Dispatchers.Main) {
binding.configSelectDirection.isErrorEnabled = false
stopAdapter.notifyDataSetChanged()
}
}
private suspend fun loadDirectionList(position: Int) {
directionList.clear()
if (db != null) {
val cursor = db!!.rawQuery(
"SELECT ${LineEntry.COLUMN_NAME_LINE_ID}, ${LineEntry.COLUMN_NAME_LINE_CODE}, ${DirectionEntry.COLUMN_NAME_ROUTE_NAME}, ${DirectionEntry.COLUMN_NAME_ROUTE_ID}, ${LineEntry.COLUMN_NAME_BACKGROUND_COLOR}, ${LineEntry.COLUMN_NAME_TEXT_COLOR} FROM ${LineEntry.TABLE_NAME} NATURAL JOIN ${DirectionEntry.TABLE_NAME} WHERE ${LineEntry.COLUMN_NAME_LINE_ID} = ?",
arrayOf(lineList[position].lineId)
)
while (cursor.moveToNext()) {
directionList.add(
DirectionAdapter.Direction(
cursor.getString(0),
cursor.getString(1),
cursor.getString(2),
cursor.getString(3),
Color.parseColor(cursor.getString(4)),
Color.parseColor(cursor.getString(5))
)
)
}
cursor.close()
}
withContext(Dispatchers.Main) {
binding.configSelectLine.isErrorEnabled = false
directionAdapter.notifyDataSetChanged()
}
}
override fun onDestroy() {
super.onDestroy()
if (BuildConfig.DEBUG)
Log.v(TAG, "Closing database")
db?.close()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
false
}
else -> super.onOptionsItemSelected(item)
}
}
}

View File

@ -0,0 +1,66 @@
package fr.oupson.taotoolbox.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import fr.oupson.taotoolbox.R
import fr.oupson.common.api.LineColors
class DirectionAdapter(context: Context, private val lines: List<Direction>) :
ArrayAdapter<DirectionAdapter.Direction>(context, 0, lines) {
data class Direction(
val lineId: String,
val lineCode: String,
val directionName: String,
val routeId: String,
val bgColor: Int,
val textColor: Int
) {
override fun toString(): String {
return "${this.lineCode} : ${this.directionName}"
}
}
override fun getView(position: Int, c: View?, parent: ViewGroup): View {
var convertView = c
val holder = if (convertView == null) {
convertView =
LayoutInflater.from(context).inflate(R.layout.item_line_direction, parent, false)
LineViewHolder(convertView).also {
convertView.tag = it
}
} else {
convertView.tag as LineViewHolder
}
holder.bind(context, lines[position])
return convertView!!
}
class LineViewHolder(view: View) {
private val directionNameTextView: TextView =
view.findViewById(R.id.item_line_direction_name_text_view)
private val directionImageView: ImageView =
view.findViewById(R.id.item_line_direction_image_view)
fun bind(context: Context, line: Direction) {
this.directionNameTextView.text = line.directionName
this.directionImageView.setImageBitmap(
LineColors.getLineBtm(
context,
line.lineCode,
line.bgColor,
line.textColor,
256,
256
)
)
}
}
}

View File

@ -0,0 +1,61 @@
package fr.oupson.taotoolbox.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import fr.oupson.common.api.LineColors
import fr.oupson.taotoolbox.R
class LineAdapter(context: Context, private val lines: List<Line>) :
ArrayAdapter<LineAdapter.Line>(context, 0, lines) {
data class Line(
val lineId: String,
val lineCode: String,
val lineName: String,
val bgColor: Int,
val textColor: Int
) {
override fun toString(): String {
return "${this.lineCode} : ${this.lineName}"
}
}
override fun getView(position: Int, c: View?, parent: ViewGroup): View {
var convertView = c
val holder = if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_line, parent, false)
LineViewHolder(convertView).also {
convertView.tag = it
}
} else {
convertView.tag as LineViewHolder
}
holder.bind(context, lines[position])
return convertView!!
}
class LineViewHolder(view: View) {
private val lineNameTextView: TextView = view.findViewById(R.id.item_line_name_text_view)
private val lineImageView: ImageView = view.findViewById(R.id.item_line_image_view)
fun bind(context: Context, line: Line) {
this.lineNameTextView.text = line.lineName
this.lineImageView.setImageBitmap(
LineColors.getLineBtm(
context,
line.lineCode,
line.bgColor,
line.textColor,
256,
256
)
)
}
}
}

View File

@ -0,0 +1,82 @@
package fr.oupson.taotoolbox.adapters
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ScheduleAdapter(
private val scheduleList: SimplePtr<Array<Schedule>>
) :
RecyclerView.Adapter<ScheduleAdapter.ScheduleViewHolder>() {
class SimplePtr<T>(private var value: T? = null) {
fun get(): T? = value
fun set(value: T?) {
this.value = value
}
}
class ScheduleViewHolder(
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 scheduleAfterTextView =
view.findViewById<TextView>(R.id.item_schedule_after_text_view)
fun bind(schedule: Schedule) {
scheduleDirectionTextView.text = view.context.getString(
R.string.line_and_direction_name,
schedule.lineColors.lineCode,
schedule.lineDirectionName
)
scheduleNextTextView.text =
view.context.getString(R.string.time_remaining, schedule.timeRemaining() ?: "")
scheduleAfterTextView.text = view.context.getString(
R.string.time_remaining,
schedule.timeRemainingAfter() ?: ""
)
GlobalScope.launch(Dispatchers.Default) {
val btm = LineColors.getLineBtm(
view.context,
schedule.lineColors.lineCode,
Color.parseColor(schedule.lineColors.background),
Color.parseColor(schedule.lineColors.text),
48,
48
)
withContext(Dispatchers.Main) {
scheduleImageView.setImageBitmap(btm)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduleViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ScheduleViewHolder(inflater.inflate(R.layout.item_schedule, parent, false))
}
override fun onBindViewHolder(holder: ScheduleViewHolder, position: Int) {
val ref = scheduleList.get()
if (ref != null)
holder.bind(ref[position])
}
override fun getItemCount(): Int = scheduleList.get()?.size ?: 0
}

View File

@ -0,0 +1,42 @@
package fr.oupson.taotoolbox.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import fr.oupson.taotoolbox.R
class StopAdapter(context: Context, private val lines: List<Stop>) :
ArrayAdapter<StopAdapter.Stop>(context, 0, lines) {
data class Stop(val stopId: String, val stopName: String) {
override fun toString(): String {
return this.stopName
}
}
override fun getView(position: Int, c: View?, parent: ViewGroup): View {
var convertView = c
val holder = if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_stop, parent, false)
LineViewHolder(convertView).also {
convertView.tag = it
}
} else {
convertView.tag as LineViewHolder
}
holder.bind(lines[position])
return convertView!!
}
class LineViewHolder(view: View) {
private val stopNameTextView: TextView = view.findViewById(R.id.item_stop_name)
fun bind(line: Stop) {
this.stopNameTextView.text = line.stopName
}
}
}

View File

@ -0,0 +1,238 @@
package fr.oupson.taotoolbox.receivers
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.graphics.*
import android.net.Uri
import android.util.Log
import android.widget.RemoteViews
import fr.oupson.common.api.LineColors
import fr.oupson.common.api.RealTimes
import fr.oupson.common.api.TaoRestApi
import fr.oupson.taotoolbox.R
import fr.oupson.taotoolbox.activities.TaoWidgetConfigurationActivity
import kotlinx.coroutines.*
import java.util.*
class TaoWidget : AppWidgetProvider() {
companion object {
private const val TAG = "TaoWidget"
suspend fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
taoRestApi: TaoRestApi
) {
Log.d(TAG, "updateAppWidget, appWidgetId : $appWidgetId")
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.widget_tao)
val serviceIntent = Intent(context, TaoWidget::class.java)
serviceIntent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
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
)
views.setOnClickPendingIntent(R.id.tao_widget_root, pending)
try {
val prefs = TaoWidgetConfigurationActivity.getPrefs(context, appWidgetId)
if (prefs != null) {
val depart = taoRestApi.getNextDeparturesForLine(
prefs.stopId,
prefs.routeId
)
views.setRealtime(
context,
prefs.stopName,
prefs.routeName.toDirection(),
LineColors.getLineBtm(
context,
prefs.lineCode,
prefs.bgColor,
prefs.textColor,
256,
256
),
depart
)
}
} catch (e: Exception) {
Log.e(TAG, "While updating widget", e)
views.setTextViewText(
R.id.widget_tao_stop_name_text_view,
context.getString(R.string.widget_tao_error_title)
)
views.setTextViewText(
R.id.widget_tao_stop_direction_text_view,
context.getString(R.string.widget_tao_error_description)
)
views.setImageViewResource(
R.id.widget_tao_line_icon_image_view,
R.drawable.ic_outline_error_outline_24
)
val now = GregorianCalendar()
views.setTextViewText(
R.id.widget_tao_refresh_date_text_view,
context.getString(
R.string.widget_tao_time,
now.get(GregorianCalendar.HOUR_OF_DAY),
now.get(GregorianCalendar.MINUTE)
)
)
views.setTextViewText(
R.id.widget_tao_next_text_view, ""
)
views.setTextViewText(
R.id.widget_tao_after_next_text_view,
""
)
views.setTextViewText(
R.id.widget_tao_third_text_view,
""
)
}
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
private fun String.toDirection(): String {
return if (indexOf(" - ") > 0)
split(" - ").last()
else
this
}
private fun RemoteViews.setRealtime(
context: Context,
stopName: String,
lineName: String,
lineBtm: Bitmap,
realTimes: RealTimes
) {
setImageViewBitmap(
R.id.widget_tao_line_icon_image_view,
lineBtm.roundCorner(context.resources.getDimension(R.dimen.widget_tao_corner))
)
setTextViewText(R.id.widget_tao_stop_name_text_view, stopName)
setTextViewText(R.id.widget_tao_stop_direction_text_view, lineName)
val now = GregorianCalendar()
setTextViewText(
R.id.widget_tao_refresh_date_text_view,
context.getString(
R.string.widget_tao_time,
now.get(GregorianCalendar.HOUR_OF_DAY),
now.get(GregorianCalendar.MINUTE)
)
)
setTextViewText(
R.id.widget_tao_next_text_view, context.getString(
R.string.time_remaining,
realTimes.timeRemaining(0) ?: ""
)
)
setTextViewText(
R.id.widget_tao_after_next_text_view,
context.getString(
R.string.time_remaining,
realTimes.timeRemainingAfter(0) ?: ""
)
)
setTextViewText(
R.id.widget_tao_third_text_view,
context.getString(
R.string.time_remaining,
realTimes.timeRemainingThird(0) ?: ""
)
)
}
private fun Bitmap.roundCorner(radius: Float): Bitmap {
val res = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(res)
canvas.drawColor(Color.TRANSPARENT)
val shader = BitmapShader(
this,
Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP
)
val paint = Paint()
paint.isAntiAlias = true
paint.shader = shader
val rect = RectF(0.0f, 0.0f, width.toFloat(), height.toFloat())
canvas.drawRoundRect(rect, radius, radius, paint)
return res
}
}
private val job = Job()
private val coroutineContext = Dispatchers.IO + job
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
GlobalScope.launch(coroutineContext) {
val taoRestApi = TaoRestApi()
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId, taoRestApi)
}
}
}
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
super.onDeleted(context, appWidgetIds)
Log.d(TAG, "onDeleted : ${appWidgetIds?.contentToString()}")
runBlocking {
job.cancelAndJoin()
}
if (context != null && appWidgetIds != null) {
for (widgetId in appWidgetIds) {
TaoWidgetConfigurationActivity.destroyPrefs(context, widgetId)
}
}
}
}

View File

@ -0,0 +1,60 @@
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.TaoRestApi
import kotlinx.coroutines.*
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.infowindow.InfoWindow
class StopInfoWindow(
mapView: MapView,
private val taoRestApi: TaoRestApi = TaoRestApi()
) : InfoWindow(R.layout.window_stop_info, mapView) {
private val job = Job()
private val windowsContext = Dispatchers.IO + job
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(
null
)
}
private val adapter: ScheduleAdapter by lazy { ScheduleAdapter(scheduleList) }
private val recyclerView: RecyclerView by lazy {
view.findViewById<RecyclerView>(R.id.window_stop_info_schedule_recycler_view).also {
it.adapter = adapter
it.layoutManager = LinearLayoutManager(view.context)
}
}
override fun onOpen(item: Any?) {
scheduleList.set(null)
recyclerView.adapter?.notifyDataSetChanged()
if (item is Marker) {
val id = item.id
titleTextView.text = item.title
GlobalScope.launch(windowsContext) {
val schedule = taoRestApi.getSchedule(id)
withContext(Dispatchers.Main) {
scheduleList.set(schedule)
adapter.notifyDataSetChanged()
}
}
}
}
override fun onClose() {
windowsContext.cancelChildren()
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/widget_background_color" />
<stroke
android:width="2dp"
android:color="@color/widget_border_color" />
<corners android:radius="@dimen/widget_tao_corner" />
<padding
android:bottom="0dp"
android:left="0dp"
android:right="0dp"
android:top="0dp" />
</shape>

View File

@ -0,0 +1,5 @@
<vector android:height="48dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="#FFFFFF"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="2.1141"
android:scaleY="2.1141"
android:translateX="28.6308"
android:translateY="28.6308">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2c-4.42,0 -8,0.5 -8,4v10c0,0.88 0.39,1.67 1,2.22L5,20c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h8v1c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1.78c0.61,-0.55 1,-1.34 1,-2.22L20,6c0,-3.5 -3.58,-4 -8,-4zM17.66,4.99L6.34,4.99C6.89,4.46 8.31,4 12,4s5.11,0.46 5.66,0.99zM18,6.99L18,10L6,10L6,6.99h12zM17.66,16.73l-0.29,0.27L6.63,17l-0.29,-0.27C6.21,16.62 6,16.37 6,16v-4h12v4c0,0.37 -0.21,0.62 -0.34,0.73z" />
<path
android:fillColor="@android:color/white"
android:pathData="M8.5,14.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
<path
android:fillColor="@android:color/white"
android:pathData="M15.5,14.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
</group>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11,15h2v2h-2v-2zM11,7h2v6h-2L11,7zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
tools:context=".activities.MainActivity">
<org.osmdroid.views.MapView
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?actionBarSize"
android:fitsSystemWindows="false" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="?attr/colorAccent"
app:layout_anchor="@id/bottomAppBar" />
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
style="@style/Widget.MaterialComponents.BottomAppBar.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:fabAlignmentMode="end" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.TaoWidgetConfigurationActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?actionBarSize">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/config_select_line"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:hint="@string/activity_tao_config_line"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/activity_tao_config_line"
android:inputType="none"
android:labelFor="@id/config_select_line" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/config_select_direction"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:hint="@string/activity_tao_config_direction"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/config_select_line">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/activity_tao_config_direction"
android:inputType="none"
android:labelFor="@id/config_select_direction" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/config_select_stop"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:hint="@string/activity_tao_config_stop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/config_select_direction">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/activity_tao_config_stop"
android:inputType="none"
android:labelFor="@id/config_select_stop" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabConfirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/activity_tao_config_save_button_description"
android:focusable="true"
android:src="@drawable/ic_outline_save_24"
app:layout_anchor="@id/bottomAppBar" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loading_progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="?attr/colorAccent" />
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
style="@style/Widget.MaterialComponents.BottomAppBar.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:fabAlignmentMode="end" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,19 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView
android:id="@+id/item_line_image_view"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/widget_tao_line_image_view_description"
android:padding="8dp" />
<TextView
android:id="@+id/item_line_name_text_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:padding="8dp" />
</LinearLayout>

View File

@ -0,0 +1,33 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView
android:id="@+id/item_line_direction_image_view"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/widget_tao_line_image_view_description"
android:padding="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:text="@string/activity_tao_config_direction"
android:textStyle="italic" />
<TextView
android:id="@+id/item_line_direction_name_text_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:gravity="center_vertical" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/item_schedule_direction_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:gravity="center_vertical"
android:textColor="@color/widget_text_color"
android:textSize="14sp"
android:textStyle="bold|italic"
tools:text="L" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp">
<ImageView
android:id="@+id/item_schedule_line_image_view"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@android:color/transparent"
android:contentDescription="@string/widget_tao_line_image_view_description"
android:maxWidth="48dp"
android:maxHeight="48dp"
android:scaleType="fitCenter"
android:textColor="@color/widget_text_color"
tools:src="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/item_schedule_next_text_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:textColor="@color/widget_text_color"
android:textSize="12sp"
android:textStyle="bold|italic"
tools:text="10mn" />
<TextView
android:id="@+id/item_schedule_after_text_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:textColor="@color/widget_text_color"
android:textSize="12sp"
android:textStyle="bold|italic"
tools:text="20mn" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,6 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_stop_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:padding="8dp" />

View File

@ -0,0 +1,115 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tao_widget_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/widget_tao_margin"
android:background="@drawable/bg_widget"
android:orientation="vertical"
android:padding="@dimen/widget_tao_margin"
android:theme="@style/ThemeOverlay.TaoToolbox.AppWidgetContainer">
<!-- TODO ALL dp/sp IN DIMEN -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3">
<TextView
android:id="@+id/widget_tao_stop_name_text_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:gravity="center_vertical"
android:textColor="@color/widget_text_color"
android:textSize="12sp"
android:textStyle="bold"
tools:text="Gascogne" />
<TextView
android:id="@+id/widget_tao_refresh_date_text_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical|end"
android:textColor="@color/widget_text_color"
android:textSize="12sp"
android:textStyle="bold"
tools:text="12:58" />
</LinearLayout>
<TextView
android:id="@+id/widget_tao_stop_direction_text_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:gravity="center_vertical"
android:textColor="@color/widget_text_color"
android:textSize="12sp"
android:textStyle="bold|italic"
tools:text="L" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp">
<ImageView
android:id="@+id/widget_tao_line_icon_image_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:contentDescription="@string/widget_tao_line_image_view_description"
android:maxWidth="48dp"
android:maxHeight="48dp"
android:scaleType="fitCenter"
android:textColor="@color/widget_text_color"
tools:src="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/widget_tao_next_text_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:textColor="@color/widget_text_color"
android:textSize="12sp"
android:textStyle="bold|italic"
tools:text="10mn" />
<TextView
android:id="@+id/widget_tao_after_next_text_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:textColor="@color/widget_text_color"
android:textSize="12sp"
android:textStyle="bold|italic"
tools:text="20mn" />
<TextView
android:id="@+id/widget_tao_third_text_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:textColor="@color/widget_text_color"
android:textSize="12sp"
android:textStyle="bold|italic"
tools:text="30mn" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_widget"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/window_stop_info_title_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textSize="18sp"
android:textColor="@color/widget_text_color"
tools:text="Gascogne" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/window_stop_info_schedule_recycler_view"
android:layout_width="@dimen/window_stop_info_dimen"
android:layout_height="@dimen/window_stop_info_dimen" />
</LinearLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/tao_blue" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Boite à outils TAO</string>
<string name="line_popup_description">Direction : %s</string>
<string name="activity_tao_config_missing_value">Il manque une valeur</string>
<string name="activity_tao_config_line">Ligne</string>
<string name="activity_tao_config_direction">Direction</string>
<string name="activity_tao_config_stop">Arret</string>
<string name="activity_tao_config_save_button_description">Sauvegarder</string>
<string name="widget_tao_line_image_view_description">Le logo de la ligne</string>
<string name="widget_tao_description">Un widget pour savoir le temps restant avant un départ</string>
<string name="widget_tao_error_title">Erreur</string>
<string name="widget_tao_error_description">Quelque chose s\'est mal passé</string>
<string name="time_remaining">%smn</string>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="is_theme_light">false</bool>
<bool name="nav_bar_light">true</bool>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="widget_background_color">#99000000</color>
<color name="widget_border_color">#000</color>
<color name="widget_text_color">#fff</color>
</resources>

View File

@ -0,0 +1,27 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TaoToolbox" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/tao_blue</item>
<item name="colorPrimaryVariant">@color/tao_blue_variant</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/tao_pink</item>
<item name="colorSecondaryVariant">@color/tao_pink_variant</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<!-- <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> -->
<!-- Customize your theme here. -->
<item name="android:windowLightStatusBar" tools:targetApi="m">@bool/is_theme_light</item>
<item name="android:navigationBarColor" tools:targetApi="lollipop">?attr/colorPrimary</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">@bool/nav_bar_light
</item>
<item name="colorControlHighlight">?attr/colorPrimary</item>
</style>
<style name="ThemeOverlay.TaoToolbox.AppWidgetContainer" parent="">
<item name="appWidgetTextColor">@color/widget_text_color</item>
</style>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetBackgroundColor" format="color" />
<attr name="appWidgetTextColor" format="color" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="is_theme_light">true</bool>
<bool name="nav_bar_light">false</bool>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="tao_blue">#4baed5</color>
<color name="tao_blue_variant">#16527c</color>
<color name="tao_night_blue">#26285b</color>
<color name="tao_night_blue_variant">#7c86b2</color>
<color name="tao_pink">#e0217e</color>
<color name="tao_pink_variant">#a10058</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="widget_background_color">#99ffffff</color>
<color name="widget_border_color">#fff</color>
<color name="widget_text_color">#000</color>
</resources>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Refer to App Widget Documentation for margin information
http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
TODO DOC
-->
<!-- Tao Widget -->
<dimen name="widget_tao_margin">2dp</dimen>
<dimen name="widget_tao_corner">10dp</dimen>
<!-- Stop Info Window -->
<dimen name="window_stop_info_dimen">200dp</dimen>
</resources>

View File

@ -0,0 +1,25 @@
<resources>
<string name="app_name">TAO ToolBox</string>
<!-- MainActivity -->
<string name="line_popup_description">Direction : %s</string>
<!-- TaoWidgetConfigurationActivity -->
<string name="activity_tao_config_missing_value">Missing value</string>
<string name="activity_tao_config_line">Line</string>
<string name="activity_tao_config_direction">Direction</string>
<string name="activity_tao_config_stop">Stop</string>
<string name="activity_tao_config_save_button_description">Save</string>
<!-- Widget -->
<string name="widget_tao_line_image_view_description">The logo of the line</string>
<string name="widget_tao_description">A widget to know the remaining time</string>
<string name="widget_tao_time" translatable="false">%02d:%02d</string>
<string name="widget_tao_error_title">Error</string>
<string name="widget_tao_error_description">Something went wrong</string>
<string name="time_remaining">%smn</string>
<string name="line_and_direction_name" translatable="false">%s : %s</string>
</resources>

View File

@ -0,0 +1,26 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TaoToolbox" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/tao_night_blue</item>
<item name="colorPrimaryVariant">@color/tao_night_blue_variant</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/tao_pink</item>
<item name="colorSecondaryVariant">@color/tao_pink_variant</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">@android:color/transparent</item>
<!-- Customize your theme here. -->
<item name="android:windowLightStatusBar" tools:targetApi="m">@bool/is_theme_light</item>
<item name="android:navigationBarColor" tools:targetApi="lollipop">?attr/colorPrimary</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">@bool/nav_bar_light
</item>
<item name="colorControlHighlight">?attr/colorPrimary</item>
</style>
<style name="ThemeOverlay.TaoToolbox.AppWidgetContainer" parent="">
<item name="appWidgetTextColor">@color/widget_text_color</item>
</style>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- Exclude specific shared preferences that contain GCM registration Id -->
<!-- TODO -->
</full-backup-content>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="fr.oupson.taotoolbox.activities.TaoWidgetConfigurationActivity"
android:initialKeyguardLayout="@layout/widget_tao"
android:initialLayout="@layout/widget_tao"
android:minWidth="120dp"
android:minHeight="20dp"
android:minResizeWidth="40dp"
android:minResizeHeight="40dp"
android:previewImage="@drawable/tao_widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="600000"
android:widgetCategory="home_screen"
android:description="@string/widget_tao_description" />

View File

@ -0,0 +1,17 @@
package fr.oupson.taotoolbox
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

27
build.gradle Normal file
View File

@ -0,0 +1,27 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.32"
ext.ktor_version = "1.5.3"
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

1
common/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

49
common/build.gradle Normal file
View File

@ -0,0 +1,49 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.30'
}
android {
compileSdkVersion 30
buildToolsVersion "29.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
//minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
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'
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"
implementation "io.ktor:ktor-client-serialization:$ktor_version"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-cio:$ktor_version"
}

20
common/consumer-rules.pro Normal file
View File

@ -0,0 +1,20 @@
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
# Change here com.yourcompany.yourpackage
-keep,includedescriptorclasses class fr.oupson.common.**$$serializer { *; } # <-- change package name to your app's
-keepclassmembers class fr.oupson.common.** { # <-- change package name to your app's
*** Companion;
}
-keepclasseswithmembers class fr.oupson.common.** { # <-- change package name to your app's
kotlinx.serialization.KSerializer serializer(...);
}

21
common/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package fr.oupson.common
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.oupson.common.test", appContext.packageName)
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.oupson.common">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -0,0 +1,89 @@
package fr.oupson.common.api
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import androidx.core.content.res.ResourcesCompat
import fr.oupson.common.R
import kotlinx.serialization.Serializable
@Serializable
data class LineColors(
val lineCode: String,
val background: String,
val text: String,
val commuteMode: String,
val lineOrder: Int,
val _id: String,
val lineShortCode: String
) {
companion object {
fun getLineBtm(
context: Context,
lineCode: String,
lineColor: Int,
textColor: Int,
width: Int = 24,
height: Int = 24
): Bitmap {
val res = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
val canvas = Canvas(res)
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), Paint().apply {
color = lineColor
})
val textPaint = Paint().apply {
typeface = ResourcesCompat.getFont(context, R.font.noto_sans_bold)
textAlign = Paint.Align.CENTER
color = textColor
}
setTextSizeForWidth(
textPaint,
(4f * width.toFloat()) / 5,
(4f * height.toFloat()) / 5f,
lineCode
)
val rect = Rect()
canvas.getClipBounds(rect)
val cHeight: Int = rect.height()
textPaint.getTextBounds(lineCode, 0, lineCode.length, rect)
val xPos =
width.toFloat() / 2f // cWidth / 2f - rect.width().toFloat() / 2f - rect.left.toFloat()
val yPos = cHeight / 2f + rect.height() / 2f - rect.bottom
canvas.drawText(lineCode, xPos, yPos, textPaint)
return res
}
private fun setTextSizeForWidth(
paint: Paint, desiredWidth: Float, desiredHeight: Float,
text: String
) {
// Pick a reasonably large value for the test. Larger values produce
// more accurate results, but may cause problems with hardware
// acceleration. But there are workarounds for that, too; refer to
// http://stackoverflow.com/questions/6253528/font-size-too-large-to-fit-in-cache
val testTextSize = 48f
// Get the bounds of the text, using our testTextSize.
paint.textSize = testTextSize
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
// Calculate the desired size as a proportion of our testTextSize.
val desiredTextSize: Float =
(testTextSize * desiredWidth / bounds.width()).coerceAtMost(testTextSize * desiredHeight / bounds.height())
// Set the paint for that size.
paint.textSize = desiredTextSize - 1
}
}
}

View File

@ -0,0 +1,167 @@
package fr.oupson.common.api
import android.util.Log
import fr.oupson.common.BuildConfig
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.text.SimpleDateFormat
import java.util.*
@Serializable
data class Stop(
val type: String,
val stopName: String,
val city: String,
val _id: String,
// LONG THEN LAT
val coord: FloatArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Stop
if (stopName != other.stopName) return false
if (city != other.city) return false
if (_id != other._id) return false
if (!coord.contentEquals(other.coord)) return false
return true
}
override fun hashCode(): Int {
var result = stopName.hashCode()
result = 31 * result + city.hashCode()
result = 31 * result + _id.hashCode()
result = 31 * result + coord.contentHashCode()
return result
}
}
@Serializable
data class RealTimes(
val realTimes: Array<Departure>,
val disruptions: String?,
val summaryMessage: String?,
val summaryStatus: String?
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as RealTimes
if (!realTimes.contentEquals(other.realTimes)) return false
if (disruptions != other.disruptions) return false
if (summaryMessage != other.summaryMessage) return false
if (summaryStatus != other.summaryStatus) return false
return true
}
override fun hashCode(): Int {
var result = realTimes.contentHashCode()
result = 31 * result + (disruptions?.hashCode() ?: 0)
result = 31 * result + (summaryMessage?.hashCode() ?: 0)
result = 31 * result + (summaryStatus?.hashCode() ?: 0)
return result
}
fun timeRemaining(index: Int): Long? =
this.realTimes.getOrNull(index)?.nextAsDate?.let {
(it.time - Date().time) / (1000 * 60)
}
fun timeRemainingAfter(index: Int): Long? =
this.realTimes.getOrNull(index)?.nextAfterAsDate?.let {
(it.time - Date().time) / (1000 * 60)
}
fun timeRemainingThird(index: Int): Long? =
this.realTimes.getOrNull(index)?.nextThirdAsDate?.let {
(it.time - Date().time) / (1000 * 60)
}
}
@Serializable
data class Departure(
val lineId: String,
val lineCode: String,
val routeId: String,
val lineColors: LineColors,
@Serializable(with = DateAsStringSerializer::class)
val nextAsDate: Date?,
@Serializable(with = DateAsStringSerializer::class)
val nextAfterAsDate: Date?,
@Serializable(with = DateAsStringSerializer::class)
val nextThirdAsDate: Date?,
val nextVehicleId: String?,
val nextAfterVehicleId: String?,
val nextThirdVehicleId: String?,
val lineDirectionName: String
)
object DateAsStringSerializer : KSerializer<Date> {
private const val TAG = "DateAsStringSerializer"
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("GregorianCalendar", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Date) {
encoder.encodeString(
SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSSX",
Locale.getDefault()
).format(value)
)
}
override fun deserialize(decoder: Decoder): Date {
val string = decoder.decodeString()
if (BuildConfig.DEBUG)
Log.v(TAG, "Deserialize : $string")
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
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
} else {
throw Exception("$string is not supported") // TODO ?
}
if (BuildConfig.DEBUG)
Log.v(TAG, "New date str : $newDateStr")
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.getDefault()).parse(newDateStr)!!
}
}
}
@Serializable
data class Schedule(
val lineId: String,
val routeId: String,
val lineColors: LineColors,
val lineDirectionName: String,
@Serializable(with = DateAsStringSerializer::class)
val nextSchedule: Date?,
@Serializable(with = DateAsStringSerializer::class)
val nextScheduleAfter: Date?
) {
fun timeRemaining(): Long? =
nextSchedule?.let {
(it.time - Date().time) / (1000 * 60)
}
fun timeRemainingAfter(): Long? =
nextScheduleAfter?.let {
(it.time - Date().time) / (1000 * 60)
}
}

View File

@ -0,0 +1,73 @@
package fr.oupson.common.api
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.features.json.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
class TaoRestApi(private val httpClient: HttpClient) {
constructor() : this(HttpClient(CIO) {
install(JsonFeature)
})
var requestContext: CoroutineContext = Dispatchers.IO
private var baseUrl = "https://navigorleans.c-t.io/api"
suspend fun getNextDeparturesForLine(
stopId: String,
routeId: String,
limit: Int = 100
): RealTimes = withContext(
requestContext
) {
httpClient.submitForm(
"$baseUrl/2.0/realtime/byStopAreaAndRoute",
Parameters.build {
append(
"stopAreaId", stopId
)
append(
"routeId", routeId
)
append("limit", "$limit")
},
true
) {}
}
suspend fun getTaoLineJson(
clientLineVersion: Int,
clientStopVersion: Int,
clientLastUpdate: String?
): String = withContext(requestContext) {
httpClient.submitForm(
"$baseUrl/2.0/version/withDate",
Parameters.build {
append("clientLineVersion", "$clientLineVersion")
append("clientStopVersion", "$clientStopVersion")
append("clientLastUpdate", clientLastUpdate ?: "1970-01-01T00:00:00.000Z")
},
true
) {}
}
suspend fun getSchedule(stopId: String): Array<Schedule> = withContext(Dispatchers.IO) {
httpClient.submitForm("https://navigorleans.c-t.io/api/2.0/schedule", Parameters.build {
append(
"stopId", stopId
)
}, true) {}
}
suspend fun getTaoGeoJson(
lineId: String
): String = withContext(requestContext) {
httpClient.get("$baseUrl/2.0/lines/$lineId/geojson")
}
}

View File

@ -0,0 +1,333 @@
package fr.oupson.common.db
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
import fr.oupson.common.api.TaoRestApi
import org.json.JSONObject
class TaoDatabaseHelper(
context: Context,
private val taoRestApi: TaoRestApi = TaoRestApi()
) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
class TaoDatabase {
object LineEntry {
const val TABLE_NAME = "LINE"
const val COLUMN_NAME_LINE_ID = "lineID"
const val COLUMN_NAME_LINE_NAME = "lineName"
const val COLUMN_NAME_LINE_CODE = "lineCode"
const val COLUMN_NAME_LINE_SHORT_CODE = "lineShortCode"
const val COLUMN_NAME_BACKGROUND_COLOR = "backgroundColor"
const val COLUMN_NAME_TEXT_COLOR = "textColor"
const val COLUMN_NAME_COMMUTE_MODE = "commuteMode"
const val COLUMN_NAME_LINE_ORDER = "lineOrder"
const val SQL_CREATE_ENTRIES =
"CREATE TABLE $TABLE_NAME (" +
"$COLUMN_NAME_LINE_ID TEXT PRIMARY KEY," +
"$COLUMN_NAME_LINE_NAME TEXT," +
"$COLUMN_NAME_LINE_CODE TEXT," +
"$COLUMN_NAME_LINE_SHORT_CODE TEXT," +
"$COLUMN_NAME_BACKGROUND_COLOR TEXT," +
"$COLUMN_NAME_TEXT_COLOR TEXT," +
"$COLUMN_NAME_COMMUTE_MODE TEXT," +
"$COLUMN_NAME_LINE_ORDER INTEGER" +
")"
const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS $TABLE_NAME"
}
object StopEntry {
const val TABLE_NAME = "STOP"
const val COLUMN_NAME_STOP_ID = "stopID"
const val COLUMN_NAME_STOP_NAME = "stopName"
const val COLUMN_NAME_STOP_NAME_NORMALIZED = "stopNameNormalized"
const val COLUMN_NAME_CITY_NAME = "cityName"
const val COLUMN_NAME_COORD_LONG = "coordLong"
const val COLUMN_NAME_COORD_LAT = "coordLat"
const val SQL_CREATE_ENTRIES =
"CREATE TABLE $TABLE_NAME(" +
"$COLUMN_NAME_STOP_ID TEXT PRIMARY KEY," +
"$COLUMN_NAME_STOP_NAME TEXT," +
"$COLUMN_NAME_STOP_NAME_NORMALIZED TEXT," +
"$COLUMN_NAME_CITY_NAME TEXT," +
"$COLUMN_NAME_COORD_LONG REAL," +
"$COLUMN_NAME_COORD_LAT REAL" +
")"
const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS $TABLE_NAME"
}
object DirectionEntry {
const val TABLE_NAME = "DIRECTION"
const val COLUMN_NAME_ROUTE_ID = "routeID"
const val COLUMN_NAME_ROUTE_NAME = "routeName"
const val SQL_CREATE_ENTRIES = "CREATE TABLE $TABLE_NAME(" +
"$COLUMN_NAME_ROUTE_ID TEXT PRIMARY KEY," +
"${LineEntry.COLUMN_NAME_LINE_ID} TEXT," +
"$COLUMN_NAME_ROUTE_NAME TEXT" +
")"
const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS $TABLE_NAME"
}
object IsStopEntry {
const val TABLE_NAME = "IS_STOP"
const val COLUMN_NAME_STOP_INDEX = "stopIndex"
const val COLUMN_NAME_STOP_POINT_ID = "stopPointID"
const val SQL_CREATE_ENTRIES = "CREATE TABLE $TABLE_NAME(" +
"${DirectionEntry.COLUMN_NAME_ROUTE_ID} TEXT," +
"${StopEntry.COLUMN_NAME_STOP_ID} TEXT," +
"$COLUMN_NAME_STOP_INDEX INTEGER," +
"$COLUMN_NAME_STOP_POINT_ID TEXT," +
"PRIMARY KEY(${DirectionEntry.COLUMN_NAME_ROUTE_ID}, ${StopEntry.COLUMN_NAME_STOP_ID}, ${IsStopEntry.COLUMN_NAME_STOP_INDEX})" +
")"
const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS $TABLE_NAME"
}
object VersionEntry {
const val TABLE_NAME = "VERSION"
const val PRIMARY_KEY = "versionPrimaryKey"
const val COLUMN_NAME_STOP_VERSION = "stopVersion"
const val COLUMN_NAME_LINE_VERSION = "lineVersion"
const val COLUMN_NAME_LAST_UPDATE = "lastUpdate"
const val SQL_CREATE_ENTRIES = "CREATE TABLE $TABLE_NAME(" +
"$PRIMARY_KEY INTEGER PRIMARY KEY," +
"$COLUMN_NAME_STOP_VERSION INTEGER," +
"$COLUMN_NAME_LINE_VERSION INTEGER," +
"$COLUMN_NAME_LAST_UPDATE TEXT" +
")"
const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS $TABLE_NAME"
}
}
override fun onCreate(db: SQLiteDatabase?) {
db?.execSQL(TaoDatabase.LineEntry.SQL_CREATE_ENTRIES)
db?.execSQL(TaoDatabase.StopEntry.SQL_CREATE_ENTRIES)
db?.execSQL(TaoDatabase.DirectionEntry.SQL_CREATE_ENTRIES)
db?.execSQL(TaoDatabase.IsStopEntry.SQL_CREATE_ENTRIES)
db?.execSQL(TaoDatabase.VersionEntry.SQL_CREATE_ENTRIES)
}
suspend fun checkUpdate() {
val str = taoRestApi.getTaoLineJson(11, 11, null)
val json = JSONObject(str)
val db = this.writableDatabase
val cursor = db.rawQuery(
"SELECT ${TaoDatabase.VersionEntry.COLUMN_NAME_LINE_VERSION}, ${TaoDatabase.VersionEntry.COLUMN_NAME_STOP_VERSION}, ${TaoDatabase.VersionEntry.COLUMN_NAME_LAST_UPDATE} FROM ${TaoDatabase.VersionEntry.TABLE_NAME}",
null
)
val updateVersion = cursor.moveToNext()
val updateDb = if (updateVersion) {
val currentDBLineVersion = cursor.getInt(0)
val currentDBStopVersion = cursor.getInt(1)
val currentDBLastUpdate = cursor.getString(2)
val jsonLineVersion = json.getInt("actualLineVersion")
val jsonStopVersion = json.getInt("actualStopVersion")
val jsonLastUpdate = json.getString("lastUpdate")
currentDBLineVersion != jsonLineVersion || currentDBStopVersion != jsonStopVersion || currentDBLastUpdate != jsonLastUpdate
} else {
true
}
cursor.close()
Log.d(TAG, "updating database : $updateDb")
if (updateDb) {
db?.execSQL(TaoDatabase.LineEntry.SQL_DELETE_ENTRIES)
db?.execSQL(TaoDatabase.StopEntry.SQL_DELETE_ENTRIES)
db?.execSQL(TaoDatabase.DirectionEntry.SQL_DELETE_ENTRIES)
db?.execSQL(TaoDatabase.IsStopEntry.SQL_DELETE_ENTRIES)
db?.execSQL(TaoDatabase.LineEntry.SQL_CREATE_ENTRIES)
db?.execSQL(TaoDatabase.StopEntry.SQL_CREATE_ENTRIES)
db?.execSQL(TaoDatabase.DirectionEntry.SQL_CREATE_ENTRIES)
db?.execSQL(TaoDatabase.IsStopEntry.SQL_CREATE_ENTRIES)
val stops = json.getJSONArray("stops")
for (i in 0 until stops.length()) {
val stop = stops.getJSONObject(i)
val stopEntryValues = ContentValues().apply {
put(TaoDatabase.StopEntry.COLUMN_NAME_STOP_ID, stop.getString("_id"))
put(TaoDatabase.StopEntry.COLUMN_NAME_STOP_NAME, stop.getString("stopName"))
put(
TaoDatabase.StopEntry.COLUMN_NAME_STOP_NAME_NORMALIZED,
stop.getString("stopNameNormalized")
)
put(TaoDatabase.StopEntry.COLUMN_NAME_CITY_NAME, stop.getString("city"))
val coordArray = stop.getJSONArray("coord")
put(TaoDatabase.StopEntry.COLUMN_NAME_COORD_LONG, coordArray.getDouble(0))
put(TaoDatabase.StopEntry.COLUMN_NAME_COORD_LAT, coordArray.getDouble(1))
}
db?.insert(TaoDatabase.StopEntry.TABLE_NAME, null, stopEntryValues)
val stopAreasOnLine = stop.getJSONArray("stopAreaOnLines")
for (stopAreaIndex in 0 until stopAreasOnLine.length()) {
val stopArea = stopAreasOnLine.getJSONObject(stopAreaIndex)
val isStopEntryValues = ContentValues().apply {
put(
TaoDatabase.DirectionEntry.COLUMN_NAME_ROUTE_ID,
stopArea.getString("routeId")
)
put(TaoDatabase.StopEntry.COLUMN_NAME_STOP_ID, stopArea.getString("stopId"))
put(
TaoDatabase.IsStopEntry.COLUMN_NAME_STOP_INDEX,
stopArea.getString("stopIndex")
)
put(
TaoDatabase.IsStopEntry.COLUMN_NAME_STOP_POINT_ID,
stopArea.getString("stopPointId")
)
}
db?.insert(TaoDatabase.IsStopEntry.TABLE_NAME, null, isStopEntryValues)
}
}
val lines = json.getJSONArray("lines")
for (i in 0 until lines.length()) {
val line = lines.getJSONObject(i)
val lineEntryValues = ContentValues().apply {
put(TaoDatabase.LineEntry.COLUMN_NAME_LINE_ID, line.getString("_id"))
put(TaoDatabase.LineEntry.COLUMN_NAME_LINE_NAME, line.getString("lineName"))
put(TaoDatabase.LineEntry.COLUMN_NAME_LINE_CODE, line.getString("lineCode"))
put(
TaoDatabase.LineEntry.COLUMN_NAME_LINE_SHORT_CODE,
line.getString("lineShortCode")
)
val lineColors = line.getJSONObject("lineColors")
put(
TaoDatabase.LineEntry.COLUMN_NAME_BACKGROUND_COLOR,
lineColors.getString("background")
)
put(TaoDatabase.LineEntry.COLUMN_NAME_TEXT_COLOR, lineColors.getString("text"))
put(
TaoDatabase.LineEntry.COLUMN_NAME_COMMUTE_MODE,
lineColors.getString("commuteMode")
)
put(
TaoDatabase.LineEntry.COLUMN_NAME_LINE_ORDER,
lineColors.getString("lineOrder")
)
}
db?.insert(TaoDatabase.LineEntry.TABLE_NAME, null, lineEntryValues)
val directions = line.getJSONArray("lineDirections")
for (directionIndex in 0 until directions.length()) {
val direction = directions.getJSONObject(directionIndex)
val directionEntryValues = ContentValues().apply {
put(
TaoDatabase.DirectionEntry.COLUMN_NAME_ROUTE_ID,
direction.getString("routeCode")
)
put(
TaoDatabase.LineEntry.COLUMN_NAME_LINE_ID,
direction.getString("lineCode")
)
put(
TaoDatabase.DirectionEntry.COLUMN_NAME_ROUTE_NAME,
direction.getString("routeName")
)
}
db?.insert(TaoDatabase.DirectionEntry.TABLE_NAME, null, directionEntryValues)
}
}
if (updateVersion) {
db?.update(
TaoDatabase.VersionEntry.TABLE_NAME,
ContentValues().apply {
put(TaoDatabase.VersionEntry.PRIMARY_KEY, 0)
put(
TaoDatabase.VersionEntry.COLUMN_NAME_LINE_VERSION,
json.getInt("actualLineVersion")
)
put(
TaoDatabase.VersionEntry.COLUMN_NAME_STOP_VERSION,
json.getInt("actualStopVersion")
)
put(
TaoDatabase.VersionEntry.COLUMN_NAME_LAST_UPDATE,
json.getString("lastUpdate")
)
},
"${TaoDatabase.VersionEntry.PRIMARY_KEY} = 0",
null
)
} else {
db.insert(TaoDatabase.VersionEntry.TABLE_NAME, null, ContentValues().apply {
put(TaoDatabase.VersionEntry.PRIMARY_KEY, 0)
put(
TaoDatabase.VersionEntry.COLUMN_NAME_LINE_VERSION,
json.getInt("actualLineVersion")
)
put(
TaoDatabase.VersionEntry.COLUMN_NAME_STOP_VERSION,
json.getInt("actualStopVersion")
)
put(
TaoDatabase.VersionEntry.COLUMN_NAME_LAST_UPDATE,
json.getString("lastUpdate")
)
})
}
}
db.close()
}
fun getRouteName(db: SQLiteDatabase, routeId: String): String? {
val cursor = db.rawQuery(
"SELECT ${TaoDatabase.DirectionEntry.COLUMN_NAME_ROUTE_NAME} FROM ${TaoDatabase.DirectionEntry.TABLE_NAME} WHERE ${TaoDatabase.DirectionEntry.COLUMN_NAME_ROUTE_ID} = ?",
arrayOf(routeId)
)
val res = if (cursor.moveToNext()) {
cursor.getString(0)
} else {
null
}
cursor.close()
return res
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.execSQL(TaoDatabase.LineEntry.SQL_DELETE_ENTRIES)
db?.execSQL(TaoDatabase.StopEntry.SQL_DELETE_ENTRIES)
db?.execSQL(TaoDatabase.DirectionEntry.SQL_DELETE_ENTRIES)
db?.execSQL(TaoDatabase.IsStopEntry.SQL_DELETE_ENTRIES)
db?.execSQL(TaoDatabase.VersionEntry.SQL_DELETE_ENTRIES)
onCreate(db)
}
override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
onUpgrade(db, oldVersion, newVersion)
}
companion object {
const val DATABASE_VERSION = 3
const val DATABASE_NAME = "TaoDb.sql"
private const val TAG = "TaoDatabaseHelper"
}
}

Binary file not shown.

View File

@ -0,0 +1,17 @@
package fr.oupson.common
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

21
gradle.properties Normal file
View File

@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Thu Apr 01 20:55:44 CEST 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

172
gradlew vendored Normal file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View File

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

4
settings.gradle Normal file
View File

@ -0,0 +1,4 @@
include ':wear_tao'
include ':common'
include ':app'
rootProject.name = "Tao Toolbox"

1
wear_tao/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

51
wear_tao/build.gradle Normal file
View File

@ -0,0 +1,51 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 30
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "fr.oupson.wear_tao"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
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-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'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.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"
implementation project(":common")
}

21
wear_tao/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package fr.oupson.wear_tao
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("fr.oupson.wear_tao", appContext.packageName)
}
}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.oupson.wear_tao">
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TaoToolbox">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true" />
<service
android:name=".TaoTile"
android:description="@string/tile_description"
android:exported="true"
android:label="Tuile TAO"
android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
<intent-filter>
<action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
</intent-filter>
<intent-filter>
<action android:name="com.google.android.clockwork.ACTION_TILE_UPDATE_REQUEST" />
</intent-filter>
<meta-data
android:name="androidx.wear.tiles.PREVIEW"
android:resource="@drawable/ic_launcher_background" />
</service>
</application>
</manifest>

View File

@ -0,0 +1,30 @@
package fr.oupson.wear_tao
import android.content.ComponentName
import android.os.Bundle
import android.widget.FrameLayout
import androidx.activity.ComponentActivity
import androidx.wear.tiles.manager.TileManager
class MainActivity : ComponentActivity() {
private lateinit var tileManager: TileManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rootLayout = findViewById<FrameLayout>(R.id.tile_container)
tileManager = TileManager(
context = this,
component = ComponentName(this, TaoTile::class.java),
parentView = rootLayout
)
tileManager.create()
}
override fun onDestroy() {
super.onDestroy()
tileManager.close()
}
}

View File

@ -0,0 +1,235 @@
package fr.oupson.wear_tao
import android.graphics.Color
import android.util.Log
import androidx.wear.tiles.TileProviderService
import androidx.wear.tiles.builders.*
import androidx.wear.tiles.builders.DimensionBuilders.*
import androidx.wear.tiles.builders.LayoutElementBuilders.*
import androidx.wear.tiles.readers.EventReaders
import androidx.wear.tiles.readers.RequestReaders
import com.google.common.util.concurrent.ListenableFuture
import fr.oupson.common.api.LineColors.Companion.getLineBtm
import fr.oupson.common.api.RealTimes
import fr.oupson.common.api.TaoRestApi
import fr.oupson.common.db.TaoDatabaseHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.guava.future
import java.nio.ByteBuffer
// TODO
class TaoTile : TileProviderService() {
companion object {
private const val TAG = "TaoTile"
private const val RESOURCE_VERSION = "1"
}
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private val taoRestApi : TaoRestApi by lazy {
TaoRestApi().apply {
this.requestContext = Dispatchers.IO + serviceJob
}
}
override fun onTileRequest(requestParams: RequestReaders.TileRequest) = serviceScope.future {
val gascogne = async {
taoRestApi.getNextDeparturesForLine(
"a",
"route:OLS:1_R_388"
)
}
val coligny = async {
taoRestApi.getNextDeparturesForLine(
"b",
"route:OLS:1_R_388"
)
}
val iut = async {
taoRestApi.getNextDeparturesForLine(
"c",
"route:OLS:1_A_419"
)
}
TileBuilders.Tile.builder()
.setResourcesVersion(RESOURCE_VERSION)
.setTimeline(
TimelineBuilders.Timeline.builder().addTimelineEntry(
TimelineBuilders.TimelineEntry.builder().setLayout(
Layout.builder().setRoot(
Box.builder()
.setWidth(expand())
.setHeight(expand())
.setModifiers(
ModifiersBuilders.Modifiers.builder()
.setClickable(
ModifiersBuilders.Clickable.builder()
.setId("reLoad")
.setOnClick(ActionBuilders.LoadAction.builder())
)
)
.setVerticalAlignment(VALIGN_CENTER)
.addContent(
Column.builder()
.setWidth(expand())
.setHeight(wrap())
.setHorizontalAlignment(HALIGN_CENTER)
.addContent(generateLayout("a", gascogne.await()))
.addContent(generateLayout("b", coligny.await()))
.addContent(generateLayout("c", iut.await()))
.build()
)
)
)
)
).build()
}
private fun generateLayout(stopName: String, realTimes: RealTimes): LayoutElement =
Column.builder()
.setWidth(expand())
.setHeight(wrap()).setModifiers(
ModifiersBuilders.Modifiers.builder()
.setPadding(
ModifiersBuilders.Padding.builder()
.setTop(dp(2f))
.setBottom(dp(2f))
.setStart(dp(2f))
.setEnd(dp(2f))
)
)
.addContent(
Text.builder().setText(stopName).setFontStyle(FontStyle.builder().setSize(sp(16f)))
.setLineHeight(sp(0f))
.build()
).also {
val r = realTimes.realTimes.getOrNull(0)
realTimes.realTimes.getOrNull(0)?.lineDirectionName?.let { dir ->
it.addContent(
line(dir)
)
}
}
.addContent(
layout(realTimes)
)
.build()
private fun layout(realTimes: RealTimes): LayoutElement = Row.builder()
.setHeight(wrap())
.addContent(
Text.builder()
.setText("${realTimes.timeRemaining(0) ?: "∅"}")
.setFontStyle(FontStyle.builder().setSize(sp(14f)))
)
.addContent(Spacer.builder().setWidth(dp(16f)))
.addContent(
Text.builder()
.setText("${realTimes.timeRemainingAfter(0) ?: "∅"}")
.setFontStyle(FontStyle.builder().setSize(sp(14f)))
)
.addContent(Spacer.builder().setWidth(dp(16f)))
.addContent(
Text.builder()
.setText("${realTimes.timeRemainingThird(0) ?: "∅"}")
.setFontStyle(FontStyle.builder().setSize(sp(14f)))
)
.build()
private fun line2(dir: String): LayoutElement = Spannable.builder()
.addSpan(
SpanImage.builder()
.setHeight(dp(18f))
.setWidth(dp(18f))
.setResourceId("line1")
).addSpan(
SpanText.builder().setText(dir)
.setFontStyle(FontStyle.builder().setSize(sp(16f)))
.build()
)
.build()
private fun line(dir: String): LayoutElement = Row.builder()
.setVerticalAlignment(VALIGN_CENTER)
.addContent(
Image.builder()
.setHeight(dp(18f))
.setWidth(dp(18f))
.setResourceId("line1")
)
.addContent(Spacer.builder().setWidth(dp(2f)))
.addContent(
Text.builder().setText(dir)
.setFontStyle(FontStyle.builder().setSize(sp(16f)))
.build()
).build()
override fun onTileAddEvent(requestParams: EventReaders.TileAddEvent) {
super.onTileAddEvent(requestParams)
Log.d(TAG, "onTileAddEvent")
}
override fun onResourcesRequest(requestParams: RequestReaders.ResourcesRequest): ListenableFuture<ResourceBuilders.Resources> =
serviceScope.future {
val db = TaoDatabaseHelper(this@TaoTile).readableDatabase
val c = db.rawQuery(
"SELECT backgroundColor, textColor FROM LINE WHERE lineCode = ?",
arrayOf("1")
)
c.moveToNext()
val bg = c.getString(0)
val tc = c.getString(1)
c.close()
db.close()
val btm = getLineBtm(
this@TaoTile,
"1",
Color.parseColor(bg),
Color.parseColor(tc),
width = 24,
height = 24
)
val bitmapData = ByteBuffer.allocate(btm.byteCount).apply {
btm.copyPixelsToBuffer(this)
}.array()
ResourceBuilders.Resources.builder()
.setVersion(RESOURCE_VERSION)
.addIdToImageMapping(
"line1", ResourceBuilders.ImageResource.builder()
.setInlineResource(
ResourceBuilders.InlineImageResource.builder()
.setData(bitmapData)
.setWidthPx(24)
.setHeightPx(24)
.setFormat(ResourceBuilders.IMAGE_FORMAT_RGB_565)
.build()
)
.build()
).build()
}
override fun onDestroy() {
super.onDestroy()
// Cleans up the coroutine
serviceJob.cancel()
}
}

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tile_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"/>

View File

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TaoToolbox" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,4 @@
<resources>
<string name="app_name">WearTAO</string>
<string name="tile_description">Sample tiles</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More