commit 730a7edc0ea806f1bfd5cfdcc21d07c3bbb6cdcf Author: Oupson Date: Sun May 9 19:23:52 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffdee74 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..ee07fc5 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Tao Toolbox \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3c7772a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..2370474 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d5d35ec --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..6d3946f --- /dev/null +++ b/app/build.gradle @@ -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' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/androidTest/java/fr/oupson/taotoolbox/ExampleInstrumentedTest.kt b/app/src/androidTest/java/fr/oupson/taotoolbox/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..ab0ef15 --- /dev/null +++ b/app/src/androidTest/java/fr/oupson/taotoolbox/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c4bf984 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..f6a22b6 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/fr/oupson/taotoolbox/activities/MainActivity.kt b/app/src/main/java/fr/oupson/taotoolbox/activities/MainActivity.kt new file mode 100644 index 0000000..596e26d --- /dev/null +++ b/app/src/main/java/fr/oupson/taotoolbox/activities/MainActivity.kt @@ -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, + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/oupson/taotoolbox/activities/TaoWidgetConfigurationActivity.kt b/app/src/main/java/fr/oupson/taotoolbox/activities/TaoWidgetConfigurationActivity.kt new file mode 100644 index 0000000..0ec76cc --- /dev/null +++ b/app/src/main/java/fr/oupson/taotoolbox/activities/TaoWidgetConfigurationActivity.kt @@ -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() + private val directionList = arrayListOf() + private val stopList = arrayListOf() + + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/oupson/taotoolbox/adapters/DirectionAdapter.kt b/app/src/main/java/fr/oupson/taotoolbox/adapters/DirectionAdapter.kt new file mode 100644 index 0000000..69a04e6 --- /dev/null +++ b/app/src/main/java/fr/oupson/taotoolbox/adapters/DirectionAdapter.kt @@ -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) : + ArrayAdapter(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 + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/oupson/taotoolbox/adapters/LineAdapter.kt b/app/src/main/java/fr/oupson/taotoolbox/adapters/LineAdapter.kt new file mode 100644 index 0000000..6d75fe2 --- /dev/null +++ b/app/src/main/java/fr/oupson/taotoolbox/adapters/LineAdapter.kt @@ -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) : + ArrayAdapter(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 + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/oupson/taotoolbox/adapters/ScheduleAdapter.kt b/app/src/main/java/fr/oupson/taotoolbox/adapters/ScheduleAdapter.kt new file mode 100644 index 0000000..8707d19 --- /dev/null +++ b/app/src/main/java/fr/oupson/taotoolbox/adapters/ScheduleAdapter.kt @@ -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> +) : + RecyclerView.Adapter() { + class SimplePtr(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(R.id.item_schedule_direction_text_view) + private val scheduleImageView = view.findViewById(R.id.item_schedule_line_image_view) + private val scheduleNextTextView = view.findViewById(R.id.item_schedule_next_text_view) + private val scheduleAfterTextView = + view.findViewById(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 +} \ No newline at end of file diff --git a/app/src/main/java/fr/oupson/taotoolbox/adapters/StopAdapter.kt b/app/src/main/java/fr/oupson/taotoolbox/adapters/StopAdapter.kt new file mode 100644 index 0000000..530cfe6 --- /dev/null +++ b/app/src/main/java/fr/oupson/taotoolbox/adapters/StopAdapter.kt @@ -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) : + ArrayAdapter(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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/oupson/taotoolbox/receivers/TaoWidget.kt b/app/src/main/java/fr/oupson/taotoolbox/receivers/TaoWidget.kt new file mode 100644 index 0000000..24e6f0d --- /dev/null +++ b/app/src/main/java/fr/oupson/taotoolbox/receivers/TaoWidget.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/oupson/taotoolbox/windows/StopInfoWindow.kt b/app/src/main/java/fr/oupson/taotoolbox/windows/StopInfoWindow.kt new file mode 100644 index 0000000..f917793 --- /dev/null +++ b/app/src/main/java/fr/oupson/taotoolbox/windows/StopInfoWindow.kt @@ -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> by lazy { + ScheduleAdapter.SimplePtr( + null + ) + } + + private val adapter: ScheduleAdapter by lazy { ScheduleAdapter(scheduleList) } + private val recyclerView: RecyclerView by lazy { + view.findViewById(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() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_widget.xml b/app/src/main/res/drawable/bg_widget.xml new file mode 100644 index 0000000..c47cdf8 --- /dev/null +++ b/app/src/main/res/drawable/bg_widget.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_place_48.xml b/app/src/main/res/drawable/ic_baseline_place_48.xml new file mode 100644 index 0000000..d411330 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_place_48.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..17b423a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_outline_error_outline_24.xml b/app/src/main/res/drawable/ic_outline_error_outline_24.xml new file mode 100644 index 0000000..45ba5f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_error_outline_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_save_24.xml b/app/src/main/res/drawable/ic_outline_save_24.xml new file mode 100644 index 0000000..3c59fbc --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_save_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/tao_widget_preview.png b/app/src/main/res/drawable/tao_widget_preview.png new file mode 100644 index 0000000..a5e671a Binary files /dev/null and b/app/src/main/res/drawable/tao_widget_preview.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..d131c32 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_tao_widget_configuration.xml b/app/src/main/res/layout/activity_tao_widget_configuration.xml new file mode 100644 index 0000000..b30dbfa --- /dev/null +++ b/app/src/main/res/layout/activity_tao_widget_configuration.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_line.xml b/app/src/main/res/layout/item_line.xml new file mode 100644 index 0000000..02cc747 --- /dev/null +++ b/app/src/main/res/layout/item_line.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_line_direction.xml b/app/src/main/res/layout/item_line_direction.xml new file mode 100644 index 0000000..41fc834 --- /dev/null +++ b/app/src/main/res/layout/item_line_direction.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_schedule.xml b/app/src/main/res/layout/item_schedule.xml new file mode 100644 index 0000000..ae3f7e4 --- /dev/null +++ b/app/src/main/res/layout/item_schedule.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_stop.xml b/app/src/main/res/layout/item_stop.xml new file mode 100644 index 0000000..ed8cf91 --- /dev/null +++ b/app/src/main/res/layout/item_stop.xml @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_tao.xml b/app/src/main/res/layout/widget_tao.xml new file mode 100644 index 0000000..a12332e --- /dev/null +++ b/app/src/main/res/layout/widget_tao.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/window_stop_info.xml b/app/src/main/res/layout/window_stop_info.xml new file mode 100644 index 0000000..67f2a87 --- /dev/null +++ b/app/src/main/res/layout/window_stop_info.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ecf790 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..318c478 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..5a1d9e7 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..19e9409 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..fcc6593 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..5221cc4 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml new file mode 100644 index 0000000..7365fcc --- /dev/null +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -0,0 +1,15 @@ + + + Boite à outils TAO + Direction : %s + Il manque une valeur + Ligne + Direction + Arret + Sauvegarder + Le logo de la ligne + Un widget pour savoir le temps restant avant un départ + Erreur + Quelque chose s\'est mal passé + %smn + \ No newline at end of file diff --git a/app/src/main/res/values-night/bools.xml b/app/src/main/res/values-night/bools.xml new file mode 100644 index 0000000..9cf91a4 --- /dev/null +++ b/app/src/main/res/values-night/bools.xml @@ -0,0 +1,5 @@ + + + false + true + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..bf21992 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,6 @@ + + + #99000000 + #000 + #fff + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..5afeec7 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..97531a2 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 0000000..7040b9b --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -0,0 +1,5 @@ + + + true + false + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..0b4d4e3 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,18 @@ + + + #4baed5 + #16527c + + #26285b + #7c86b2 + + #e0217e + #a10058 + + #FF000000 + #FFFFFFFF + + #99ffffff + #fff + #000 + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..e2ee615 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,16 @@ + + + + + + + 2dp + 10dp + + + 200dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..370e989 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,25 @@ + + TAO ToolBox + + + Direction : %s + + + Missing value + Line + Direction + Stop + Save + + + + The logo of the line + A widget to know the remaining time + %02d:%02d + Error + Something went wrong + + %smn + + %s : %s + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..be79322 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_descriptor.xml b/app/src/main/res/xml/backup_descriptor.xml new file mode 100644 index 0000000..93222bc --- /dev/null +++ b/app/src/main/res/xml/backup_descriptor.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/xml/widget_tao_info.xml b/app/src/main/res/xml/widget_tao_info.xml new file mode 100644 index 0000000..0af3988 --- /dev/null +++ b/app/src/main/res/xml/widget_tao_info.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/app/src/test/java/fr/oupson/taotoolbox/ExampleUnitTest.kt b/app/src/test/java/fr/oupson/taotoolbox/ExampleUnitTest.kt new file mode 100644 index 0000000..4928ef2 --- /dev/null +++ b/app/src/test/java/fr/oupson/taotoolbox/ExampleUnitTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..8dcfd25 --- /dev/null +++ b/build.gradle @@ -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 +} \ No newline at end of file diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/common/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..a13b163 --- /dev/null +++ b/common/build.gradle @@ -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" +} \ No newline at end of file diff --git a/common/consumer-rules.pro b/common/consumer-rules.pro new file mode 100644 index 0000000..a971d58 --- /dev/null +++ b/common/consumer-rules.pro @@ -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(...); +} \ No newline at end of file diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/common/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/common/src/androidTest/java/fr/oupson/common/ExampleInstrumentedTest.kt b/common/src/androidTest/java/fr/oupson/common/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..336b149 --- /dev/null +++ b/common/src/androidTest/java/fr/oupson/common/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8587731 --- /dev/null +++ b/common/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/common/src/main/java/fr/oupson/common/api/Line.kt b/common/src/main/java/fr/oupson/common/api/Line.kt new file mode 100644 index 0000000..0f4b06c --- /dev/null +++ b/common/src/main/java/fr/oupson/common/api/Line.kt @@ -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 + } + } +} + diff --git a/common/src/main/java/fr/oupson/common/api/Stop.kt b/common/src/main/java/fr/oupson/common/api/Stop.kt new file mode 100644 index 0000000..aef30b0 --- /dev/null +++ b/common/src/main/java/fr/oupson/common/api/Stop.kt @@ -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, + 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 { + 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) + } +} \ No newline at end of file diff --git a/common/src/main/java/fr/oupson/common/api/TaoRestApi.kt b/common/src/main/java/fr/oupson/common/api/TaoRestApi.kt new file mode 100644 index 0000000..75665bb --- /dev/null +++ b/common/src/main/java/fr/oupson/common/api/TaoRestApi.kt @@ -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 = 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") + } +} \ No newline at end of file diff --git a/common/src/main/java/fr/oupson/common/db/TaoDatabaseHelper.kt b/common/src/main/java/fr/oupson/common/db/TaoDatabaseHelper.kt new file mode 100644 index 0000000..1d85c56 --- /dev/null +++ b/common/src/main/java/fr/oupson/common/db/TaoDatabaseHelper.kt @@ -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" + } +} \ No newline at end of file diff --git a/common/src/main/res/font/noto_sans_bold.ttf b/common/src/main/res/font/noto_sans_bold.ttf new file mode 100644 index 0000000..54ad879 Binary files /dev/null and b/common/src/main/res/font/noto_sans_bold.ttf differ diff --git a/common/src/test/java/fr/oupson/common/ExampleUnitTest.kt b/common/src/test/java/fr/oupson/common/ExampleUnitTest.kt new file mode 100644 index 0000000..f1de65f --- /dev/null +++ b/common/src/test/java/fr/oupson/common/ExampleUnitTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..98bed16 --- /dev/null +++ b/gradle.properties @@ -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 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..21b28f4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..2c3fc71 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +include ':wear_tao' +include ':common' +include ':app' +rootProject.name = "Tao Toolbox" \ No newline at end of file diff --git a/wear_tao/.gitignore b/wear_tao/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/wear_tao/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/wear_tao/build.gradle b/wear_tao/build.gradle new file mode 100644 index 0000000..3b420b3 --- /dev/null +++ b/wear_tao/build.gradle @@ -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") +} \ No newline at end of file diff --git a/wear_tao/proguard-rules.pro b/wear_tao/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/wear_tao/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/wear_tao/src/androidTest/java/fr/oupson/wear_tao/ExampleInstrumentedTest.kt b/wear_tao/src/androidTest/java/fr/oupson/wear_tao/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..ac9b0d5 --- /dev/null +++ b/wear_tao/src/androidTest/java/fr/oupson/wear_tao/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/wear_tao/src/main/AndroidManifest.xml b/wear_tao/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9074d0e --- /dev/null +++ b/wear_tao/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wear_tao/src/main/java/fr/oupson/wear_tao/MainActivity.kt b/wear_tao/src/main/java/fr/oupson/wear_tao/MainActivity.kt new file mode 100644 index 0000000..3b34700 --- /dev/null +++ b/wear_tao/src/main/java/fr/oupson/wear_tao/MainActivity.kt @@ -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(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() + } + +} \ No newline at end of file diff --git a/wear_tao/src/main/java/fr/oupson/wear_tao/TaoTile.kt b/wear_tao/src/main/java/fr/oupson/wear_tao/TaoTile.kt new file mode 100644 index 0000000..40e107f --- /dev/null +++ b/wear_tao/src/main/java/fr/oupson/wear_tao/TaoTile.kt @@ -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 = + 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() + } +} \ No newline at end of file diff --git a/wear_tao/src/main/res/drawable/ic_launcher_background.xml b/wear_tao/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/wear_tao/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wear_tao/src/main/res/drawable/ic_launcher_foreground.xml b/wear_tao/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/wear_tao/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/wear_tao/src/main/res/layout/activity_main.xml b/wear_tao/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..2ffc8db --- /dev/null +++ b/wear_tao/src/main/res/layout/activity_main.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/wear_tao/src/main/res/mipmap-anydpi b/wear_tao/src/main/res/mipmap-anydpi new file mode 100644 index 0000000..e69de29 diff --git a/wear_tao/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/wear_tao/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/wear_tao/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/wear_tao/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/wear_tao/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/wear_tao/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/wear_tao/src/main/res/mipmap-hdpi/ic_launcher.png b/wear_tao/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/wear_tao/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/wear_tao/src/main/res/mipmap-hdpi/ic_launcher_round.png b/wear_tao/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/wear_tao/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/wear_tao/src/main/res/mipmap-mdpi/ic_launcher.png b/wear_tao/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/wear_tao/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/wear_tao/src/main/res/mipmap-mdpi/ic_launcher_round.png b/wear_tao/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/wear_tao/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/wear_tao/src/main/res/mipmap-xhdpi/ic_launcher.png b/wear_tao/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/wear_tao/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/wear_tao/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/wear_tao/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/wear_tao/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/wear_tao/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wear_tao/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/wear_tao/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/wear_tao/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/wear_tao/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/wear_tao/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/wear_tao/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/wear_tao/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/wear_tao/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/wear_tao/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/wear_tao/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/wear_tao/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/wear_tao/src/main/res/values-night/themes.xml b/wear_tao/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..fd43956 --- /dev/null +++ b/wear_tao/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/wear_tao/src/main/res/values/colors.xml b/wear_tao/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/wear_tao/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/wear_tao/src/main/res/values/strings.xml b/wear_tao/src/main/res/values/strings.xml new file mode 100644 index 0000000..d80533a --- /dev/null +++ b/wear_tao/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + WearTAO + Sample tiles + \ No newline at end of file diff --git a/wear_tao/src/main/res/values/themes.xml b/wear_tao/src/main/res/values/themes.xml new file mode 100644 index 0000000..c24fc99 --- /dev/null +++ b/wear_tao/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/wear_tao/src/test/java/fr/oupson/wear_tao/ExampleUnitTest.kt b/wear_tao/src/test/java/fr/oupson/wear_tao/ExampleUnitTest.kt new file mode 100644 index 0000000..719f5a5 --- /dev/null +++ b/wear_tao/src/test/java/fr/oupson/wear_tao/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package fr.oupson.wear_tao + +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) + } +} \ No newline at end of file