diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 8068c52b8..629202ee6 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -13,7 +13,7 @@ android { applicationId "org.joinmastodon.android" minSdk 23 targetSdk 34 - versionCode 112 + versionCode 114 versionName "2.6.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java index 3a027e7e2..ed1e46cf2 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java @@ -27,10 +27,8 @@ import org.joinmastodon.android.model.Status; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Comparator; import java.util.EnumSet; @@ -43,7 +41,7 @@ import me.grishka.appkit.utils.WorkerThread; public class CacheController{ private static final String TAG="CacheController"; - private static final int DB_VERSION=3; + private static final int DB_VERSION=4; public static final WorkerThread databaseThread=new WorkerThread("databaseThread"); public static final Handler uiHandler=new Handler(Looper.getMainLooper()); @@ -320,7 +318,7 @@ public class CacheController{ lists=result; if(callback!=null) callback.onSuccess(result); - writeListsToFile(); + writeLists(); } @Override @@ -332,26 +330,22 @@ public class CacheController{ .exec(accountID); } - private List loadListsFromFile(){ - File file=getListsFile(); - if(!file.exists()) - return null; - try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){ - return MastodonAPIController.gson.fromJson(in, new TypeToken>(){}.getType()); - }catch(Exception x){ - Log.w(TAG, "failed to read lists from cache file", x); - return null; + private List loadLists(){ + SQLiteDatabase db=getOrOpenDatabase(); + try(Cursor cursor=db.query("misc", new String[]{"value"}, "`key`=?", new String[]{"lists"}, null, null, null)){ + if(!cursor.moveToFirst()) + return null; + return MastodonAPIController.gson.fromJson(cursor.getString(0), new TypeToken>(){}.getType()); } } - private void writeListsToFile(){ - databaseThread.postRunnable(()->{ - try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){ - MastodonAPIController.gson.toJson(lists, out); - }catch(IOException x){ - Log.w(TAG, "failed to write lists to cache file", x); - } - }, 0); + private void writeLists(){ + runOnDbThread(db->{ + ContentValues values=new ContentValues(); + values.put("key", "lists"); + values.put("value", MastodonAPIController.gson.toJson(lists)); + db.insertWithOnConflict("misc", null, values, SQLiteDatabase.CONFLICT_REPLACE); + }); } public void getLists(Callback> callback){ @@ -361,7 +355,7 @@ public class CacheController{ return; } databaseThread.postRunnable(()->{ - List lists=loadListsFromFile(); + List lists=loadLists(); if(lists!=null){ this.lists=lists; if(callback!=null) @@ -372,23 +366,19 @@ public class CacheController{ }, 0); } - public File getListsFile(){ - return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json"); - } - public void addList(FollowList list){ if(lists==null) return; lists.add(list); lists.sort(Comparator.comparing(l->l.title)); - writeListsToFile(); + writeLists(); } public void deleteList(String id){ if(lists==null) return; lists.removeIf(l->l.id.equals(id)); - writeListsToFile(); + writeLists(); } public void updateList(FollowList list){ @@ -398,7 +388,7 @@ public class CacheController{ if(lists.get(i).id.equals(list.id)){ lists.set(i, list); lists.sort(Comparator.comparing(l->l.title)); - writeListsToFile(); + writeLists(); break; } } @@ -436,6 +426,7 @@ public class CacheController{ `time` INTEGER NOT NULL )"""); createRecentSearchesTable(db); + createMiscTable(db); } @Override @@ -446,6 +437,9 @@ public class CacheController{ if(oldVersion<3){ addTimeColumns(db); } + if(oldVersion<4){ + createMiscTable(db); + } } private void createRecentSearchesTable(SQLiteDatabase db){ @@ -465,5 +459,13 @@ public class CacheController{ db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0"); db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0"); } + + private void createMiscTable(SQLiteDatabase db){ + db.execSQL(""" + CREATE TABLE `misc` ( + `key` TEXT NOT NULL PRIMARY KEY, + `value` TEXT + )"""); + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/PushSubscriptionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/PushSubscriptionManager.java index db6aecfea..6a31d9841 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/PushSubscriptionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/PushSubscriptionManager.java @@ -146,7 +146,7 @@ public class PushSubscriptionManager{ session.pushPublicKey=Base64.encodeToString(publicKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); session.pushAuthKey=encodedAuthKey=Base64.encodeToString(authKey, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); session.pushAccountID=pushAccountID=Base64.encodeToString(randomAccountID, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); - AccountSessionManager.getInstance().writeAccountsFile(); + AccountSessionManager.getInstance().writeAccountPushSettings(accountID); }catch(NoSuchAlgorithmException|InvalidAlgorithmParameterException e){ Log.e(TAG, "registerAccountForPush: error generating encryption key", e); return; @@ -165,7 +165,7 @@ public class PushSubscriptionManager{ if(session==null) return; session.pushSubscription=result; - AccountSessionManager.getInstance().writeAccountsFile(); + AccountSessionManager.getInstance().writeAccountPushSettings(accountID); Log.d(TAG, "Successfully registered "+accountID+" for push notifications"); }); } @@ -191,7 +191,7 @@ public class PushSubscriptionManager{ result.policy=subscription.policy; session.pushSubscription=result; session.needUpdatePushSettings=false; - AccountSessionManager.getInstance().writeAccountsFile(); + AccountSessionManager.getInstance().writeAccountPushSettings(accountID); } @Override @@ -204,7 +204,7 @@ public class PushSubscriptionManager{ return; session.needUpdatePushSettings=true; session.pushSubscription=subscription; - AccountSessionManager.getInstance().writeAccountsFile(); + AccountSessionManager.getInstance().writeAccountPushSettings(accountID); } } }) diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index 1276bf81e..396100d8a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java @@ -1,11 +1,16 @@ package org.joinmastodon.android.api.session; import android.app.Activity; +import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.text.TextUtils; import android.util.Log; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; + import org.joinmastodon.android.E; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; @@ -13,6 +18,7 @@ import org.joinmastodon.android.api.CacheController; import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.StatusInteractionController; +import org.joinmastodon.android.api.gson.JsonObjectBuilder; import org.joinmastodon.android.api.requests.accounts.GetPreferences; import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences; import org.joinmastodon.android.api.requests.markers.GetMarkers; @@ -47,6 +53,9 @@ public class AccountSession{ private static final String TAG="AccountSession"; private static final int MIN_DAYS_ACCOUNT_AGE_FOR_DONATIONS=28; + public static final int FLAG_ACTIVATED=1; + public static final int FLAG_NEED_UPDATE_PUSH_SETTINGS=1 << 1; + public Token token; public Account self; public String domain; @@ -70,7 +79,6 @@ public class AccountSession{ private transient SharedPreferences prefs; private transient boolean preferencesNeedSaving; private transient AccountLocalPreferences localPreferences; - private transient List lists; AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){ this.token=token; @@ -84,6 +92,62 @@ public class AccountSession{ AccountSession(){} + AccountSession(ContentValues values){ + domain=values.getAsString("domain"); + self=MastodonAPIController.gson.fromJson(values.getAsString("account_obj"), Account.class); + token=MastodonAPIController.gson.fromJson(values.getAsString("token"), Token.class); + app=MastodonAPIController.gson.fromJson(values.getAsString("application"), Application.class); + infoLastUpdated=values.getAsLong("info_last_updated"); + long flags=values.getAsLong("flags"); + activated=(flags & FLAG_ACTIVATED)==FLAG_ACTIVATED; + needUpdatePushSettings=(flags & FLAG_NEED_UPDATE_PUSH_SETTINGS)==FLAG_NEED_UPDATE_PUSH_SETTINGS; + JsonObject pushKeys=JsonParser.parseString(values.getAsString("push_keys")).getAsJsonObject(); + pushAuthKey=pushKeys.get("auth").getAsString(); + pushPrivateKey=pushKeys.get("private").getAsString(); + pushPublicKey=pushKeys.get("public").getAsString(); + pushSubscription=MastodonAPIController.gson.fromJson(values.getAsString("push_subscription"), PushSubscription.class); + JsonObject legacyFilters=JsonParser.parseString(values.getAsString("legacy_filters")).getAsJsonObject(); + wordFilters=MastodonAPIController.gson.fromJson(legacyFilters.getAsJsonArray("filters"), new TypeToken>(){}.getType()); + filtersLastUpdated=legacyFilters.get("updated").getAsLong(); + pushAccountID=values.getAsString("push_id"); + activationInfo=MastodonAPIController.gson.fromJson(values.getAsString("activation_info"), AccountActivationInfo.class); + preferences=MastodonAPIController.gson.fromJson(values.getAsString("preferences"), Preferences.class); + } + + public void toContentValues(ContentValues values){ + values.put("id", getID()); + values.put("domain", domain.toLowerCase()); + values.put("account_obj", MastodonAPIController.gson.toJson(self)); + values.put("token", MastodonAPIController.gson.toJson(token)); + values.put("application", MastodonAPIController.gson.toJson(app)); + values.put("info_last_updated", infoLastUpdated); + values.put("flags", getFlagsForDatabase()); + values.put("push_keys", new JsonObjectBuilder() + .add("auth", pushAuthKey) + .add("private", pushPrivateKey) + .add("public", pushPublicKey) + .build() + .toString()); + values.put("push_subscription", MastodonAPIController.gson.toJson(pushSubscription)); + values.put("legacy_filters", new JsonObjectBuilder() + .add("filters", MastodonAPIController.gson.toJsonTree(wordFilters)) + .add("updated", filtersLastUpdated) + .build() + .toString()); + values.put("push_id", pushAccountID); + values.put("activation_info", MastodonAPIController.gson.toJson(activationInfo)); + values.put("preferences", MastodonAPIController.gson.toJson(preferences)); + } + + public long getFlagsForDatabase(){ + long flags=0; + if(activated) + flags|=FLAG_ACTIVATED; + if(needUpdatePushSettings) + flags|=FLAG_NEED_UPDATE_PUSH_SETTINGS; + return flags; + } + public String getID(){ return domain+"_"+self.id; } @@ -124,7 +188,7 @@ public class AccountSession{ preferences=result; if(callback!=null) callback.accept(result); - AccountSessionManager.getInstance().writeAccountsFile(); + AccountSessionManager.getInstance().updateAccountPreferences(getID(), result); } @Override @@ -206,7 +270,7 @@ public class AccountSession{ public void onSuccess(Account result){ preferencesNeedSaving=false; self=result; - AccountSessionManager.getInstance().writeAccountsFile(); + AccountSessionManager.getInstance().updateAccountInfo(getID(), self); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index b55e07b61..9f80fbda4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -10,6 +10,7 @@ import android.content.SharedPreferences; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.database.Cursor; +import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; @@ -18,6 +19,11 @@ import android.net.Uri; import android.os.Build; import android.util.Log; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; + import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.E; import org.joinmastodon.android.MainActivity; @@ -27,6 +33,7 @@ import org.joinmastodon.android.api.CacheController; import org.joinmastodon.android.api.DatabaseRunnable; import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.PushSubscriptionManager; +import org.joinmastodon.android.api.gson.JsonObjectBuilder; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.filters.GetLegacyFilters; import org.joinmastodon.android.api.requests.instance.GetCustomEmojis; @@ -39,15 +46,14 @@ import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.LegacyFilter; +import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Token; import org.joinmastodon.android.ui.utils.UiUtils; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; @@ -68,7 +74,7 @@ public class AccountSessionManager{ private static final String TAG="AccountSessionManager"; public static final String SCOPE="read write follow push"; public static final String REDIRECT_URI="mastodon-android-auth://callback"; - private static final int DB_VERSION=1; + private static final int DB_VERSION=2; private static final AccountSessionManager instance=new AccountSessionManager(); @@ -84,6 +90,7 @@ public class AccountSessionManager{ private boolean loadedInstances; private DatabaseHelper db; private final Runnable databaseCloseRunnable=this::closeDatabase; + private final Object databaseLock=new Object(); public static AccountSessionManager getInstance(){ return instance; @@ -91,21 +98,20 @@ public class AccountSessionManager{ private AccountSessionManager(){ prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE); - File file=new File(MastodonApp.context.getFilesDir(), "accounts.json"); - if(!file.exists()) - return; - HashSet domains=new HashSet<>(); - try(FileInputStream in=new FileInputStream(file)){ - SessionsStorageWrapper w=MastodonAPIController.gson.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), SessionsStorageWrapper.class); - for(AccountSession session:w.accounts){ - domains.add(session.domain.toLowerCase()); - sessions.put(session.getID(), session); + runWithDatabase(db->{ + HashSet domains=new HashSet<>(); + try(Cursor cursor=db.query("accounts", null, null, null, null, null, null)){ + ContentValues values=new ContentValues(); + while(cursor.moveToNext()){ + DatabaseUtils.cursorRowToContentValues(cursor, values); + AccountSession session=new AccountSession(values); + domains.add(session.domain.toLowerCase()); + sessions.put(session.getID(), session); + } } - }catch(Exception x){ - Log.e(TAG, "Error loading accounts", x); - } + readInstanceInfo(db, domains); + }); lastActiveAccountID=prefs.getString("lastActiveAccount", null); - readInstanceInfo(domains); maybeUpdateShortcuts(); } @@ -114,7 +120,11 @@ public class AccountSessionManager{ AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo); sessions.put(session.getID(), session); lastActiveAccountID=session.getID(); - writeAccountsFile(); + runOnDbThread(db->{ + ContentValues values=new ContentValues(); + session.toContentValues(values); + db.insertWithOnConflict("accounts", null, values, SQLiteDatabase.CONFLICT_REPLACE); + }); updateInstanceEmojis(instance, instance.uri); if(PushSubscriptionManager.arePushNotificationsAvailable()){ session.getPushSubscriptionManager().registerAccountForPush(null); @@ -122,22 +132,6 @@ public class AccountSessionManager{ maybeUpdateShortcuts(); } - public synchronized void writeAccountsFile(){ - File file=new File(MastodonApp.context.getFilesDir(), "accounts.json"); - try{ - try(FileOutputStream out=new FileOutputStream(file)){ - SessionsStorageWrapper w=new SessionsStorageWrapper(); - w.accounts=new ArrayList<>(sessions.values()); - OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8); - MastodonAPIController.gson.toJson(w, writer); - writer.flush(); - } - }catch(IOException x){ - Log.e(TAG, "Error writing accounts file", x); - } - prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply(); - } - @NonNull public List getLoggedInAccounts(){ return new ArrayList<>(sessions.values()); @@ -167,7 +161,7 @@ public class AccountSessionManager{ if(!sessions.containsKey(lastActiveAccountID)){ // TODO figure out why this happens. It should not be possible. lastActiveAccountID=getLoggedInAccounts().get(0).getID(); - writeAccountsFile(); + prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply(); } return getAccount(lastActiveAccountID); } @@ -186,7 +180,6 @@ public class AccountSessionManager{ public void removeAccount(String id){ AccountSession session=getAccount(id); session.getCacheController().closeDatabase(); - session.getCacheController().getListsFile().delete(); MastodonApp.context.deleteDatabase(id+".db"); MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit(); if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ @@ -206,11 +199,10 @@ public class AccountSessionManager{ lastActiveAccountID=getLoggedInAccounts().get(0).getID(); prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply(); } - writeAccountsFile(); - String domain=session.domain.toLowerCase(); - if(sessions.isEmpty() || !sessions.values().stream().map(s->s.domain.toLowerCase()).collect(Collectors.toSet()).contains(domain)){ - getInstanceInfoFile(domain).delete(); - } + runOnDbThread(db->{ + db.delete("accounts", "`id`=?", new String[]{id}); + db.delete("instances", "`domain` NOT IN (SELECT DISTINCT `domain` FROM `accounts`)", new String[]{}); + }); if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){ NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class); nm.deleteNotificationChannelGroup(id); @@ -302,7 +294,12 @@ public class AccountSessionManager{ public void onSuccess(Account result){ session.self=result; session.infoLastUpdated=System.currentTimeMillis(); - writeAccountsFile(); + runOnDbThread(db->{ + ContentValues values=new ContentValues(); + values.put("account_obj", MastodonAPIController.gson.toJson(result)); + values.put("info_last_updated", session.infoLastUpdated); + db.update("accounts", values, "`id`=?", new String[]{session.getID()}); + }); } @Override @@ -320,7 +317,15 @@ public class AccountSessionManager{ public void onSuccess(List result){ session.wordFilters=result; session.filtersLastUpdated=System.currentTimeMillis(); - writeAccountsFile(); + runOnDbThread(db->{ + ContentValues values=new ContentValues(); + values.put("legacy_filters", new JsonObjectBuilder() + .add("filters", MastodonAPIController.gson.toJsonTree(session.wordFilters)) + .add("updated", session.filtersLastUpdated) + .build() + .toString()); + db.update("accounts", values, "`id`=?", new String[]{session.getID()}); + }); } @Override @@ -353,13 +358,10 @@ public class AccountSessionManager{ .setCallback(new Callback<>(){ @Override public void onSuccess(List result){ - InstanceInfoStorageWrapper emojis=new InstanceInfoStorageWrapper(); - emojis.lastUpdated=System.currentTimeMillis(); - emojis.emojis=result; - emojis.instance=instance; - customEmojis.put(domain, groupCustomEmojis(emojis)); - instancesLastUpdated.put(domain, emojis.lastUpdated); - MastodonAPIController.runInBackground(()->writeInstanceInfoFile(emojis, domain)); + long lastUpdated=System.currentTimeMillis(); + customEmojis.put(domain, groupCustomEmojis(result)); + instancesLastUpdated.put(domain, lastUpdated); + runOnDbThread(db->insertInstanceIntoDatabase(db, domain, instance, result, lastUpdated)); E.post(new EmojiUpdatedEvent(domain)); } @@ -371,30 +373,17 @@ public class AccountSessionManager{ .execNoAuth(domain); } - private File getInstanceInfoFile(String domain){ - return new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json"); - } - - private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){ - try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){ - OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8); - MastodonAPIController.gson.toJson(emojis, writer); - writer.flush(); - }catch(IOException x){ - Log.w(TAG, "Error writing instance info file for "+domain, x); - } - } - - private void readInstanceInfo(Set domains){ - for(String domain:domains){ - try(FileInputStream in=new FileInputStream(getInstanceInfoFile(domain))){ - InputStreamReader reader=new InputStreamReader(in, StandardCharsets.UTF_8); - InstanceInfoStorageWrapper emojis=MastodonAPIController.gson.fromJson(reader, InstanceInfoStorageWrapper.class); + private void readInstanceInfo(SQLiteDatabase db, Set domains){ + try(Cursor cursor=db.query("instances", null, "`domain` IN ("+String.join(", ", Collections.nCopies(domains.size(), "?"))+")", domains.toArray(new String[0]), null, null, null)){ + ContentValues values=new ContentValues(); + while(cursor.moveToNext()){ + DatabaseUtils.cursorRowToContentValues(cursor, values); + String domain=values.getAsString("domain"); + Instance instance=MastodonAPIController.gson.fromJson(values.getAsString("instance_obj"), Instance.class); + List emojis=MastodonAPIController.gson.fromJson(values.getAsString("emojis"), new TypeToken>(){}.getType()); + instances.put(domain, instance); customEmojis.put(domain, groupCustomEmojis(emojis)); - instances.put(domain, emojis.instance); - instancesLastUpdated.put(domain, emojis.lastUpdated); - }catch(Exception x){ - Log.w(TAG, "Error reading instance info file for "+domain, x); + instancesLastUpdated.put(domain, values.getAsLong("last_updated")); } } if(!loadedInstances){ @@ -403,8 +392,8 @@ public class AccountSessionManager{ } } - private List groupCustomEmojis(InstanceInfoStorageWrapper emojis){ - return emojis.emojis.stream() + private List groupCustomEmojis(List emojis){ + return emojis.stream() .filter(e->e.visibleInPicker) .collect(Collectors.groupingBy(e->e.category==null ? "" : e.category)) .entrySet() @@ -427,7 +416,48 @@ public class AccountSessionManager{ AccountSession session=getAccount(id); session.self=account; session.infoLastUpdated=System.currentTimeMillis(); - writeAccountsFile(); + runOnDbThread(db->{ + ContentValues values=new ContentValues(); + values.put("account_obj", MastodonAPIController.gson.toJson(account)); + values.put("info_last_updated", session.infoLastUpdated); + db.update("accounts", values, "`id`=?", new String[]{session.getID()}); + }); + } + + public void updateAccountPreferences(String id, Preferences prefs){ + AccountSession session=getAccount(id); + session.preferences=prefs; + runOnDbThread(db->{ + ContentValues values=new ContentValues(); + values.put("preferences", MastodonAPIController.gson.toJson(prefs)); + db.update("accounts", values, "`id`=?", new String[]{session.getID()}); + }); + } + + public void writeAccountPushSettings(String id){ + AccountSession session=getAccount(id); + runWithDatabase(db->{ // Called from a background thread anyway + ContentValues values=new ContentValues(); + values.put("push_keys", new JsonObjectBuilder() + .add("auth", session.pushAuthKey) + .add("private", session.pushPrivateKey) + .add("public", session.pushPublicKey) + .build() + .toString()); + values.put("push_subscription", MastodonAPIController.gson.toJson(session.pushSubscription)); + values.put("flags", session.getFlagsForDatabase()); + db.update("accounts", values, "`id`=?", new String[]{id}); + }); + } + + public void writeAccountActivationInfo(String id){ + AccountSession session=getAccount(id); + runOnDbThread(db->{ + ContentValues values=new ContentValues(); + values.put("activation_info", MastodonAPIController.gson.toJson(session.activationInfo)); + values.put("flags", session.getFlagsForDatabase()); + db.update("accounts", values, "`id`=?", new String[]{id}); + }); } private void maybeUpdateShortcuts(){ @@ -487,8 +517,24 @@ public class AccountSessionManager{ } private void runOnDbThread(DatabaseRunnable r){ - cancelDelayedClose(); CacheController.databaseThread.postRunnable(()->{ + synchronized(databaseLock){ + cancelDelayedClose(); + try{ + SQLiteDatabase db=getOrOpenDatabase(); + r.run(db); + }catch(SQLiteException|IOException x){ + Log.w(TAG, x); + }finally{ + closeDelayed(); + } + } + }, 0); + } + + private void runWithDatabase(DatabaseRunnable r){ + synchronized(databaseLock){ + cancelDelayedClose(); try{ SQLiteDatabase db=getOrOpenDatabase(); r.run(db); @@ -497,7 +543,7 @@ public class AccountSessionManager{ }finally{ closeDelayed(); } - }, 0); + } } public void runIfDonationCampaignNotDismissed(String id, Runnable action){ @@ -523,14 +569,13 @@ public class AccountSessionManager{ runOnDbThread(db->db.delete("dismissed_donation_campaigns", null, null)); } - private static class SessionsStorageWrapper{ - public List accounts; - } - - private static class InstanceInfoStorageWrapper{ - public Instance instance; - public List emojis; - public long lastUpdated; + private static void insertInstanceIntoDatabase(SQLiteDatabase db, String domain, Instance instance, List emojis, long lastUpdated){ + ContentValues values=new ContentValues(); + values.put("domain", domain); + values.put("instance_obj", MastodonAPIController.gson.toJson(instance)); + values.put("emojis", MastodonAPIController.gson.toJson(emojis)); + values.put("last_updated", lastUpdated); + db.insertWithOnConflict("instances", null, values, SQLiteDatabase.CONFLICT_REPLACE); } private static class DatabaseHelper extends SQLiteOpenHelper{ @@ -545,11 +590,71 @@ public class AccountSessionManager{ `id` text PRIMARY KEY, `dismissed_at` bigint )"""); + createAccountsAndInstancesTables(db); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ + if(oldVersion<2){ + createAccountsAndInstancesTables(db); + File accountsFile=new File(MastodonApp.context.getFilesDir(), "accounts.json"); + if(accountsFile.exists()){ + HashSet domains=new HashSet<>(); + try(FileInputStream in=new FileInputStream(accountsFile)){ + JsonObject jobj=JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject(); + ContentValues values=new ContentValues(); + for(JsonElement jacc:jobj.getAsJsonArray("accounts")){ + AccountSession session=MastodonAPIController.gson.fromJson(jacc, AccountSession.class); + domains.add(session.domain.toLowerCase()); + session.toContentValues(values); + db.insertWithOnConflict("accounts", null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + }catch(Exception x){ + Log.e(TAG, "Error migrating accounts", x); + return; + } + accountsFile.delete(); + for(String domain:domains){ + File file=new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json"); + try(FileInputStream in=new FileInputStream(file)){ + JsonObject jobj=JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject(); + + insertInstanceIntoDatabase(db, domain, MastodonAPIController.gson.fromJson(jobj.get("instance"), Instance.class), + MastodonAPIController.gson.fromJson(jobj.get("emojis"), new TypeToken<>(){}.getType()), jobj.get("last_updated").getAsLong()); + }catch(Exception x){ + Log.w(TAG, "Error reading instance info file for "+domain, x); + } + file.delete(); + } + } + } + } + + private void createAccountsAndInstancesTables(SQLiteDatabase db){ + db.execSQL(""" + CREATE TABLE `accounts` ( + `id` text PRIMARY KEY, + `domain` text, + `account_obj` text, + `token` text, + `application` text, + `info_last_updated` bigint, + `flags` bigint, + `push_keys` text, + `push_subscription` text, + `legacy_filters` text DEFAULT NULL, + `push_id` text, + `activation_info` text, + `preferences` text + )"""); + db.execSQL(""" + CREATE TABLE `instances` ( + `domain` text PRIMARY KEY, + `instance_obj` text, + `emojis` text, + `last_updated` bigint + )"""); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java index c3d36b9f2..5a71fcafc 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java @@ -149,7 +149,7 @@ public class AccountActivationFragment extends ToolbarFragment{ session.activationInfo.lastEmailConfirmationResend=System.currentTimeMillis(); } lastResendTime=session.activationInfo.lastEmailConfirmationResend; - AccountSessionManager.getInstance().writeAccountsFile(); + AccountSessionManager.getInstance().writeAccountActivationInfo(accountID); updateResendTimer(); }