From 53628d010310aa74ca10ad13ece0c86649e22c8f Mon Sep 17 00:00:00 2001 From: charlag Date: Thu, 21 Mar 2019 21:23:52 +0100 Subject: [PATCH] Start handling expired authentication. --- .../14.json | 680 ++++++++++++++++++ .../keylesspalace/tusky/FiltersActivity.kt | 57 +- .../com/keylesspalace/tusky/LoginActivity.kt | 34 +- .../com/keylesspalace/tusky/MainActivity.java | 80 ++- .../keylesspalace/tusky/TuskyApplication.java | 14 +- .../keylesspalace/tusky/db/AccountEntity.kt | 4 + .../keylesspalace/tusky/db/AccountManager.kt | 57 +- .../keylesspalace/tusky/db/AppDatabase.java | 13 +- .../keylesspalace/tusky/di/NetworkModule.kt | 2 +- .../keylesspalace/tusky/entity/AccessToken.kt | 4 +- .../tusky/fragment/TimelineFragment.java | 29 +- .../tusky/network/MastodonApi.java | 7 +- .../tusky/viewmodel/EditProfileViewModel.kt | 47 +- gradle.properties | 4 +- 14 files changed, 880 insertions(+), 152 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json new file mode 100644 index 000000000..f1523a53d --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json @@ -0,0 +1,680 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "c2bd59061bbebebae98729ab1a7d726f", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `expiresAt` TEXT, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "expiresAt", + "columnName": "expiresAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"c2bd59061bbebebae98729ab1a7d726f\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index eafad499f..796f35ec7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -1,6 +1,7 @@ package com.keylesspalace.tusky import android.os.Bundle +import android.util.Log import android.view.MenuItem import android.widget.AdapterView import android.widget.ArrayAdapter @@ -10,6 +11,8 @@ import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.network.MastodonApi +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from +import com.uber.autodispose.autoDisposable import kotlinx.android.synthetic.main.activity_filters.* import kotlinx.android.synthetic.main.dialog_filter.* import kotlinx.android.synthetic.main.toolbar_basic.* @@ -19,14 +22,14 @@ import retrofit2.Callback import retrofit2.Response import javax.inject.Inject -class FiltersActivity: BaseActivity() { +class FiltersActivity : BaseActivity() { @Inject lateinit var api: MastodonApi @Inject lateinit var eventHub: EventHub - private lateinit var context : String + private lateinit var context: String private lateinit var filters: MutableList private lateinit var dialog: AlertDialog @@ -37,29 +40,29 @@ class FiltersActivity: BaseActivity() { private fun updateFilter(filter: Filter, itemIndex: Int) { api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, filter.expiresAt) - .enqueue(object: Callback{ - override fun onFailure(call: Call, t: Throwable) { - Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show() - } + .enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show() + } - override fun onResponse(call: Call, response: Response) { - val updatedFilter = response.body()!! - if (updatedFilter.context.contains(context)) { - filters[itemIndex] = updatedFilter - } else { - filters.removeAt(itemIndex) + override fun onResponse(call: Call, response: Response) { + val updatedFilter = response.body()!! + if (updatedFilter.context.contains(context)) { + filters[itemIndex] = updatedFilter + } else { + filters.removeAt(itemIndex) + } + refreshFilterDisplay() + eventHub.dispatch(PreferenceChangedEvent(context)) } - refreshFilterDisplay() - eventHub.dispatch(PreferenceChangedEvent(context)) - } - }) + }) } private fun deleteFilter(itemIndex: Int) { val filter = filters[itemIndex] if (filter.context.count() == 1) { // This is the only context for this filter; delete it - api.deleteFilter(filters[itemIndex].id).enqueue(object: Callback { + api.deleteFilter(filters[itemIndex].id).enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show() } @@ -80,7 +83,7 @@ class FiltersActivity: BaseActivity() { } private fun createFilter(phrase: String) { - api.createFilter(phrase, listOf(context), false, true, "").enqueue(object: Callback { + api.createFilter(phrase, listOf(context), false, true, "").enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { filters.add(response.body()!!) refreshFilterDisplay() @@ -97,7 +100,7 @@ class FiltersActivity: BaseActivity() { dialog = AlertDialog.Builder(this@FiltersActivity) .setTitle(R.string.filter_addition_dialog_title) .setView(R.layout.dialog_filter) - .setPositiveButton(android.R.string.ok){ _, _ -> + .setPositiveButton(android.R.string.ok) { _, _ -> createFilter(dialog.phraseEditText.text.toString()) } .setNeutralButton(android.R.string.cancel, null) @@ -132,16 +135,12 @@ class FiltersActivity: BaseActivity() { } private fun loadFilters() { - api.filters.enqueue(object : Callback> { - override fun onResponse(call: Call>, response: Response>) { - filters = response.body()!!.filter { filter -> filter.context.contains(context) }.toMutableList() - refreshFilterDisplay() - } - - override fun onFailure(call: Call>, t: Throwable) { - // Anything? - } - }) + api.filters.autoDisposable(from(this)) + .subscribe({ response -> + filters = response + .filter { filter -> filter.context.contains(context) } + .toMutableList() + }, { t -> Log.w(javaClass.simpleName, "Failed to get filters", t) }) } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt index 1b9810a93..12456966d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt @@ -100,7 +100,7 @@ class LoginActivity : BaseActivity(), Injectable { override fun finish() { super.finish() - if(isAdditionalLogin()) { + if (isAdditionalLogin()) { overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right) } } @@ -228,30 +228,17 @@ class LoginActivity : BaseActivity(), Injectable { setLoading(true) /* Since authorization has succeeded, the final step to log in is to exchange * the authorization code for an access token. */ - val callback = object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - onLoginSuccess(response.body()!!.accessToken) - } else { + val disposable = mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, + redirectUri, code, /*rerreshToken*/null, /*grantType=*/"authorization_code") + .subscribe({ tokenResponse -> + onLoginSuccess(tokenResponse) + }, { t -> setLoading(false) domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) Log.e(TAG, String.format("%s %s", getString(R.string.error_retrieving_oauth_token), - response.message())) - } - } - - override fun onFailure(call: Call, t: Throwable) { - setLoading(false) - domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) - Log.e(TAG, String.format("%s %s", - getString(R.string.error_retrieving_oauth_token), - t.message)) - } - } - - mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code, - "authorization_code").enqueue(callback) + t.message)) + }) } else if (error != null) { /* Authorization failed. Put the error response where the user can read it and they * can try again. */ @@ -286,11 +273,12 @@ class LoginActivity : BaseActivity(), Injectable { return intent.getBooleanExtra(LOGIN_MODE, false) } - private fun onLoginSuccess(accessToken: String) { + private fun onLoginSuccess(tokenResponse: AccessToken) { setLoading(true) - accountManager.addAccount(accessToken, domain) + accountManager.addAccount(tokenResponse.accessToken, tokenResponse.refreshToken, + tokenResponse.expiresIn, domain, clientId!!, clientSecret!!) val intent = Intent(this, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 2c581c260..3abff7af4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -15,27 +15,18 @@ package com.keylesspalace.tusky; -import androidx.lifecycle.Lifecycle; import android.content.Intent; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.tabs.TabLayout; -import androidx.emoji.text.EmojiCompat; -import androidx.fragment.app.Fragment; -import androidx.core.content.ContextCompat; -import androidx.viewpager.widget.ViewPager; -import androidx.appcompat.app.AlertDialog; import android.os.Handler; -import android.util.Log; import android.view.KeyEvent; import android.widget.ImageButton; import android.widget.ImageView; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.tabs.TabLayout; import com.keylesspalace.tusky.appstore.CacheUpdater; import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.MainTabsChangedEvent; @@ -69,12 +60,20 @@ import java.util.List; import javax.inject.Inject; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.emoji.text.EmojiCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Lifecycle; +import androidx.viewpager.widget.ViewPager; import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; import dagger.android.support.HasSupportFragmentInjector; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; -import retrofit2.Call; -import retrofit2.Callback; +import kotlin.Unit; +import retrofit2.HttpException; import retrofit2.Response; import static com.keylesspalace.tusky.util.MediaUtilsKt.deleteStaleCachedMedia; @@ -119,7 +118,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if(accountManager.getActiveAccount() == null) { + if (accountManager.getActiveAccount() == null) { // will be redirected to LoginActivity by BaseActivity return; } @@ -420,9 +419,9 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut .setIcon(tabs.get(i).getIcon()) .setContentDescription(tabs.get(i).getText()); tabLayout.addTab(tab); - if(tabs.get(i).getId().equals(TabDataKt.NOTIFICATIONS)) { + if (tabs.get(i).getId().equals(TabDataKt.NOTIFICATIONS)) { notificationTabPosition = i; - if(selectNotificationTab) { + if (selectNotificationTab) { tab.select(); } } @@ -505,22 +504,22 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut } private void fetchUserInfo() { - - mastodonApi.accountVerifyCredentials().enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful()) { - onFetchUserInfoSuccess(response.body()); - } else { - onFetchUserInfoFailure(new Exception(response.message())); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onFetchUserInfoFailure((Exception) t); - } - }); + mastodonApi.accountVerifyCredentials() + .onErrorResumeNext((verifyError) -> { + if (isNotAuthenticatedException(verifyError)) { + return accountManager.recoverAuthentication() + .toSingleDefault(Unit.INSTANCE) + .flatMap(__ -> mastodonApi.accountVerifyCredentials()); + } + return Single.error(verifyError); + }) + .as(autoDisposable(from(this))) + .subscribe( + this::onFetchUserInfoSuccess, + (err) -> { + accountManager.logActiveAccountOut(); + redirectIfNotLoggedIn(); + }); } private void onFetchUserInfoSuccess(Account me) { @@ -544,7 +543,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut .withSelectable(false) .withIcon(GoogleMaterial.Icon.gmd_person_add); drawer.addItemAtPosition(followRequestsItem, 3); - } else if(!me.getLocked()){ + } else if (!me.getLocked()) { drawer.removeItem(DRAWER_ITEM_FOLLOW_REQUESTS); } @@ -556,7 +555,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut List allAccounts = accountManager.getAllAccountsOrderedByActive(); - List profiles = new ArrayList<>(allAccounts.size()+1); + List profiles = new ArrayList<>(allAccounts.size() + 1); for (AccountEntity acc : allAccounts) { CharSequence emojifiedName = CustomEmojiHelper.emojifyString(acc.getDisplayName(), acc.getEmojis(), headerResult.getView()); @@ -574,7 +573,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut } // reuse the already existing "add account" item - for (IProfile profile: headerResult.getProfiles()) { + for (IProfile profile : headerResult.getProfiles()) { if (profile.getIdentifier() == DRAWER_ITEM_ADD_ACCOUNT) { profiles.add(profile); break; @@ -585,10 +584,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut headerResult.setActiveProfile(accountManager.getActiveAccount().getId()); } - private void onFetchUserInfoFailure(Exception exception) { - Log.e(TAG, "Failed to fetch user info. " + exception.getMessage()); - } - @Nullable @Override public FloatingActionButton getActionButton() { @@ -600,4 +595,11 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut return fragmentInjector; } + private boolean isNotAuthenticatedException(Throwable error) { + if (error instanceof HttpException) { + Response response = ((HttpException) error).response(); + return response.code() == 401 || response.code() == 403; + } + return false; + } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java index 04e67a674..3ced34e4e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java @@ -18,18 +18,17 @@ package com.keylesspalace.tusky; import android.app.Activity; import android.app.Application; import android.app.Service; -import androidx.room.Room; import android.content.BroadcastReceiver; import android.content.Context; import android.content.res.Configuration; import android.preference.PreferenceManager; -import androidx.emoji.text.EmojiCompat; import com.evernote.android.job.JobManager; import com.jakewharton.picasso.OkHttp3Downloader; import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.db.AppDatabase; import com.keylesspalace.tusky.di.AppInjector; +import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.util.EmojiCompatFont; import com.keylesspalace.tusky.util.LocaleManager; import com.keylesspalace.tusky.util.NotificationPullJobCreator; @@ -41,6 +40,8 @@ import java.security.Security; import javax.inject.Inject; +import androidx.emoji.text.EmojiCompat; +import androidx.room.Room; import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; import dagger.android.HasActivityInjector; @@ -48,6 +49,8 @@ import dagger.android.HasBroadcastReceiverInjector; import dagger.android.HasServiceInjector; import okhttp3.OkHttpClient; +import static com.keylesspalace.tusky.db.AppDatabase.MIGRATION_13_14; + public class TuskyApplication extends Application implements HasActivityInjector, HasServiceInjector, HasBroadcastReceiverInjector { @Inject DispatchingAndroidInjector dispatchingAndroidInjector; @@ -59,6 +62,8 @@ public class TuskyApplication extends Application implements HasActivityInjector NotificationPullJobCreator notificationPullJobCreator; @Inject OkHttpClient okHttpClient; + @Inject + MastodonApi mastodonApi; private AppDatabase appDatabase; private AccountManager accountManager; @@ -78,9 +83,10 @@ public class TuskyApplication extends Application implements HasActivityInjector .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8, AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11, - AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13) + AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, + MIGRATION_13_14) .build(); - accountManager = new AccountManager(appDatabase); + accountManager = new AccountManager(appDatabase, () -> mastodonApi); serviceLocator = new ServiceLocator() { @Override public T get(Class clazz) { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt index 5b3390e2e..4bc5b4f92 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -31,6 +31,10 @@ import com.keylesspalace.tusky.entity.Status data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long, val domain: String, var accessToken: String, + var refreshToken: String?, + var expiresAt: String?, + var clientId: String?, + var clientSecret: String?, var isActive: Boolean, var accountId: String = "", var username: String = "", diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index b02d8c75a..067954454 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -18,6 +18,9 @@ package com.keylesspalace.tusky.db import android.util.Log import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import dagger.Lazy +import io.reactivex.Completable /** * This class caches the account database and handles all account related operations @@ -26,7 +29,11 @@ import com.keylesspalace.tusky.entity.Status private const val TAG = "AccountManager" -class AccountManager(db: AppDatabase) { +class AccountManager( + db: AppDatabase, + // Inject API lazily because of the cyclic dependency. Should be re-organized instead. + private val mastodonApi: Lazy +) { @Volatile var activeAccount: AccountEntity? = null @@ -49,7 +56,8 @@ class AccountManager(db: AppDatabase) { * @param accessToken the access token for the new account * @param domain the domain of the accounts Mastodon instance */ - fun addAccount(accessToken: String, domain: String) { + fun addAccount(accessToken: String, refreshToken: String?, expiresIn: String?, + domain: String, clientId: String, clientSecret: String) { activeAccount?.let { it.isActive = false @@ -60,7 +68,16 @@ class AccountManager(db: AppDatabase) { val maxAccountId = accounts.maxBy { it.id }?.id ?: 0 val newAccountId = maxAccountId + 1 - activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(), accessToken = accessToken, isActive = true) + activeAccount = AccountEntity( + id = newAccountId, + domain = domain.toLowerCase(), + accessToken = accessToken, + refreshToken = refreshToken, + expiresAt = expiresIn, + clientId = clientId, + clientSecret = clientSecret, + isActive = true + ) } @@ -74,7 +91,6 @@ class AccountManager(db: AppDatabase) { Log.d(TAG, "saveAccount: saving account with id " + account.id) accountDao.insertOrReplace(account) } - } /** @@ -190,4 +206,37 @@ class AccountManager(db: AppDatabase) { } } + /** + * Uses refresh_token to get a new token if possible. If it fails, returns false + */ + fun recoverAuthentication(): Completable { + val activeAccount = this.activeAccount + ?: throw IllegalStateException("Tried to refresh token but no active account present") + + return if (activeAccount.refreshToken != null) { + refreshToken(activeAccount) + } else { + Completable.error(NoRefreshTokenException()) + } + } + + private fun refreshToken(account: AccountEntity): Completable { + return mastodonApi.get().fetchOAuthToken( + account.domain, + account.clientId, + account.clientSecret, + null, + null, + account.refreshToken, + "refresh_token" + ).doOnSuccess { tokenResponse -> + account.accessToken = tokenResponse.accessToken + // Server may or may not issue new refreshToken as per OAuth spec. + // I assume that it means "use the old one" but I may be wrong + // ref: https://tools.ietf.org/html/rfc6749#section-1.5 + account.refreshToken = tokenResponse.refreshToken ?: account.refreshToken + }.ignoreElement() + } + + class NoRefreshTokenException : RuntimeException() } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 83e7898a1..470f3e756 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -30,7 +30,7 @@ import androidx.annotation.NonNull; @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 13) + }, version = 14) public abstract class AppDatabase extends RoomDatabase { public abstract TootDao tootDao(); @@ -264,4 +264,15 @@ public abstract class AppDatabase extends RoomDatabase { } }; + + public static final Migration MIGRATION_13_14 = new Migration(13, 14) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `refreshToken` TEXT"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `expiresIn` TEXT"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientId` TEXT"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientSecret` TEXT"); + } + }; + } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index bdbe70493..652c61cbd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -77,7 +77,7 @@ class NetworkModule { .apply { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) if (BuildConfig.DEBUG) { - addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) + addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) } } .build() diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt b/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt index 181078839..2d40f8317 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt @@ -18,5 +18,7 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName data class AccessToken( - @SerializedName("access_token") val accessToken: String + @SerializedName("access_token") val accessToken: String, + @SerializedName("refresh_token") val refreshToken: String?, + @SerializedName("expires_in") val expiresIn: String? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index edc415c0b..3d682e68a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -29,7 +29,6 @@ import android.widget.ProgressBar; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; -import io.reactivex.Observable; import com.keylesspalace.tusky.AccountListActivity; import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.R; @@ -66,10 +65,9 @@ import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.viewdata.StatusViewData; -import java.util.ArrayList; import java.io.IOException; -import java.util.Collections; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; @@ -95,6 +93,7 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import at.connyduck.sparkbutton.helpers.Utils; +import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import kotlin.Unit; import kotlin.collections.CollectionsKt; @@ -317,17 +316,12 @@ public class TimelineFragment extends SFragment implements } private void reloadFilters(boolean refresh) { - mastodonApi.getFilters().enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - applyFilters(response.body(), refresh); - } - - @Override - public void onFailure(Call> call, Throwable t) { - Log.e(TAG, "Error getting filters from server"); - } - }); + mastodonApi.getFilters() + .as(autoDisposable(from(this))) + .subscribe( + (filters) -> applyFilters(filters, refresh), + (t) -> Log.e(TAG, "Error getting filters from server") + ); } private void setupTimelinePreferences() { @@ -348,7 +342,7 @@ public class TimelineFragment extends SFragment implements private static boolean filterContextMatchesKind(Kind kind, List filterContext) { // home, notifications, public, thread - switch(kind) { + switch (kind) { case HOME: return filterContext.contains(Filter.HOME); case PUBLIC_FEDERATED: @@ -1314,13 +1308,12 @@ public class TimelineFragment extends SFragment implements @Nullable @Override public Object getChangePayload(@NonNull StatusViewData oldItem, @NonNull StatusViewData newItem) { - if (oldItem.deepEquals(newItem)){ + if (oldItem.deepEquals(newItem)) { //If items are equal - update timestamp only List payload = new ArrayList<>(); payload.add(StatusBaseViewHolder.Key.KEY_CREATED); return payload; - } - else + } else // If items are different - update a whole view holder return null; } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index 38574e147..dcafb2ffa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -173,7 +173,7 @@ public interface MastodonApi { Single unpinStatus(@Path("id") String statusId); @GET("api/v1/accounts/verify_credentials") - Call accountVerifyCredentials(); + Single accountVerifyCredentials(); @FormUrlEncoded @PATCH("api/v1/accounts/update_credentials") @@ -300,12 +300,13 @@ public interface MastodonApi { @FormUrlEncoded @POST("oauth/token") - Call fetchOAuthToken( + Single fetchOAuthToken( @Header(DOMAIN_HEADER) String domain, @Field("client_id") String clientId, @Field("client_secret") String clientSecret, @Field("redirect_uri") String redirectUri, @Field("code") String code, + @Field("refresh_token") String refreshToken, @Field("grant_type") String grantType ); @@ -348,7 +349,7 @@ public interface MastodonApi { @GET("/api/v1/conversations") Call> getConversations(@Nullable @Query("max_id") String maxId, @Query("limit") int limit); @GET("api/v1/filters") - Call> getFilters(); + Single> getFilters(); @FormUrlEncoded @POST("api/v1/filters") diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index 6fec56f84..cd6227f80 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -31,6 +31,8 @@ import com.keylesspalace.tusky.entity.StringField import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.* import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo import io.reactivex.schedulers.Schedulers import okhttp3.MediaType import okhttp3.MultipartBody @@ -51,10 +53,10 @@ private const val AVATAR_FILE_NAME = "avatar.png" private const val TAG = "EditProfileViewModel" -class EditProfileViewModel @Inject constructor( +class EditProfileViewModel @Inject constructor( private val mastodonApi: MastodonApi, private val eventHub: EventHub -): ViewModel() { +) : ViewModel() { val profileData = MutableLiveData>() val avatarData = MutableLiveData>() @@ -64,31 +66,21 @@ class EditProfileViewModel @Inject constructor( private var oldProfileData: Account? = null private val callList: MutableList> = mutableListOf() + private val disposable = CompositeDisposable() fun obtainProfile() { - if(profileData.value == null || profileData.value is Error) { + if (profileData.value == null || profileData.value is Error) { profileData.postValue(Loading()) - val call = mastodonApi.accountVerifyCredentials() - call.enqueue(object : Callback { - override fun onResponse(call: Call, - response: Response) { - if (response.isSuccessful) { - val profile = response.body() + mastodonApi.accountVerifyCredentials() + .subscribe({ profile -> oldProfileData = profile profileData.postValue(Success(profile)) - } else { + }, { profileData.postValue(Error()) - } - } - - override fun onFailure(call: Call, t: Throwable) { - profileData.postValue(Error()) - } - }) - - callList.add(call) + }) + .addTo(disposable) } } @@ -132,17 +124,19 @@ class EditProfileViewModel @Inject constructor( } bitmap - }.subscribeOn(Schedulers.io()) + } + .subscribeOn(Schedulers.io()) .subscribe({ imageLiveData.postValue(Success(it)) }, { imageLiveData.postValue(Error()) }) + .addTo(disposable) } fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List, context: Context) { - if(saveData.value is Loading || profileData.value !is Success) { + if (saveData.value is Loading || profileData.value !is Success) { return } @@ -199,7 +193,7 @@ class EditProfileViewModel @Inject constructor( val newProfileData = response.body() if (!response.isSuccessful || newProfileData == null) { val errorResponse = response.errorBody()?.string() - val errorMsg = if(!errorResponse.isNullOrBlank()) { + val errorMsg = if (!errorResponse.isNullOrBlank()) { try { JSONObject(errorResponse).optString("error", null) } catch (e: JSONException) { @@ -224,7 +218,7 @@ class EditProfileViewModel @Inject constructor( // cache activity state for rotation change fun updateProfile(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List) { - if(profileData.value is Success) { + if (profileData.value is Success) { val newProfileSource = profileData.value?.data?.source?.copy(note = newNote, fields = newFields) val newProfile = profileData.value?.data?.copy(displayName = newDisplayName, locked = newLocked, source = newProfileSource) @@ -236,7 +230,7 @@ class EditProfileViewModel @Inject constructor( private fun calculateFieldToUpdate(newField: StringField?, fieldsUnchanged: Boolean): Pair? { - if(fieldsUnchanged || newField == null) { + if (fieldsUnchanged || newField == null) { return null } return Pair( @@ -267,9 +261,8 @@ class EditProfileViewModel @Inject constructor( } override fun onCleared() { - callList.forEach { - it.cancel() - } + disposable.clear() + callList.forEach(Call<*>::cancel) } diff --git a/gradle.properties b/gradle.properties index 8e49a0651..fdc673ca9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,10 +10,10 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx4096m +org.gradle.jvmargs=-Xmx1024m # use parallel execution -org.gradle.parallel=true +org.gradle.parallel=false android.enableJetifier=true android.useAndroidX=true