package jp.juggler.subwaytooter.actmain import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import androidx.appcompat.app.AlertDialog import androidx.core.net.toUri import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.R 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.entity.Acct import jp.juggler.subwaytooter.api.entity.TootAccount 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 import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.startLoading import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.notification.PushSubscriptionHelper import jp.juggler.subwaytooter.notification.checkNotificationImmediate import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll import jp.juggler.subwaytooter.notification.recycleClickedNotification import jp.juggler.subwaytooter.table.SavedAccount 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 private val log = LogCategory("ActMainIntent") // ActOAuthCallbackで受け取ったUriを処理する fun ActMain.handleIntentUri(uri: Uri) { try { log.d("handleIntentUri $uri") when (uri.scheme) { "subwaytooter", "misskeyclientproto" -> handleCustomSchemaUri(uri) else -> handleOtherUri(uri) } } catch (ex: Throwable) { log.e(ex, "handleIntentUri failed.") showToast(ex, "handleIntentUri failed.") } } fun ActMain.handleOtherUri(uri: Uri): Boolean { val url = uri.toString() url.findStatusIdFromUrl()?.let { statusInfo -> // ステータスをアプリ内で開く conversationOtherInstance( defaultInsertPosition, statusInfo.url, statusInfo.statusId, statusInfo.host, statusInfo.statusId, isReference = statusInfo.isReference, ) return true } TootAccount.reAccountUrl.matcher(url).takeIf { it.find() }?.let { m -> // ユーザページをアプリ内で開く 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 } TootAccount.reAccountUrl2.matcher(url).takeIf { it.find() }?.let { m -> // intentFilterの都合でこの形式のURLが飛んでくることはないのだが…。 val host = m.groupEx(1)!! val user = m.groupEx(2)!!.decodePercent() userProfile( defaultInsertPosition, null, acct = Acct.parse(user, host), userUrl = url, ) return true } // このアプリでは処理できないURLだった // 外部ブラウザを開きなおそうとすると無限ループの恐れがある // アプリケーションチューザーを表示する val errorMessage = getString(R.string.cant_handle_uri_of, url) try { // Android 6.0以降 // MATCH_DEFAULT_ONLY だと標準の設定に指定されたアプリがあるとソレしか出てこない // MATCH_ALL を指定すると 以前と同じ挙動になる val queryFlag = PackageManager.MATCH_ALL // queryIntentActivities に渡すURLは実在しないホストのものにする val intent = Intent(Intent.ACTION_VIEW, "https://dummy.subwaytooter.club/".toUri()) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK val myName = packageName val resolveInfoList = packageManager.queryIntentActivitiesCompat(intent, queryFlag) .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 } catch (ex: Throwable) { log.e(ex, "can't open app to handle intent.") } AlertDialog.Builder(this) .setCancelable(true) .setMessage(errorMessage) .setPositiveButton(R.string.close, null) .show() return false } private fun ActMain.handleCustomSchemaUri(uri: Uri) { val dataIdString = uri.getQueryParameter("db_id") if (dataIdString != null) { // subwaytooter://notification_click/?db_id=(db_id) handleNotificationClick(uri, dataIdString) } else { // OAuth2 認証コールバック // subwaytooter://oauth(\d*)/?... handleOAuth2Callback(uri) } } private fun ActMain.handleNotificationClick(uri: Uri, dataIdString: String) { try { val account = dataIdString.toLongOrNull()?.let { SavedAccount.loadAccount(this, it) } if (account == null) { showToast(true, "handleNotificationClick: missing SavedAccount. id=$dataIdString") return } recycleClickedNotification(this, uri) val columnList = appState.columnList val column = columnList.firstOrNull { it.type == ColumnType.NOTIFICATIONS && it.accessInfo == account && !it.systemNotificationNotRelated }?.also { scrollToColumn(columnList.indexOf(it)) } ?: addColumn( true, defaultInsertPosition, account, ColumnType.NOTIFICATIONS ) // 通知を読み直す if (!column.bInitialLoading) column.startLoading() } catch (ex: Throwable) { log.e(ex, "handleNotificationClick failed.") } } private fun ActMain.handleOAuth2Callback(uri: Uri) { launchMain { try { val auth2Result = runApiTask2 { client -> AuthBase.findAuthForAuthCallback(client, uri.toString()) .authStep2(uri) } afterAccountVerify(auth2Result) } catch (ex: Throwable) { showApiError(ex) } } } /** * アカウントを確認した後に呼ばれる * @return 何かデータを更新したら真 */ fun ActMain.afterAccountVerify(auth2Result: Auth2Result): Boolean = auth2Result.run { // ユーザ情報中の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 = SavedAccount.loadAccountByAcct(this@afterAccountVerify, newAcct.ascii)) { null -> afterAccountAdd(newAcct, auth2Result) else -> afterAccessTokenUpdate(auth2Result, sa) } } private fun ActMain.afterAccessTokenUpdate( auth2Result: Auth2Result, sa: SavedAccount, ): Boolean { // DBの情報を更新する sa.updateTokenInfo(auth2Result) // 各カラムの持つアカウント情報をリロードする reloadAccountSetting() // 自動でリロードする appState.columnList .filter { it.accessInfo == sa } .forEach { it.startLoading() } // 通知の更新が必要かもしれない PushSubscriptionHelper.clearLastCheck(sa) checkNotificationImmediateAll(this, onlySubscription = true) checkNotificationImmediate(this, sa.db_id) showToast(false, R.string.access_token_updated_for, sa.acct.pretty) return true } private fun ActMain.afterAccountAdd( newAcct: Acct, auth2Result: Auth2Result, ): Boolean { val ta = auth2Result.tootAccount val rowId = SavedAccount.insert( acct = newAcct.ascii, host = auth2Result.apiHost.ascii, domain = auth2Result.apDomain.ascii, account = auth2Result.accountJson, token = auth2Result.tokenJson, misskeyVersion = auth2Result.tootInstance.misskeyVersionMajor, ) val account = SavedAccount.loadAccount(applicationContext, rowId) if (account == null) { showToast(false, "loadAccount failed.") return false } var bModified = false if (account.loginAccount?.locked == true) { bModified = true account.visibility = TootVisibility.PrivateFollowers } if (!account.isMisskey) { val source = ta.source if (source != null) { val privacy = TootVisibility.parseMastodon(source.privacy) if (privacy != null) { bModified = true account.visibility = privacy } // XXX ta.source.sensitive パラメータを読んで「添付画像をデフォルトでNSFWにする」を実現する // 現在、アカウント設定にはこの項目はない( 「NSFWな添付メディアを隠さない」はあるが全く別の効果) } if (bModified) { account.saveSetting() } } // 適当にカラムを追加する addColumn(false, defaultInsertPosition, account, ColumnType.HOME) if (SavedAccount.count == 1) { addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS) addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL) addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE) } // 通知の更新が必要かもしれない checkNotificationImmediateAll(this, onlySubscription = true) checkNotificationImmediate(this, account.db_id) showToast(false, R.string.account_confirmed) return true } fun ActMain.handleSharedIntent(intent: Intent) { launchMain { ActMain.sharedIntent2 = intent val ai = pickAccount( bAllowPseudo = false, bAuto = true, message = getString(R.string.account_picker_toot), ) ActMain.sharedIntent2 = null ai?.let { openActPostImpl(it.db_id, sharedIntent = intent) } } }