package app.fedilab.android.mastodon.client.entities.app; /* Copyright 2024 Thomas Schneider * * This file is a part of Fedilab * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Parcel; import android.util.Base64; import com.google.gson.annotations.SerializedName; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Calendar; import java.util.Date; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import app.fedilab.android.mastodon.client.entities.api.Account; import app.fedilab.android.mastodon.client.entities.api.Status; import app.fedilab.android.mastodon.exception.DBException; import app.fedilab.android.mastodon.helper.Helper; import app.fedilab.android.sqlite.Sqlite; /** * Class that manages Bundle of Intent from database */ public class CachedBundle { public String id; public Bundle bundle; public CacheType cacheType; public String instance; public String user_id; public String target_id; public Date created_at; private SQLiteDatabase db; private transient Context context; public CachedBundle() { } public CachedBundle(Context context) { //Creation of the DB with tables this.context = context; this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); } public long insertAccountBundle(Account account, BaseAccount currentUser) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } ContentValues valuesAccount = new ContentValues(); Bundle bundleAccount = new Bundle(); if (account != null) { bundleAccount.putSerializable(Helper.ARG_ACCOUNT, account); valuesAccount.put(Sqlite.COL_BUNDLE, serializeBundle(bundleAccount)); valuesAccount.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date())); valuesAccount.put(Sqlite.COL_TARGET_ID, account.id); valuesAccount.put(Sqlite.COL_USER_ID, currentUser.user_id); valuesAccount.put(Sqlite.COL_INSTANCE, currentUser.instance); valuesAccount.put(Sqlite.COL_TYPE, CacheType.ACCOUNT.getValue()); removeIntent(currentUser, account.id); return db.insertOrThrow(Sqlite.TABLE_INTENT, null, valuesAccount); } return -1; } /** * Insert a bundle in db * * @param bundle {@link Bundle} * @return long - db id * @throws DBException exception with database */ private long insertIntent(Bundle bundle, BaseAccount currentUser) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } ContentValues values = new ContentValues(); values.put(Sqlite.COL_BUNDLE, serializeBundle(bundle)); values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date())); values.put(Sqlite.COL_TYPE, CacheType.ARGS.getValue()); if (bundle.containsKey(Helper.ARG_ACCOUNT) && currentUser != null) { ContentValues valuesAccount = new ContentValues(); Bundle bundleAccount = new Bundle(); Account account = (Account) bundle.getSerializable(Helper.ARG_ACCOUNT); if (account != null) { bundleAccount.putSerializable(Helper.ARG_ACCOUNT, account); valuesAccount.put(Sqlite.COL_BUNDLE, serializeBundle(bundleAccount)); valuesAccount.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date())); valuesAccount.put(Sqlite.COL_TARGET_ID, account.id); valuesAccount.put(Sqlite.COL_USER_ID, currentUser.user_id); valuesAccount.put(Sqlite.COL_INSTANCE, currentUser.instance); valuesAccount.put(Sqlite.COL_TYPE, CacheType.ACCOUNT.getValue()); removeIntent(currentUser, account.id); db.insertOrThrow(Sqlite.TABLE_INTENT, null, valuesAccount); } } if (bundle.containsKey(Helper.ARG_STATUS) && currentUser != null) { ContentValues valuesAccount = new ContentValues(); Bundle bundleStatus = new Bundle(); Status status = (Status) bundle.getSerializable(Helper.ARG_STATUS); if (status != null) { bundleStatus.putSerializable(Helper.ARG_STATUS, status); valuesAccount.put(Sqlite.COL_BUNDLE, serializeBundle(bundleStatus)); valuesAccount.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date())); valuesAccount.put(Sqlite.COL_TARGET_ID, status.id); valuesAccount.put(Sqlite.COL_USER_ID, currentUser.user_id); valuesAccount.put(Sqlite.COL_INSTANCE, currentUser.instance); valuesAccount.put(Sqlite.COL_TYPE, CacheType.STATUS.getValue()); removeIntent(currentUser, status.id); db.insertOrThrow(Sqlite.TABLE_INTENT, null, valuesAccount); } } try { return db.insertOrThrow(Sqlite.TABLE_INTENT, null, values); } catch (Exception e) { e.printStackTrace(); return -1; } } public void getBundle(long id, BaseAccount Account, BundleCallback callback) { new Thread(() -> { Bundle bundle = null; try { CachedBundle cachedBundle = getCachedBundle(String.valueOf(id)); if (cachedBundle != null) { bundle = cachedBundle.bundle; if (bundle != null && bundle.containsKey(Helper.ARG_CACHED_ACCOUNT_ID)) { Account cachedAccount = getCachedAccount(Account, bundle.getString(Helper.ARG_CACHED_ACCOUNT_ID)); if (cachedAccount != null) { bundle.putSerializable(Helper.ARG_ACCOUNT, cachedAccount); } } if (bundle != null && bundle.containsKey(Helper.ARG_CACHED_STATUS_ID)) { Status cachedStatus = getCachedStatus(Account, bundle.getString(Helper.ARG_CACHED_STATUS_ID)); if (cachedStatus != null) { bundle.putSerializable(Helper.ARG_STATUS, cachedStatus); } } } } catch (DBException ignored) { } if( bundle == null) { bundle = new Bundle(); } Handler mainHandler = new Handler(Looper.getMainLooper()); Bundle finalBundle = bundle; Runnable myRunnable = () -> callback.get(finalBundle); mainHandler.post(myRunnable); }).start(); } public void insertBundle(Bundle bundle, BaseAccount Account, BundleInsertCallback callback) { new Thread(() -> { long dbBundleId = -1; try { dbBundleId = insertIntent(bundle, Account); } catch (DBException ignored) { } Handler mainHandler = new Handler(Looper.getMainLooper()); long finalDbBundleId = dbBundleId; Runnable myRunnable = () -> callback.inserted(finalDbBundleId); mainHandler.post(myRunnable); }).start(); } /** * Returns a bundle by targeted account id * * @param target_id String * @return Account {@link Account} */ public Account getCachedAccount(BaseAccount account, String target_id) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } if (account == null || target_id == null) { return null; } try { Cursor c = db.query(Sqlite.TABLE_INTENT, null, Sqlite.COL_USER_ID + " = '" + account.user_id + "' AND " + Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_TYPE + " = '" + CacheType.ACCOUNT.getValue() + "' AND " + Sqlite.COL_TARGET_ID + " = '" + target_id + "'", null, null, null, null, "1"); CachedBundle cachedBundle = cursorToCachedBundle(c); if (cachedBundle != null && cachedBundle.bundle.containsKey(Helper.ARG_ACCOUNT)) { return (Account) cachedBundle.bundle.getSerializable(Helper.ARG_ACCOUNT); } } catch (Exception e) { e.printStackTrace(); return null; } return null; } /** * Returns a bundle by targeted status id * * @param target_id String * @return Status {@link Status} */ private Status getCachedStatus(BaseAccount account, String target_id) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } if (account == null || target_id == null) { return null; } try { Cursor c = db.query(Sqlite.TABLE_INTENT, null, Sqlite.COL_USER_ID + " = '" + account.user_id + "' AND " + Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_TYPE + " = '" + CacheType.STATUS.getValue() + "' AND " + Sqlite.COL_TARGET_ID + " = '" + target_id + "'", null, null, null, null, "1"); CachedBundle cachedBundle = cursorToCachedBundle(c); if (cachedBundle != null && cachedBundle.bundle.containsKey(Helper.ARG_STATUS)) { return (Status) cachedBundle.bundle.getSerializable(Helper.ARG_STATUS); } } catch (Exception e) { return null; } return null; } /** * Returns a bundle by its ID * * @param id String * @return CachedBundle {@link CachedBundle} */ private CachedBundle getCachedBundle(String id) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } try { Cursor c = db.query(Sqlite.TABLE_INTENT, null, Sqlite.COL_ID + " = \"" + id + "\"", null, null, null, null, "1"); return cursorToCachedBundle(c); } catch (Exception e) { return null; } } /** * Remove a bundle from db * * @param id - intent id */ private void removeIntent(String id) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } db.delete(Sqlite.TABLE_INTENT, Sqlite.COL_ID + " = '" + id + "'", null); } /** * Remove a bundle from db * */ public long deleteOldIntent() throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } Calendar cal = Calendar.getInstance(); cal.setTime(new Date()); cal.add(Calendar.DATE, -1); Date date = cal.getTime(); String dateStr = Helper.dateToString(date); try { return db.delete(Sqlite.TABLE_INTENT, Sqlite.COL_CREATED_AT + " < ?", new String[]{dateStr}); }catch (Exception e) { e.printStackTrace(); return -1; } } /** * Remove a bundle from db */ private void removeIntent(BaseAccount account, String target_id) throws DBException { if (db == null) { throw new DBException("db is null. Wrong initialization."); } if (account == null || target_id == null) { return; } db.delete(Sqlite.TABLE_INTENT, Sqlite.COL_USER_ID + " = '" + account.user_id + "' AND " + Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_TARGET_ID + " = '" + target_id + "'", null); } /*** * Method to hydrate an CachedBundle from database * @param c Cursor * @return CachedBundle {@link CachedBundle} */ private CachedBundle cursorToCachedBundle(Cursor c) { //No element found if (c.getCount() == 0) { c.close(); return null; } //Take the first element c.moveToFirst(); //New user CachedBundle account = convertCursorToCachedBundle(c); //Close the cursor c.close(); return account; } /** * Read cursor and hydrate without closing it * * @param c - Cursor * @return Account */ private CachedBundle convertCursorToCachedBundle(Cursor c) { CachedBundle cachedBundle = new CachedBundle(); cachedBundle.id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_ID)); cachedBundle.bundle = deserializeBundle(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_BUNDLE))); cachedBundle.user_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID)); cachedBundle.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE)); cachedBundle.target_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_TARGET_ID)); cachedBundle.cacheType = CacheType.valueOf(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_TYPE))); cachedBundle.created_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_CREATED_AT))); return cachedBundle; } private String serializeBundle(final Bundle bundle) { String base64 = null; final Parcel parcel = Parcel.obtain(); try { parcel.writeBundle(bundle); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); final GZIPOutputStream zos = new GZIPOutputStream(new BufferedOutputStream(bos)); zos.write(parcel.marshall()); zos.close(); base64 = Base64.encodeToString(bos.toByteArray(), 0); } catch (IOException e) { e.printStackTrace(); } finally { parcel.recycle(); } return base64; } private Bundle deserializeBundle(final String base64) { Bundle bundle = null; final Parcel parcel = Parcel.obtain(); try { final ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); final byte[] buffer = new byte[1024]; final GZIPInputStream zis = new GZIPInputStream(new ByteArrayInputStream(Base64.decode(base64, 0))); int len; while ((len = zis.read(buffer)) != -1) { byteBuffer.write(buffer, 0, len); } zis.close(); parcel.unmarshall(byteBuffer.toByteArray(), 0, byteBuffer.size()); parcel.setDataPosition(0); bundle = parcel.readBundle(getClass().getClassLoader()); } catch (IOException e) { e.printStackTrace(); } finally { parcel.recycle(); } return bundle; } public enum CacheType { @SerializedName("ARGS") ARGS("ARGS"), @SerializedName("ACCOUNT") ACCOUNT("ACCOUNT"), @SerializedName("STATUS") STATUS("STATUS"); private final String value; CacheType(String value) { this.value = value; } public String getValue() { return value; } } public interface BundleCallback { void get(Bundle bundle); } public interface BundleInsertCallback { void inserted(long bundleId); } }