Commit 8040a063 authored by Thomas's avatar Thomas
Browse files

Prepare db for caching bundle + logic to pass/get data

parent 36e258ad
Loading
Loading
Loading
Loading
+228 −0
Original line number Diff line number Diff line
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 <http://www.gnu.org/licenses>. */

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 java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

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 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();
    }


    /**
     * Insert a bundle in db
     *
     * @param bundle {@link Bundle}
     * @return long - db id
     * @throws DBException exception with database
     */
    private long insertIntent(Bundle bundle) 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()));
        //Inserts token
        try {
            return db.insertOrThrow(Sqlite.TABLE_INTENT, null, values);
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        }
    }

    public interface BundleCallback{
        public void get(Bundle bundle);
    }

    public interface BundleInsertCallback{
        public void inserted(long bundleId);
    }

    public void getBundle(long id, BundleCallback callback) {
        new Thread(()->{
            Bundle bundle = null;
            try {
                CachedBundle cachedBundle = getCachedBundle(String.valueOf(id));
                if (cachedBundle != null) {
                    bundle = cachedBundle.bundle;
                }
                removeIntent(String.valueOf(id));
            } catch (DBException ignored) {}
            Handler mainHandler = new Handler(Looper.getMainLooper());
            Bundle finalBundle = bundle;
            Runnable myRunnable = () -> callback.get(finalBundle);
            mainHandler.post(myRunnable);
        }).start();
    }

    public void insertBundle(Bundle bundle, BundleInsertCallback callback) {
        new Thread(()->{
            long dbBundleId = -1;
            try {
                dbBundleId = insertIntent(bundle);
            } 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 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);
    }



    /***
     * 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 BaseAccount
     */
    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.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;
    }

}
+2 −0
Original line number Diff line number Diff line
@@ -210,6 +210,8 @@ public class Helper {
    public static final String RECEIVE_REDRAW_PROFILE = "RECEIVE_REDRAW_PROFILE";

    public static final String ARG_TIMELINE_TYPE = "ARG_TIMELINE_TYPE";

    public static final String ARG_INTENT_ID = "ARG_INTENT_ID";
    public static final String ARG_PEERTUBE_NAV_REMOTE = "ARG_PEERTUBE_NAV_REMOTE";

    public static final String ARG_REMOTE_INSTANCE_STRING = "ARG_REMOTE_INSTANCE_STRING";
+100 −92
Original line number Diff line number Diff line
@@ -57,10 +57,12 @@ import app.fedilab.android.mastodon.client.entities.api.Pagination;
import app.fedilab.android.mastodon.client.entities.api.Status;
import app.fedilab.android.mastodon.client.entities.api.Statuses;
import app.fedilab.android.mastodon.client.entities.app.BubbleTimeline;
import app.fedilab.android.mastodon.client.entities.app.CachedBundle;
import app.fedilab.android.mastodon.client.entities.app.PinnedTimeline;
import app.fedilab.android.mastodon.client.entities.app.RemoteInstance;
import app.fedilab.android.mastodon.client.entities.app.TagTimeline;
import app.fedilab.android.mastodon.client.entities.app.Timeline;
import app.fedilab.android.mastodon.exception.DBException;
import app.fedilab.android.mastodon.helper.CrossActionHelper;
import app.fedilab.android.mastodon.helper.GlideApp;
import app.fedilab.android.mastodon.helper.Helper;
@@ -346,29 +348,12 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        timelinesVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, TimelinesVM.class);
        accountsVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, AccountsVM.class);
        initialStatuses = null;
        lockForResumeCall = 0;
        binding.loader.setVisibility(View.VISIBLE);
        binding.recyclerView.setVisibility(View.GONE);
        SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
        max_id = statusReport != null ? statusReport.id : null;
        offset = 0;

        rememberPosition = sharedpreferences.getBoolean(getString(R.string.SET_REMEMBER_POSITION), true);
        //Inner marker are only for pinned timelines and main timelines, they have isViewInitialized set to false
        if (max_id == null && !isViewInitialized && rememberPosition) {
            max_id = sharedpreferences.getString(getString(R.string.SET_INNER_MARKER) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance + slug, null);
        }
        if (search != null) {
            binding.swipeContainer.setRefreshing(false);
            binding.swipeContainer.setEnabled(false);
        }
        //Only fragment in main view pager should not have the view initialized
        //AND Only the first fragment will initialize its view
        flagLoading = false;

    }

    @Override
@@ -378,16 +363,43 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        timelinesVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, TimelinesVM.class);
        accountsVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, AccountsVM.class);
        initialStatuses = null;
        lockForResumeCall = 0;
        timelineType = Timeline.TimeLineEnum.HOME;
        canBeFederated = true;
        retry_for_home_done = false;
        binding = FragmentPaginationBinding.inflate(inflater, container, false);
        SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
        max_id = statusReport != null ? statusReport.id : null;
        offset = 0;
        rememberPosition = sharedpreferences.getBoolean(getString(R.string.SET_REMEMBER_POSITION), true);
        //Inner marker are only for pinned timelines and main timelines, they have isViewInitialized set to false
        if (max_id == null && !isViewInitialized && rememberPosition) {
            max_id = sharedpreferences.getString(getString(R.string.SET_INNER_MARKER) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance + slug, null);
        }
        //Only fragment in main view pager should not have the view initialized
        //AND Only the first fragment will initialize its view
        flagLoading = false;
        if (getArguments() != null) {
            timelineType = (Timeline.TimeLineEnum) getArguments().get(Helper.ARG_TIMELINE_TYPE);
            lemmy_post_id = getArguments().getString(Helper.ARG_LEMMY_POST_ID, null);
            list_id = getArguments().getString(Helper.ARG_LIST_ID, null);
            search = getArguments().getString(Helper.ARG_SEARCH_KEYWORD, null);
            searchCache = getArguments().getString(Helper.ARG_SEARCH_KEYWORD_CACHE, null);
            pinnedTimeline = (PinnedTimeline) getArguments().getSerializable(Helper.ARG_REMOTE_INSTANCE);
            long bundleId = getArguments().getLong(Helper.ARG_INTENT_ID, -1);
            new CachedBundle(requireActivity()).getBundle(bundleId, this::initializeAfterBundle);
        }
        boolean displayScrollBar = sharedpreferences.getBoolean(getString(R.string.SET_TIMELINE_SCROLLBAR), false);
        binding.recyclerView.setVerticalScrollBarEnabled(displayScrollBar);
        return binding.getRoot();
    }

    private void initializeAfterBundle(Bundle bundle) {
        new Thread(()->{
            if (bundle != null) {
                timelineType = (Timeline.TimeLineEnum) bundle.get(Helper.ARG_TIMELINE_TYPE);
                lemmy_post_id = bundle.getString(Helper.ARG_LEMMY_POST_ID, null);
                list_id = bundle.getString(Helper.ARG_LIST_ID, null);
                search = bundle.getString(Helper.ARG_SEARCH_KEYWORD, null);
                searchCache = bundle.getString(Helper.ARG_SEARCH_KEYWORD_CACHE, null);
                pinnedTimeline = (PinnedTimeline) bundle.getSerializable(Helper.ARG_REMOTE_INSTANCE);
                if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null) {
                    if (pinnedTimeline.remoteInstance.type != RemoteInstance.InstanceType.NITTER) {
                        remoteInstance = pinnedTimeline.remoteInstance.host;
@@ -400,24 +412,24 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
                if (timelineType == Timeline.TimeLineEnum.TREND_MESSAGE_PUBLIC) {
                    canBeFederated = false;
                }
            publicTrendsDomain = getArguments().getString(Helper.ARG_REMOTE_INSTANCE_STRING, null);
            isViewInitialized = getArguments().getBoolean(Helper.ARG_INITIALIZE_VIEW, true);
                publicTrendsDomain = bundle.getString(Helper.ARG_REMOTE_INSTANCE_STRING, null);
                isViewInitialized = bundle.getBoolean(Helper.ARG_INITIALIZE_VIEW, true);
                isNotPinnedTimeline = isViewInitialized;
            tagTimeline = (TagTimeline) getArguments().getSerializable(Helper.ARG_TAG_TIMELINE);
            bubbleTimeline = (BubbleTimeline) getArguments().getSerializable(Helper.ARG_BUBBLE_TIMELINE);
            accountTimeline = (Account) getArguments().getSerializable(Helper.ARG_ACCOUNT);
            exclude_replies = !getArguments().getBoolean(Helper.ARG_SHOW_REPLIES, true);
            checkRemotely = getArguments().getBoolean(Helper.ARG_CHECK_REMOTELY, false);
            show_pinned = getArguments().getBoolean(Helper.ARG_SHOW_PINNED, false);
            exclude_reblogs = !getArguments().getBoolean(Helper.ARG_SHOW_REBLOGS, true);
            media_only = getArguments().getBoolean(Helper.ARG_SHOW_MEDIA_ONY, false);
            viewModelKey = getArguments().getString(Helper.ARG_VIEW_MODEL_KEY, "");
            minified = getArguments().getBoolean(Helper.ARG_MINIFIED, false);
            statusReport = (Status) getArguments().getSerializable(Helper.ARG_STATUS_REPORT);
            initialStatus = (Status) getArguments().getSerializable(Helper.ARG_STATUS);
                tagTimeline = (TagTimeline) bundle.getSerializable(Helper.ARG_TAG_TIMELINE);
                bubbleTimeline = (BubbleTimeline) bundle.getSerializable(Helper.ARG_BUBBLE_TIMELINE);
                accountTimeline = (Account) bundle.getSerializable(Helper.ARG_ACCOUNT);
                exclude_replies = !bundle.getBoolean(Helper.ARG_SHOW_REPLIES, true);
                checkRemotely = bundle.getBoolean(Helper.ARG_CHECK_REMOTELY, false);
                show_pinned = bundle.getBoolean(Helper.ARG_SHOW_PINNED, false);
                exclude_reblogs = !bundle.getBoolean(Helper.ARG_SHOW_REBLOGS, true);
                media_only = bundle.getBoolean(Helper.ARG_SHOW_MEDIA_ONY, false);
                viewModelKey = bundle.getString(Helper.ARG_VIEW_MODEL_KEY, "");
                minified = bundle.getBoolean(Helper.ARG_MINIFIED, false);
                statusReport = (Status) bundle.getSerializable(Helper.ARG_STATUS_REPORT);
                initialStatus = (Status) bundle.getSerializable(Helper.ARG_STATUS);
            }


            Handler mainHandler = new Handler(Looper.getMainLooper());
            Runnable myRunnable = () -> {
                //When visiting a profile without being authenticated
                if (checkRemotely) {
                    String[] acctArray = accountTimeline.acct.split("@");
@@ -453,14 +465,10 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
                if (timelineType != null) {
                    slug = timelineType != Timeline.TimeLineEnum.ART ? timelineType.getValue() + (ident != null ? "|" + ident : "") : Timeline.TimeLineEnum.TAG.getValue() + (ident != null ? "|" + ident : "");
                }


                ContextCompat.registerReceiver(requireActivity(), receive_action, new IntentFilter(Helper.RECEIVE_STATUS_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED);
        binding = FragmentPaginationBinding.inflate(inflater, container, false);
        SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
        boolean displayScrollBar = sharedpreferences.getBoolean(getString(R.string.SET_TIMELINE_SCROLLBAR), false);
        binding.recyclerView.setVerticalScrollBarEnabled(displayScrollBar);
        return binding.getRoot();
            };
            mainHandler.post(myRunnable);
        }).start();
    }

    /**
+13 −1
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ import android.database.sqlite.SQLiteOpenHelper;
public class Sqlite extends SQLiteOpenHelper {


    public static final int DB_VERSION = 11;
    public static final int DB_VERSION = 12;
    public static final String DB_NAME = "fedilab_db";

    //Table of owned accounts
@@ -105,6 +105,9 @@ public class Sqlite extends SQLiteOpenHelper {
    public static final String COL_TAG = "TAG";

    public static final String TABLE_TIMELINE_CACHE_LOGS = "TIMELINE_CACHE_LOGS";
    public static final String TABLE_INTENT = "INTENT";

    public static final String COL_BUNDLE = "BUNDLE";


    private static final String CREATE_TABLE_USER_ACCOUNT = "CREATE TABLE " + TABLE_USER_ACCOUNT + " ("
@@ -233,6 +236,12 @@ public class Sqlite extends SQLiteOpenHelper {
            + COL_TYPE + " TEXT NOT NULL, "
            + COL_CREATED_AT + " TEXT NOT NULL)";

    private final String CREATE_TABLE_INTENT = "CREATE TABLE "
            + TABLE_INTENT + "("
            + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
            + COL_BUNDLE + " TEXT NOT NULL, "
            + COL_CREATED_AT + " TEXT NOT NULL)";


    public Sqlite(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
@@ -263,6 +272,7 @@ public class Sqlite extends SQLiteOpenHelper {
        db.execSQL(CREATE_TABLE_STORED_INSTANCES);
        db.execSQL(CREATE_TABLE_CACHE_TAGS);
        db.execSQL(CREATE_TABLE_TIMELINE_CACHE_LOGS);
        db.execSQL(CREATE_TABLE_INTENT);
    }

    @Override
@@ -295,6 +305,8 @@ public class Sqlite extends SQLiteOpenHelper {
                db.execSQL(CREATE_TABLE_CACHE_TAGS);
            case 10:
                db.execSQL(CREATE_TABLE_TIMELINE_CACHE_LOGS);
            case 11:
                db.execSQL(CREATE_TABLE_INTENT);
            default:
                break;
        }