Loading app/src/main/java/app/fedilab/android/mastodon/client/entities/app/CachedBundle.java 0 → 100644 +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; } } app/src/main/java/app/fedilab/android/mastodon/helper/Helper.java +2 −0 Original line number Diff line number Diff line Loading @@ -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"; Loading app/src/main/java/app/fedilab/android/mastodon/ui/fragment/timeline/FragmentMastodonTimeline.java +100 −92 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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; Loading @@ -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("@"); Loading Loading @@ -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(); } /** Loading app/src/main/java/app/fedilab/android/sqlite/Sqlite.java +13 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 + " (" Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -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; } Loading Loading
app/src/main/java/app/fedilab/android/mastodon/client/entities/app/CachedBundle.java 0 → 100644 +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; } }
app/src/main/java/app/fedilab/android/mastodon/helper/Helper.java +2 −0 Original line number Diff line number Diff line Loading @@ -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"; Loading
app/src/main/java/app/fedilab/android/mastodon/ui/fragment/timeline/FragmentMastodonTimeline.java +100 −92 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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; Loading @@ -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("@"); Loading Loading @@ -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(); } /** Loading
app/src/main/java/app/fedilab/android/sqlite/Sqlite.java +13 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 + " (" Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -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; } Loading