Bimba.git

commit 9ed812dd6dda7186b82524cbc9582e2e4edbf6c4

Author: Adam Pioterek <adam.pioterek@protonmail.ch>

Downloading timetable

 app/build.gradle | 3 
 app/src/main/AndroidManifest.xml | 7 
 app/src/main/java/ml/adamsprogs/bimba/MainActivity.kt | 32 +
 app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt | 27 +
 app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt | 95 ++++++
 app/src/main/res/layout/activity_main.xml | 3 


diff --git a/app/build.gradle b/app/build.gradle
index 2e2714592b81c6dd2bcf67f13bd73d83b1068430..f71f944ad8a132439c3c3a927be939c074d7330c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -34,6 +34,7 @@     testImplementation 'junit:junit:4.12'
     implementation 'com.android.support.constraint:constraint-layout:1.0.2'
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
     implementation 'com.github.arimorty:floatingsearchview:2.1.1'
+    implementation 'org.tukaani:xz:1.6'
 }
 repositories {
     maven {
@@ -41,3 +42,5 @@         url "https://maven.google.com"
     }
     mavenCentral()
 }
+
+apply plugin: 'kotlin-android-extensions'
\ No newline at end of file




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4e04cbad3e2b067a1dcc24ea26c2b9a77a7dacd8..01a80a30c4361b8dbb1210f086a49e91bc3c630e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,9 @@ 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="ml.adamsprogs.bimba">
 
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+
     <application
         android:allowBackup="true"
         android:icon="@drawable/logo"
@@ -15,6 +18,10 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
+        <service
+            android:name=".TimetableDownloader"
+            android:exported="false"></service>
     </application>
 
 </manifest>
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/MainActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/MainActivity.kt
index 5972bef099e393a09cc67cde36e9118006ecb40a..42cd26a4bc007e08e93d6228a3234fe667b0acd9 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/MainActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/MainActivity.kt
@@ -1,10 +1,10 @@
 package ml.adamsprogs.bimba
 
 import android.content.Context
-import android.os.Build
-import android.os.Bundle
-import android.os.Parcel
-import android.os.Parcelable
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.*
+import android.support.design.widget.Snackbar
 import android.support.v7.app.AppCompatActivity
 import android.support.v7.app.AppCompatDelegate
 import android.text.Html
@@ -12,8 +12,9 @@ import android.widget.Toast
 import com.arlib.floatingsearchview.FloatingSearchView
 import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
 
-
-class MainActivity : AppCompatActivity() {
+class MainActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener {
+    lateinit var listener: MessageReceiver.OnTimetableDownloadListener
+    lateinit var receiver: MessageReceiver
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -21,6 +22,14 @@         setContentView(R.layout.activity_main)
         AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO)
 
         val context = this as Context
+        listener = this
+
+        val filter = IntentFilter("ml.adamsprogs.bimba.timetableDownloaded")
+        filter.addCategory(Intent.CATEGORY_DEFAULT)
+        receiver = MessageReceiver()
+        registerReceiver(receiver, filter)
+        receiver.addOnTimetableDownloadListener(listener)
+        startService(Intent(context, TimetableDownloader::class.java))
 
         val stops = listOf(Suggestion("Kołłątaja\n610 -> Dębiec"), Suggestion("Dębiecka\n610 -> Górczyn")) //todo get from db
         val searchView = findViewById(R.id.search_view) as FloatingSearchView
@@ -53,6 +62,12 @@
         //todo searchView.attachNavigationDrawerToMenuButton(mDrawerLayout)
     }
 
+    override fun onDestroy() {
+        super.onDestroy()
+        receiver.removeOnTimetableDownloadListener(listener)
+        unregisterReceiver(receiver)
+    }
+
     fun deAccent(str: String): String {
         var result = str.replace('ę', 'e')
         result = result.replace('ó', 'o')
@@ -64,6 +79,11 @@         result = result.replace('ź', 'ź')
         result = result.replace('ć', 'ć')
         result = result.replace('ń', 'n')
         return result
+    }
+
+    override fun onTimetableDownload() {
+        val layout = findViewById(R.id.main_layout)
+        Snackbar.make(layout, "New timetable downloaded", Snackbar.LENGTH_LONG).show()
     }
 
     class Suggestion(text: String) : SearchSuggestion {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4e2c81f4ef4cb432099ea2c7d7733b1ccc833583
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt
@@ -0,0 +1,27 @@
+package ml.adamsprogs.bimba
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+class MessageReceiver: BroadcastReceiver() {
+    val onTimetableDownloadListeners: HashSet<OnTimetableDownloadListener> = HashSet()
+
+    override fun onReceive(context: Context?, intent: Intent?) {
+        for (listener in onTimetableDownloadListeners) {
+            listener.onTimetableDownload()
+        }
+    }
+
+    fun addOnTimetableDownloadListener(listener: OnTimetableDownloadListener) {
+        onTimetableDownloadListeners.add(listener)
+    }
+
+    fun removeOnTimetableDownloadListener(listener: OnTimetableDownloadListener) {
+        onTimetableDownloadListeners.remove(listener)
+    }
+
+    interface OnTimetableDownloadListener {
+        fun onTimetableDownload()
+    }
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e023dcd84f89733bb532d4da92ce111065d01b8e
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
@@ -0,0 +1,95 @@
+package ml.adamsprogs.bimba
+
+import android.app.IntentService
+import android.content.Context
+import android.content.Intent
+import java.net.HttpURLConnection
+import java.net.URL
+import org.tukaani.xz.XZInputStream
+import java.io.*
+import java.security.MessageDigest
+import kotlin.experimental.and
+import android.net.ConnectivityManager
+import android.util.Log
+
+class TimetableDownloader : IntentService("TimetableDownloader") {
+    override fun onHandleIntent(intent: Intent?) {
+        if (intent != null) {
+            val prefs = this.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)!!
+            if (!isNetworkAvailable())
+                return
+            val metadataUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/timetable.db.meta")
+            var httpCon = metadataUrl.openConnection() as HttpURLConnection
+            if (httpCon.responseCode != HttpURLConnection.HTTP_OK)
+                throw Exception("Failed to connect")
+            Log.i("Downloader", "Got metadata")
+            val reader = BufferedReader(InputStreamReader(httpCon.inputStream))
+            val lastModified = reader.readLine()
+            val checksum = reader.readLine()
+            val currentLastModified = prefs.getString("timetableLastModified", "1979-10-12T00:00")
+            if (lastModified <= currentLastModified)
+                return
+            Log.i("Downloader", "timetable is newer ($lastModified > $currentLastModified)")
+
+            val xzDbUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/timetable.db.xz")
+            httpCon = xzDbUrl.openConnection() as HttpURLConnection
+            if (httpCon.responseCode != HttpURLConnection.HTTP_OK)
+                throw Exception("Failed to connect")
+            Log.i("Downloader", "connected to db")
+            val xzIn = XZInputStream(httpCon.inputStream)
+            val file = File(this.filesDir, "new_timetable.db")
+            if (copyInputStreamToFile(xzIn, file, checksum)) {
+                Log.i("Downloader", "downloaded")
+                val oldFile = File(this.filesDir, "timetable.db")
+                oldFile.delete()
+                file.renameTo(File("timetable.db"))
+                val prefsEditor = prefs.edit()
+                prefsEditor.putString("timetableLastModified", lastModified)
+                prefsEditor.apply()
+                val broadcastIntent = Intent()
+                broadcastIntent.action = "ml.adamsprogs.bimba.timetableDownloaded"
+                broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT)
+                sendBroadcast(broadcastIntent)
+            } else {
+                Log.i("Downloader", "downloaded but is wrong")
+            }
+        }
+    }
+
+    private fun copyInputStreamToFile(ins: InputStream, file: File, checksum: String): Boolean {
+        val md = MessageDigest.getInstance("SHA-512")
+        var hex = ""
+        try {
+            val out = FileOutputStream(file)
+            val buf = ByteArray(1024)
+            var lenSum = 0.0f
+            var len = 42
+            while (len > 0) {
+                len = ins.read(buf)
+                if (len <= 0)
+                    break
+                md.update(buf, 0, len)
+                out.write(buf, 0, len)
+                lenSum += len.toFloat() / 1024.0f
+                Log.i("Downloader", "downloading $len B: $lenSum KiB")
+            }
+            out.close()
+        } catch (e: Exception) {
+            e.printStackTrace()
+        } finally {
+            ins.close()
+            val digest = md.digest()
+            for (i in 0..digest.size - 1) {
+                hex += Integer.toString((digest[i] and 0xff.toByte()) + 0x100, 16).padStart(3, '0').substring(1)
+            }
+            Log.i("Downloader", "checksum is $checksum, and hex is $hex")
+            return checksum == hex //todo verify signature
+        }
+    }
+
+    private fun isNetworkAvailable(): Boolean {
+        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+        val activeNetworkInfo = connectivityManager.activeNetworkInfo
+        return activeNetworkInfo != null && activeNetworkInfo.isConnected
+    }
+}




diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 2118179d144cfce2aee6e45af628f83bf862a2c5..739666dc2ed00f729c50555a9a7681dd3ee1cce6 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -4,7 +4,8 @@     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context="ml.adamsprogs.bimba.MainActivity">
+    tools:context="ml.adamsprogs.bimba.MainActivity"
+    android:id="@+id/main_layout">
     <com.arlib.floatingsearchview.FloatingSearchView
         android:id="@+id/search_view"
         android:layout_width="match_parent"