SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt

460 lines
17 KiB
Kotlin
Raw Normal View History

2021-06-28 09:09:00 +02:00
package jp.juggler.subwaytooter.actmain
2021-06-23 06:14:25 +02:00
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
2021-06-28 09:09:00 +02:00
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R
2021-06-23 06:14:25 +02:00
import jp.juggler.subwaytooter.action.conversationOtherInstance
import jp.juggler.subwaytooter.action.openActPostImpl
import jp.juggler.subwaytooter.action.userProfile
import jp.juggler.subwaytooter.api.auth.Auth2Result
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.auth.authRepo
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.TootAccount
2021-06-23 06:14:25 +02:00
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.findStatusIdFromUrl
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.api.runApiTask2
import jp.juggler.subwaytooter.api.showApiError
2021-06-28 09:09:00 +02:00
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.startLoading
import jp.juggler.subwaytooter.dialog.DlgConfirm.okDialog
import jp.juggler.subwaytooter.dialog.actionsDialog
2021-06-23 06:14:25 +02:00
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.dialog.runInProgress
2022-06-13 19:23:46 +02:00
import jp.juggler.subwaytooter.notification.checkNotificationImmediate
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
2022-06-13 19:23:46 +02:00
import jp.juggler.subwaytooter.notification.recycleClickedNotification
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.FcmFlavor
import jp.juggler.subwaytooter.push.fcmHandler
import jp.juggler.subwaytooter.push.pushRepo
2021-06-23 06:14:25 +02:00
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.decodePercent
import jp.juggler.util.data.groupEx
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.queryIntentActivitiesCompat
2023-02-09 01:05:59 +01:00
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.unifiedpush.android.connector.UnifiedPush
2021-06-23 06:14:25 +02:00
private val log = LogCategory("ActMainIntent")
// ActOAuthCallbackで受け取ったUriを処理する
fun ActMain.handleIntentUri(uri: Uri) {
2021-06-28 16:26:42 +02:00
try {
log.i("handleIntentUri $uri")
2021-06-28 16:26:42 +02:00
when (uri.scheme) {
FcmFlavor.CUSTOM_SCHEME -> handleCustomSchemaUri(uri)
2021-06-28 16:26:42 +02:00
else -> handleOtherUri(uri)
2021-06-23 06:14:25 +02:00
}
2021-06-28 16:26:42 +02:00
} catch (ex: Throwable) {
log.e(ex, "handleIntentUri failed.")
2021-06-28 16:26:42 +02:00
showToast(ex, "handleIntentUri failed.")
2021-06-23 06:14:25 +02:00
}
2021-06-28 16:26:42 +02:00
}
2021-06-23 06:14:25 +02:00
fun ActMain.handleOtherUri(uri: Uri): Boolean {
2021-06-23 06:14:25 +02:00
val url = uri.toString()
if (uri.scheme == "web+activitypub" && uri.authority == "post") {
val postUri = uri.pathSegments?.elementAtOrNull(0)
log.i("postUri=$postUri")
if (!postUri.isNullOrEmpty()) {
conversationOtherInstance(
pos = defaultInsertPosition,
urlArg = postUri,
statusIdOriginal = null,
hostAccess = null,
statusIdAccess = null,
isReference = false,
)
return true
}
}
2021-06-28 16:26:42 +02:00
url.findStatusIdFromUrl()?.let { statusInfo ->
2021-06-23 06:14:25 +02:00
// ステータスをアプリ内で開く
conversationOtherInstance(
defaultInsertPosition,
statusInfo.url,
statusInfo.statusId,
statusInfo.host,
statusInfo.statusId,
isReference = statusInfo.isReference,
2021-06-23 06:14:25 +02:00
)
return true
2021-06-23 06:14:25 +02:00
}
2021-06-28 16:26:42 +02:00
TootAccount.reAccountUrl.matcher(url).takeIf { it.find() }?.let { m ->
// ユーザページをアプリ内で開く
2021-06-23 06:14:25 +02:00
val host = m.groupEx(1)!!
val user = m.groupEx(2)!!.decodePercent()
val instance = m.groupEx(3)?.decodePercent()
if (instance?.isNotEmpty() == true) {
userProfile(
defaultInsertPosition,
null,
Acct.parse(user, instance),
userUrl = "https://$instance/@$user",
originalUrl = url
)
} else {
userProfile(
defaultInsertPosition,
null,
acct = Acct.parse(user, host),
userUrl = url,
)
}
return true
2021-06-23 06:14:25 +02:00
}
2021-06-28 16:26:42 +02:00
TootAccount.reAccountUrl2.matcher(url).takeIf { it.find() }?.let { m ->
// intentFilterの都合でこの形式のURLが飛んでくることはないのだが…。
2021-06-23 06:14:25 +02:00
val host = m.groupEx(1)!!
val user = m.groupEx(2)!!.decodePercent()
userProfile(
defaultInsertPosition,
null,
acct = Acct.parse(user, host),
userUrl = url,
)
return true
2021-06-23 06:14:25 +02:00
}
// このアプリでは処理できないURLだった
// 外部ブラウザを開きなおそうとすると無限ループの恐れがある
// アプリケーションチューザーを表示する
val errorMessage = getString(R.string.cant_handle_uri_of, url)
try {
2022-08-11 09:51:49 +02:00
// Android 6.0以降
// MATCH_DEFAULT_ONLY だと標準の設定に指定されたアプリがあるとソレしか出てこない
// MATCH_ALL を指定すると 以前と同じ挙動になる
val queryFlag = PackageManager.MATCH_ALL
2021-06-23 06:14:25 +02:00
// queryIntentActivities に渡すURLは実在しないホストのものにする
val intent = Intent(Intent.ACTION_VIEW, "https://dummy.subwaytooter.club/".toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
val myName = packageName
2022-09-10 23:09:26 +02:00
val resolveInfoList = packageManager.queryIntentActivitiesCompat(intent, queryFlag)
2021-06-23 06:14:25 +02:00
.filter { myName != it.activityInfo.packageName }
if (resolveInfoList.isEmpty()) error("resolveInfoList is empty.")
// このアプリ以外の選択肢を集める
val choiceList = resolveInfoList
.map {
Intent(Intent.ACTION_VIEW, uri).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
`package` = it.activityInfo.packageName
setClassName(it.activityInfo.packageName, it.activityInfo.name)
}
}.toMutableList()
val chooser = Intent.createChooser(choiceList.removeAt(0), errorMessage)
// 2つめ以降はEXTRAに渡す
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, choiceList.toTypedArray())
// 指定した選択肢でチューザーを作成して開く
startActivity(chooser)
return true
2021-06-23 06:14:25 +02:00
} catch (ex: Throwable) {
log.e(ex, "can't open app to handle intent.")
2021-06-23 06:14:25 +02:00
}
AlertDialog.Builder(this)
.setCancelable(true)
.setMessage(errorMessage)
.setPositiveButton(R.string.close, null)
.show()
return false
2021-06-23 06:14:25 +02:00
}
private fun ActMain.handleCustomSchemaUri(uri: Uri) = launchAndShowError {
2021-06-23 06:14:25 +02:00
val dataIdString = uri.getQueryParameter("db_id")
if (dataIdString == null) {
2021-06-23 06:14:25 +02:00
// OAuth2 認証コールバック
// subwaytooter://oauth(\d*)/?...
handleOAuth2Callback(uri)
} else {
// ${FcmFlavor.customScheme}://notification_click/?db_id=(db_id)
handleNotificationClick(uri, dataIdString)
2021-06-23 06:14:25 +02:00
}
}
2021-06-28 16:26:42 +02:00
private fun ActMain.handleNotificationClick(uri: Uri, dataIdString: String) {
2021-06-23 06:14:25 +02:00
try {
val account = dataIdString.toLongOrNull()
?.let { daoSavedAccount.loadAccount(it) }
2021-06-23 06:14:25 +02:00
if (account == null) {
showToast(true, "handleNotificationClick: missing SavedAccount. id=$dataIdString")
return
}
pushRepo.onTapNotification(account)
2022-06-13 19:23:46 +02:00
recycleClickedNotification(this, uri)
2021-06-23 06:14:25 +02:00
val columnList = appState.columnList
val column = columnList.firstOrNull {
it.type == ColumnType.NOTIFICATIONS &&
it.accessInfo == account &&
!it.systemNotificationNotRelated
2021-06-23 06:14:25 +02:00
}?.also {
scrollToColumn(columnList.indexOf(it))
} ?: addColumn(
true,
defaultInsertPosition,
account,
ColumnType.NOTIFICATIONS
)
// 通知を読み直す
if (!column.bInitialLoading) column.startLoading()
} catch (ex: Throwable) {
log.e(ex, "handleNotificationClick failed.")
2021-06-23 06:14:25 +02:00
}
}
2021-06-28 16:26:42 +02:00
private fun ActMain.handleOAuth2Callback(uri: Uri) {
2021-06-23 06:14:25 +02:00
launchMain {
try {
val auth2Result = runApiTask2 { client ->
AuthBase.findAuthForAuthCallback(client, uri.toString())
.authStep2(uri)
2021-06-23 06:14:25 +02:00
}
afterAccountVerify(auth2Result)
} catch (ex: Throwable) {
showApiError(ex)
2021-06-23 06:14:25 +02:00
}
}
}
2023-02-09 01:05:59 +01:00
val accountVerifyMutex = Mutex()
/**
* アカウントを確認した後に呼ばれる
* @return 何かデータを更新したら真
*/
2023-02-09 01:05:59 +01:00
suspend fun ActMain.afterAccountVerify(auth2Result: Auth2Result): Boolean = auth2Result.run {
accountVerifyMutex.withLock {
// ユーザ情報中のacctはfull acct ではないので、組み立てる
val newAcct = Acct.parse(tootAccount.username, apDomain)
// full acctだよな
"""\A[^@]+@[^@]+\z""".toRegex().find(newAcct.ascii)
?: error("afterAccountAdd: incorrect userAcct. ${newAcct.ascii}")
// 「アカウント追加のハズが既存アカウントで認証していた」
// 「アクセストークン更新のハズが別アカウントで認証していた」
// などを防止するため、full acctでアプリ内DBを検索
when (val sa = daoSavedAccount.loadAccountByAcct(newAcct)) {
null -> afterAccountAdd(newAcct, auth2Result)
else -> afterAccessTokenUpdate(auth2Result, sa)
}
2021-06-28 16:26:42 +02:00
}
}
2021-06-23 06:14:25 +02:00
2023-02-09 01:05:59 +01:00
private suspend fun ActMain.afterAccessTokenUpdate(
auth2Result: Auth2Result,
sa: SavedAccount,
): Boolean {
2023-02-09 01:05:59 +01:00
log.i("afterAccessTokenUpdate token ${sa.bearerAccessToken ?: sa.misskeyApiToken} =>${auth2Result.tokenJson}")
2021-06-28 16:26:42 +02:00
// DBの情報を更新する
authRepo.updateTokenInfo(sa, auth2Result)
2021-06-23 06:14:25 +02:00
2021-06-28 16:26:42 +02:00
// 各カラムの持つアカウント情報をリロードする
reloadAccountSetting(daoSavedAccount.loadAccountList())
2021-06-23 06:14:25 +02:00
2021-06-28 16:26:42 +02:00
// 自動でリロードする
appState.columnList
.filter { it.accessInfo == sa }
.forEach { it.startLoading() }
2021-06-23 06:14:25 +02:00
2021-06-28 16:26:42 +02:00
// 通知の更新が必要かもしれない
2023-02-06 10:56:35 +01:00
checkNotificationImmediateAll(this, onlyEnqueue = true)
checkNotificationImmediate(this, sa.db_id)
updatePushDistributer()
2021-06-23 06:14:25 +02:00
2021-06-28 16:26:42 +02:00
showToast(false, R.string.access_token_updated_for, sa.acct.pretty)
return true
}
2021-06-23 06:14:25 +02:00
2023-02-09 01:05:59 +01:00
private suspend fun ActMain.afterAccountAdd(
newAcct: Acct,
auth2Result: Auth2Result,
2021-06-28 16:26:42 +02:00
): Boolean {
val ta = auth2Result.tootAccount
2021-06-28 16:26:42 +02:00
val rowId = daoSavedAccount.saveNew(
acct = newAcct.ascii,
host = auth2Result.apiHost.ascii,
domain = auth2Result.apDomain.ascii,
account = auth2Result.accountJson,
token = auth2Result.tokenJson,
misskeyVersion = auth2Result.tootInstance.misskeyVersionMajor,
2021-06-28 16:26:42 +02:00
)
val account = daoSavedAccount.loadAccount(rowId)
2021-06-28 16:26:42 +02:00
if (account == null) {
showToast(false, "loadAccount failed.")
return false
}
2021-06-23 06:14:25 +02:00
2021-06-28 16:26:42 +02:00
var bModified = false
2021-06-23 06:14:25 +02:00
2021-06-28 16:26:42 +02:00
if (account.loginAccount?.locked == true) {
bModified = true
account.visibility = TootVisibility.PrivateFollowers
}
2021-06-23 06:14:25 +02:00
2021-06-28 16:26:42 +02:00
if (!account.isMisskey) {
val source = ta.source
if (source != null) {
val privacy = TootVisibility.parseMastodon(source.privacy)
if (privacy != null) {
bModified = true
account.visibility = privacy
2021-06-23 06:14:25 +02:00
}
2021-06-28 16:26:42 +02:00
// XXX ta.source.sensitive パラメータを読んで「添付画像をデフォルトでNSFWにする」を実現する
// 現在、アカウント設定にはこの項目はない( 「NSFWな添付メディアを隠さない」はあるが全く別の効果)
}
if (bModified) {
daoSavedAccount.save(account)
2021-06-23 06:14:25 +02:00
}
}
2021-06-28 16:26:42 +02:00
// 適当にカラムを追加する
addColumn(false, defaultInsertPosition, account, ColumnType.HOME, protect = true)
if (daoSavedAccount.isSingleAccount()) {
addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS, protect = true)
addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL, protect = true)
addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE, protect = true)
2021-06-28 16:26:42 +02:00
}
// 通知の更新が必要かもしれない
2023-02-06 10:56:35 +01:00
checkNotificationImmediateAll(this, onlyEnqueue = true)
checkNotificationImmediate(this, account.db_id)
updatePushDistributer()
2021-06-28 16:26:42 +02:00
showToast(false, R.string.account_confirmed)
return true
2021-06-23 06:14:25 +02:00
}
2021-06-28 09:09:00 +02:00
fun ActMain.handleSharedIntent(intent: Intent) {
2021-06-23 06:14:25 +02:00
launchMain {
2021-06-28 09:09:00 +02:00
ActMain.sharedIntent2 = intent
2021-06-23 06:14:25 +02:00
val ai = pickAccount(
bAllowPseudo = false,
bAuto = true,
message = getString(R.string.account_picker_toot),
)
2021-06-28 09:09:00 +02:00
ActMain.sharedIntent2 = null
2021-06-23 06:14:25 +02:00
ai?.let { openActPostImpl(it.db_id, sharedIntent = intent) }
}
}
// アカウントを追加/更新したらappServerHashの取得をやりなおす
2023-02-09 01:05:59 +01:00
suspend fun ActMain.updatePushDistributer() {
when {
fcmHandler.noFcm(this) && prefDevice.pushDistributor.isNullOrEmpty() -> {
2023-02-09 01:05:59 +01:00
selectPushDistributor()
// 選択しなかった場合は購読の更新を行わない
}
2023-02-09 01:05:59 +01:00
else -> {
runInProgress(cancellable = false) { reporter ->
withContext(AppDispatchers.DEFAULT) {
pushRepo.switchDistributor(
prefDevice.pushDistributor,
reporter = reporter
)
}
}
}
}
}
fun AppCompatActivity.selectPushDistributor() {
val context = this
launchAndShowError {
val prefDevice = prefDevice
val lastDistributor = prefDevice.pushDistributor
fun String.appendChecked(checked: Boolean) = when (checked) {
true -> "$this"
else -> this
}
val upDistrobutors = UnifiedPush.getDistributors(
context,
features = ArrayList(listOf(UnifiedPush.FEATURE_BYTES_MESSAGE))
)
val hasFcm = fcmHandler.hasFcm(context)
if (upDistrobutors.isEmpty() && !hasFcm) {
okDialog(R.string.push_distributor_not_available)
} else {
actionsDialog(getString(R.string.select_push_delivery_service)) {
if (hasFcm) {
action(
getString(R.string.firebase_cloud_messaging)
.appendChecked(lastDistributor == PrefDevice.PUSH_DISTRIBUTOR_FCM)
) {
runInProgress(cancellable = false) { reporter ->
withContext(AppDispatchers.DEFAULT) {
pushRepo.switchDistributor(
PrefDevice.PUSH_DISTRIBUTOR_FCM,
reporter = reporter
)
}
}
}
}
for (packageName in upDistrobutors) {
action(
packageName.appendChecked(lastDistributor == packageName)
) {
runInProgress(cancellable = false) { reporter ->
withContext(AppDispatchers.DEFAULT) {
pushRepo.switchDistributor(
packageName,
reporter = reporter
)
}
}
}
}
action(
getString(R.string.none)
.appendChecked(lastDistributor == PrefDevice.PUSH_DISTRIBUTOR_NONE)
) {
runInProgress(cancellable = false) { reporter ->
withContext(AppDispatchers.DEFAULT) {
pushRepo.switchDistributor(
PrefDevice.PUSH_DISTRIBUTOR_NONE,
reporter = reporter
)
}
}
}
}
}
}
}