Local database

This commit is contained in:
Grishka 2022-03-11 20:13:11 +03:00
parent 03c0b183cb
commit 04405b028c
6 changed files with 283 additions and 13 deletions

View File

@ -10,7 +10,7 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 31
versionCode 8
versionCode 9
versionName "0.1"
}

View File

@ -0,0 +1,238 @@
package org.joinmastodon.android.api;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.Nullable;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.WorkerThread;
public class CacheController{
private static final String TAG="CacheController";
private static final int DB_VERSION=1;
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
private final String accountID;
private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase;
static{
databaseThread.start();
}
public CacheController(String accountID){
this.accountID=accountID;
}
public void getHomeTimeline(String maxID, int count, Callback<List<Status>> callback){
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query("home_timeline", new String[]{"json"}, maxID==null ? null : "`id`>?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Status> result=new ArrayList<>();
cursor.moveToFirst();
do{
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
status.postprocess();
result.add(status);
}while(cursor.moveToNext());
uiHandler.post(()->callback.onSuccess(result));
return;
}
}catch(IOException x){
Log.w(TAG, "getHomeTimeline: corrupted status object in database", x);
}
new GetHomeTimeline(maxID, null, count)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
callback.onSuccess(result);
putHomeTimeline(result, maxID==null);
}
@Override
public void onError(ErrorResponse error){
callback.onError(error);
}
})
.exec(accountID);
}catch(SQLiteException x){
Log.w(TAG, x);
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage())));
}finally{
closeDelayed();
}
}, 0);
}
private void putHomeTimeline(List<Status> posts, boolean clear){
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
SQLiteDatabase db=getOrOpenDatabase();
if(clear)
db.delete("home_timeline", null, null);
ContentValues values=new ContentValues(2);
for(Status s:posts){
values.put("id", s.id);
values.put("json", MastodonAPIController.gson.toJson(s));
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
}catch(SQLiteException x){
Log.w(TAG, x);
}finally{
closeDelayed();
}
}, 0);
}
public void getNotifications(String maxID, int count, boolean onlyMentions, Callback<List<Notification>> callback){
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`>?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Notification> result=new ArrayList<>();
cursor.moveToFirst();
do{
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
ntf.postprocess();
result.add(ntf);
}while(cursor.moveToNext());
uiHandler.post(()->callback.onSuccess(result));
return;
}
}catch(IOException x){
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
}
new GetNotifications(maxID, count, onlyMentions ? EnumSet.complementOf(EnumSet.of(Notification.Type.MENTION)): null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){
callback.onSuccess(result);
putNotifications(result, onlyMentions, maxID==null);
}
@Override
public void onError(ErrorResponse error){
callback.onError(error);
}
})
.exec(accountID);
}catch(SQLiteException x){
Log.w(TAG, x);
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage())));
}finally{
closeDelayed();
}
}, 0);
}
private void putNotifications(List<Notification> notifications, boolean onlyMentions, boolean clear){
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
SQLiteDatabase db=getOrOpenDatabase();
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
if(clear)
db.delete(table, null, null);
ContentValues values=new ContentValues(3);
for(Notification n:notifications){
values.put("id", n.id);
values.put("json", MastodonAPIController.gson.toJson(n));
values.put("type", n.type.ordinal());
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
}catch(SQLiteException x){
Log.w(TAG, x);
}finally{
closeDelayed();
}
}, 0);
}
private void closeDelayed(){
databaseThread.postRunnable(databaseCloseRunnable, 10_000);
}
public void closeDatabase(){
if(db!=null){
if(BuildConfig.DEBUG)
Log.d(TAG, "closeDatabase");
db.close();
db=null;
}
}
private void cancelDelayedClose(){
if(db!=null){
databaseThread.handler.removeCallbacks(databaseCloseRunnable);
}
}
private SQLiteDatabase getOrOpenDatabase(){
if(db==null)
db=new DatabaseHelper();
return db.getWritableDatabase();
}
private class DatabaseHelper extends SQLiteOpenHelper{
public DatabaseHelper(){
super(MastodonApp.context, accountID+".db", null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db){
db.execSQL("""
CREATE TABLE `home_timeline` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0
)""");
db.execSQL("""
CREATE TABLE `notifications_all` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
)""");
db.execSQL("""
CREATE TABLE `notifications_mentions` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
)""");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
}
}
}

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.api.session;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.model.Account;
@ -19,6 +20,7 @@ public class AccountSession{
public boolean activated=true;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
private transient CacheController cacheController;
AccountSession(Token token, Account self, Application app, String domain, int tootCharLimit, Instance instance, boolean activated){
this.token=token;
@ -48,4 +50,10 @@ public class AccountSession{
statusInteractionController=new StatusInteractionController(getID());
return statusInteractionController;
}
public CacheController getCacheController(){
if(cacheController==null)
cacheController=new CacheController(getID());
return cacheController;
}
}

View File

@ -143,6 +143,8 @@ public class AccountSessionManager{
public void removeAccount(String id){
AccountSession session=getAccount(id);
session.getCacheController().closeDatabase();
MastodonApp.context.deleteDatabase(id+".db");
sessions.remove(id);
if(lastActiveAccountID.equals(id)){
if(sessions.isEmpty())

View File

@ -58,14 +58,22 @@ public class HomeTimelineFragment extends StatusListFragment{
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetHomeTimeline(offset>0 ? getMaxID() : null, null, count)
.setCallback(new SimpleCallback<>(this){
// currentRequest=new GetHomeTimeline(offset>0 ? getMaxID() : null, null, count)
// .setCallback(new SimpleCallback<>(this){
// @Override
// public void onSuccess(List<Status> result){
// onDataLoaded(result, !result.isEmpty());
// }
// })
// .exec(accountID);
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getHomeTimeline(offset>0 ? getMaxID() : null, count, new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
});
}
@Override

View File

@ -10,6 +10,7 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Status;
@ -36,15 +37,13 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
private EnumSet<Notification.Type> types;
private boolean onlyMentions;
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.notifications);
if(getArguments().getBoolean("onlyMentions", false)){
types=EnumSet.complementOf(EnumSet.of(Notification.Type.MENTION));
}
onlyMentions=getArguments().getBoolean("onlyMentions", false);
}
@Override
@ -79,8 +78,24 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
protected void doLoadData(int offset, int count){
new GetNotifications(offset>0 ? getMaxID() : null, count, types)
.setCallback(new SimpleCallback<>(this){
// new GetNotifications(offset>0 ? getMaxID() : null, count, types)
// .setCallback(new SimpleCallback<>(this){
// @Override
// public void onSuccess(List<Notification> result){
// if(refreshing)
// relationships.clear();
// onDataLoaded(result, !result.isEmpty());
// Set<String> needRelationships=result.stream()
// .filter(ntf->ntf.status==null && !relationships.containsKey(ntf.account.id))
// .map(ntf->ntf.account.id)
// .collect(Collectors.toSet());
// loadRelationships(needRelationships);
// }
// })
// .exec(accountID);
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? getMaxID() : null, count, onlyMentions, new SimpleCallback<List<Notification>>(this){
@Override
public void onSuccess(List<Notification> result){
if(refreshing)
@ -92,8 +107,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
.collect(Collectors.toSet());
loadRelationships(needRelationships);
}
})
.exec(accountID);
});
}
@Override