mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-02-01 11:26:48 +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.TootInstance
|
||||
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.util.SimpleHttpClientImpl
|
||||
import jp.juggler.util.log.LogCategory
|
||||
@ -30,7 +30,7 @@ class TestTootInstance {
|
||||
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
||||
// プロパティは記述順に初期化されることに注意
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
val mainDispatcherRule = TestDispatcherRule()
|
||||
|
||||
private val client by lazy {
|
||||
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.table.ClientInfo
|
||||
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.util.SimpleHttpClient
|
||||
import jp.juggler.util.data.*
|
||||
@ -36,7 +36,7 @@ class TestTootApiClient {
|
||||
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
||||
// プロパティは記述順に初期化されることに注意
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
val mainDispatcherRule = TestDispatcherRule()
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("TestTootApiClient")
|
||||
@ -1069,7 +1069,7 @@ class TestTootApiClient {
|
||||
val (ti, ri) = TootInstance.get(client)
|
||||
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)
|
||||
println("authUri=$authUri")
|
||||
|
||||
@ -1189,8 +1189,7 @@ class TestTootApiClient {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetInstanceInformation() =
|
||||
runTest {
|
||||
fun testGetInstanceInformation() = runTest {
|
||||
val callback = ProgressRecordTootApiCallback()
|
||||
val client = TootApiClient(
|
||||
appContext,
|
||||
|
@ -15,7 +15,7 @@ import org.junit.runner.Description
|
||||
* https://developer.android.com/kotlin/coroutines/test?hl=ja
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainDispatcherRule(
|
||||
class TestDispatcherRule(
|
||||
/**
|
||||
* UnconfinedTestDispatcher か StandardTestDispatcher のどちらかを指定する
|
||||
*/
|
@ -21,7 +21,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.actmain.*
|
||||
import jp.juggler.subwaytooter.actpost.CompletionHelper
|
||||
@ -289,7 +289,8 @@ class ActMain : AppCompatActivity(),
|
||||
|
||||
ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN ->
|
||||
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 jp.juggler.subwaytooter.api.TootParser
|
||||
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.UserRelation
|
||||
import jp.juggler.subwaytooter.util.matchHost
|
||||
@ -24,14 +25,12 @@ internal suspend fun AppCompatActivity.addPseudoAccount(
|
||||
|
||||
try {
|
||||
suspend fun AppCompatActivity.getInstanceInfo(): TootInstance? {
|
||||
var resultTi: TootInstance? = null
|
||||
val result = runApiTask(host) { client ->
|
||||
val (instance, instanceResult) = TootInstance.get(client)
|
||||
resultTi = instance
|
||||
instanceResult
|
||||
return try {
|
||||
runApiTask2(host) { TootInstance.getOrThrow(it) }
|
||||
} catch (ex: Throwable) {
|
||||
showApiError(ex)
|
||||
null
|
||||
}
|
||||
result?.error?.let { showToast(true, it) }
|
||||
return resultTi
|
||||
}
|
||||
|
||||
val acct = Acct.parse("?", host)
|
||||
|
@ -7,23 +7,24 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.*
|
||||
import jp.juggler.subwaytooter.actmain.addColumn
|
||||
import jp.juggler.subwaytooter.actmain.afterAccountVerify
|
||||
import jp.juggler.subwaytooter.actmain.defaultInsertPosition
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
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.column.ColumnType
|
||||
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.pref.*
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.LinkHelper
|
||||
import jp.juggler.subwaytooter.util.openBrowser
|
||||
import jp.juggler.util.*
|
||||
import jp.juggler.util.coroutine.AppDispatchers
|
||||
import jp.juggler.util.coroutine.launchIO
|
||||
import jp.juggler.util.coroutine.launchMain
|
||||
import jp.juggler.util.data.JsonObject
|
||||
import jp.juggler.util.data.buildJsonObject
|
||||
import jp.juggler.util.data.encodePercent
|
||||
import jp.juggler.util.log.LogCategory
|
||||
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() {
|
||||
val activity = this
|
||||
LoginForm.showLoginForm(this, null) { dialogHost, instance, action ->
|
||||
showLoginForm { dialogHost, apiHost, serverInfo, action ->
|
||||
launchMain {
|
||||
try {
|
||||
when (action) {
|
||||
// ログイン画面を開く
|
||||
LoginForm.Action.Existing ->
|
||||
runApiTask2(instance) { client ->
|
||||
val authUri = client.authStep1()
|
||||
withContext(AppDispatchers.MainImmediate) {
|
||||
openBrowser(authUri)
|
||||
dialogHost.dismissSafe()
|
||||
}
|
||||
LoginForm.Action.Login -> {
|
||||
val authUri = runApiTask2(apiHost) { it.authStep1() }
|
||||
openBrowser(authUri)
|
||||
dialogHost.dismissSafe()
|
||||
}
|
||||
LoginForm.Action.Pseudo -> {
|
||||
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 ->
|
||||
runApiTask2(instance) { client ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createUser(apiHost, serverInfo) { dialogHost.dismissSafe() }
|
||||
LoginForm.Action.Token ->
|
||||
runApiTask2(instance) { client ->
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
accessTokenPrompt(apiHost) { dialogHost.dismissSafe() }
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
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) {
|
||||
// if account is default account of tablet mode,
|
||||
// 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.entity.TootAccount
|
||||
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.util.coroutine.launchMain
|
||||
import jp.juggler.util.data.wrapWeakReference
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.ui.attrColor
|
||||
|
||||
private val log = LogCategory("ActPostCharCount")
|
||||
|
||||
// 最大文字数を取得する
|
||||
// 暫定で仮の値を返すことがある
|
||||
// 裏で取得し終わったら updateTextCount() を呼び出す
|
||||
@ -25,17 +28,17 @@ private fun ActPost.getMaxCharCount(): Int {
|
||||
// 同時に実行するタスクは1つまで
|
||||
if (jobMaxCharCount?.get()?.isActive != true) {
|
||||
jobMaxCharCount = launchMain {
|
||||
var newInfo: TootInstance? = null
|
||||
runApiTask(account, progressStyle = ApiTask.PROGRESS_NONE) { client ->
|
||||
val (ti, result) = TootInstance.get(client)
|
||||
newInfo = ti
|
||||
result
|
||||
try {
|
||||
runApiTask2(account, progressStyle = ApiTask.PROGRESS_NONE) {
|
||||
TootInstance.getOrThrow(it)
|
||||
}
|
||||
if (isFinishing || isDestroyed) return@launchMain
|
||||
updateTextCount()
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "getMaxCharCount failed.")
|
||||
}
|
||||
if (isFinishing || isDestroyed) return@launchMain
|
||||
if (newInfo != null) updateTextCount()
|
||||
}.wrapWeakReference
|
||||
}
|
||||
|
||||
// fall thru
|
||||
}
|
||||
|
||||
|
@ -430,35 +430,21 @@ class TootApiClient(
|
||||
forceUpdateClient: Boolean = false,
|
||||
): Uri {
|
||||
val (ti, ri) = TootInstance.get(this)
|
||||
// 情報が取れなくても続ける
|
||||
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}")
|
||||
else -> auth.authStep1(ti, forceUpdateClient)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUserCredential(
|
||||
suspend fun verifyAccount(
|
||||
accessToken: String,
|
||||
outTokenInfo: JsonObject?,
|
||||
misskeyVersion: Int = 0,
|
||||
) = AuthBase.findAuthForUserCredentian(this, misskeyVersion)
|
||||
) = AuthBase.findAuthForVerifyAccount(this, 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データ以外を扱うリクエスト
|
||||
|
||||
|
@ -2,7 +2,6 @@ package jp.juggler.subwaytooter.api.auth
|
||||
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccount
|
||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.data.JsonObject
|
||||
|
||||
/**
|
||||
@ -13,14 +12,14 @@ class Auth2Result(
|
||||
// サーバ情報
|
||||
val tootInstance: TootInstance,
|
||||
|
||||
// アクセストークンを含むJsonObject
|
||||
val tokenJson: JsonObject,
|
||||
|
||||
// TootAccountユーザ情報の元となるJSONデータ
|
||||
val accountJson: JsonObject,
|
||||
|
||||
// AccountJsonのパース結果
|
||||
val tootAccount: TootAccount,
|
||||
|
||||
// アクセストークンを含むJsonObject
|
||||
val tokenJson: JsonObject,
|
||||
) {
|
||||
// 対象サーバのAPIホスト
|
||||
val apiHost get() = tootInstance.apiHost
|
||||
|
@ -38,44 +38,34 @@ abstract class AuthBase {
|
||||
).firstNotNullOfOrNull { it.notBlank() }
|
||||
?: DEFAULT_CLIENT_NAME
|
||||
|
||||
fun findAuth(client: TootApiClient, ti: TootInstance?, ri: TootApiResult?): AuthBase? =
|
||||
fun findAuthForVerifyAccount(client: TootApiClient, misskeyVersionMajor: Int) =
|
||||
when {
|
||||
// インスタンス情報を取得できない
|
||||
ti == null -> when (ri?.response?.code) {
|
||||
misskeyVersionMajor >= 13 -> MisskeyAuth13(client)
|
||||
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
|
||||
// Mastodon's WHITELIST_MODE
|
||||
401 -> MastodonAuth(client)
|
||||
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 {
|
||||
MisskeyAuth10.isCallbackUrl(callbackUrl) -> MisskeyAuth10(client)
|
||||
MisskeyAuth13.isCallbackUrl(callbackUrl) -> MisskeyAuth13(client)
|
||||
else -> MastodonAuth(client)
|
||||
}
|
||||
|
||||
fun findAuthForUserCredentian(client: TootApiClient, misskeyVersion: Int) =
|
||||
when {
|
||||
misskeyVersion >= 13 -> MisskeyAuth13(client)
|
||||
misskeyVersion > 0 -> MisskeyAuth10(client)
|
||||
else -> MastodonAuth(client)
|
||||
}
|
||||
|
||||
fun findAuthForCreateUser(client: TootApiClient, ti: TootInstance) =
|
||||
when (ti.instanceType) {
|
||||
InstanceType.Misskey -> null
|
||||
InstanceType.Pleroma -> null
|
||||
InstanceType.Pixelfed -> null
|
||||
else -> MastodonAuth(client)
|
||||
fun findAuthForCreateUser(client: TootApiClient, ti: TootInstance?) =
|
||||
when (ti?.instanceType) {
|
||||
InstanceType.Mastodon -> MastodonAuth(client)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,7 +75,6 @@ abstract class AuthBase {
|
||||
protected val account get() = client.account
|
||||
protected val context get() = client.context
|
||||
|
||||
|
||||
/**
|
||||
* クライアントを登録してブラウザで開く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.TootApiClient
|
||||
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.InstanceType
|
||||
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.util.LinkHelper
|
||||
import jp.juggler.util.data.JsonObject
|
||||
import jp.juggler.util.data.buildJsonObject
|
||||
import jp.juggler.util.data.notEmpty
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.log.errorEx
|
||||
@ -233,11 +235,10 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
|
||||
// ?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
|
||||
// &state=db%3A3
|
||||
arrayOf(
|
||||
uri.getQueryParameter("error")?.trim()?.notEmpty(),
|
||||
uri.getQueryParameter("error_description")?.trim()?.notEmpty()
|
||||
).filterNotNull().joinToString(" ")
|
||||
.notEmpty()?.let { error(it) }
|
||||
arrayOf("error_description", "error")
|
||||
.mapNotNull { uri.getQueryParameter(it)?.trim()?.notEmpty() }
|
||||
.notEmpty()
|
||||
?.let { error(it.joinToString("\n")) }
|
||||
|
||||
// subwaytooter://oauth(\d*)/
|
||||
// ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
|
||||
@ -296,16 +297,14 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
|
||||
misskeyVersion = 0
|
||||
)
|
||||
|
||||
val (ti, ri) = TootInstance.getEx(client, forceAccessToken = accessToken)
|
||||
ti ?: error("can't get server information. ${ri?.error}")
|
||||
|
||||
val ti = TootInstance.getExOrThrow(client, forceAccessToken = accessToken)
|
||||
val parser = TootParser(context, linkHelper = LinkHelper.create(ti))
|
||||
return Auth2Result(
|
||||
tootInstance = ti,
|
||||
accountJson = accountJson,
|
||||
tokenJson = tokenInfo,
|
||||
tootAccount = TootParser(context, linkHelper = LinkHelper.create(ti))
|
||||
.account(accountJson)
|
||||
?: error("can't parse user information."),
|
||||
accountJson = accountJson,
|
||||
tootAccount = parser.account(accountJson)
|
||||
?: error("can't parse user information.")
|
||||
)
|
||||
}
|
||||
|
||||
@ -322,26 +321,48 @@ class MastodonAuth(override val client: TootApiClient) : AuthBase() {
|
||||
forceUpdateClient = false
|
||||
)
|
||||
|
||||
/**
|
||||
* ユーザ作成
|
||||
* - クライアントは登録済みであること
|
||||
*/
|
||||
|
||||
suspend fun createUser(
|
||||
clientInfo: JsonObject,
|
||||
username: String,
|
||||
email: String,
|
||||
password: String,
|
||||
agreement: Boolean,
|
||||
reason: String?,
|
||||
) = api.createUser(
|
||||
apiHost = apiHost ?: error("createUser: missing apiHost"),
|
||||
clientCredential = clientInfo.string(KEY_CLIENT_CREDENTIAL)
|
||||
?: error("createUser: missing client credential"),
|
||||
username = username,
|
||||
email = email,
|
||||
password = password,
|
||||
agreement = agreement,
|
||||
reason = reason
|
||||
)
|
||||
params: CreateUserParams,
|
||||
): Auth2Result {
|
||||
val apiHost = apiHost ?: error("createUser: missing apiHost")
|
||||
|
||||
val tokenJson = api.createUser(
|
||||
apiHost = apiHost,
|
||||
clientCredential = clientInfo.string(KEY_CLIENT_CREDENTIAL)
|
||||
?: error("createUser: missing client credential"),
|
||||
params = params,
|
||||
)
|
||||
|
||||
val accessToken = tokenJson.string("access_token")
|
||||
?: error("can't get user access token")
|
||||
|
||||
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(
|
||||
apiHost: Host,
|
||||
clientCredential: String,
|
||||
username: String,
|
||||
email: String,
|
||||
password: String,
|
||||
agreement: Boolean,
|
||||
reason: String?,
|
||||
params: CreateUserParams,
|
||||
) = buildJsonObject {
|
||||
put("username", username)
|
||||
put("email", email)
|
||||
put("password", password)
|
||||
put("agreement", agreement)
|
||||
reason?.notEmpty()?.let { put("reason", reason) }
|
||||
put("username", params.username)
|
||||
put("email", params.email)
|
||||
put("password", params.password)
|
||||
put("agreement", params.agreement)
|
||||
params.reason?.notEmpty()?.let { put("reason", it) }
|
||||
}.encodeQuery().toFormRequestBody().toPost()
|
||||
.url("https://${apiHost.ascii}/api/v1/accounts")
|
||||
.header("Authorization", "Bearer $clientCredential")
|
||||
|
@ -201,8 +201,7 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
|
||||
} ?: error("missing account db_id=$dbId")
|
||||
}
|
||||
|
||||
val (ti, r2) = TootInstance.get(client)
|
||||
ti ?: error("${r2?.error} ($apiHost)")
|
||||
val ti = TootInstance.getOrThrow(client)
|
||||
|
||||
val parser = TootParser(
|
||||
context,
|
||||
@ -230,14 +229,10 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
|
||||
val accountJson = tokenInfo["user"].cast<JsonObject>()
|
||||
?: error("missing user in the userkey response.")
|
||||
|
||||
val tootAccount = parser.account(accountJson)
|
||||
?: error("can't parse user information")
|
||||
|
||||
tokenInfo.remove("user")
|
||||
|
||||
return Auth2Result(
|
||||
tootInstance = ti,
|
||||
accountJson = accountJson,
|
||||
tokenJson = tokenInfo.also {
|
||||
EntityId.mayNull(accountJson.string("id"))?.putTo(it, KEY_USER_ID)
|
||||
it[KEY_MISSKEY_VERSION] = ti.misskeyVersionMajor
|
||||
@ -245,7 +240,9 @@ class MisskeyAuth10(override val client: TootApiClient) : AuthBase() {
|
||||
val apiKey = "$accessToken$appSecret".encodeUTF8().digestSHA256().encodeHexLower()
|
||||
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.")
|
||||
}
|
||||
|
||||
val (ti, r2) = TootInstance.get(client)
|
||||
ti ?: error("missing server information. ${r2?.error}")
|
||||
val ti = TootInstance.getOrThrow(client)
|
||||
|
||||
val misskeyVersion = ti.misskeyVersionMajor
|
||||
|
||||
@ -136,13 +135,13 @@ class MisskeyAuth13(override val client: TootApiClient) : AuthBase() {
|
||||
|
||||
return Auth2Result(
|
||||
tootInstance = ti,
|
||||
accountJson = accountJson,
|
||||
tokenJson = JsonObject().apply {
|
||||
user.id.putTo(this, KEY_USER_ID)
|
||||
put(KEY_MISSKEY_VERSION, misskeyVersion)
|
||||
put(KEY_AUTH_VERSION, AUTH_VERSION)
|
||||
put(KEY_API_KEY_MISSKEY, apiKey)
|
||||
},
|
||||
accountJson = accountJson,
|
||||
tootAccount = user,
|
||||
)
|
||||
}
|
||||
|
@ -480,7 +480,32 @@ class TootInstance(parser: TootParser, src: JsonObject) {
|
||||
fun getCached(apiHost: Host) = 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(
|
||||
client: TootApiClient,
|
||||
|
@ -10,6 +10,7 @@ import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.TootInstance
|
||||
import jp.juggler.subwaytooter.util.DecodeOptions
|
||||
@ -22,18 +23,16 @@ import jp.juggler.util.ui.*
|
||||
class DlgCreateAccount(
|
||||
val activity: AppCompatActivity,
|
||||
val apiHost: Host,
|
||||
val onClickOk: (
|
||||
dialog: Dialog,
|
||||
username: String,
|
||||
email: String,
|
||||
password: String,
|
||||
agreement: Boolean,
|
||||
reason: String?,
|
||||
) -> Unit,
|
||||
val onClickOk: (dialog: Dialog, params: CreateUserParams) -> Unit,
|
||||
) : View.OnClickListener {
|
||||
|
||||
companion object {
|
||||
// private val log = LogCategory("DlgCreateAccount")
|
||||
|
||||
fun AppCompatActivity.showUserCreateDialog(
|
||||
apiHost: Host,
|
||||
onClickOk: (dialog: Dialog, params: CreateUserParams) -> Unit,
|
||||
) = DlgCreateAccount(this, apiHost, onClickOk).show()
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@ -123,14 +122,16 @@ class DlgCreateAccount(
|
||||
|
||||
else -> onClickOk(
|
||||
dialog,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
cbAgreement.isChecked,
|
||||
when (etReason.visibility) {
|
||||
View.VISIBLE -> etReason.text.toString().trim()
|
||||
else -> null
|
||||
}
|
||||
CreateUserParams(
|
||||
username = username,
|
||||
email = email,
|
||||
password = password,
|
||||
agreement = cbAgreement.isChecked,
|
||||
reason = when (etReason.visibility) {
|
||||
View.VISIBLE -> etReason.text.toString().trim()
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,56 @@
|
||||
package jp.juggler.subwaytooter.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import jp.juggler.subwaytooter.R
|
||||
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.ui.*
|
||||
import org.jetbrains.anko.textColor
|
||||
import org.jetbrains.anko.textResource
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.IDN
|
||||
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>()
|
||||
|
||||
@ -28,103 +59,51 @@ object LoginForm {
|
||||
@StringRes val idName: Int,
|
||||
@StringRes val idDesc: Int,
|
||||
) {
|
||||
|
||||
Existing(0, R.string.existing_account, R.string.existing_account_desc),
|
||||
Login(0, R.string.existing_account, R.string.existing_account_desc),
|
||||
Pseudo(1, R.string.pseudo_account, R.string.pseudo_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),
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
fun showLoginForm(
|
||||
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 views = DlgAccountAddBinding.inflate(activity.layoutInflater)
|
||||
val dialog = Dialog(activity)
|
||||
|
||||
val tvActionDesc: TextView = view.findViewById(R.id.tvActionDesc)
|
||||
private var targetServer: Host? = null
|
||||
private var targetServerInfo: TootInstance? = null
|
||||
|
||||
fun Spinner.getActionDesc(): String =
|
||||
Action.values()
|
||||
.find { it.pos == selectedItemPosition }
|
||||
?.let { activity.getString(it.idDesc) }
|
||||
?: "(null)"
|
||||
|
||||
val spAction = view.findViewById<Spinner>(R.id.spAction).also { sp ->
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
init {
|
||||
for (a in Action.values()) {
|
||||
val subViews =
|
||||
LvAuthTypeBinding.inflate(activity.layoutInflater, views.llPageAuthType, true)
|
||||
subViews.btnAuthType.textResource = a.idName
|
||||
subViews.tvDesc.textResource = a.idDesc
|
||||
subViews.btnAuthType.setOnClickListener { onAuthTypeSelect(a) }
|
||||
}
|
||||
|
||||
tvActionDesc.text = spAction.getActionDesc()
|
||||
|
||||
if (instanceArg != null && instanceArg.isNotEmpty()) {
|
||||
etInstance.setText(instanceArg)
|
||||
etInstance.inputType = InputType.TYPE_NULL
|
||||
etInstance.isEnabled = false
|
||||
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)
|
||||
}
|
||||
}
|
||||
views.btnPrev.setOnClickListener { showPage(0) }
|
||||
views.btnNext.setOnClickListener { nextPage() }
|
||||
views.btnCancel.setOnClickListener { dialog.cancel() }
|
||||
views.etInstance.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
nextPage()
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
}
|
||||
view.findViewById<View>(R.id.btnCancel).setOnClickListener { dialog.cancel() }
|
||||
false
|
||||
})
|
||||
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 {
|
||||
try {
|
||||
activity.resources.openRawResource(R.raw.server_list).use { inStream ->
|
||||
@ -146,13 +125,11 @@ object LoginForm {
|
||||
val adapter = object : ArrayAdapter<String>(
|
||||
activity, R.layout.lv_spinner_dropdown, ArrayList()
|
||||
) {
|
||||
|
||||
val nameFilter: Filter = object : Filter() {
|
||||
override fun convertResultToString(value: Any): CharSequence {
|
||||
return value as String
|
||||
}
|
||||
override fun convertResultToString(value: Any) =
|
||||
value as String
|
||||
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults =
|
||||
override fun performFiltering(constraint: CharSequence?) =
|
||||
FilterResults().also { result ->
|
||||
if (constraint?.isNotEmpty() == true) {
|
||||
val key = constraint.toString().lowercase()
|
||||
@ -169,10 +146,7 @@ object LoginForm {
|
||||
}
|
||||
}
|
||||
|
||||
override fun publishResults(
|
||||
constraint: CharSequence?,
|
||||
results: FilterResults?,
|
||||
) {
|
||||
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
|
||||
clear()
|
||||
val values = results?.values
|
||||
if (values is StringArray) {
|
||||
@ -184,17 +158,109 @@ object LoginForm {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return nameFilter
|
||||
}
|
||||
override fun getFilter(): Filter = nameFilter
|
||||
}
|
||||
adapter.setDropDownViewResource(R.layout.lv_spinner_dropdown)
|
||||
etInstance.setAdapter<ArrayAdapter<String>>(adapter)
|
||||
views.etInstance.setAdapter<ArrayAdapter<String>>(adapter)
|
||||
}
|
||||
|
||||
dialog.window?.setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
dialog.show()
|
||||
// return validated name. else null
|
||||
private fun validateAndShow(): String? {
|
||||
fun showError(s: String) {
|
||||
views.btnNext.isEnabledAlpha = false
|
||||
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,
|
||||
outTokenInfo = null,
|
||||
misskeyVersion = 0, // Mastodon only
|
||||
|
@ -157,8 +157,19 @@ class CustomEmojiLister(
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedEmoji(apiHostAscii: String?, shortcode: String): CustomEmoji? =
|
||||
getCached(elapsedTime, apiHostAscii)?.mapShortCode?.get(shortcode)
|
||||
fun getCachedEmoji(apiHostAscii: String?, shortcode: String): CustomEmoji? {
|
||||
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() {
|
||||
|
||||
@ -303,21 +314,14 @@ class CustomEmojiLister(
|
||||
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
|
||||
for (item in list) {
|
||||
cache.remove(item.key)
|
||||
if (++removed >= over) break
|
||||
}
|
||||
val now = elapsedTime
|
||||
cache.entries
|
||||
.filter { now - it.value.timeUsed > 1000L }
|
||||
.sortedBy { it.value.timeUsed }
|
||||
.take(over)
|
||||
.forEach { cache.remove(it.key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -407,6 +407,7 @@ object EmojiDecoder {
|
||||
)
|
||||
|
||||
when {
|
||||
// 絵文字プロクシを利用できない
|
||||
apiHostAscii == null -> {
|
||||
log.w("decodeEmoji Misskey13 missing apiHostAscii")
|
||||
}
|
||||
@ -434,26 +435,20 @@ object EmojiDecoder {
|
||||
|
||||
// 通常の絵文字
|
||||
when {
|
||||
reHohoemi.matcher(name).find() -> builder.addImageSpan(
|
||||
part,
|
||||
R.drawable.emoji_hohoemi
|
||||
)
|
||||
reNicoru.matcher(name).find() -> builder.addImageSpan(
|
||||
part,
|
||||
R.drawable.emoji_nicoru
|
||||
)
|
||||
reHohoemi.matcher(name).find() ->
|
||||
builder.addImageSpan(part, R.drawable.emoji_hohoemi)
|
||||
reNicoru.matcher(name).find() ->
|
||||
builder.addImageSpan(part, R.drawable.emoji_nicoru)
|
||||
else -> {
|
||||
// EmojiOneのショートコード
|
||||
val emoji = if (useEmojioneShortcode) {
|
||||
EmojiMap.shortNameMap[name.lowercase().replace('-', '_')]
|
||||
} else {
|
||||
null
|
||||
val emoji = when {
|
||||
useEmojioneShortcode ->
|
||||
EmojiMap.shortNameMap[name.lowercase().replace('-', '_')]
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (emoji == null) {
|
||||
builder.addUnicodeString(part)
|
||||
} else {
|
||||
builder.addImageSpan(part, emoji)
|
||||
when (emoji) {
|
||||
null -> builder.addUnicodeString(part)
|
||||
else -> builder.addImageSpan(part, emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -481,19 +476,13 @@ object EmojiDecoder {
|
||||
}
|
||||
|
||||
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) {
|
||||
EmojiMap.shortNameMap[name.lowercase().replace('-', '_')]
|
||||
} else {
|
||||
null
|
||||
val emoji = when {
|
||||
decodeEmojioneShortcode &&
|
||||
emojiMapCustom?.get(name) == null ->
|
||||
EmojiMap.shortNameMap[name.lowercase().replace('-', '_')]
|
||||
else -> null
|
||||
}
|
||||
sb.append(emoji?.unifiedCode ?: part)
|
||||
}
|
||||
|
@ -1,68 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView 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_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
android:layout_height="match_parent"
|
||||
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
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCancel"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:orientation="vertical">
|
||||
<!-- header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/cancel"
|
||||
/>
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnOk"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
<TextView
|
||||
android:id="@+id/tvHeader"
|
||||
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_weight="1"
|
||||
android:text="@string/ok"
|
||||
/>
|
||||
android:layout_margin="12dp"
|
||||
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>
|
||||
</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="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>
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
<color name="Light_colorPostFormBackground">#eee</color>
|
||||
<color name="Light_colorActionBarBg">#ccc</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_colorRefreshErrorBg">#D222</color>
|
||||
<color name="Light_colorRegexFilterError">#f00</color>
|
||||
@ -41,6 +41,8 @@
|
||||
<color name="Light_colorTextContent">#ff333333</color>
|
||||
<color name="Light_colorTextDivider">#80000000</color>
|
||||
<color name="Light_colorTextHelp">#5a5a5a</color>
|
||||
<color name="Light_colorTextHint">#40000000</color>
|
||||
|
||||
<color name="Light_colorTextTimeSmall">#ff666666</color>
|
||||
<color name="Light_colorThumbnailBackground">#20000000</color>
|
||||
<color name="Light_colotListItemDrag">#AACCCCCC</color>
|
||||
@ -88,6 +90,8 @@
|
||||
<color name="Dark_colorTextColumnListItem">#66FFFFFF</color>
|
||||
<color name="Dark_colorTextContent">#dddddd</color>
|
||||
<color name="Dark_colorTextHelp">#ccFFFFFF</color>
|
||||
<color name="Dark_colorTextHint">#40ffffff</color>
|
||||
|
||||
<color name="Dark_colorTextTimeSmall">#BBBBBB</color>
|
||||
<color name="Dark_colorThumbnailBackground">#20ffffff</color>
|
||||
<color name="Dark_colorAppCompatAccent">#0080ff</color>
|
||||
@ -134,7 +138,9 @@
|
||||
<color name="Mastodon_colorTextColumnHeaderPageNumber">#e4e4e4</color>
|
||||
<color name="Mastodon_colorTextColumnListItem">#66FFFFFF</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_colorThumbnailBackground">#20ffffff</color>
|
||||
<color name="Mastodon_colorAppCompatAccent">#0080ff</color>
|
||||
@ -146,4 +152,8 @@
|
||||
|
||||
<!-- 通知のアクセント色 -->
|
||||
<color name="colorOsNotificationAccent">#B3E1FF</color>
|
||||
|
||||
<!-- 白テーマのナビゲーションバーは白ではない -->
|
||||
<color name="colorNavigationBarWorkaround">#707070</color>
|
||||
|
||||
</resources>
|
||||
|
@ -1186,5 +1186,13 @@
|
||||
<string name="acct_customize">Acct customize</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="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>
|
||||
|
@ -10,6 +10,7 @@
|
||||
<item name="android:textColorPrimary">@color/Light_colorTextContent</item>
|
||||
<item name="android:textColorSecondary">@color/Light_colorTextContent</item>
|
||||
<item name="android:textColorTertiary">@color/Light_colorTextContent</item>
|
||||
<item name="android:textColorHint">@color/Light_colorTextHint</item>
|
||||
|
||||
<!-- TabLayoutの下線やRadioButton選択時などの色 -->
|
||||
<item name="colorAccent">@color/Light_colorAppCompatAccent</item>
|
||||
@ -23,7 +24,12 @@
|
||||
<item name="colorPrimaryDark">@color/Light_colorStatusBarBg</item>
|
||||
|
||||
<!-- ナビゲーションバー(戻るキー・ホームキーなどがあるバー)の背景色 -->
|
||||
<item name="android:navigationBarColor">@color/Light_colorColumnStripBackground</item>
|
||||
<!--
|
||||
ナビゲーションバーのデフォルト色は通常はカラムストリップと揃えたいが、
|
||||
Lightテーマでそれをやるとボタン図柄の色が白いままの端末で問題になる。
|
||||
仕方ないので白テーマだけデフォルト色が異なる。
|
||||
-->
|
||||
<item name="android:navigationBarColor">@color/colorNavigationBarWorkaround</item>
|
||||
|
||||
<!-- ウィンドウ背景Drawable -->
|
||||
<item name="android:windowBackground">@drawable/window_background</item>
|
||||
@ -104,6 +110,7 @@
|
||||
<item name="android:textColorPrimary">@color/Dark_colorTextContent</item>
|
||||
<item name="android:textColorSecondary">@color/Dark_colorTextContent</item>
|
||||
<item name="android:textColorTertiary">@color/Dark_colorTextContent</item>
|
||||
<item name="android:textColorHint">@color/Dark_colorTextHint</item>
|
||||
|
||||
<!-- TabLayoutの下線やRadioButton選択時などの色 -->
|
||||
<item name="colorAccent">@color/Dark_colorAppCompatAccent</item>
|
||||
@ -205,6 +212,7 @@
|
||||
<item name="android:textColorPrimary">@color/Mastodon_colorTextContent</item>
|
||||
<item name="android:textColorSecondary">@color/Mastodon_colorTextContent</item>
|
||||
<item name="android:textColorTertiary">@color/Mastodon_colorTextContent</item>
|
||||
<item name="android:textColorHint">@color/Mastodon_colorTextHint</item>
|
||||
|
||||
<!-- TabLayoutの下線やRadioButton選択時などの色 -->
|
||||
<item name="colorAccent">@color/Mastodon_colorAppCompatAccent</item>
|
||||
|
@ -52,7 +52,7 @@ class DispatchersTest {
|
||||
|
||||
// プロパティの定義順序に注意
|
||||
@get:Rule
|
||||
val dispatcheRule = AppTestDispatcherRule()
|
||||
val dispatcheRule = TestDispatcherRule()
|
||||
|
||||
// リポジトリのスケジューラを共有する
|
||||
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
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
class AppTestDispatcherRule(
|
||||
class TestDispatcherRule(
|
||||
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
|
||||
) : TestWatcher() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user