Browse Source

Start handling expired authentication.

expired-auth
charlag 7 years ago
parent
commit
53628d0103
  1. 680
      app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json
  2. 57
      app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt
  3. 34
      app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt
  4. 80
      app/src/main/java/com/keylesspalace/tusky/MainActivity.java
  5. 14
      app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
  6. 4
      app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
  7. 57
      app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
  8. 13
      app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
  9. 2
      app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
  10. 4
      app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt
  11. 29
      app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
  12. 7
      app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
  13. 47
      app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt
  14. 4
      gradle.properties

680
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\")"
]
}
}

57
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<Filter>
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<Filter>{
override fun onFailure(call: Call<Filter>, t: Throwable) {
Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show()
}
.enqueue(object : Callback<Filter> {
override fun onFailure(call: Call<Filter>, t: Throwable) {
Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
val updatedFilter = response.body()!!
if (updatedFilter.context.contains(context)) {
filters[itemIndex] = updatedFilter
} else {
filters.removeAt(itemIndex)
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
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<ResponseBody> {
api.deleteFilter(filters[itemIndex].id).enqueue(object : Callback<ResponseBody> {
override fun onFailure(call: Call<ResponseBody>, 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<Filter> {
api.createFilter(phrase, listOf(context), false, true, "").enqueue(object : Callback<Filter> {
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
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<List<Filter>> {
override fun onResponse(call: Call<List<Filter>>, response: Response<List<Filter>>) {
filters = response.body()!!.filter { filter -> filter.context.contains(context) }.toMutableList()
refreshFilterDisplay()
}
override fun onFailure(call: Call<List<Filter>>, 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?) {

34
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<AccessToken> {
override fun onResponse(call: Call<AccessToken>, response: Response<AccessToken>) {
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<AccessToken>, 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

80
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<Account>() {
@Override
public void onResponse(@NonNull Call<Account> call, @NonNull Response<Account> response) {
if (response.isSuccessful()) {
onFetchUserInfoSuccess(response.body());
} else {
onFetchUserInfoFailure(new Exception(response.message()));
}
}
@Override
public void onFailure(@NonNull Call<Account> 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<AccountEntity> allAccounts = accountManager.getAllAccountsOrderedByActive();
List<IProfile> profiles = new ArrayList<>(allAccounts.size()+1);
List<IProfile> 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;
}
}

14
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<Activity> 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> T get(Class<T> clazz) {

4
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 = "",

57
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<MastodonApi>
) {
@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()
}

13
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");
}
};
}

2
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()

4
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?
)

29
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<List<Filter>>() {
@Override
public void onResponse(Call<List<Filter>> call, Response<List<Filter>> response) {
applyFilters(response.body(), refresh);
}
@Override
public void onFailure(Call<List<Filter>> 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<String> 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<String> payload = new ArrayList<>();
payload.add(StatusBaseViewHolder.Key.KEY_CREATED);
return payload;
}
else
} else
// If items are different - update a whole view holder
return null;
}

7
app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java

@ -173,7 +173,7 @@ public interface MastodonApi {
Single<Status> unpinStatus(@Path("id") String statusId);
@GET("api/v1/accounts/verify_credentials")
Call<Account> accountVerifyCredentials();
Single<Account> accountVerifyCredentials();
@FormUrlEncoded
@PATCH("api/v1/accounts/update_credentials")
@ -300,12 +300,13 @@ public interface MastodonApi {
@FormUrlEncoded
@POST("oauth/token")
Call<AccessToken> fetchOAuthToken(
Single<AccessToken> 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<List<Conversation>> getConversations(@Nullable @Query("max_id") String maxId, @Query("limit") int limit);
@GET("api/v1/filters")
Call<List<Filter>> getFilters();
Single<List<Filter>> getFilters();
@FormUrlEncoded
@POST("api/v1/filters")

47
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<Resource<Account>>()
val avatarData = MutableLiveData<Resource<Bitmap>>()
@ -64,31 +66,21 @@ class EditProfileViewModel @Inject constructor(
private var oldProfileData: Account? = null
private val callList: MutableList<Call<*>> = 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<Account> {
override fun onResponse(call: Call<Account>,
response: Response<Account>) {
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<Account>, 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<StringField>, 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<StringField>) {
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<RequestBody, RequestBody>? {
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)
}

4
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

Loading…
Cancel
Save