package app.fedilab.android.viewmodel.mastodon; /* Copyright 2021 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.Application; import android.os.Handler; import android.os.Looper; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.io.IOException; import java.util.List; import java.util.concurrent.TimeUnit; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.client.entities.StatusCache; import app.fedilab.android.client.entities.StatusDraft; import app.fedilab.android.client.mastodon.MastodonTimelinesService; import app.fedilab.android.client.mastodon.entities.Account; import app.fedilab.android.client.mastodon.entities.Conversation; import app.fedilab.android.client.mastodon.entities.Conversations; import app.fedilab.android.client.mastodon.entities.Marker; import app.fedilab.android.client.mastodon.entities.MastodonList; import app.fedilab.android.client.mastodon.entities.Pagination; import app.fedilab.android.client.mastodon.entities.Status; import app.fedilab.android.client.mastodon.entities.Statuses; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.TimelineHelper; import okhttp3.OkHttpClient; import retrofit2.Call; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class TimelinesVM extends AndroidViewModel { final OkHttpClient okHttpClient = new OkHttpClient.Builder() .readTimeout(60, TimeUnit.SECONDS) .connectTimeout(60, TimeUnit.SECONDS) .proxy(Helper.getProxy(getApplication().getApplicationContext())) .build(); private MutableLiveData> accountListMutableLiveData; private MutableLiveData> statusDraftListMutableLiveData; private MutableLiveData statusMutableLiveData; private MutableLiveData statusesMutableLiveData; private MutableLiveData conversationListMutableLiveData; private MutableLiveData mastodonListMutableLiveData; private MutableLiveData> mastodonListListMutableLiveData; private MutableLiveData markerMutableLiveData; public TimelinesVM(@NonNull Application application) { super(application); } private MastodonTimelinesService init(String instance) { Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://" + instance + "/api/v1/") .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build(); return retrofit.create(MastodonTimelinesService.class); } /** * Public timeline * * @param local Show only local statuses? Defaults to false. * @param remote Show only remote statuses? Defaults to false. * @param onlyMedia Show only statuses with media attached? Defaults to false. * @param maxId Return results older than this id * @param sinceId Return results newer than this id * @param minId Return results immediately newer than this id * @param limit Maximum number of results to return. Defaults to 20. * @return {@link LiveData} containing a {@link Statuses} */ public LiveData getPublic(String token, @NonNull String instance, Boolean local, Boolean remote, Boolean onlyMedia, String maxId, String sinceId, String minId, Integer limit) { MastodonTimelinesService mastodonTimelinesService = init(instance); statusesMutableLiveData = new MutableLiveData<>(); new Thread(() -> { Call> publicTlCall = mastodonTimelinesService.getPublic(token, local, remote, onlyMedia, maxId, sinceId, minId, limit); Statuses statuses = new Statuses(); if (publicTlCall != null) { try { Response> publicTlResponse = publicTlCall.execute(); if (publicTlResponse.isSuccessful()) { List notFilteredStatuses = publicTlResponse.body(); List filteredStatuses = TimelineHelper.filterStatus(getApplication(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.PUBLIC); statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); statuses.pagination = MastodonHelper.getPagination(publicTlResponse.headers()); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); mainHandler.post(myRunnable); }).start(); return statusesMutableLiveData; } /** * View public statuses containing the given hashtag. * * @param hashtag Content of a #hashtag, not including # symbol. * @param local If true, return only local statuses. Defaults to false. * @param onlyMedia If true, return only statuses with media attachments. Defaults to false. * @param maxId Return results older than this ID. * @param sinceId Return results newer than this ID. * @param minId Return results immediately newer than this ID. * @param limit Maximum number of results to return. Defaults to 20. * @return {@link LiveData} containing a {@link Statuses} */ public LiveData getHashTag(String token, @NonNull String instance, @NonNull String hashtag, boolean local, boolean onlyMedia, List all, List any, List none, String maxId, String sinceId, String minId, int limit) { statusesMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Statuses statuses = new Statuses(); Call> hashTagTlCall = mastodonTimelinesService.getHashTag(token, hashtag, local, onlyMedia, all, any, none, maxId, sinceId, minId, limit); if (hashTagTlCall != null) { try { Response> hashTagTlResponse = hashTagTlCall.execute(); if (hashTagTlResponse.isSuccessful()) { List notFilteredStatuses = hashTagTlResponse.body(); List filteredStatuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.PUBLIC); statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); statuses.pagination = MastodonHelper.getPagination(hashTagTlResponse.headers()); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); mainHandler.post(myRunnable); }).start(); return statusesMutableLiveData; } /** * View statuses from followed users. * * @param maxId Return results older than id * @param sinceId Return results newer than id * @param minId Return results immediately newer than id * @param limit Maximum number of results to return. Defaults to 20. * @param local Return only local statuses? * @return {@link LiveData} containing a {@link Statuses} */ public LiveData getHome(@NonNull String instance, String token, String maxId, String sinceId, String minId, int limit, boolean local) { statusesMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Statuses statuses = new Statuses(); Call> homeTlCall = mastodonTimelinesService.getHome(token, maxId, sinceId, minId, limit, local); if (homeTlCall != null) { try { Response> homeTlResponse = homeTlCall.execute(); if (homeTlResponse.isSuccessful()) { List notFilteredStatuses = homeTlResponse.body(); List filteredStatuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), notFilteredStatuses, TimelineHelper.FilterTimeLineType.HOME); statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses); statuses.pagination = MastodonHelper.getPagination(homeTlResponse.headers()); for (Status status : statuses.statuses) { StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); StatusCache statusCache = new StatusCache(); statusCache.instance = instance; statusCache.user_id = BaseMainActivity.currentUserID; statusCache.status = status; statusCache.type = StatusCache.CacheEnum.HOME; statusCache.status_id = status.id; try { statusCacheDAO.insertOrUpdate(statusCache); } catch (DBException e) { e.printStackTrace(); } } } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); mainHandler.post(myRunnable); }).start(); return statusesMutableLiveData; } /** * Get home status from cache * * @param instance String - instance * @param user_id String - user id * @param maxId String - max id * @param minId String - min id * @return LiveData */ public LiveData getHomeCache(@NonNull String instance, String user_id, String maxId, String minId) { statusesMutableLiveData = new MutableLiveData<>(); new Thread(() -> { StatusCache statusCacheDAO = new StatusCache(getApplication().getApplicationContext()); Statuses statuses = null; try { statuses = statusCacheDAO.geStatuses(StatusCache.CacheEnum.HOME, instance, user_id, maxId, minId); if (statuses != null) { statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statuses.statuses); if (statuses.statuses != null && statuses.statuses.size() > 0) { statuses.pagination = new Pagination(); statuses.pagination.min_id = statuses.statuses.get(0).id; statuses.pagination.max_id = statuses.statuses.get(statuses.statuses.size() - 1).id; } } } catch (DBException e) { e.printStackTrace(); } Handler mainHandler = new Handler(Looper.getMainLooper()); Statuses finalStatuses = statuses; Runnable myRunnable = () -> statusesMutableLiveData.setValue(finalStatuses); mainHandler.post(myRunnable); }).start(); return statusesMutableLiveData; } /** * Get user drafts * * @param account app.fedilab.android.client.entities.Account * @return LiveData> */ public LiveData> getDrafts(app.fedilab.android.client.entities.Account account) { statusDraftListMutableLiveData = new MutableLiveData<>(); new Thread(() -> { List statusCacheDAO = null; try { statusCacheDAO = new StatusDraft(getApplication().getApplicationContext()).geStatusDraftList(account); } catch (DBException e) { e.printStackTrace(); } Handler mainHandler = new Handler(Looper.getMainLooper()); List finalStatusCacheDAO = statusCacheDAO; Runnable myRunnable = () -> statusDraftListMutableLiveData.setValue(finalStatusCacheDAO); mainHandler.post(myRunnable); }).start(); return statusDraftListMutableLiveData; } /** * View statuses in the given list timeline. * * @param listId Local ID of the list in the database. * @param maxId Return results older than this ID. * @param sinceId Return results newer than this ID. * @param minId Return results immediately newer than this ID. * @param limit Maximum number of results to return. Defaults to 20.Return results older than this ID. * @return {@link LiveData} containing a {@link Statuses} */ public LiveData getList(@NonNull String instance, String token, @NonNull String listId, String maxId, String sinceId, String minId, int limit) { statusesMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Statuses statuses = new Statuses(); Call> listTlCall = mastodonTimelinesService.getList(token, listId, maxId, sinceId, minId, limit); if (listTlCall != null) { try { Response> listTlResponse = listTlCall.execute(); if (listTlResponse.isSuccessful()) { statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), listTlResponse.body()); statuses.pagination = MastodonHelper.getPagination(listTlResponse.headers()); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses); mainHandler.post(myRunnable); }).start(); return statusesMutableLiveData; } /** * Show conversations * * @return {@link LiveData} containing a {@link Conversations} */ public LiveData getConversations(@NonNull String instance, String token, String maxId, String sinceId, String minId, int limit) { conversationListMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Conversations conversations = null; Call> conversationsCall = mastodonTimelinesService.getConversations(token, maxId, sinceId, minId, limit); if (conversationsCall != null) { conversations = new Conversations(); try { Response> conversationsResponse = conversationsCall.execute(); if (conversationsResponse.isSuccessful()) { conversations.conversations = conversationsResponse.body(); if (conversations.conversations != null) { for (Conversation conversation : conversations.conversations) { conversation.last_status = SpannableHelper.convertStatus(getApplication().getApplicationContext(), conversation.last_status); } } conversations.pagination = MastodonHelper.getPagination(conversationsResponse.headers()); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); Conversations finalConversations = conversations; Runnable myRunnable = () -> conversationListMutableLiveData.setValue(finalConversations); mainHandler.post(myRunnable); }).start(); return conversationListMutableLiveData; } /** * Remove conversation * * @param id ID of the conversation */ public void deleteConversation(@NonNull String instance, String token, @NonNull String id) { MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Call deleteStatusCall = mastodonTimelinesService.deleteConversation(token, id); if (deleteStatusCall != null) { try { deleteStatusCall.execute(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } /** * Mark a conversation as read * * @param id ID of the conversation * @return {@link LiveData} containing a {@link Status} */ public LiveData markReadConversation(@NonNull String instance, String token, @NonNull String id) { statusMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Status status = null; Call markReadConversationCall = mastodonTimelinesService.markReadConversation(token, id); if (markReadConversationCall != null) { try { Response markReadConversationResponse = markReadConversationCall.execute(); if (markReadConversationResponse.isSuccessful()) { status = markReadConversationResponse.body(); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); Status finalStatus = status; Runnable myRunnable = () -> statusMutableLiveData.setValue(finalStatus); mainHandler.post(myRunnable); }).start(); return statusMutableLiveData; } /** * Fetch all lists that the user owns. * * @return {@link LiveData} containing a {@link List} of {@link MastodonList}s */ public LiveData> getLists(@NonNull String instance, String token) { mastodonListListMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { List mastodonListList = null; Call> getListsCall = mastodonTimelinesService.getLists(token); if (getListsCall != null) { try { Response> getListsResponse = getListsCall.execute(); if (getListsResponse.isSuccessful()) { mastodonListList = getListsResponse.body(); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); List finalMastodonListList = mastodonListList; Runnable myRunnable = () -> mastodonListListMutableLiveData.setValue(finalMastodonListList); mainHandler.post(myRunnable); }).start(); return mastodonListListMutableLiveData; } /** * Fetch the list with the given ID. Used for verifying the title of a list, * and which replies to show within that list. * * @param id ID of the list * @return {@link LiveData} containing a {@link MastodonList} */ public LiveData getList(@NonNull String instance, String token, @NonNull String id) { mastodonListMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { MastodonList mastodonList = null; Call getListCall = mastodonTimelinesService.getList(token, id); if (getListCall != null) { try { Response getListResponse = getListCall.execute(); if (getListResponse.isSuccessful()) { mastodonList = getListResponse.body(); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); MastodonList finalMastodonList = mastodonList; Runnable myRunnable = () -> mastodonListMutableLiveData.setValue(finalMastodonList); mainHandler.post(myRunnable); }).start(); return mastodonListMutableLiveData; } /** * Create a new list. * * @param title The title of the list to be created. * @param repliesPolicy Enumerable oneOf "followed", "list", "none". Defaults to "list". * @return {@link LiveData} containing a {@link MastodonList} */ public LiveData createList(@NonNull String instance, String token, @NonNull String title, String repliesPolicy) { mastodonListMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { MastodonList mastodonList = null; Call createListCall = mastodonTimelinesService.createList(token, title, repliesPolicy); if (createListCall != null) { try { Response createListResponse = createListCall.execute(); if (createListResponse.isSuccessful()) { mastodonList = createListResponse.body(); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); MastodonList finalMastodonList = mastodonList; Runnable myRunnable = () -> mastodonListMutableLiveData.setValue(finalMastodonList); mainHandler.post(myRunnable); }).start(); return mastodonListMutableLiveData; } /** * Change the title of a list, or which replies to show. * * @param id ID of the list * @param title The title of the list to be updated. * @param repliesPolicy Enumerable oneOf "followed", "list", "none". * @return {@link LiveData} containing a {@link MastodonList} */ public LiveData updateList(@NonNull String instance, String token, @NonNull String id, String title, String repliesPolicy) { mastodonListMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { MastodonList mastodonList = null; Call updateListCall = mastodonTimelinesService.updateList(token, id, title, repliesPolicy); if (updateListCall != null) { try { Response updateListResponse = updateListCall.execute(); if (updateListResponse.isSuccessful()) { mastodonList = updateListResponse.body(); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); MastodonList finalMastodonList = mastodonList; Runnable myRunnable = () -> mastodonListMutableLiveData.setValue(finalMastodonList); mainHandler.post(myRunnable); }).start(); return mastodonListMutableLiveData; } /** * Delete a list * * @param id ID of the list */ public void deleteList(@NonNull String instance, String token, @NonNull String id) { MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Call deleteListCall = mastodonTimelinesService.deleteList(token, id); if (deleteListCall != null) { try { deleteListCall.execute(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } /** * View accounts in list * * @param id ID of the list * @return {@link LiveData} containing a {@link List} of {@link Account}s */ public LiveData> getAccountsInList(@NonNull String instance, String token, @NonNull String id, String maxId, String sinceId, int limit) { accountListMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { List accountList = null; Call> getAccountsInListCall = mastodonTimelinesService.getAccountsInList(token, id, maxId, sinceId, limit); if (getAccountsInListCall != null) { try { Response> getAccountsInListResponse = getAccountsInListCall.execute(); if (getAccountsInListResponse.isSuccessful()) { accountList = getAccountsInListResponse.body(); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); List finalAccountList = accountList; Runnable myRunnable = () -> accountListMutableLiveData.setValue(finalAccountList); mainHandler.post(myRunnable); }).start(); return accountListMutableLiveData; } /** * Add accounts to the given list. Note that the user must be following these accounts. * * @param listId ID of the list * @param accountIds Array of account IDs to add to the list. */ public void addAccountsList(@NonNull String instance, String token, @NonNull String listId, @NonNull List accountIds) { MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Call addAccountsListCall = mastodonTimelinesService.addAccountsList(token, listId, accountIds); if (addAccountsListCall != null) { try { addAccountsListCall.execute(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } /** * Remove accounts from the given list. * * @param listId ID of the list * @param accountIds Array of account IDs to remove from the list. */ public void deleteAccountsList(@NonNull String instance, String token, @NonNull String listId, @NonNull List accountIds) { MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Call deleteAccountsListCall = mastodonTimelinesService.deleteAccountsList(token, listId, accountIds); if (deleteAccountsListCall != null) { try { deleteAccountsListCall.execute(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } /** * Get saved timeline position * * @param timeline Array of markers to fetch. String enum anyOf "home", "notifications". * If not provided, an empty object will be returned. * @return {@link LiveData} containing a {@link Marker} */ public LiveData getMarker(@NonNull String instance, String token, @NonNull List timeline) { markerMutableLiveData = new MutableLiveData<>(); MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Marker marker = null; Call getMarkerCall = mastodonTimelinesService.getMarker(token, timeline); if (getMarkerCall != null) { try { Response getMarkerResponse = getMarkerCall.execute(); if (getMarkerResponse.isSuccessful()) { marker = getMarkerResponse.body(); } } catch (IOException e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); Marker finalMarker = marker; Runnable myRunnable = () -> markerMutableLiveData.setValue(finalMarker); mainHandler.post(myRunnable); }).start(); return markerMutableLiveData; } /** * Save position in timeline * * @param homeLastReadId ID of the last status read in the home timeline. * @param notificationLastReadId ID of the last notification read. */ public void addMarker(@NonNull String instance, String token, String homeLastReadId, String notificationLastReadId) { MastodonTimelinesService mastodonTimelinesService = init(instance); new Thread(() -> { Call addMarkerCall = mastodonTimelinesService.addMarker(token, homeLastReadId, notificationLastReadId); if (addMarkerCall != null) { try { addMarkerCall.execute(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } }