PluginManager.kt 5.82 KB
Newer Older
1
/*
2
 * Copyright (c) 2019 Bevilacqua Joey
3 4 5 6 7 8
 *
 * Licensed under the GNU GPLv3 license
 *
 * The text of the license can be found in the LICENSE file
 * or at https://www.gnu.org/licenses/gpl.txt
 */
9
package it.diab.core.util
10

Joey's avatar
Joey committed
11
import android.annotation.SuppressLint
12
import android.content.Context
13
import android.content.Intent
Joey's avatar
Joey committed
14
import android.os.Build
15
import android.preference.PreferenceManager
16
import androidx.annotation.WorkerThread
Joey's avatar
Joey committed
17
import androidx.core.content.ContextCompat
18
import it.diab.core.data.entities.Glucose
19
import it.diab.core.data.entities.TimeFrame
20
import it.diab.core.util.extensions.set
21 22 23 24 25
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
26
import kotlinx.coroutines.delay
27 28 29 30 31 32 33 34 35 36 37 38 39 40
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.InputStreamReader
import java.util.regex.Pattern
import java.util.stream.Collectors
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream

Joey's avatar
Joey committed
41
class PluginManager(context: Context) {
42 43 44 45 46
    private val job = Job()
    private val scope = CoroutineScope(IO + job)

    private val preferences = PreferenceManager.getDefaultSharedPreferences(context)

Joey's avatar
Joey committed
47
    private val pluginDir = File(ContextCompat.getDataDir(context), "plugin")
48 49 50
    private val emptyStream by lazy {
        ByteArrayInputStream("{\n}".toByteArray(Charsets.UTF_8))
    }
51 52 53 54 55

    fun isInstalled() = pluginDir.exists() && pluginDir.list().isNotEmpty()

    fun install(iStream: InputStream) {
        scope.launch {
56
            val pattern = Pattern.compile("^estimator_[0-6].json")
57 58
            var wasValid = false

59 60 61 62 63 64 65 66 67 68 69
            ZipInputStream(iStream).use { zipStream ->
                var entry: ZipEntry? = zipStream.nextEntry

                while (entry != null) {
                    // Filter out unneeded files
                    val matcher = pattern.matcher(entry.name)
                    if (!matcher.find()) {
                        entry = zipStream.nextEntry
                        continue
                    }

Joey's avatar
Joey committed
70 71
                    extractZipEntry(zipStream, entry)
                    wasValid = true
72
                    entry = zipStream.nextEntry
73 74
                }

75
                zipStream.closeEntry()
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
            }

            if (wasValid) {
                preferences[LAST_UPDATE] = System.currentTimeMillis()
            }
        }
    }

    fun uninstall() {
        scope.launch {
            pluginDir.deleteRecursively()
            preferences[LAST_UPDATE] = 0L
        }
    }

91 92 93 94 95 96
    fun getPickerIntent(): Intent {
        return Intent(Intent.ACTION_GET_CONTENT).apply {
            type = "application/zip"
        }
    }

97 98 99 100 101 102 103 104 105 106 107 108
    suspend fun fetchSuggestion(glucose: Glucose, onExecuted: (Float) -> Unit) {
        delay(1000)

        val value = glucose.value / 10 * 10
        if (value <= LOWEST_SUGGESTION) {
            GlobalScope.launch(Dispatchers.Main) { onExecuted(TOO_LOW) }
            return
        }
        if (value >= HIGHEST_SUGGESTION) {
            GlobalScope.launch(Dispatchers.Main) { onExecuted(TOO_HIGH) }
            return
        }
109

110 111
        val iStream = getStreamFor(glucose.timeFrame)
        val map = parseInputStream(iStream)
112

113 114 115 116
        val result = map[value] ?: PARSE_ERROR

        GlobalScope.launch(Dispatchers.Main) {
            onExecuted(if (result == PARSE_ERROR) PARSE_ERROR else result + glucose.eatLevel - 1)
117 118 119
        }
    }

Joey's avatar
Joey committed
120
    @SuppressLint("UseSparseArrays")
121 122 123
    @WorkerThread
    private fun parseInputStream(iStream: InputStream): HashMap<Int, Float> {
        val map = HashMap<Int, Float>()
124 125
        iStream.use {
            val content = BufferedReader(InputStreamReader(iStream)).readLines()
126

127 128
            val json = JSONObject(content)
            val iterator = json.keys()
129

130 131 132 133 134 135 136
            while (iterator.hasNext()) {
                val key = iterator.next()
                val value = json[key]

                if (value is Double) {
                    map[key.toInt()] = value.toFloat()
                }
Joey's avatar
Joey committed
137
            }
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
        }

        return map
    }

    @WorkerThread
    private fun getStreamFor(timeFrame: TimeFrame): InputStream {
        val file = File(pluginDir, MODEL_NAME.format(timeFrame.ordinal))

        if (!file.exists() || !file.canRead()) {
            return emptyStream
        }

        return FileInputStream(file)
    }

Joey's avatar
Joey committed
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
    @WorkerThread
    private fun extractZipEntry(zipStream: ZipInputStream, entry: ZipEntry) {
        if (!pluginDir.exists()) {
            pluginDir.mkdir()
        }

        val buffer = ByteArray(1024)
        val extractedFile = File(pluginDir, entry.name)
        extractedFile.createNewFile()
        FileOutputStream(extractedFile).use { oStream ->
            var len = zipStream.read(buffer)
            while (len > 0) {
                oStream.write(buffer, 0, len)
                len = zipStream.read(buffer)
            }
        }
    }

Joey's avatar
Joey committed
172 173
    private fun BufferedReader.readLines(): String {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
174
            return lines().collect(Collectors.joining("\n"))
Joey's avatar
Joey committed
175 176 177 178 179
        }

        val builder = StringBuilder()
        var line: String? = readLine()
        while (line != null) {
180
            builder.append(line).append('\n')
Joey's avatar
Joey committed
181 182 183 184 185
            line = readLine()
        }
        return builder.toString()
    }

186 187 188 189 190 191 192 193 194 195 196 197 198 199
    companion object {
        private const val MODEL_NAME = "estimator_%1\$d.json"

        private const val LOWEST_SUGGESTION = 40
        private const val HIGHEST_SUGGESTION = 420

        const val LAST_UPDATE = "pref_plugin_last_update"

        const val TOO_LOW = -1f
        const val TOO_HIGH = -2f
        const val PARSE_ERROR = -3f
        const val NO_MODEL = -4f
    }
}