package jp.juggler.subwaytooter import android.annotation.SuppressLint import android.content.ContentValues import android.content.Context import android.content.SharedPreferences import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.provider.BaseColumns import android.util.JsonReader import android.util.JsonToken import android.util.JsonWriter import jp.juggler.subwaytooter.table.* import org.json.JSONException import org.json.JSONObject import java.io.IOException import java.util.ArrayList import java.util.HashMap import java.util.Locale import jp.juggler.subwaytooter.util.LogCategory import jp.juggler.subwaytooter.util.parseLong object AppDataExporter { internal val log = LogCategory("AppDataExporter") private const val MAGIC_NAN = - 76287755398823900.0 private const val KEY_PREF = "pref" private const val KEY_ACCOUNT = "account" private const val KEY_COLUMN = "column" private const val KEY_ACCT_COLOR = "acct_color" private const val KEY_MUTED_APP = "muted_app" private const val KEY_MUTED_WORD = "muted_word" private const val KEY_FAV_MUTE = "fav_mute" private const val KEY_CLIENT_INFO = "client_info2" private const val KEY_HIGHLIGHT_WORD = "highlight_word" @Throws(IOException::class, JSONException::class) private fun writeJSONObject(writer : JsonWriter, src : JSONObject) { writer.beginObject() val it = src.keys() while(it.hasNext()) { val k = it.next() if(src.isNull(k)) { writer.name(k) writer.nullValue() } else { val o = src.get(k) when(o) { is String -> { writer.name(k) writer.value(o) } is Boolean -> { writer.name(k) writer.value(o) } is Number -> { writer.name(k) writer.value(o) } else -> throw RuntimeException( String.format( Locale.JAPAN, "bad data type: JSONObject key =%s", k ) ) } } } writer.endObject() } @Throws(IOException::class, JSONException::class) private fun readJsonObject(reader : JsonReader) : JSONObject { val dst = JSONObject() reader.beginObject() while(reader.hasNext()) { val name = reader.nextName() val token = reader.peek() when(token) { JsonToken.NULL -> reader.nextNull() JsonToken.STRING -> dst.put(name, reader.nextString()) JsonToken.BOOLEAN -> dst.put(name, reader.nextBoolean()) JsonToken.NUMBER -> dst.put(name, reader.nextDouble()) else -> throw RuntimeException( String.format( Locale.JAPAN, "bad data type: %s key =%s", token, name ) ) } } reader.endObject() return dst } @Throws(IOException::class) private fun writeFromTable(writer : JsonWriter, json_key : String, table : String) { writer.name(json_key) writer.beginArray() App1.database.query(table, null, null, null, null, null, null)?.use { cursor -> val names = ArrayList() val column_count = cursor.columnCount for(i in 0 until column_count) { names.add(cursor.getColumnName(i)) } while(cursor.moveToNext()) { writer.beginObject() for(i in 0 until column_count) { when(cursor.getType(i)) { Cursor.FIELD_TYPE_NULL -> { writer.name(names[i]) writer.nullValue() } Cursor.FIELD_TYPE_INTEGER -> { writer.name(names[i]) writer.value(cursor.getLong(i)) } Cursor.FIELD_TYPE_STRING -> { writer.name(names[i]) writer.value(cursor.getString(i)) } Cursor.FIELD_TYPE_FLOAT -> { val d = cursor.getDouble(i) if(d.isNaN() || d.isInfinite()) { log.w("column %s is nan or infinite value.", names[i]) } else { writer.name(names[i]) writer.value(d) } } Cursor.FIELD_TYPE_BLOB -> log.w("column %s is blob.", names[i]) } } writer.endObject() } } writer.endArray() } @Throws(IOException::class) private fun importTable(reader : JsonReader, table : String, id_map : HashMap?) { val db = App1.database if(table == SavedAccount.table) { SavedAccount.onDBDelete(db) SavedAccount.onDBCreate(db) } db.execSQL("BEGIN TRANSACTION") try { db.execSQL("delete from " + table) val cv = ContentValues() reader.beginArray() while(reader.hasNext()) { var old_id = - 1L cv.clear() reader.beginObject() while(reader.hasNext()) { val name = reader.nextName() if(BaseColumns._ID == name) { old_id = reader.nextLong() continue } if(SavedAccount.table == table) { // 一時的に存在したが現在のDBスキーマにはない項目は読み飛ばす if("nickname" == name || "color" == name) { reader.skipValue() continue } // リアルタイム通知に関連する項目は読み飛ばす if(SavedAccount.COL_NOTIFICATION_TAG == name || SavedAccount.COL_REGISTER_KEY == name || SavedAccount.COL_REGISTER_TIME == name) { reader.skipValue() continue } } val token = reader.peek() when(token) { JsonToken.NULL -> { reader.skipValue() cv.putNull(name) } JsonToken.BOOLEAN -> cv.put(name, if(reader.nextBoolean()) 1 else 0) JsonToken.NUMBER -> cv.put(name, reader.nextLong()) JsonToken.STRING -> cv.put(name, reader.nextString()) else -> reader.skipValue() } // 無視する } reader.endObject() val new_id = db.insertWithOnConflict(table, null, cv, SQLiteDatabase.CONFLICT_REPLACE) if(new_id == - 1L) { throw RuntimeException("importTable: invalid row_id") } id_map?.put(old_id, new_id) } reader.endArray() db.execSQL("COMMIT TRANSACTION") } catch(ex : Throwable) { log.trace(ex) log.e(ex, "importTable failed.") try { db.execSQL("ROLLBACK TRANSACTION") } catch(ignored : Throwable) { } throw ex } } @Throws(IOException::class) private fun writePref(writer : JsonWriter, pref : SharedPreferences) { writer.beginObject() for((k, v) in pref.all) { writer.name(k) when(v) { null -> writer.nullValue() is String -> writer.value(v) is Boolean -> writer.value(v) is Number -> when { (v is Double && v.isNaN()) -> writer.value(MAGIC_NAN) (v is Float && v.isNaN()) -> writer.value(MAGIC_NAN) else -> writer.value(v) } else -> throw RuntimeException( String.format( Locale.JAPAN, "writePref. bad data type: Preference key =%s", k ) ) } } writer.endObject() } @Throws(IOException::class) private fun importPref(reader : JsonReader, pref : SharedPreferences) { val e = pref.edit() reader.beginObject() while(reader.hasNext()) { val k = reader.nextName() ?: throw RuntimeException("importPref: name is null") val token = reader.peek() if(token == JsonToken.NULL) { reader.nextNull() e.remove(k) continue } val prefItem = Pref.map[k] when(prefItem) { is Pref.BooleanPref -> e.putBoolean(k, reader.nextBoolean()) is Pref.IntPref -> e.putInt(k, reader.nextInt()) is Pref.LongPref -> e.putLong(k, reader.nextLong()) is Pref.StringPref -> if(prefItem.skipImport) { reader.skipValue() e.remove(k) } else { e.putString(k, reader.nextString()) } is Pref.FloatPref -> { val dv = reader.nextDouble() e.putFloat(k, if(dv <= MAGIC_NAN) Float.NaN else dv.toFloat()) } else -> { // ignore or force reset reader.skipValue() e.remove(k) } } } reader.endObject() e.apply() } @Throws(IOException::class, JSONException::class) private fun writeColumn(app_state : AppState, writer : JsonWriter) { writer.beginArray() for(column in app_state.column_list) { val dst = JSONObject() column.encodeJSON(dst, 0) writeJSONObject(writer, dst) } writer.endArray() } @Throws(IOException::class, JSONException::class) private fun readColumn( app_state : AppState, reader : JsonReader, id_map : HashMap ) : ArrayList { val result = ArrayList() reader.beginArray() while(reader.hasNext()) { val item = readJsonObject(reader) val old_id = item.parseLong(Column.KEY_ACCOUNT_ROW_ID) ?: - 1L if(old_id == - 1L) { // 検索カラムは NAアカウントと紐ついている。変換の必要はない } else { val new_id = id_map[old_id] ?: throw RuntimeException("readColumn: can't convert account id") item.put(Column.KEY_ACCOUNT_ROW_ID, new_id) } try { result.add(Column(app_state, item)) } catch(ex : Throwable) { log.trace(ex) log.e(ex, "column load failed.") throw ex } } reader.endArray() return result } @Throws(IOException::class, JSONException::class) fun encodeAppData(context : Context, writer : JsonWriter) { writer.setIndent(" ") writer.beginObject() val app_state = App1.getAppState(context) ////////////////////////////////////// run { writer.name(KEY_PREF) writePref(writer, app_state.pref) } ////////////////////////////////////// writeFromTable(writer, KEY_ACCOUNT, SavedAccount.table) writeFromTable(writer, KEY_ACCT_COLOR, AcctColor.table) writeFromTable(writer, KEY_MUTED_APP, MutedApp.table) writeFromTable(writer, KEY_MUTED_WORD, MutedWord.table) writeFromTable(writer, KEY_FAV_MUTE, FavMute.table) writeFromTable(writer, KEY_CLIENT_INFO, ClientInfo.table) writeFromTable(writer, KEY_HIGHLIGHT_WORD, HighlightWord.table) ////////////////////////////////////// run { writer.name(KEY_COLUMN) writeColumn(app_state, writer) } writer.endObject() } @SuppressLint("UseSparseArrays") @Throws(IOException::class, JSONException::class) internal fun decodeAppData(context : Context, reader : JsonReader) : ArrayList { var result : ArrayList? = null val app_state = App1.getAppState(context) reader.beginObject() val account_id_map = HashMap() while(reader.hasNext()) { val name = reader.nextName() when(name) { KEY_PREF -> importPref(reader, app_state.pref) KEY_ACCOUNT -> importTable(reader, SavedAccount.table, account_id_map) KEY_ACCT_COLOR -> { importTable(reader, AcctColor.table, null) AcctColor.clearMemoryCache() } KEY_MUTED_APP -> importTable(reader, MutedApp.table, null) KEY_MUTED_WORD -> importTable(reader, MutedWord.table, null) KEY_FAV_MUTE -> importTable(reader, FavMute.table, null) KEY_HIGHLIGHT_WORD -> importTable(reader, HighlightWord.table, null) KEY_CLIENT_INFO -> importTable(reader, ClientInfo.table, null) KEY_COLUMN -> result = readColumn(app_state, reader, account_id_map) } } run { val old_id = Pref.lpTabletTootDefaultAccount(app_state.pref) if(old_id != - 1L) { val new_id = account_id_map[old_id] app_state.pref.edit().put(Pref.lpTabletTootDefaultAccount, new_id ?: - 1L).apply() } } if(result == null) { throw RuntimeException("import data does not includes column list!") } return result } }