mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-02-07 23:58:44 +01:00
認証周りのコード整理、バグ修正
This commit is contained in:
parent
9d712e9cc7
commit
ea2ac889f1
@ -7,7 +7,7 @@ import jp.juggler.subwaytooter.api.TootApiClient
|
|||||||
import jp.juggler.subwaytooter.api.entity.Host
|
import jp.juggler.subwaytooter.api.entity.Host
|
||||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.testutil.MainDispatcherRule
|
import jp.juggler.subwaytooter.testutil.TestDispatcherRule
|
||||||
import jp.juggler.subwaytooter.testutil.MockInterceptor
|
import jp.juggler.subwaytooter.testutil.MockInterceptor
|
||||||
import jp.juggler.subwaytooter.util.SimpleHttpClientImpl
|
import jp.juggler.subwaytooter.util.SimpleHttpClientImpl
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
@ -30,7 +30,7 @@ class TestTootInstance {
|
|||||||
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
||||||
// プロパティは記述順に初期化されることに注意
|
// プロパティは記述順に初期化されることに注意
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val mainDispatcherRule = MainDispatcherRule()
|
val mainDispatcherRule = TestDispatcherRule()
|
||||||
|
|
||||||
private val client by lazy {
|
private val client by lazy {
|
||||||
val mockInterceptor = MockInterceptor(
|
val mockInterceptor = MockInterceptor(
|
||||||
|
@ -11,7 +11,7 @@ import jp.juggler.subwaytooter.api.entity.Host
|
|||||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
import jp.juggler.subwaytooter.table.ClientInfo
|
import jp.juggler.subwaytooter.table.ClientInfo
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.testutil.MainDispatcherRule
|
import jp.juggler.subwaytooter.testutil.TestDispatcherRule
|
||||||
import jp.juggler.subwaytooter.testutil.assertThrowsSuspend
|
import jp.juggler.subwaytooter.testutil.assertThrowsSuspend
|
||||||
import jp.juggler.subwaytooter.util.SimpleHttpClient
|
import jp.juggler.subwaytooter.util.SimpleHttpClient
|
||||||
import jp.juggler.util.data.*
|
import jp.juggler.util.data.*
|
||||||
@ -36,7 +36,7 @@ class TestTootApiClient {
|
|||||||
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
||||||
// プロパティは記述順に初期化されることに注意
|
// プロパティは記述順に初期化されることに注意
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val mainDispatcherRule = MainDispatcherRule()
|
val mainDispatcherRule = TestDispatcherRule()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LogCategory("TestTootApiClient")
|
private val log = LogCategory("TestTootApiClient")
|
||||||
@ -1069,7 +1069,7 @@ class TestTootApiClient {
|
|||||||
val (ti, ri) = TootInstance.get(client)
|
val (ti, ri) = TootInstance.get(client)
|
||||||
ti ?: error("can't get server information. ${ri?.error}")
|
ti ?: error("can't get server information. ${ri?.error}")
|
||||||
|
|
||||||
val auth = AuthBase.findAuth(client, ti, ri) as MastodonAuth
|
val auth = AuthBase.findAuthForAuthStep1(client, ti, ri) as MastodonAuth
|
||||||
val authUri = auth.authStep1(ti, forceUpdateClient = false)
|
val authUri = auth.authStep1(ti, forceUpdateClient = false)
|
||||||
println("authUri=$authUri")
|
println("authUri=$authUri")
|
||||||
|
|
||||||
@ -1189,8 +1189,7 @@ class TestTootApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testGetInstanceInformation() =
|
fun testGetInstanceInformation() = runTest {
|
||||||
runTest {
|
|
||||||
val callback = ProgressRecordTootApiCallback()
|
val callback = ProgressRecordTootApiCallback()
|
||||||
val client = TootApiClient(
|
val client = TootApiClient(
|
||||||
appContext,
|
appContext,
|
||||||
|
@ -15,7 +15,7 @@ import org.junit.runner.Description
|
|||||||
* https://developer.android.com/kotlin/coroutines/test?hl=ja
|
* https://developer.android.com/kotlin/coroutines/test?hl=ja
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class MainDispatcherRule(
|
class TestDispatcherRule(
|
||||||
/**
|
/**
|
||||||
* UnconfinedTestDispatcher か StandardTestDispatcher のどちらかを指定する
|
* UnconfinedTestDispatcher か StandardTestDispatcher のどちらかを指定する
|
||||||
*/
|
*/
|
@ -21,7 +21,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager.widget.ViewPager
|
import androidx.viewpager.widget.ViewPager
|
||||||
import jp.juggler.subwaytooter.action.checkAccessToken2
|
import jp.juggler.subwaytooter.action.accessTokenPrompt
|
||||||
import jp.juggler.subwaytooter.action.timeline
|
import jp.juggler.subwaytooter.action.timeline
|
||||||
import jp.juggler.subwaytooter.actmain.*
|
import jp.juggler.subwaytooter.actmain.*
|
||||||
import jp.juggler.subwaytooter.actpost.CompletionHelper
|
import jp.juggler.subwaytooter.actpost.CompletionHelper
|
||||||
@ -289,7 +289,8 @@ class ActMain : AppCompatActivity(),
|
|||||||
|
|
||||||
ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN ->
|
ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN ->
|
||||||
r.data?.long(ActAccountSetting.EXTRA_DB_ID)
|
r.data?.long(ActAccountSetting.EXTRA_DB_ID)
|
||||||
?.let { checkAccessToken2(it) }
|
?.let { SavedAccount.loadAccount(this, it) }
|
||||||
|
?.let { accessTokenPrompt(it.apiHost) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ package jp.juggler.subwaytooter.action
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
import jp.juggler.subwaytooter.api.entity.*
|
||||||
import jp.juggler.subwaytooter.api.runApiTask
|
import jp.juggler.subwaytooter.api.runApiTask2
|
||||||
|
import jp.juggler.subwaytooter.api.showApiError
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.table.UserRelation
|
import jp.juggler.subwaytooter.table.UserRelation
|
||||||
import jp.juggler.subwaytooter.util.matchHost
|
import jp.juggler.subwaytooter.util.matchHost
|
||||||
@ -24,14 +25,12 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
suspend fun AppCompatActivity.getInstanceInfo(): TootInstance? {
|
suspend fun AppCompatActivity.getInstanceInfo(): TootInstance? {
|
||||||
var resultTi: TootInstance? = null
|
return try {
|
||||||
val result = runApiTask(host) { client ->
|
runApiTask2(host) { TootInstance.getOrThrow(it) }
|
||||||
val (instance, instanceResult) = TootInstance.get(client)
|
} catch (ex: Throwable) {
|
||||||
resultTi = instance
|
showApiError(ex)
|
||||||
instanceResult
|
null
|
||||||
}
|
}
|
||||||
result?.error?.let { showToast(true, it) }
|
|
||||||
return resultTi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val acct = Acct.parse("?", host)
|
val acct = Acct.parse("?", host)
|
||||||
|
@ -7,23 +7,24 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import jp.juggler.subwaytooter.*
|
import jp.juggler.subwaytooter.*
|
||||||
import jp.juggler.subwaytooter.actmain.addColumn
|
import jp.juggler.subwaytooter.actmain.addColumn
|
||||||
import jp.juggler.subwaytooter.actmain.afterAccountVerify
|
import jp.juggler.subwaytooter.actmain.afterAccountVerify
|
||||||
|
import jp.juggler.subwaytooter.actmain.defaultInsertPosition
|
||||||
import jp.juggler.subwaytooter.api.*
|
import jp.juggler.subwaytooter.api.*
|
||||||
import jp.juggler.subwaytooter.api.auth.Auth2Result
|
import jp.juggler.subwaytooter.api.auth.Auth2Result
|
||||||
import jp.juggler.subwaytooter.api.auth.MastodonAuth
|
import jp.juggler.subwaytooter.api.auth.AuthBase
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
import jp.juggler.subwaytooter.api.entity.*
|
||||||
import jp.juggler.subwaytooter.column.ColumnType
|
import jp.juggler.subwaytooter.column.ColumnType
|
||||||
import jp.juggler.subwaytooter.dialog.*
|
import jp.juggler.subwaytooter.dialog.*
|
||||||
|
import jp.juggler.subwaytooter.dialog.DlgCreateAccount.Companion.showUserCreateDialog
|
||||||
|
import jp.juggler.subwaytooter.dialog.LoginForm.Companion.showLoginForm
|
||||||
import jp.juggler.subwaytooter.notification.APP_SERVER
|
import jp.juggler.subwaytooter.notification.APP_SERVER
|
||||||
import jp.juggler.subwaytooter.pref.*
|
import jp.juggler.subwaytooter.pref.*
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.util.LinkHelper
|
import jp.juggler.subwaytooter.util.LinkHelper
|
||||||
import jp.juggler.subwaytooter.util.openBrowser
|
import jp.juggler.subwaytooter.util.openBrowser
|
||||||
import jp.juggler.util.*
|
import jp.juggler.util.*
|
||||||
import jp.juggler.util.coroutine.AppDispatchers
|
|
||||||
import jp.juggler.util.coroutine.launchIO
|
import jp.juggler.util.coroutine.launchIO
|
||||||
import jp.juggler.util.coroutine.launchMain
|
import jp.juggler.util.coroutine.launchMain
|
||||||
import jp.juggler.util.data.JsonObject
|
import jp.juggler.util.data.JsonObject
|
||||||
import jp.juggler.util.data.buildJsonObject
|
|
||||||
import jp.juggler.util.data.encodePercent
|
import jp.juggler.util.data.encodePercent
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.log.showToast
|
import jp.juggler.util.log.showToast
|
||||||
@ -55,149 +56,33 @@ fun isAndroid7TlsBug(errorText: String) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ActMain.accountCreate(
|
/**
|
||||||
apiHost: Host,
|
* サイドメニューで「アカウントの追加」を選ぶと呼び出される。
|
||||||
clientInfo: JsonObject,
|
* - サーバ名とアクションを指定するダイアログを開く。
|
||||||
dialogHost: Dialog,
|
* - 選択されたアクションに応じて分岐する。
|
||||||
) {
|
*/
|
||||||
val activity = this
|
|
||||||
DlgCreateAccount(
|
|
||||||
activity,
|
|
||||||
apiHost
|
|
||||||
) { dialog_create, username, email, password, agreement, reason ->
|
|
||||||
// dialog引数が二つあるのに注意
|
|
||||||
launchMain {
|
|
||||||
try {
|
|
||||||
val auth2Result = runApiTask2(apiHost) { client ->
|
|
||||||
// Mastodon限定
|
|
||||||
val misskeyVersion = 0 // TootInstance.parseMisskeyVersion(tokenJson)
|
|
||||||
|
|
||||||
val auth = MastodonAuth(client)
|
|
||||||
|
|
||||||
val tokenJson = auth.createUser(
|
|
||||||
clientInfo,
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
agreement,
|
|
||||||
reason
|
|
||||||
)
|
|
||||||
|
|
||||||
val accessToken = tokenJson.string("access_token")
|
|
||||||
?: error("can't get user access token")
|
|
||||||
|
|
||||||
var accountJson = auth.verifyAccount(
|
|
||||||
accessToken = accessToken,
|
|
||||||
outTokenJson = tokenJson,
|
|
||||||
misskeyVersion = misskeyVersion
|
|
||||||
)
|
|
||||||
|
|
||||||
client.apiHost = apiHost
|
|
||||||
val (ti, ri) = TootInstance.getEx(client, forceAccessToken = accessToken)
|
|
||||||
ti ?: error("missing server information. ${ri?.error}")
|
|
||||||
|
|
||||||
val parser = TootParser(
|
|
||||||
activity,
|
|
||||||
linkHelper = LinkHelper.create(ti)
|
|
||||||
)
|
|
||||||
|
|
||||||
var ta = parser.account(accountJson)
|
|
||||||
if (ta == null) {
|
|
||||||
accountJson = buildJsonObject {
|
|
||||||
put("id", EntityId.CONFIRMING.toString())
|
|
||||||
put("username", username)
|
|
||||||
put("acct", username)
|
|
||||||
put("url", "https://$apiHost/@$username")
|
|
||||||
}
|
|
||||||
ta = parser.account(accountJson)!!
|
|
||||||
}
|
|
||||||
Auth2Result(
|
|
||||||
tootInstance = ti,
|
|
||||||
accountJson = accountJson,
|
|
||||||
tootAccount = ta,
|
|
||||||
tokenJson = tokenJson,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val verified = activity.afterAccountVerify(auth2Result)
|
|
||||||
if (verified) {
|
|
||||||
dialogHost.dismissSafe()
|
|
||||||
dialog_create.dismissSafe()
|
|
||||||
}
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
showApiError(ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// アカウントの追加
|
|
||||||
fun ActMain.accountAdd() {
|
fun ActMain.accountAdd() {
|
||||||
val activity = this
|
showLoginForm { dialogHost, apiHost, serverInfo, action ->
|
||||||
LoginForm.showLoginForm(this, null) { dialogHost, instance, action ->
|
|
||||||
launchMain {
|
launchMain {
|
||||||
try {
|
try {
|
||||||
when (action) {
|
when (action) {
|
||||||
// ログイン画面を開く
|
LoginForm.Action.Login -> {
|
||||||
LoginForm.Action.Existing ->
|
val authUri = runApiTask2(apiHost) { it.authStep1() }
|
||||||
runApiTask2(instance) { client ->
|
openBrowser(authUri)
|
||||||
val authUri = client.authStep1()
|
dialogHost.dismissSafe()
|
||||||
withContext(AppDispatchers.MainImmediate) {
|
}
|
||||||
openBrowser(authUri)
|
LoginForm.Action.Pseudo -> {
|
||||||
dialogHost.dismissSafe()
|
val tootInstance = runApiTask2(apiHost) { TootInstance.getOrThrow(it) }
|
||||||
}
|
addPseudoAccount(apiHost, tootInstance)?.let { a ->
|
||||||
|
showToast(false, R.string.server_confirmed)
|
||||||
|
addColumn(defaultInsertPosition, a, ColumnType.LOCAL)
|
||||||
|
dialogHost.dismissSafe()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// ユーザ作成
|
|
||||||
LoginForm.Action.Create ->
|
LoginForm.Action.Create ->
|
||||||
runApiTask2(instance) { client ->
|
createUser(apiHost, serverInfo) { dialogHost.dismissSafe() }
|
||||||
val clientInfo = client.prepareClient()
|
|
||||||
withContext(AppDispatchers.MainImmediate) {
|
|
||||||
accountCreate(instance, clientInfo, dialogHost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 疑似アカウント
|
|
||||||
LoginForm.Action.Pseudo ->
|
|
||||||
runApiTask2(instance) { client ->
|
|
||||||
val (ti, ri) = TootInstance.get(client)
|
|
||||||
ti ?: error("${ri?.error}")
|
|
||||||
withContext(AppDispatchers.MainImmediate) {
|
|
||||||
addPseudoAccount(instance, ti)?.let { a ->
|
|
||||||
showToast(false, R.string.server_confirmed)
|
|
||||||
val pos = activity.appState.columnCount
|
|
||||||
addColumn(pos, a, ColumnType.LOCAL)
|
|
||||||
dialogHost.dismissSafe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LoginForm.Action.Token ->
|
LoginForm.Action.Token ->
|
||||||
runApiTask2(instance) { client ->
|
accessTokenPrompt(apiHost) { dialogHost.dismissSafe() }
|
||||||
val (ti, ri) = TootInstance.get(client)
|
|
||||||
ti ?: error("${ri?.error}")
|
|
||||||
withContext(AppDispatchers.MainImmediate) {
|
|
||||||
DlgTextInput.show(
|
|
||||||
activity,
|
|
||||||
getString(R.string.access_token_or_api_token),
|
|
||||||
null,
|
|
||||||
callback = object : DlgTextInput.Callback {
|
|
||||||
override fun onEmptyError() {
|
|
||||||
showToast(true, R.string.token_not_specified)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOK(dialog: Dialog, text: String) {
|
|
||||||
// dialog引数が二つあるのに注意
|
|
||||||
activity.checkAccessToken(
|
|
||||||
dialogHost = dialogHost,
|
|
||||||
dialogToken = dialog,
|
|
||||||
apiHost = instance,
|
|
||||||
accessToken = text,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
showApiError(ex)
|
showApiError(ex)
|
||||||
@ -206,6 +91,110 @@ fun ActMain.accountAdd() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun ActMain.createUser(
|
||||||
|
apiHost: Host,
|
||||||
|
serverInfo: TootInstance?,
|
||||||
|
onComplete: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
serverInfo ?: error(
|
||||||
|
getString(
|
||||||
|
R.string.user_creation_not_supported,
|
||||||
|
apiHost.pretty,
|
||||||
|
"(unknown)",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun TootApiClient.authUserCreate() =
|
||||||
|
AuthBase.findAuthForCreateUser(this, serverInfo)
|
||||||
|
?: error(
|
||||||
|
getString(
|
||||||
|
R.string.user_creation_not_supported,
|
||||||
|
apiHost.pretty,
|
||||||
|
serverInfo.instanceType.toString(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// クライアント情報を取得。サーバ種別によってはユーザ作成ができないのでエラーとなる
|
||||||
|
val clientInfo = runApiTask2(apiHost) {
|
||||||
|
it.authUserCreate().prepareClient(serverInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
showUserCreateDialog(apiHost) { dialogCreate, params ->
|
||||||
|
launchMain {
|
||||||
|
try {
|
||||||
|
val auth2Result = runApiTask2(apiHost) {
|
||||||
|
it.authUserCreate().createUser(clientInfo = clientInfo, params = params)
|
||||||
|
}
|
||||||
|
if (afterAccountVerify(auth2Result)) {
|
||||||
|
dialogCreate.dismissSafe()
|
||||||
|
onComplete?.invoke()
|
||||||
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
showApiError(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アクセストークンを手動入力する。
|
||||||
|
*
|
||||||
|
* @param apiHost アクセストークンと関係のあるサーバのAPIホスト
|
||||||
|
* @param onComplete 非nullならアカウント認証が終わったタイミングで呼ばれる
|
||||||
|
*/
|
||||||
|
// アクセストークンの手動入力(更新)
|
||||||
|
fun ActMain.accessTokenPrompt(
|
||||||
|
apiHost: Host,
|
||||||
|
onComplete: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
DlgTextInput.show(
|
||||||
|
this,
|
||||||
|
getString(R.string.access_token_or_api_token),
|
||||||
|
null,
|
||||||
|
callback = object : DlgTextInput.Callback {
|
||||||
|
override fun onEmptyError() {
|
||||||
|
showToast(true, R.string.token_not_specified)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOK(dialog: Dialog, text: String) {
|
||||||
|
launchMain {
|
||||||
|
try {
|
||||||
|
val accessToken = text.trim()
|
||||||
|
val auth2Result = runApiTask2(apiHost) { client ->
|
||||||
|
val ti =
|
||||||
|
TootInstance.getExOrThrow(client, forceAccessToken = accessToken)
|
||||||
|
|
||||||
|
val tokenJson = JsonObject()
|
||||||
|
|
||||||
|
val userJson = client.verifyAccount(
|
||||||
|
accessToken,
|
||||||
|
outTokenInfo = tokenJson, // 更新される
|
||||||
|
misskeyVersion = ti.misskeyVersionMajor
|
||||||
|
)
|
||||||
|
|
||||||
|
val parser = TootParser(this, linkHelper = LinkHelper.create(ti))
|
||||||
|
|
||||||
|
Auth2Result(
|
||||||
|
tootInstance = ti,
|
||||||
|
tokenJson = tokenJson,
|
||||||
|
accountJson = userJson,
|
||||||
|
tootAccount = parser.account(userJson)
|
||||||
|
?: error("can't parse user information."),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (afterAccountVerify(auth2Result)) {
|
||||||
|
dialog.dismissSafe()
|
||||||
|
onComplete?.invoke()
|
||||||
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
showApiError(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun AppCompatActivity.accountRemove(account: SavedAccount) {
|
fun AppCompatActivity.accountRemove(account: SavedAccount) {
|
||||||
// if account is default account of tablet mode,
|
// if account is default account of tablet mode,
|
||||||
// reset default.
|
// reset default.
|
||||||
@ -397,71 +386,3 @@ suspend fun ActMain.accountListCanSeeMyReactions(pickupHost: Host? = null) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// アクセストークンを手動で入力した場合
|
|
||||||
fun ActMain.checkAccessToken(
|
|
||||||
dialogHost: Dialog?,
|
|
||||||
dialogToken: Dialog?,
|
|
||||||
apiHost: Host,
|
|
||||||
accessToken: String,
|
|
||||||
) {
|
|
||||||
launchMain {
|
|
||||||
try {
|
|
||||||
val auth2Result = runApiTask2(apiHost) { client ->
|
|
||||||
val (ti, ri) = TootInstance.getEx(client, forceAccessToken = accessToken)
|
|
||||||
ti ?: error("missing uri in Instance Information. ${ri?.error}")
|
|
||||||
|
|
||||||
val tokenJson = JsonObject()
|
|
||||||
|
|
||||||
val userJson = client.getUserCredential(
|
|
||||||
accessToken,
|
|
||||||
outTokenInfo = tokenJson, // 更新される
|
|
||||||
misskeyVersion = ti.misskeyVersionMajor
|
|
||||||
)
|
|
||||||
|
|
||||||
val parser = TootParser(this, linkHelper = LinkHelper.create(ti))
|
|
||||||
|
|
||||||
Auth2Result(
|
|
||||||
tootInstance = ti,
|
|
||||||
accountJson = userJson,
|
|
||||||
tootAccount = parser.account(userJson)
|
|
||||||
?: error("can't parse user information."),
|
|
||||||
tokenJson = tokenJson,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val verified = afterAccountVerify(auth2Result)
|
|
||||||
if (verified) {
|
|
||||||
dialogHost?.dismissSafe()
|
|
||||||
dialogToken?.dismissSafe()
|
|
||||||
}
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
showApiError(ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// アクセストークンの手動入力(更新)
|
|
||||||
fun ActMain.checkAccessToken2(dbId: Long) {
|
|
||||||
val apiHost = SavedAccount.loadAccount(this, dbId)
|
|
||||||
?.apiHost
|
|
||||||
?: return
|
|
||||||
|
|
||||||
DlgTextInput.show(
|
|
||||||
this,
|
|
||||||
getString(R.string.access_token_or_api_token),
|
|
||||||
null,
|
|
||||||
callback = object : DlgTextInput.Callback {
|
|
||||||
override fun onEmptyError() {
|
|
||||||
showToast(true, R.string.token_not_specified)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOK(dialog: Dialog, text: String) {
|
|
||||||
checkAccessToken(
|
|
||||||
dialogHost = null,
|
|
||||||
dialogToken = dialog,
|
|
||||||
apiHost = apiHost,
|
|
||||||
accessToken = text,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -5,12 +5,15 @@ import jp.juggler.subwaytooter.R
|
|||||||
import jp.juggler.subwaytooter.api.ApiTask
|
import jp.juggler.subwaytooter.api.ApiTask
|
||||||
import jp.juggler.subwaytooter.api.entity.TootAccount
|
import jp.juggler.subwaytooter.api.entity.TootAccount
|
||||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
import jp.juggler.subwaytooter.api.runApiTask
|
import jp.juggler.subwaytooter.api.runApiTask2
|
||||||
import jp.juggler.subwaytooter.util.EmojiDecoder
|
import jp.juggler.subwaytooter.util.EmojiDecoder
|
||||||
import jp.juggler.util.coroutine.launchMain
|
import jp.juggler.util.coroutine.launchMain
|
||||||
import jp.juggler.util.data.wrapWeakReference
|
import jp.juggler.util.data.wrapWeakReference
|
||||||
|
import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.ui.attrColor
|
import jp.juggler.util.ui.attrColor
|
||||||
|
|
||||||
|
private val log = LogCategory("ActPostCharCount")
|
||||||
|
|
||||||
// 最大文字数を取得する
|
// 最大文字数を取得する
|
||||||
// 暫定で仮の値を返すことがある
|
// 暫定で仮の値を返すことがある
|
||||||
// 裏で取得し終わったら updateTextCount() を呼び出す
|
// 裏で取得し終わったら updateTextCount() を呼び出す
|
||||||
@ -25,17 +28,17 @@ private fun ActPost.getMaxCharCount(): Int {
|
|||||||
// 同時に実行するタスクは1つまで
|
// 同時に実行するタスクは1つまで
|
||||||
if (jobMaxCharCount?.get()?.isActive != true) {
|
if (jobMaxCharCount?.get()?.isActive != true) {
|
||||||
jobMaxCharCount = launchMain {
|
jobMaxCharCount = launchMain {
|
||||||
var newInfo: TootInstance? = null
|
try {
|
||||||
runApiTask(account, progressStyle = ApiTask.PROGRESS_NONE) { client ->
|
runApiTask2(account, progressStyle = ApiTask.PROGRESS_NONE) {
|
||||||
val (ti, result) = TootInstance.get(client)
|
TootInstance.getOrThrow(it)
|
||||||
newInfo = ti
|
}
|
||||||
result
|
if (isFinishing || isDestroyed) return@launchMain
|
||||||
|
updateTextCount()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.w(ex, "getMaxCharCount failed.")
|
||||||
}
|
}
|
||||||
if (isFinishing || isDestroyed) return@launchMain
|
|
||||||
if (newInfo != null) updateTextCount()
|
|
||||||
}.wrapWeakReference
|
}.wrapWeakReference
|
||||||
}
|
}
|
||||||
|
|
||||||
// fall thru
|
// fall thru
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -430,35 +430,21 @@ class TootApiClient(
|
|||||||
forceUpdateClient: Boolean = false,
|
forceUpdateClient: Boolean = false,
|
||||||
): Uri {
|
): Uri {
|
||||||
val (ti, ri) = TootInstance.get(this)
|
val (ti, ri) = TootInstance.get(this)
|
||||||
|
// 情報が取れなくても続ける
|
||||||
log.i("authentication1: instance info version=${ti?.version} misskeyVersion=${ti?.misskeyVersionMajor} responseCode=${ri?.response?.code}")
|
log.i("authentication1: instance info version=${ti?.version} misskeyVersion=${ti?.misskeyVersionMajor} responseCode=${ri?.response?.code}")
|
||||||
return when (val auth = AuthBase.findAuth(this, ti, ri)) {
|
return when (val auth = AuthBase.findAuthForAuthStep1(this, ti, ri)) {
|
||||||
null -> error("can't get server information. ${ri?.error}")
|
null -> error("can't get server information. ${ri?.error}")
|
||||||
else -> auth.authStep1(ti, forceUpdateClient)
|
else -> auth.authStep1(ti, forceUpdateClient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUserCredential(
|
suspend fun verifyAccount(
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
outTokenInfo: JsonObject?,
|
outTokenInfo: JsonObject?,
|
||||||
misskeyVersion: Int = 0,
|
misskeyVersion: Int = 0,
|
||||||
) = AuthBase.findAuthForUserCredentian(this, misskeyVersion)
|
) = AuthBase.findAuthForVerifyAccount(this, misskeyVersion)
|
||||||
.verifyAccount(accessToken, outTokenInfo, misskeyVersion)
|
.verifyAccount(accessToken, outTokenInfo, misskeyVersion)
|
||||||
|
|
||||||
/**
|
|
||||||
* サーバにクライアントアプリを登録する
|
|
||||||
* - Mastodonのユーザ作成で呼ばれる
|
|
||||||
*
|
|
||||||
* @return clientInfo
|
|
||||||
*/
|
|
||||||
suspend fun prepareClient(): JsonObject {
|
|
||||||
val (ti, ri) = TootInstance.get(this)
|
|
||||||
ti ?: error("${ri?.error}")
|
|
||||||
return when (val auth = AuthBase.findAuthForCreateUser(this, ti)) {
|
|
||||||
null -> error("user creation not supported for server type [${ti.instanceType}]")
|
|
||||||
else -> auth.prepareClient(ti)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
// JSONデータ以外を扱うリクエスト
|
// JSONデータ以外を扱うリクエスト
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ package jp.juggler.subwaytooter.api.auth
|
|||||||
|
|
||||||
import jp.juggler.subwaytooter.api.entity.TootAccount
|
import jp.juggler.subwaytooter.api.entity.TootAccount
|
||||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
|
||||||
import jp.juggler.util.data.JsonObject
|
import jp.juggler.util.data.JsonObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -13,14 +12,14 @@ class Auth2Result(
|
|||||||
// サーバ情報
|
// サーバ情報
|
||||||
val tootInstance: TootInstance,
|
val tootInstance: TootInstance,
|
||||||
|
|
||||||
|
// アクセストークンを含むJsonObject
|
||||||
|
val tokenJson: JsonObject,
|
||||||
|
|
||||||
// TootAccountユーザ情報の元となるJSONデータ
|
// TootAccountユーザ情報の元となるJSONデータ
|
||||||
val accountJson: JsonObject,
|
val accountJson: JsonObject,
|
||||||
|
|
||||||
// AccountJsonのパース結果
|
// AccountJsonのパース結果
|
||||||
val tootAccount: TootAccount,
|
val tootAccount: TootAccount,
|
||||||
|
|
||||||
// アクセストークンを含むJsonObject
|
|
||||||
val tokenJson: JsonObject,
|
|
||||||
) {
|
) {
|
||||||
// 対象サーバのAPIホスト
|
// 対象サーバのAPIホスト
|
||||||
val apiHost get() = tootInstance.apiHost
|
val apiHost get() = tootInstance.apiHost
|
||||||
|
@ -38,44 +38,34 @@ abstract class AuthBase {
|
|||||||
).firstNotNullOfOrNull { it.notBlank() }
|
).firstNotNullOfOrNull { it.notBlank() }
|
||||||
?: DEFAULT_CLIENT_NAME
|
?: DEFAULT_CLIENT_NAME
|
||||||
|
|
||||||
fun findAuth(client: TootApiClient, ti: TootInstance?, ri: TootApiResult?): AuthBase? =
|
fun findAuthForVerifyAccount(client: TootApiClient, misskeyVersionMajor: Int) =
|
||||||
when {
|
when {
|
||||||
// インスタンス情報を取得できない
|
misskeyVersionMajor >= 13 -> MisskeyAuth13(client)
|
||||||
ti == null -> when (ri?.response?.code) {
|
misskeyVersionMajor > 0 -> MisskeyAuth10(client)
|
||||||
|
else -> MastodonAuth(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAuthForAuthStep1(client: TootApiClient, ti: TootInstance?, ri: TootApiResult?) =
|
||||||
|
ti?.let { findAuthForVerifyAccount(client, ti.misskeyVersionMajor) }
|
||||||
|
?: when (ri?.response?.code) {
|
||||||
|
// インスタンス情報を取得できないが、マストドンだと分かる場合がある
|
||||||
// https://github.com/tateisu/SubwayTooter/issues/155
|
// https://github.com/tateisu/SubwayTooter/issues/155
|
||||||
// Mastodon's WHITELIST_MODE
|
// Mastodon's WHITELIST_MODE
|
||||||
401 -> MastodonAuth(client)
|
401 -> MastodonAuth(client)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
ti.isMisskey -> when {
|
|
||||||
ti.versionGE(TootInstance.MISSKEY_VERSION_13) ->
|
|
||||||
MisskeyAuth13(client)
|
|
||||||
else ->
|
|
||||||
MisskeyAuth10(client)
|
|
||||||
}
|
|
||||||
else -> MastodonAuth(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findAuthForAuthCallback(client: TootApiClient, callbackUrl: String): AuthBase =
|
fun findAuthForAuthCallback(client: TootApiClient, callbackUrl: String) =
|
||||||
when {
|
when {
|
||||||
MisskeyAuth10.isCallbackUrl(callbackUrl) -> MisskeyAuth10(client)
|
MisskeyAuth10.isCallbackUrl(callbackUrl) -> MisskeyAuth10(client)
|
||||||
MisskeyAuth13.isCallbackUrl(callbackUrl) -> MisskeyAuth13(client)
|
MisskeyAuth13.isCallbackUrl(callbackUrl) -> MisskeyAuth13(client)
|
||||||
else -> MastodonAuth(client)
|
else -> MastodonAuth(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findAuthForUserCredentian(client: TootApiClient, misskeyVersion: Int) =
|
fun findAuthForCreateUser(client: TootApiClient, ti: TootInstance?) =
|
||||||
when {
|
when (ti?.instanceType) {
|
||||||
misskeyVersion >= 13 -> MisskeyAuth13(client)
|
InstanceType.Mastodon -> MastodonAuth(client)
|
||||||
misskeyVersion > 0 -> MisskeyAuth10(client)
|
else -> null
|
||||||
else -> MastodonAuth(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findAuthForCreateUser(client: TootApiClient, ti: TootInstance) =
|
|
||||||
when (ti.instanceType) {
|
|
||||||
InstanceType.Misskey -> null
|
|
||||||
InstanceType.Pleroma -> null
|
|
||||||
InstanceType.Pixelfed -> null
|
|
||||||
else -> MastodonAuth(client)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +75,6 @@ abstract class AuthBase {
|
|||||||
protected val account get() = client.account
|
protected val account get() = client.account
|
||||||
protected val context get() = client.context
|
protected val context get() = client.context
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* クライアントを登録してブラウザで開くURLを生成する
|
* クライアントを登録してブラウザで開くURLを生成する
|
||||||
* 成功したら TootApiResult.data にURL文字列を格納すること
|
* 成功したら TootApiResult.data にURL文字列を格納すること
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package jp.juggler.subwaytooter.api.auth
|
||||||
|
|
||||||
|
class CreateUserParams(
|
||||||
|
val username: String,
|
||||||
|
val email: String,
|
||||||
|
val password: String,
|
||||||
|
val agreement: Boolean,
|
||||||
|
val reason: String?,
|
||||||
|
)
|
@ -4,6 +4,7 @@ import android.net.Uri
|
|||||||
import jp.juggler.subwaytooter.api.SendException
|
import jp.juggler.subwaytooter.api.SendException
|
||||||
import jp.juggler.subwaytooter.api.TootApiClient
|
import jp.juggler.subwaytooter.api.TootApiClient
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
|
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||||
import jp.juggler.subwaytooter.api.entity.Host
|
import jp.juggler.subwaytooter.api.entity.Host
|
||||||
import jp.juggler.subwaytooter.api.entity.InstanceType
|
import jp.juggler.subwaytooter.api.entity.InstanceType
|
||||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
@ -11,6 +12,7 @@ import jp.juggler.subwaytooter.table.ClientInfo
|
|||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.util.LinkHelper
|
import jp.juggler.subwaytooter.util.LinkHelper
|
||||||
import jp.juggler.util.data.JsonObject
|
import jp.juggler.util.data.JsonObject
|
||||||
|
import jp.juggler.util.data.buildJsonObject
|
||||||
import jp.juggler.util.data.notEmpty
|
import jp.juggler.util.data.notEmpty
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.log.errorEx
|
import jp.juggler.util.log.errorEx
|
||||||
@ -233,11 +235,10 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
|
|||||||
// ?error=access_denied
|
// ?error=access_denied
|
||||||
// &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
|
// &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
|
||||||
// &state=db%3A3
|
// &state=db%3A3
|
||||||
arrayOf(
|
arrayOf("error_description", "error")
|
||||||
uri.getQueryParameter("error")?.trim()?.notEmpty(),
|
.mapNotNull { uri.getQueryParameter(it)?.trim()?.notEmpty() }
|
||||||
uri.getQueryParameter("error_description")?.trim()?.notEmpty()
|
.notEmpty()
|
||||||
).filterNotNull().joinToString(" ")
|
?.let { error(it.joinToString("\n")) }
|
||||||
.notEmpty()?.let { error(it) }
|
|
||||||
|
|
||||||
// subwaytooter://oauth(\d*)/
|
// subwaytooter://oauth(\d*)/
|
||||||
// ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
|
// ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
|
||||||
@ -296,16 +297,14 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
|
|||||||
misskeyVersion = 0
|
misskeyVersion = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
val (ti, ri) = TootInstance.getEx(client, forceAccessToken = accessToken)
|
val ti = TootInstance.getExOrThrow(client, forceAccessToken = accessToken)
|
||||||
ti ?: error("can't get server information. ${ri?.error}")
|
val parser = TootParser(context, linkHelper = LinkHelper.create(ti))
|
||||||
|
|
||||||
return Auth2Result(
|
return Auth2Result(
|
||||||
tootInstance = ti,
|
tootInstance = ti,
|
||||||
accountJson = accountJson,
|
|
||||||
tokenJson = tokenInfo,
|
tokenJson = tokenInfo,
|
||||||
tootAccount = TootParser(context, linkHelper = LinkHelper.create(ti))
|
accountJson = accountJson,
|
||||||
.account(accountJson)
|
tootAccount = parser.account(accountJson)
|
||||||
?: error("can't parse user information."),
|
?: error("can't parse user information.")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,26 +321,48 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
|
|||||||
forceUpdateClient = false
|
forceUpdateClient = false
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* ユーザ作成
|
|
||||||
* - クライアントは登録済みであること
|
|
||||||
*/
|
|
||||||
|
|
||||||
suspend fun createUser(
|
suspend fun createUser(
|
||||||
clientInfo: JsonObject,
|
clientInfo: JsonObject,
|
||||||
username: String,
|
params: CreateUserParams,
|
||||||
email: String,
|
): Auth2Result {
|
||||||
password: String,
|
val apiHost = apiHost ?: error("createUser: missing apiHost")
|
||||||
agreement: Boolean,
|
|
||||||
reason: String?,
|
val tokenJson = api.createUser(
|
||||||
) = api.createUser(
|
apiHost = apiHost,
|
||||||
apiHost = apiHost ?: error("createUser: missing apiHost"),
|
clientCredential = clientInfo.string(KEY_CLIENT_CREDENTIAL)
|
||||||
clientCredential = clientInfo.string(KEY_CLIENT_CREDENTIAL)
|
?: error("createUser: missing client credential"),
|
||||||
?: error("createUser: missing client credential"),
|
params = params,
|
||||||
username = username,
|
)
|
||||||
email = email,
|
|
||||||
password = password,
|
val accessToken = tokenJson.string("access_token")
|
||||||
agreement = agreement,
|
?: error("can't get user access token")
|
||||||
reason = reason
|
|
||||||
)
|
val ti = TootInstance.getExOrThrow(client, forceAccessToken = accessToken)
|
||||||
|
val parser = TootParser(context, linkHelper = LinkHelper.create(ti))
|
||||||
|
|
||||||
|
val accountJson = try {
|
||||||
|
verifyAccount(
|
||||||
|
accessToken = accessToken,
|
||||||
|
outTokenJson = tokenJson,
|
||||||
|
misskeyVersion = 0, // Mastodon限定
|
||||||
|
)
|
||||||
|
// メール確認が不要な場合は成功する
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
// メール確認がまだなら、verifyAccount は失敗する
|
||||||
|
log.e(ex, "createUser: can't verify account.")
|
||||||
|
buildJsonObject {
|
||||||
|
put("id", EntityId.CONFIRMING.toString())
|
||||||
|
put("username", params.username)
|
||||||
|
put("acct", params.username)
|
||||||
|
put("url", "https://$apiHost/@${params.username}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Auth2Result(
|
||||||
|
tootInstance = ti,
|
||||||
|
tokenJson = tokenJson,
|
||||||
|
accountJson = accountJson,
|
||||||
|
tootAccount = parser.account(accountJson)
|
||||||
|
?: error("can't verify user information."),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,17 +151,13 @@ class MastodonAuthApi(
|
|||||||
suspend fun createUser(
|
suspend fun createUser(
|
||||||
apiHost: Host,
|
apiHost: Host,
|
||||||
clientCredential: String,
|
clientCredential: String,
|
||||||
username: String,
|
params: CreateUserParams,
|
||||||
email: String,
|
|
||||||
password: String,
|
|
||||||
agreement: Boolean,
|
|
||||||
reason: String?,
|
|
||||||
) = buildJsonObject {
|
) = buildJsonObject {
|
||||||
put("username", username)
|
put("username", params.username)
|
||||||
put("email", email)
|
put("email", params.email)
|
||||||
put("password", password)
|
put("password", params.password)
|
||||||
put("agreement", agreement)
|
put("agreement", params.agreement)
|
||||||
reason?.notEmpty()?.let { put("reason", reason) }
|
params.reason?.notEmpty()?.let { put("reason", it) }
|
||||||
}.encodeQuery().toFormRequestBody().toPost()
|
}.encodeQuery().toFormRequestBody().toPost()
|
||||||
.url("https://${apiHost.ascii}/api/v1/accounts")
|
.url("https://${apiHost.ascii}/api/v1/accounts")
|
||||||
.header("Authorization", "Bearer $clientCredential")
|
.header("Authorization", "Bearer $clientCredential")
|
||||||
|
@ -201,8 +201,7 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
|
|||||||
} ?: error("missing account db_id=$dbId")
|
} ?: error("missing account db_id=$dbId")
|
||||||
}
|
}
|
||||||
|
|
||||||
val (ti, r2) = TootInstance.get(client)
|
val ti = TootInstance.getOrThrow(client)
|
||||||
ti ?: error("${r2?.error} ($apiHost)")
|
|
||||||
|
|
||||||
val parser = TootParser(
|
val parser = TootParser(
|
||||||
context,
|
context,
|
||||||
@ -230,14 +229,10 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
|
|||||||
val accountJson = tokenInfo["user"].cast<JsonObject>()
|
val accountJson = tokenInfo["user"].cast<JsonObject>()
|
||||||
?: error("missing user in the userkey response.")
|
?: error("missing user in the userkey response.")
|
||||||
|
|
||||||
val tootAccount = parser.account(accountJson)
|
|
||||||
?: error("can't parse user information")
|
|
||||||
|
|
||||||
tokenInfo.remove("user")
|
tokenInfo.remove("user")
|
||||||
|
|
||||||
return Auth2Result(
|
return Auth2Result(
|
||||||
tootInstance = ti,
|
tootInstance = ti,
|
||||||
accountJson = accountJson,
|
|
||||||
tokenJson = tokenInfo.also {
|
tokenJson = tokenInfo.also {
|
||||||
EntityId.mayNull(accountJson.string("id"))?.putTo(it, KEY_USER_ID)
|
EntityId.mayNull(accountJson.string("id"))?.putTo(it, KEY_USER_ID)
|
||||||
it[KEY_MISSKEY_VERSION] = ti.misskeyVersionMajor
|
it[KEY_MISSKEY_VERSION] = ti.misskeyVersionMajor
|
||||||
@ -245,7 +240,9 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
|
|||||||
val apiKey = "$accessToken$appSecret".encodeUTF8().digestSHA256().encodeHexLower()
|
val apiKey = "$accessToken$appSecret".encodeUTF8().digestSHA256().encodeHexLower()
|
||||||
it[KEY_API_KEY_MISSKEY] = apiKey
|
it[KEY_API_KEY_MISSKEY] = apiKey
|
||||||
},
|
},
|
||||||
tootAccount = tootAccount,
|
accountJson = accountJson,
|
||||||
|
tootAccount = parser.account(accountJson)
|
||||||
|
?: error("can't parse user information"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,8 +109,7 @@ class MisskeyAuth13(override val client: TootApiClient) : AuthBase() {
|
|||||||
error("auth session id not match.")
|
error("auth session id not match.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val (ti, r2) = TootInstance.get(client)
|
val ti = TootInstance.getOrThrow(client)
|
||||||
ti ?: error("missing server information. ${r2?.error}")
|
|
||||||
|
|
||||||
val misskeyVersion = ti.misskeyVersionMajor
|
val misskeyVersion = ti.misskeyVersionMajor
|
||||||
|
|
||||||
@ -136,13 +135,13 @@ class MisskeyAuth13(override val client: TootApiClient) : AuthBase() {
|
|||||||
|
|
||||||
return Auth2Result(
|
return Auth2Result(
|
||||||
tootInstance = ti,
|
tootInstance = ti,
|
||||||
accountJson = accountJson,
|
|
||||||
tokenJson = JsonObject().apply {
|
tokenJson = JsonObject().apply {
|
||||||
user.id.putTo(this, KEY_USER_ID)
|
user.id.putTo(this, KEY_USER_ID)
|
||||||
put(KEY_MISSKEY_VERSION, misskeyVersion)
|
put(KEY_MISSKEY_VERSION, misskeyVersion)
|
||||||
put(KEY_AUTH_VERSION, AUTH_VERSION)
|
put(KEY_AUTH_VERSION, AUTH_VERSION)
|
||||||
put(KEY_API_KEY_MISSKEY, apiKey)
|
put(KEY_API_KEY_MISSKEY, apiKey)
|
||||||
},
|
},
|
||||||
|
accountJson = accountJson,
|
||||||
tootAccount = user,
|
tootAccount = user,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -480,7 +480,32 @@ class TootInstance(parser: TootParser, src: JsonObject) {
|
|||||||
fun getCached(apiHost: Host) = apiHost.getCacheEntry().cacheData
|
fun getCached(apiHost: Host) = apiHost.getCacheEntry().cacheData
|
||||||
fun getCached(a: SavedAccount?) = a?.apiHost?.getCacheEntry()?.cacheData
|
fun getCached(a: SavedAccount?) = a?.apiHost?.getCacheEntry()?.cacheData
|
||||||
|
|
||||||
suspend fun get(client: TootApiClient): Pair<TootInstance?, TootApiResult?> = getEx(client)
|
suspend fun get(client: TootApiClient): Pair<TootInstance?, TootApiResult?> =
|
||||||
|
getEx(client)
|
||||||
|
|
||||||
|
suspend fun getOrThrow(client: TootApiClient): TootInstance {
|
||||||
|
val (ti, ri) = get(client)
|
||||||
|
return ti ?: error("can't get server information. ${ri?.error}")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getExOrThrow(
|
||||||
|
client: TootApiClient,
|
||||||
|
hostArg: Host? = null,
|
||||||
|
account: SavedAccount? = null,
|
||||||
|
allowPixelfed: Boolean = false,
|
||||||
|
forceUpdate: Boolean = false,
|
||||||
|
forceAccessToken: String? = null, // マストドンのwhitelist modeでアカウント追加時に必要
|
||||||
|
): TootInstance {
|
||||||
|
val (ti, ri) = getEx(
|
||||||
|
client = client,
|
||||||
|
hostArg = hostArg,
|
||||||
|
account = account,
|
||||||
|
allowPixelfed = allowPixelfed,
|
||||||
|
forceUpdate = forceUpdate,
|
||||||
|
forceAccessToken = forceAccessToken,
|
||||||
|
)
|
||||||
|
return ti ?: error("can't get server information. ${ri?.error}")
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getEx(
|
suspend fun getEx(
|
||||||
client: TootApiClient,
|
client: TootApiClient,
|
||||||
|
@ -10,6 +10,7 @@ import android.widget.EditText
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
|
import jp.juggler.subwaytooter.api.auth.CreateUserParams
|
||||||
import jp.juggler.subwaytooter.api.entity.Host
|
import jp.juggler.subwaytooter.api.entity.Host
|
||||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
import jp.juggler.subwaytooter.util.DecodeOptions
|
import jp.juggler.subwaytooter.util.DecodeOptions
|
||||||
@ -22,18 +23,16 @@ import jp.juggler.util.ui.*
|
|||||||
class DlgCreateAccount(
|
class DlgCreateAccount(
|
||||||
val activity: AppCompatActivity,
|
val activity: AppCompatActivity,
|
||||||
val apiHost: Host,
|
val apiHost: Host,
|
||||||
val onClickOk: (
|
val onClickOk: (dialog: Dialog, params: CreateUserParams) -> Unit,
|
||||||
dialog: Dialog,
|
|
||||||
username: String,
|
|
||||||
email: String,
|
|
||||||
password: String,
|
|
||||||
agreement: Boolean,
|
|
||||||
reason: String?,
|
|
||||||
) -> Unit,
|
|
||||||
) : View.OnClickListener {
|
) : View.OnClickListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// private val log = LogCategory("DlgCreateAccount")
|
// private val log = LogCategory("DlgCreateAccount")
|
||||||
|
|
||||||
|
fun AppCompatActivity.showUserCreateDialog(
|
||||||
|
apiHost: Host,
|
||||||
|
onClickOk: (dialog: Dialog, params: CreateUserParams) -> Unit,
|
||||||
|
) = DlgCreateAccount(this, apiHost, onClickOk).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
@ -123,14 +122,16 @@ class DlgCreateAccount(
|
|||||||
|
|
||||||
else -> onClickOk(
|
else -> onClickOk(
|
||||||
dialog,
|
dialog,
|
||||||
username,
|
CreateUserParams(
|
||||||
email,
|
username = username,
|
||||||
password,
|
email = email,
|
||||||
cbAgreement.isChecked,
|
password = password,
|
||||||
when (etReason.visibility) {
|
agreement = cbAgreement.isChecked,
|
||||||
View.VISIBLE -> etReason.text.toString().trim()
|
reason = when (etReason.visibility) {
|
||||||
else -> null
|
View.VISIBLE -> etReason.text.toString().trim()
|
||||||
}
|
else -> null
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,56 @@
|
|||||||
package jp.juggler.subwaytooter.dialog
|
package jp.juggler.subwaytooter.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.text.InputType
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.subwaytooter.api.entity.Host
|
import jp.juggler.subwaytooter.api.entity.Host
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
|
import jp.juggler.subwaytooter.api.runApiTask2
|
||||||
|
import jp.juggler.subwaytooter.databinding.DlgAccountAddBinding
|
||||||
|
import jp.juggler.subwaytooter.databinding.LvAuthTypeBinding
|
||||||
|
import jp.juggler.subwaytooter.util.DecodeOptions
|
||||||
|
import jp.juggler.subwaytooter.util.LinkHelper
|
||||||
|
import jp.juggler.util.coroutine.launchAndShowError
|
||||||
|
import jp.juggler.util.data.notBlank
|
||||||
|
import jp.juggler.util.data.notEmpty
|
||||||
import jp.juggler.util.log.*
|
import jp.juggler.util.log.*
|
||||||
|
import jp.juggler.util.ui.*
|
||||||
|
import org.jetbrains.anko.textColor
|
||||||
|
import org.jetbrains.anko.textResource
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.net.IDN
|
import java.net.IDN
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object LoginForm {
|
class LoginForm(
|
||||||
|
val activity: AppCompatActivity,
|
||||||
|
val onClickOk: (
|
||||||
|
dialog: Dialog,
|
||||||
|
apiHost: Host,
|
||||||
|
serverInfo: TootInstance?,
|
||||||
|
action: Action,
|
||||||
|
) -> Unit,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val log = LogCategory("LoginForm")
|
||||||
|
|
||||||
private val log = LogCategory("LoginForm")
|
@Suppress("RegExpSimplifiable")
|
||||||
|
val reBadLetter = """[^A-Za-z0-9:;._-]+""".toRegex()
|
||||||
|
|
||||||
|
fun AppCompatActivity.showLoginForm(
|
||||||
|
onClickOk: (
|
||||||
|
dialog: Dialog,
|
||||||
|
apiHost: Host,
|
||||||
|
serverInfo: TootInstance?,
|
||||||
|
action: Action,
|
||||||
|
) -> Unit,
|
||||||
|
) = LoginForm(this, onClickOk)
|
||||||
|
}
|
||||||
|
|
||||||
private class StringArray : ArrayList<String>()
|
private class StringArray : ArrayList<String>()
|
||||||
|
|
||||||
@ -28,103 +59,51 @@ object LoginForm {
|
|||||||
@StringRes val idName: Int,
|
@StringRes val idName: Int,
|
||||||
@StringRes val idDesc: Int,
|
@StringRes val idDesc: Int,
|
||||||
) {
|
) {
|
||||||
|
Login(0, R.string.existing_account, R.string.existing_account_desc),
|
||||||
Existing(0, R.string.existing_account, R.string.existing_account_desc),
|
|
||||||
Pseudo(1, R.string.pseudo_account, R.string.pseudo_account_desc),
|
Pseudo(1, R.string.pseudo_account, R.string.pseudo_account_desc),
|
||||||
Create(2, R.string.create_account, R.string.create_account_desc),
|
Create(2, R.string.create_account, R.string.create_account_desc),
|
||||||
Token(3, R.string.input_access_token, R.string.input_access_token_desc),
|
Token(3, R.string.input_access_token, R.string.input_access_token_desc),
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
val views = DlgAccountAddBinding.inflate(activity.layoutInflater)
|
||||||
fun showLoginForm(
|
val dialog = Dialog(activity)
|
||||||
activity: Activity,
|
|
||||||
instanceArg: String?,
|
|
||||||
onClickOk: (
|
|
||||||
dialog: Dialog,
|
|
||||||
apiHost: Host,
|
|
||||||
action: Action,
|
|
||||||
) -> Unit,
|
|
||||||
) {
|
|
||||||
val view = activity.layoutInflater.inflate(R.layout.dlg_account_add, null, false)
|
|
||||||
val etInstance: AutoCompleteTextView = view.findViewById(R.id.etInstance)
|
|
||||||
val btnOk: View = view.findViewById(R.id.btnOk)
|
|
||||||
|
|
||||||
val tvActionDesc: TextView = view.findViewById(R.id.tvActionDesc)
|
private var targetServer: Host? = null
|
||||||
|
private var targetServerInfo: TootInstance? = null
|
||||||
|
|
||||||
fun Spinner.getActionDesc(): String =
|
init {
|
||||||
Action.values()
|
for (a in Action.values()) {
|
||||||
.find { it.pos == selectedItemPosition }
|
val subViews =
|
||||||
?.let { activity.getString(it.idDesc) }
|
LvAuthTypeBinding.inflate(activity.layoutInflater, views.llPageAuthType, true)
|
||||||
?: "(null)"
|
subViews.btnAuthType.textResource = a.idName
|
||||||
|
subViews.tvDesc.textResource = a.idDesc
|
||||||
val spAction = view.findViewById<Spinner>(R.id.spAction).also { sp ->
|
subViews.btnAuthType.setOnClickListener { onAuthTypeSelect(a) }
|
||||||
|
|
||||||
sp.adapter = ArrayAdapter(
|
|
||||||
activity,
|
|
||||||
android.R.layout.simple_spinner_item,
|
|
||||||
Action.values().map { activity.getString(it.idName) }.toTypedArray()
|
|
||||||
).apply {
|
|
||||||
setDropDownViewResource(R.layout.lv_spinner_dropdown)
|
|
||||||
}
|
|
||||||
|
|
||||||
sp.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) { // TODO
|
|
||||||
tvActionDesc.text = sp.getActionDesc()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemSelected(
|
|
||||||
parent: AdapterView<*>?,
|
|
||||||
view: View?,
|
|
||||||
position: Int,
|
|
||||||
id: Long,
|
|
||||||
) {
|
|
||||||
tvActionDesc.text = sp.getActionDesc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
views.btnPrev.setOnClickListener { showPage(0) }
|
||||||
tvActionDesc.text = spAction.getActionDesc()
|
views.btnNext.setOnClickListener { nextPage() }
|
||||||
|
views.btnCancel.setOnClickListener { dialog.cancel() }
|
||||||
if (instanceArg != null && instanceArg.isNotEmpty()) {
|
views.etInstance.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
|
||||||
etInstance.setText(instanceArg)
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
etInstance.inputType = InputType.TYPE_NULL
|
nextPage()
|
||||||
etInstance.isEnabled = false
|
return@OnEditorActionListener true
|
||||||
etInstance.isFocusable = false
|
|
||||||
} else {
|
|
||||||
etInstance.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
|
||||||
btnOk.performClick()
|
|
||||||
return@OnEditorActionListener true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
val dialog = Dialog(activity)
|
|
||||||
dialog.setContentView(view)
|
|
||||||
// 警告がでるが、パラメータ名の指定を削ってはいけない
|
|
||||||
btnOk.setOnClickListener { _ ->
|
|
||||||
val instance = etInstance.text.toString().trim { it <= ' ' }
|
|
||||||
|
|
||||||
when {
|
|
||||||
|
|
||||||
instance.isEmpty() ->
|
|
||||||
activity.showToast(true, R.string.instance_not_specified)
|
|
||||||
|
|
||||||
instance.contains("/") || instance.contains("@") ->
|
|
||||||
activity.showToast(true, R.string.instance_not_need_slash)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val actionPos = spAction.selectedItemPosition
|
|
||||||
when (val action = Action.values().find { it.pos == actionPos }) {
|
|
||||||
null -> {
|
|
||||||
} // will no happened
|
|
||||||
else -> onClickOk(dialog, Host.parse(instance), action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
false
|
||||||
view.findViewById<View>(R.id.btnCancel).setOnClickListener { dialog.cancel() }
|
})
|
||||||
|
views.etInstance.addTextChangedListener { validateAndShow() }
|
||||||
|
|
||||||
|
showPage(0)
|
||||||
|
initServerNameList()
|
||||||
|
validateAndShow()
|
||||||
|
|
||||||
|
dialog.setContentView(views.root)
|
||||||
|
dialog.window?.setLayout(
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initServerNameList() {
|
||||||
val instance_list = HashSet<String>().apply {
|
val instance_list = HashSet<String>().apply {
|
||||||
try {
|
try {
|
||||||
activity.resources.openRawResource(R.raw.server_list).use { inStream ->
|
activity.resources.openRawResource(R.raw.server_list).use { inStream ->
|
||||||
@ -146,13 +125,11 @@ object LoginForm {
|
|||||||
val adapter = object : ArrayAdapter<String>(
|
val adapter = object : ArrayAdapter<String>(
|
||||||
activity, R.layout.lv_spinner_dropdown, ArrayList()
|
activity, R.layout.lv_spinner_dropdown, ArrayList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val nameFilter: Filter = object : Filter() {
|
val nameFilter: Filter = object : Filter() {
|
||||||
override fun convertResultToString(value: Any): CharSequence {
|
override fun convertResultToString(value: Any) =
|
||||||
return value as String
|
value as String
|
||||||
}
|
|
||||||
|
|
||||||
override fun performFiltering(constraint: CharSequence?): FilterResults =
|
override fun performFiltering(constraint: CharSequence?) =
|
||||||
FilterResults().also { result ->
|
FilterResults().also { result ->
|
||||||
if (constraint?.isNotEmpty() == true) {
|
if (constraint?.isNotEmpty() == true) {
|
||||||
val key = constraint.toString().lowercase()
|
val key = constraint.toString().lowercase()
|
||||||
@ -169,10 +146,7 @@ object LoginForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun publishResults(
|
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
|
||||||
constraint: CharSequence?,
|
|
||||||
results: FilterResults?,
|
|
||||||
) {
|
|
||||||
clear()
|
clear()
|
||||||
val values = results?.values
|
val values = results?.values
|
||||||
if (values is StringArray) {
|
if (values is StringArray) {
|
||||||
@ -184,17 +158,109 @@ object LoginForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilter(): Filter {
|
override fun getFilter(): Filter = nameFilter
|
||||||
return nameFilter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
adapter.setDropDownViewResource(R.layout.lv_spinner_dropdown)
|
adapter.setDropDownViewResource(R.layout.lv_spinner_dropdown)
|
||||||
etInstance.setAdapter<ArrayAdapter<String>>(adapter)
|
views.etInstance.setAdapter<ArrayAdapter<String>>(adapter)
|
||||||
|
}
|
||||||
|
|
||||||
dialog.window?.setLayout(
|
// return validated name. else null
|
||||||
WindowManager.LayoutParams.MATCH_PARENT,
|
private fun validateAndShow(): String? {
|
||||||
WindowManager.LayoutParams.WRAP_CONTENT
|
fun showError(s: String) {
|
||||||
)
|
views.btnNext.isEnabledAlpha = false
|
||||||
dialog.show()
|
views.tvError.visible().text = s
|
||||||
|
}
|
||||||
|
|
||||||
|
val s = views.etInstance.text.toString().trim()
|
||||||
|
if (s.isEmpty()) {
|
||||||
|
showError(activity.getString(R.string.instance_not_specified))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// コピペミスに合わせたガイド
|
||||||
|
arrayOf(
|
||||||
|
"http://",
|
||||||
|
"https://",
|
||||||
|
).forEach {
|
||||||
|
if (s.contains(it)) {
|
||||||
|
showError(activity.getString(R.string.server_host_name_cant_contains_it, it))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s.contains("/") || s.contains("@")) {
|
||||||
|
showError(activity.getString(R.string.instance_not_need_slash))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
reBadLetter.findAll(s).joinToString("") { it.value }.notEmpty()?.let {
|
||||||
|
showError(activity.getString(R.string.server_host_name_cant_contains_it, it))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
views.tvError.invisible()
|
||||||
|
views.btnNext.isEnabledAlpha = true
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPage(n: Int) {
|
||||||
|
views.etInstance.dismissDropDown()
|
||||||
|
views.etInstance.hideKeyboard()
|
||||||
|
views.llPageServerHost.vg(n == 0)
|
||||||
|
views.llPageAuthType.vg(n == 1)
|
||||||
|
val canBack = n != 0
|
||||||
|
views.btnPrev.vg(canBack)
|
||||||
|
val canNext = n == 0
|
||||||
|
views.btnNext.visibleOrInvisible(canNext)
|
||||||
|
views.tvHeader.textResource = when (n) {
|
||||||
|
0 -> R.string.server_host_name
|
||||||
|
else -> R.string.authentication_select
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextPage() {
|
||||||
|
activity.run {
|
||||||
|
launchAndShowError {
|
||||||
|
val hostname = validateAndShow() ?: return@launchAndShowError
|
||||||
|
val host = Host.parse(hostname)
|
||||||
|
var error: String? = null
|
||||||
|
val tootInstance = try {
|
||||||
|
runApiTask2(host) {
|
||||||
|
TootInstance.getOrThrow(it)
|
||||||
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
error = ex.message
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (isDestroyed || isFinishing) return@launchAndShowError
|
||||||
|
targetServer = host
|
||||||
|
targetServerInfo = tootInstance
|
||||||
|
views.tvServerHost.text = tootInstance?.apDomain?.pretty ?: host.pretty
|
||||||
|
views.tvServerDesc.run {
|
||||||
|
when (tootInstance) {
|
||||||
|
null -> {
|
||||||
|
textColor = attrColor(R.attr.colorRegexFilterError)
|
||||||
|
text = error
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
textColor = attrColor(R.attr.colorTextContent)
|
||||||
|
text = tootInstance.short_description.notBlank()
|
||||||
|
?: DecodeOptions(
|
||||||
|
applicationContext,
|
||||||
|
LinkHelper.create(tootInstance),
|
||||||
|
forceHtml = true,
|
||||||
|
short = true,
|
||||||
|
).decodeHTML(tootInstance.description)
|
||||||
|
.replace("""\n[\s\n]+""".toRegex(),"\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showPage(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAuthTypeSelect(action: Action) {
|
||||||
|
targetServer?.let { onClickOk(dialog, it, targetServerInfo, action) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -901,7 +901,7 @@ class SavedAccount(
|
|||||||
|
|
||||||
// ユーザ情報を取得してみる。承認済みなら読めるはず
|
// ユーザ情報を取得してみる。承認済みなら読めるはず
|
||||||
// 読めなければ例外が出る
|
// 読めなければ例外が出る
|
||||||
val userJson = client.getUserCredential(
|
val userJson = client.verifyAccount(
|
||||||
accessToken = accessToken,
|
accessToken = accessToken,
|
||||||
outTokenInfo = null,
|
outTokenInfo = null,
|
||||||
misskeyVersion = 0, // Mastodon only
|
misskeyVersion = 0, // Mastodon only
|
||||||
|
@ -157,8 +157,19 @@ class CustomEmojiLister(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCachedEmoji(apiHostAscii: String?, shortcode: String): CustomEmoji? =
|
fun getCachedEmoji(apiHostAscii: String?, shortcode: String): CustomEmoji? {
|
||||||
getCached(elapsedTime, apiHostAscii)?.mapShortCode?.get(shortcode)
|
val cache = getCached(elapsedTime, apiHostAscii)
|
||||||
|
if (cache == null) {
|
||||||
|
log.w("getCachedEmoji: missing cache for $apiHostAscii")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val emoji = cache.mapShortCode.get(shortcode)
|
||||||
|
if (emoji == null) {
|
||||||
|
log.w("getCachedEmoji: missing emoji for $shortcode in $apiHostAscii")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return emoji
|
||||||
|
}
|
||||||
|
|
||||||
private inner class Worker : WorkerBase() {
|
private inner class Worker : WorkerBase() {
|
||||||
|
|
||||||
@ -303,21 +314,14 @@ class CustomEmojiLister(
|
|||||||
if (over <= 0) return
|
if (over <= 0) return
|
||||||
|
|
||||||
// 古い要素を一時リストに集める
|
// 古い要素を一時リストに集める
|
||||||
val now = elapsedTime
|
|
||||||
val list = ArrayList<CacheItem>(over)
|
|
||||||
for (item in cache.values) {
|
|
||||||
if (now - item.timeUsed > 1000L) list.add(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 昇順ソート
|
// 昇順ソート
|
||||||
list.sortBy { it.timeUsed }
|
|
||||||
|
|
||||||
// 古い物から順に捨てる
|
// 古い物から順に捨てる
|
||||||
var removed = 0
|
val now = elapsedTime
|
||||||
for (item in list) {
|
cache.entries
|
||||||
cache.remove(item.key)
|
.filter { now - it.value.timeUsed > 1000L }
|
||||||
if (++removed >= over) break
|
.sortedBy { it.value.timeUsed }
|
||||||
}
|
.take(over)
|
||||||
|
.forEach { cache.remove(it.key) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -407,6 +407,7 @@ object EmojiDecoder {
|
|||||||
)
|
)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
// 絵文字プロクシを利用できない
|
||||||
apiHostAscii == null -> {
|
apiHostAscii == null -> {
|
||||||
log.w("decodeEmoji Misskey13 missing apiHostAscii")
|
log.w("decodeEmoji Misskey13 missing apiHostAscii")
|
||||||
}
|
}
|
||||||
@ -434,26 +435,20 @@ object EmojiDecoder {
|
|||||||
|
|
||||||
// 通常の絵文字
|
// 通常の絵文字
|
||||||
when {
|
when {
|
||||||
reHohoemi.matcher(name).find() -> builder.addImageSpan(
|
reHohoemi.matcher(name).find() ->
|
||||||
part,
|
builder.addImageSpan(part, R.drawable.emoji_hohoemi)
|
||||||
R.drawable.emoji_hohoemi
|
reNicoru.matcher(name).find() ->
|
||||||
)
|
builder.addImageSpan(part, R.drawable.emoji_nicoru)
|
||||||
reNicoru.matcher(name).find() -> builder.addImageSpan(
|
|
||||||
part,
|
|
||||||
R.drawable.emoji_nicoru
|
|
||||||
)
|
|
||||||
else -> {
|
else -> {
|
||||||
// EmojiOneのショートコード
|
// EmojiOneのショートコード
|
||||||
val emoji = if (useEmojioneShortcode) {
|
val emoji = when {
|
||||||
EmojiMap.shortNameMap[name.lowercase().replace('-', '_')]
|
useEmojioneShortcode ->
|
||||||
} else {
|
EmojiMap.shortNameMap[name.lowercase().replace('-', '_')]
|
||||||
null
|
else -> null
|
||||||
}
|
}
|
||||||
|
when (emoji) {
|
||||||
if (emoji == null) {
|
null -> builder.addUnicodeString(part)
|
||||||
builder.addUnicodeString(part)
|
else -> builder.addImageSpan(part, emoji)
|
||||||
} else {
|
|
||||||
builder.addImageSpan(part, emoji)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -481,19 +476,13 @@ object EmojiDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onShortCode(prevCodePoint: Int, part: String, name: String) {
|
override fun onShortCode(prevCodePoint: Int, part: String, name: String) {
|
||||||
|
|
||||||
// カスタム絵文字にマッチするなら変換しない
|
// カスタム絵文字にマッチするなら変換しない
|
||||||
val emojiCustom = emojiMapCustom?.get(name)
|
|
||||||
if (emojiCustom != null) {
|
|
||||||
sb.append(part)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// カスタム絵文字ではなく通常の絵文字のショートコードなら絵文字に変換する
|
// カスタム絵文字ではなく通常の絵文字のショートコードなら絵文字に変換する
|
||||||
val emoji = if (decodeEmojioneShortcode) {
|
val emoji = when {
|
||||||
EmojiMap.shortNameMap[name.lowercase().replace('-', '_')]
|
decodeEmojioneShortcode &&
|
||||||
} else {
|
emojiMapCustom?.get(name) == null ->
|
||||||
null
|
EmojiMap.shortNameMap[name.lowercase().replace('-', '_')]
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
sb.append(emoji?.unifiedCode ?: part)
|
sb.append(emoji?.unifiedCode ?: part)
|
||||||
}
|
}
|
||||||
|
@ -1,68 +1,146 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:fadeScrollbars="false"
|
||||||
>
|
android:scrollbarStyle="outsideOverlay">
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:text="@string/instance"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AutoCompleteTextView
|
|
||||||
android:id="@+id/etInstance"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:hint="@string/instance_hint"
|
|
||||||
android:imeOptions="actionDone"
|
|
||||||
android:inputType="textUri"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/spAction"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
/>
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="32dp"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:id="@+id/tvActionDesc"
|
|
||||||
/>
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
style="?android:attr/buttonBarStyle"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="vertical">
|
||||||
>
|
<!-- header -->
|
||||||
|
<LinearLayout
|
||||||
<Button
|
android:layout_width="match_parent"
|
||||||
android:id="@+id/btnCancel"
|
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:baselineAligned="false"
|
||||||
android:text="@string/cancel"
|
android:gravity="center_vertical"
|
||||||
/>
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<Button
|
<TextView
|
||||||
android:id="@+id/btnOk"
|
android:id="@+id/tvHeader"
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
android:layout_width="0dp"
|
||||||
android:layout_width="0dp"
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:textSize="18sp"
|
||||||
|
tools:text="@string/server_host_name" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnCancel"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||||
|
android:contentDescription="@string/cancel"
|
||||||
|
android:src="@drawable/ic_close"
|
||||||
|
app:tint="?attr/colorColumnHeaderName" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- page 1: input server name -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/llPageServerHost"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_margin="12dp"
|
||||||
android:text="@string/ok"
|
android:orientation="vertical">
|
||||||
/>
|
|
||||||
|
<AutoCompleteTextView
|
||||||
|
android:id="@+id/etInstance"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/instance_hint"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:inputType="textUri" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:gravity="top"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvError"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center|start"
|
||||||
|
android:textColor="?attr/colorRegexFilterError"
|
||||||
|
tools:text="error error" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnNext"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:text="@string/next_step" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:autoLink="web"
|
||||||
|
android:gravity="top"
|
||||||
|
android:text="@string/input_server_name_desc"
|
||||||
|
android:textColor="?attr/colorTextHelp" />
|
||||||
|
</LinearLayout>
|
||||||
|
<!-- page 2: select action -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/llPageAuthType"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvServerHost"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:autoSizeTextType="uniform"
|
||||||
|
tools:text="mastdon.social" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnPrev"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:contentDescription="@string/previous"
|
||||||
|
android:src="@drawable/ic_edit"
|
||||||
|
app:tint="?attr/colorColumnHeaderName" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvServerDesc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:background="?attr/colorColumnSettingBackground"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:textColor="?attr/colorTextContent"
|
||||||
|
tools:text="error error" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:autoLink="web"
|
||||||
|
android:text="@string/authentication_select_desc"
|
||||||
|
android:textColor="?attr/colorTextHelp" />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</ScrollView>
|
||||||
|
25
app/src/main/res/layout/lv_auth_type.xml
Normal file
25
app/src/main/res/layout/lv_auth_type.xml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingVertical="12dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnAuthType"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="@string/input_access_token" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDesc"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:autoLink="web"
|
||||||
|
android:textColor="?attr/colorTextHelp"
|
||||||
|
tools:text="@string/input_access_token_desc" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -1181,4 +1181,13 @@
|
|||||||
<string name="color_theme_changed">色テーマのデフォルトが変わりました。カスタマイズ済の色設定と競合する場合があります。 アプリ設定の色セクションを確認することをお勧めします。</string>
|
<string name="color_theme_changed">色テーマのデフォルトが変わりました。カスタマイズ済の色設定と競合する場合があります。 アプリ設定の色セクションを確認することをお勧めします。</string>
|
||||||
<string name="tablet_snap">スクロール時にカラム端と画面端を揃える (アプリ再起動が必要)</string>
|
<string name="tablet_snap">スクロール時にカラム端と画面端を揃える (アプリ再起動が必要)</string>
|
||||||
|
|
||||||
|
<string name="server_host_name">サーバのホスト名</string>
|
||||||
|
<string name="input_server_name_desc">\n\n・サーバのURLのホスト名部分。\n・https:// や/以降のパスを含まない。\n・サーバ一覧 https://joinmastodon.org/servers</string>
|
||||||
|
<string name="authentication_select">認証方法の選択</string>
|
||||||
|
<string name="authentication_select_desc">サーバにアクセスする方法を以下から選択してください。</string>
|
||||||
|
<string name="server_host_name_cant_contains_it">ホスト名に使えない文字が含まれています。[%1$s]</string>
|
||||||
|
<string name="next_step">次へ</string>
|
||||||
|
<string name="user_creation_not_supported">[%1$s, %2$s] サーバ種別はアプリからのユーザ登録に対応していません。</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<color name="Light_colorPostFormBackground">#eee</color>
|
<color name="Light_colorPostFormBackground">#eee</color>
|
||||||
<color name="Light_colorActionBarBg">#ccc</color>
|
<color name="Light_colorActionBarBg">#ccc</color>
|
||||||
<color name="Light_colorActionBarBgStacked">#ddd</color>
|
<color name="Light_colorActionBarBgStacked">#ddd</color>
|
||||||
<color name="Light_colorStatusBarBg">#303030</color> <!-- ステータスバー背景 -->
|
<color name="Light_colorStatusBarBg">#707070</color> <!-- ステータスバー背景 -->
|
||||||
<color name="Light_colorProfileBackgroundMask">#C0FFFFFF</color>
|
<color name="Light_colorProfileBackgroundMask">#C0FFFFFF</color>
|
||||||
<color name="Light_colorRefreshErrorBg">#D222</color>
|
<color name="Light_colorRefreshErrorBg">#D222</color>
|
||||||
<color name="Light_colorRegexFilterError">#f00</color>
|
<color name="Light_colorRegexFilterError">#f00</color>
|
||||||
@ -41,6 +41,8 @@
|
|||||||
<color name="Light_colorTextContent">#ff333333</color>
|
<color name="Light_colorTextContent">#ff333333</color>
|
||||||
<color name="Light_colorTextDivider">#80000000</color>
|
<color name="Light_colorTextDivider">#80000000</color>
|
||||||
<color name="Light_colorTextHelp">#5a5a5a</color>
|
<color name="Light_colorTextHelp">#5a5a5a</color>
|
||||||
|
<color name="Light_colorTextHint">#40000000</color>
|
||||||
|
|
||||||
<color name="Light_colorTextTimeSmall">#ff666666</color>
|
<color name="Light_colorTextTimeSmall">#ff666666</color>
|
||||||
<color name="Light_colorThumbnailBackground">#20000000</color>
|
<color name="Light_colorThumbnailBackground">#20000000</color>
|
||||||
<color name="Light_colotListItemDrag">#AACCCCCC</color>
|
<color name="Light_colotListItemDrag">#AACCCCCC</color>
|
||||||
@ -88,6 +90,8 @@
|
|||||||
<color name="Dark_colorTextColumnListItem">#66FFFFFF</color>
|
<color name="Dark_colorTextColumnListItem">#66FFFFFF</color>
|
||||||
<color name="Dark_colorTextContent">#dddddd</color>
|
<color name="Dark_colorTextContent">#dddddd</color>
|
||||||
<color name="Dark_colorTextHelp">#ccFFFFFF</color>
|
<color name="Dark_colorTextHelp">#ccFFFFFF</color>
|
||||||
|
<color name="Dark_colorTextHint">#40ffffff</color>
|
||||||
|
|
||||||
<color name="Dark_colorTextTimeSmall">#BBBBBB</color>
|
<color name="Dark_colorTextTimeSmall">#BBBBBB</color>
|
||||||
<color name="Dark_colorThumbnailBackground">#20ffffff</color>
|
<color name="Dark_colorThumbnailBackground">#20ffffff</color>
|
||||||
<color name="Dark_colorAppCompatAccent">#0080ff</color>
|
<color name="Dark_colorAppCompatAccent">#0080ff</color>
|
||||||
@ -134,7 +138,9 @@
|
|||||||
<color name="Mastodon_colorTextColumnHeaderPageNumber">#e4e4e4</color>
|
<color name="Mastodon_colorTextColumnHeaderPageNumber">#e4e4e4</color>
|
||||||
<color name="Mastodon_colorTextColumnListItem">#66FFFFFF</color>
|
<color name="Mastodon_colorTextColumnListItem">#66FFFFFF</color>
|
||||||
<color name="Mastodon_colorTextContent">#dddddd</color>
|
<color name="Mastodon_colorTextContent">#dddddd</color>
|
||||||
<color name="Mastodon_colorTextHelp">#ccFFFFFF</color>
|
<color name="Mastodon_colorTextHelp">#bbFFFFFF</color>
|
||||||
|
<color name="Mastodon_colorTextHint">#40FFFFFF</color>
|
||||||
|
|
||||||
<color name="Mastodon_colorTextTimeSmall">#BBBBBB</color>
|
<color name="Mastodon_colorTextTimeSmall">#BBBBBB</color>
|
||||||
<color name="Mastodon_colorThumbnailBackground">#20ffffff</color>
|
<color name="Mastodon_colorThumbnailBackground">#20ffffff</color>
|
||||||
<color name="Mastodon_colorAppCompatAccent">#0080ff</color>
|
<color name="Mastodon_colorAppCompatAccent">#0080ff</color>
|
||||||
@ -146,4 +152,8 @@
|
|||||||
|
|
||||||
<!-- 通知のアクセント色 -->
|
<!-- 通知のアクセント色 -->
|
||||||
<color name="colorOsNotificationAccent">#B3E1FF</color>
|
<color name="colorOsNotificationAccent">#B3E1FF</color>
|
||||||
|
|
||||||
|
<!-- 白テーマのナビゲーションバーは白ではない -->
|
||||||
|
<color name="colorNavigationBarWorkaround">#707070</color>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1186,5 +1186,13 @@
|
|||||||
<string name="acct_customize">Acct customize</string>
|
<string name="acct_customize">Acct customize</string>
|
||||||
<string name="delete_confirm">delete \"%1$s\" ?</string>
|
<string name="delete_confirm">delete \"%1$s\" ?</string>
|
||||||
<string name="color_theme_changed">Default color theme has been updated. This may conflict with your customized color settings. We recommend reviewing the colors section in app settings.</string>
|
<string name="color_theme_changed">Default color theme has been updated. This may conflict with your customized color settings. We recommend reviewing the colors section in app settings.</string>
|
||||||
<string name="tablet_snap" >Align column edge to screen edge when scrolling (app restart required)</string>
|
<string name="tablet_snap">Align column edge to screen edge when scrolling (app restart required)</string>
|
||||||
|
|
||||||
|
<string name="server_host_name">Server\'s host name</string>
|
||||||
|
<string name="input_server_name_desc">・The host name part in the server\'s URL.\n・without http:// or trailing /path… .\n・Mastodon server list: https://joinmastodon.org/servers</string>
|
||||||
|
<string name="authentication_select">Select authentication method</string>
|
||||||
|
<string name="authentication_select_desc">Select how to access the server from below.</string>
|
||||||
|
<string name="server_host_name_cant_contains_it">host name can\'t contains [%1$s].</string>
|
||||||
|
<string name="next_step">Next step</string>
|
||||||
|
<string name="user_creation_not_supported">[%1$s, %2$s] server type does not support user registration from app.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
<item name="android:textColorPrimary">@color/Light_colorTextContent</item>
|
<item name="android:textColorPrimary">@color/Light_colorTextContent</item>
|
||||||
<item name="android:textColorSecondary">@color/Light_colorTextContent</item>
|
<item name="android:textColorSecondary">@color/Light_colorTextContent</item>
|
||||||
<item name="android:textColorTertiary">@color/Light_colorTextContent</item>
|
<item name="android:textColorTertiary">@color/Light_colorTextContent</item>
|
||||||
|
<item name="android:textColorHint">@color/Light_colorTextHint</item>
|
||||||
|
|
||||||
<!-- TabLayoutの下線やRadioButton選択時などの色 -->
|
<!-- TabLayoutの下線やRadioButton選択時などの色 -->
|
||||||
<item name="colorAccent">@color/Light_colorAppCompatAccent</item>
|
<item name="colorAccent">@color/Light_colorAppCompatAccent</item>
|
||||||
@ -23,7 +24,12 @@
|
|||||||
<item name="colorPrimaryDark">@color/Light_colorStatusBarBg</item>
|
<item name="colorPrimaryDark">@color/Light_colorStatusBarBg</item>
|
||||||
|
|
||||||
<!-- ナビゲーションバー(戻るキー・ホームキーなどがあるバー)の背景色 -->
|
<!-- ナビゲーションバー(戻るキー・ホームキーなどがあるバー)の背景色 -->
|
||||||
<item name="android:navigationBarColor">@color/Light_colorColumnStripBackground</item>
|
<!--
|
||||||
|
ナビゲーションバーのデフォルト色は通常はカラムストリップと揃えたいが、
|
||||||
|
Lightテーマでそれをやるとボタン図柄の色が白いままの端末で問題になる。
|
||||||
|
仕方ないので白テーマだけデフォルト色が異なる。
|
||||||
|
-->
|
||||||
|
<item name="android:navigationBarColor">@color/colorNavigationBarWorkaround</item>
|
||||||
|
|
||||||
<!-- ウィンドウ背景Drawable -->
|
<!-- ウィンドウ背景Drawable -->
|
||||||
<item name="android:windowBackground">@drawable/window_background</item>
|
<item name="android:windowBackground">@drawable/window_background</item>
|
||||||
@ -104,6 +110,7 @@
|
|||||||
<item name="android:textColorPrimary">@color/Dark_colorTextContent</item>
|
<item name="android:textColorPrimary">@color/Dark_colorTextContent</item>
|
||||||
<item name="android:textColorSecondary">@color/Dark_colorTextContent</item>
|
<item name="android:textColorSecondary">@color/Dark_colorTextContent</item>
|
||||||
<item name="android:textColorTertiary">@color/Dark_colorTextContent</item>
|
<item name="android:textColorTertiary">@color/Dark_colorTextContent</item>
|
||||||
|
<item name="android:textColorHint">@color/Dark_colorTextHint</item>
|
||||||
|
|
||||||
<!-- TabLayoutの下線やRadioButton選択時などの色 -->
|
<!-- TabLayoutの下線やRadioButton選択時などの色 -->
|
||||||
<item name="colorAccent">@color/Dark_colorAppCompatAccent</item>
|
<item name="colorAccent">@color/Dark_colorAppCompatAccent</item>
|
||||||
@ -205,6 +212,7 @@
|
|||||||
<item name="android:textColorPrimary">@color/Mastodon_colorTextContent</item>
|
<item name="android:textColorPrimary">@color/Mastodon_colorTextContent</item>
|
||||||
<item name="android:textColorSecondary">@color/Mastodon_colorTextContent</item>
|
<item name="android:textColorSecondary">@color/Mastodon_colorTextContent</item>
|
||||||
<item name="android:textColorTertiary">@color/Mastodon_colorTextContent</item>
|
<item name="android:textColorTertiary">@color/Mastodon_colorTextContent</item>
|
||||||
|
<item name="android:textColorHint">@color/Mastodon_colorTextHint</item>
|
||||||
|
|
||||||
<!-- TabLayoutの下線やRadioButton選択時などの色 -->
|
<!-- TabLayoutの下線やRadioButton選択時などの色 -->
|
||||||
<item name="colorAccent">@color/Mastodon_colorAppCompatAccent</item>
|
<item name="colorAccent">@color/Mastodon_colorAppCompatAccent</item>
|
||||||
|
@ -52,7 +52,7 @@ class DispatchersTest {
|
|||||||
|
|
||||||
// プロパティの定義順序に注意
|
// プロパティの定義順序に注意
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val dispatcheRule = AppTestDispatcherRule()
|
val dispatcheRule = TestDispatcherRule()
|
||||||
|
|
||||||
// リポジトリのスケジューラを共有する
|
// リポジトリのスケジューラを共有する
|
||||||
private val repository = Repository(dispatcheRule.testDispatcher)
|
private val repository = Repository(dispatcheRule.testDispatcher)
|
||||||
|
@ -18,7 +18,7 @@ import org.junit.runner.Description
|
|||||||
* https://stackoverflow.com/questions/69423060/viewmodel-ui-testing-with-junit-5
|
* https://stackoverflow.com/questions/69423060/viewmodel-ui-testing-with-junit-5
|
||||||
*/
|
*/
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
class AppTestDispatcherRule(
|
class TestDispatcherRule(
|
||||||
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
|
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
|
||||||
) : TestWatcher() {
|
) : TestWatcher() {
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user