From e8950f03ae320afd3f1abc251c42ed9032eb047a Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 1 Feb 2023 12:33:43 +0100 Subject: [PATCH] Fetch home in background to cache messages --- .../android/mastodon/helper/Helper.java | 1 + .../mastodon/jobs/FetchHomeWorker.java | 223 ++++++++++++++++++ .../mastodon/jobs/NotificationsWorker.java | 2 +- .../settings/FragmentHomeCacheSettings.java | 113 +++++++++ .../settings/FragmentSettingsCategories.java | 9 + .../res/layouts/mastodon/values/strings.xml | 7 + .../res/navigation/nav_graph_settings.xml | 13 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/pref_categories.xml | 7 + app/src/main/res/xml/pref_home_cache.xml | 26 ++ 10 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/app/fedilab/android/mastodon/jobs/FetchHomeWorker.java create mode 100644 app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentHomeCacheSettings.java create mode 100644 app/src/main/res/layouts/mastodon/values/strings.xml create mode 100644 app/src/main/res/xml/pref_home_cache.xml diff --git a/app/src/main/java/app/fedilab/android/mastodon/helper/Helper.java b/app/src/main/java/app/fedilab/android/mastodon/helper/Helper.java index 448be230..57336df2 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/helper/Helper.java +++ b/app/src/main/java/app/fedilab/android/mastodon/helper/Helper.java @@ -276,6 +276,7 @@ public class Helper { public static final String ARG_SCHEDULED_DATE = "ARG_SCHEDULED_DATE"; public static final String WORKER_REFRESH_NOTIFICATION = "WORKER_REFRESH_NOTIFICATION"; + public static final String WORKER_REFRESH_HOME = "WORKER_REFRESH_HOME"; public static final String WORKER_SCHEDULED_STATUSES = "WORKER_SCHEDULED_STATUSES"; public static final String WORKER_SCHEDULED_REBLOGS = "WORKER_SCHEDULED_REBLOGS"; diff --git a/app/src/main/java/app/fedilab/android/mastodon/jobs/FetchHomeWorker.java b/app/src/main/java/app/fedilab/android/mastodon/jobs/FetchHomeWorker.java new file mode 100644 index 00000000..f4c6a849 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/mastodon/jobs/FetchHomeWorker.java @@ -0,0 +1,223 @@ +package app.fedilab.android.mastodon.jobs; +/* Copyright 2023 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.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.BitmapFactory; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.preference.PreferenceManager; +import androidx.work.Data; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.ForegroundInfo; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.fedilab.android.R; +import app.fedilab.android.mastodon.client.endpoints.MastodonTimelinesService; +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.app.Account; +import app.fedilab.android.mastodon.client.entities.app.BaseAccount; +import app.fedilab.android.mastodon.client.entities.app.StatusCache; +import app.fedilab.android.mastodon.client.entities.app.Timeline; +import app.fedilab.android.mastodon.exception.DBException; +import app.fedilab.android.mastodon.helper.Helper; +import app.fedilab.android.mastodon.helper.MastodonHelper; +import okhttp3.OkHttpClient; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + + +public class FetchHomeWorker extends Worker { + + private static final int FETCH_HOME_CHANNEL_ID = 5; + private static final String CHANNEL_ID = "fedilab_home"; + final OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .callTimeout(60, TimeUnit.SECONDS) + .proxy(Helper.getProxy(getApplicationContext().getApplicationContext())) + .build(); + private final NotificationManager notificationManager; + + + public FetchHomeWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + public static void setRepeatHome(Context context, BaseAccount account) { + WorkManager.getInstance(context).cancelAllWorkByTag(Helper.WORKER_REFRESH_HOME + account.user_id + account.instance); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String value = prefs.getString(context.getString(R.string.SET_FETCH_HOME_DELAY_VALUE) + account.user_id + account.instance, "60"); + PeriodicWorkRequest notificationPeriodic = new PeriodicWorkRequest.Builder(NotificationsWorker.class, Long.parseLong(value), TimeUnit.MINUTES) + .addTag(Helper.WORKER_REFRESH_NOTIFICATION) + .build(); + WorkManager.getInstance(context).enqueueUniquePeriodicWork(Helper.WORKER_REFRESH_HOME + account.user_id + account.instance, ExistingPeriodicWorkPolicy.REPLACE, notificationPeriodic); + } + + @NonNull + @Override + public ListenableFuture getForegroundInfoAsync() { + if (Build.VERSION.SDK_INT >= 26) { + String channelName = "Notifications"; + String channelDescription = "Fetched notifications"; + NotificationChannel notifChannel = new NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_LOW); + notifChannel.setDescription(channelDescription); + notifChannel.setSound(null, null); + notifChannel.setShowBadge(false); + notificationManager.createNotificationChannel(notifChannel); + if (notificationManager.getNotificationChannel("notifications") != null) { + notificationManager.deleteNotificationChannel("notifications"); + } + + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID); + notificationBuilder.setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.ic_launcher_foreground)) + .setContentTitle(getApplicationContext().getString(R.string.notifications)) + .setContentText(getApplicationContext().getString(R.string.fetch_notifications)) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(Notification.PRIORITY_DEFAULT); + return Futures.immediateFuture(new ForegroundInfo(FETCH_HOME_CHANNEL_ID, notificationBuilder.build())); + } + + @NonNull + private ForegroundInfo createForegroundInfo() { + if (Build.VERSION.SDK_INT >= 26) { + String channelName = "Notifications"; + String channelDescription = "Fetched notifications"; + NotificationChannel notifChannel = new NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_LOW); + notifChannel.setSound(null, null); + notifChannel.setShowBadge(false); + notifChannel.setDescription(channelDescription); + notificationManager.createNotificationChannel(notifChannel); + + } + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID); + notificationBuilder.setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.ic_launcher_foreground)) + .setContentTitle(getApplicationContext().getString(R.string.notifications)) + .setContentText(getApplicationContext().getString(R.string.fetch_notifications)) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setSilent(true) + .setPriority(Notification.PRIORITY_LOW); + return new ForegroundInfo(FETCH_HOME_CHANNEL_ID, notificationBuilder.build()); + } + + @NonNull + @Override + public Result doWork() { + setForegroundAsync(createForegroundInfo()); + try { + List accounts = new Account(getApplicationContext()).getCrossAccounts(); + for (BaseAccount account : accounts) { + try { + fetchHome(getApplicationContext(), account); + } catch (IOException e) { + e.printStackTrace(); + } + } + } catch (DBException e) { + e.printStackTrace(); + } + return Result.success(new Data.Builder().putString("WORK_RESULT", getApplicationContext().getString(R.string.notifications)).build()); + } + + private void fetchHome(Context context, BaseAccount account) throws IOException { + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + boolean fetch_home = prefs.getBoolean(context.getString(R.string.SET_FETCH_HOME) + account.user_id + account.instance, false); + + if (fetch_home) { + int max_calls = 5; + int status_per_page = 80; + //Browse last 400 home messages + boolean canContinue = true; + int call = 0; + String max_id = null; + MastodonTimelinesService mastodonTimelinesService = init(account.instance); + while (canContinue && call < max_calls) { + Call> homeCall = mastodonTimelinesService.getHome(account.token, account.instance, max_id, null, status_per_page, null); + if (homeCall != null) { + Response> homeResponse = homeCall.execute(); + if (homeResponse.isSuccessful()) { + List statusList = homeResponse.body(); + if (statusList != null && statusList.size() > 0) { + for (Status status : statusList) { + StatusCache statusCacheDAO = new StatusCache(getApplicationContext()); + StatusCache statusCache = new StatusCache(); + statusCache.instance = account.instance; + statusCache.user_id = account.user_id; + statusCache.status = status; + statusCache.type = Timeline.TimeLineEnum.HOME; + statusCache.status_id = status.id; + try { + int inserted = statusCacheDAO.insertOrUpdate(statusCache, Timeline.TimeLineEnum.HOME.getValue()); + //We reached already cached messages + if (inserted == 0) { + canContinue = false; + break; + } + } catch (DBException e) { + e.printStackTrace(); + } + } + Pagination pagination = MastodonHelper.getPagination(homeResponse.headers()); + if (pagination.max_id != null) { + max_id = pagination.max_id; + } else { + canContinue = false; + } + } else { + canContinue = false; + } + } else { + canContinue = false; + } + } + call++; + } + + } + } + + private MastodonTimelinesService init(String instance) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + instance + "/api/v1/") + .addConverterFactory(GsonConverterFactory.create(Helper.getDateBuilder())) + .client(okHttpClient) + .build(); + return retrofit.create(MastodonTimelinesService.class); + } +} diff --git a/app/src/main/java/app/fedilab/android/mastodon/jobs/NotificationsWorker.java b/app/src/main/java/app/fedilab/android/mastodon/jobs/NotificationsWorker.java index 8e405468..f708c439 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/jobs/NotificationsWorker.java +++ b/app/src/main/java/app/fedilab/android/mastodon/jobs/NotificationsWorker.java @@ -105,7 +105,7 @@ public class NotificationsWorker extends Worker { public Result doWork() { setForegroundAsync(createForegroundInfo()); try { - List accounts = new Account(getApplicationContext()).getAll(); + List accounts = new Account(getApplicationContext()).getCrossAccounts(); for (BaseAccount account : accounts) { try { NotificationsHelper.task(getApplicationContext(), account.user_id + "@" + account.instance); diff --git a/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentHomeCacheSettings.java b/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentHomeCacheSettings.java new file mode 100644 index 00000000..3b1489a4 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentHomeCacheSettings.java @@ -0,0 +1,113 @@ +package app.fedilab.android.mastodon.ui.fragment.settings; +/* Copyright 2023 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.SharedPreferences; +import android.os.Bundle; + +import androidx.preference.ListPreference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; +import androidx.preference.SwitchPreferenceCompat; +import androidx.work.WorkManager; + +import app.fedilab.android.R; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.mastodon.helper.Helper; +import app.fedilab.android.mastodon.jobs.FetchHomeWorker; +import es.dmoral.toasty.Toasty; + + +public class FragmentHomeCacheSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.pref_home_cache); + createPref(); + } + + + private void createPref() { + + getPreferenceScreen().removeAll(); + addPreferencesFromResource(R.xml.pref_home_cache); + PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen == null) { + Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); + return; + } + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + SwitchPreferenceCompat SET_FETCH_HOME = findPreference(getString(R.string.SET_FETCH_HOME)); + if (SET_FETCH_HOME != null) { + boolean checked = sharedpreferences.getBoolean(getString(R.string.SET_FETCH_HOME) + MainActivity.currentUserID + MainActivity.currentInstance, false); + SET_FETCH_HOME.setChecked(checked); + } + + ListPreference SET_FETCH_HOME_DELAY_VALUE = findPreference(getString(R.string.SET_FETCH_HOME_DELAY_VALUE)); + if (SET_FETCH_HOME_DELAY_VALUE != null) { + String timeRefresh = sharedpreferences.getString(getString(R.string.SET_FETCH_HOME_DELAY_VALUE) + MainActivity.currentUserID + MainActivity.currentInstance, "60"); + SET_FETCH_HOME_DELAY_VALUE.setValue(timeRefresh); + } + } + + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + + if (key.compareToIgnoreCase(getString(R.string.SET_FETCH_HOME)) == 0) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + SwitchPreference SET_FETCH_HOME = findPreference(getString(R.string.SET_FETCH_HOME)); + if (SET_FETCH_HOME != null) { + editor.putBoolean(getString(R.string.SET_FETCH_HOME) + MainActivity.currentUserID + MainActivity.currentInstance, SET_FETCH_HOME.isChecked()); + editor.commit(); + if (SET_FETCH_HOME.isChecked()) { + FetchHomeWorker.setRepeatHome(requireActivity(), MainActivity.currentAccount); + } else { + WorkManager.getInstance(requireActivity()).cancelAllWorkByTag(Helper.WORKER_REFRESH_HOME + MainActivity.currentUserID + MainActivity.currentInstance); + } + } + } + if (key.compareToIgnoreCase(getString(R.string.SET_FETCH_HOME_DELAY_VALUE)) == 0) { + ListPreference SET_FETCH_HOME_DELAY_VALUE = findPreference(getString(R.string.SET_FETCH_HOME_DELAY_VALUE)); + if (SET_FETCH_HOME_DELAY_VALUE != null) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(getString(R.string.SET_FETCH_HOME_DELAY_VALUE) + MainActivity.currentUserID + MainActivity.currentInstance, SET_FETCH_HOME_DELAY_VALUE.getValue()); + editor.commit(); + FetchHomeWorker.setRepeatHome(requireActivity(), MainActivity.currentAccount); + } + } + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } + + +} diff --git a/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentSettingsCategories.java b/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentSettingsCategories.java index f67e940f..11b9cfc8 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentSettingsCategories.java +++ b/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentSettingsCategories.java @@ -112,6 +112,15 @@ public class FragmentSettingsCategories extends PreferenceFragmentCompat { }); } + Preference pref_category_key_home_cache = findPreference(getString(R.string.pref_category_key_home_cache)); + if (pref_category_key_home_cache != null) { + pref_category_key_home_cache.setOnPreferenceClickListener(preference -> { + NavController navController = Navigation.findNavController(requireActivity(), R.id.fragment_container); + navController.navigate(FragmentSettingsCategoriesDirections.Companion.categoriesToHomeCache()); + return false; + }); + } + Preference pref_category_key_theming = findPreference(getString(R.string.pref_category_key_theming)); if (pref_category_key_theming != null) { pref_category_key_theming.setOnPreferenceClickListener(preference -> { diff --git a/app/src/main/res/layouts/mastodon/values/strings.xml b/app/src/main/res/layouts/mastodon/values/strings.xml new file mode 100644 index 00000000..cc30206f --- /dev/null +++ b/app/src/main/res/layouts/mastodon/values/strings.xml @@ -0,0 +1,7 @@ + + + Fetch Home every + Home fetch time + Automatically fetch home messages + Home cache + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph_settings.xml b/app/src/main/res/navigation/nav_graph_settings.xml index d7284ea2..e52f44e4 100644 --- a/app/src/main/res/navigation/nav_graph_settings.xml +++ b/app/src/main/res/navigation/nav_graph_settings.xml @@ -53,6 +53,14 @@ app:popEnterAnim="@anim/pop_enter" app:popExitAnim="@anim/pop_exit" /> + + + + SET_PROXY_HOST SET_PROXY_PORT SET_DEFAULT_LOCALE_NEW + SET_FETCH_HOME_DELAY_VALUE + SET_SEND_CRASH_REPORTS SET_DISABLE_RELEASE_NOTES_ALERT SET_DISABLE_GIF @@ -1431,6 +1433,7 @@ SET_COMPOSE_LANGUAGE SET_LOGO_LAUNCHER SET_NOTIF_FOLLOW + SET_FETCH_HOME SET_NOTIF_MENTION SET_NOTIF_SHARE SET_NOTIF_FAVOURITE @@ -1996,6 +1999,7 @@ pref_category_interface pref_category_compose pref_category_privacy + pref_category_key_home_cache pref_category_theming pref_category_administration pref_category_languages diff --git a/app/src/main/res/xml/pref_categories.xml b/app/src/main/res/xml/pref_categories.xml index 826f560c..3cd38c88 100644 --- a/app/src/main/res/xml/pref_categories.xml +++ b/app/src/main/res/xml/pref_categories.xml @@ -46,6 +46,13 @@ app:icon="@drawable/ic_shield" app:key="@string/pref_category_key_privacy" /> + + + + + + + + + \ No newline at end of file