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"