SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/util/AppOpener.kt

357 lines
13 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package jp.juggler.subwaytooter.util
import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.app.Activity
import android.content.SharedPreferences
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.Pref
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.action.Action_HashTag
import jp.juggler.subwaytooter.action.Action_Toot
import jp.juggler.subwaytooter.action.Action_User
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootAccountRef
import jp.juggler.subwaytooter.api.entity.TootAttachment
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.findStatusIdFromUrl
import jp.juggler.subwaytooter.api.entity.TootTag.Companion.findHashtagFromUrl
import jp.juggler.subwaytooter.dialog.DlgAppPicker
import jp.juggler.subwaytooter.pref
import jp.juggler.subwaytooter.span.LinkInfo
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
import java.util.ArrayList
// Subway Tooterの「アプリ設定/挙動/リンクを開く際にCustom Tabsを使わない」をONにして
// 投稿のコンテキストメニューの「トゥートへのアクション/Webページを開く」「ユーザへのアクション/Webページを開く」を使うと
// 投げたインテントをST自身が受け取って「次のアカウントから開く」ダイアログが出て
// 「Webページを開く」をまた押すと無限ループしてダイアログの影が徐々に濃くなりそのうち壊れる
// これを避けるには、投稿やトゥートを開く際に bpDontUseCustomTabs がオンならST以外のアプリを列挙したアプリ選択ダイアログを出すしかない
private val log = LogCategory("AppOpener")
// returns true if activity is opened.
// returns false if fallback required
private fun Activity.openBrowserExcludeMe(
pref:SharedPreferences,
intent: Intent,
startAnimationBundle: Bundle? = null
): Boolean {
try {
if( intent.component == null){
val cn = Pref.spWebBrowser(pref).cn()
if( cn?.exists(this) == true){
intent.component = cn
}
}
// このアプリのパッケージ名
val myName = packageName
val filter: (ResolveInfo) -> Boolean = {
when {
it.activityInfo.packageName == myName -> false
!it.activityInfo.exported -> false
// Huaweiの謎Activityのせいでうまく働かないことがある
-1 != it.activityInfo.packageName.indexOf("com.huawei.android.internal") -> false
// 標準アプリが設定されていない場合、アプリを選択するためのActivityが出てくる場合がある
it.activityInfo.packageName == "android" -> false
it.activityInfo.javaClass.name.startsWith("com.android.internal") -> false
it.activityInfo.javaClass.name.startsWith("com.android.systemui") -> false
// たぶんChromeとかfirefoxとか
else -> true
}
}
// resolveActivity がこのアプリ以外のActivityを返すなら、それがベストなんだろう
// ただしAndroid M以降はMATCH_DEFAULT_ONLYだと「常時」が設定されてないとnullを返す
val ri = packageManager!!.resolveActivity(
intent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PackageManager.MATCH_ALL
} else {
PackageManager.MATCH_DEFAULT_ONLY
}
)?.takeIf(filter)
return when {
ri != null -> {
intent.setClassName(ri.activityInfo.packageName, ri.activityInfo.name)
log.d("startActivityExcludeMyApp(1) $intent")
startActivity(intent, startAnimationBundle)
true
}
else -> DlgAppPicker(
this,
intent,
autoSelect = true,
filter = filter,
addCopyAction = false
) {
try {
intent.component = it.cn()
log.d("startActivityExcludeMyApp(2) $intent")
startActivity(intent, startAnimationBundle)
} catch (ex: Throwable) {
log.trace(ex)
showToast(ex, "can't open. ${intent.data}")
}
}.show()
}
} catch (ex: Throwable) {
log.trace(ex)
showToast(ex, "can't open. ${intent.data}")
return true // fallback not required in this case
}
}
fun Activity.openBrowser(uri: Uri? , pref:SharedPreferences = pref()) {
uri ?: return
val rv = openBrowserExcludeMe(
pref,
Intent(Intent.ACTION_VIEW, uri)
.apply { addCategory(Intent.CATEGORY_BROWSABLE) }
)
if (!rv) showToast(true, "there is no app that can open $uri")
}
fun Activity.openBrowser(url: String?, pref:SharedPreferences = pref()) = openBrowser(url.mayUri(),pref)
// Chrome Custom Tab を開く
fun Activity.openCustomTab(url: String?, pref:SharedPreferences = pref()) {
url ?: return
if (url.isEmpty()) {
showToast(false, "URL is empty string.")
return
}
if (Pref.bpDontUseCustomTabs(pref)) {
openBrowser(url,pref)
return
}
try {
fun startCustomTabIntent(cn: ComponentName?) =
CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(attrColor(R.attr.colorPrimary))
.build()
)
.setShowTitle(true)
.build()
.let {
log.w("startCustomTabIntent ComponentName=$cn")
openBrowserExcludeMe(
pref,
it.intent.also { intent ->
if (cn != null) intent.component = cn
intent.data = url.toUri()
},
it.startAnimationBundle
)
}
if (url.startsWith("http") && Pref.bpPriorChrome(pref)) {
try {
// 初回はChrome指定で試す
val cn = ComponentName(
"com.android.chrome",
"com.google.android.apps.chrome.Main"
)
if (startCustomTabIntent(cn)) return
} catch (ex2: Throwable) {
log.e(ex2, "openCustomTab: missing chrome. retry to other application.")
}
}
// Chromeがないようなのでcomponent指定なしでリトライ
if (startCustomTabIntent(null)) return
showToast(true, "the browser app is not installed.")
} catch (ex: Throwable) {
log.trace(ex)
val scheme = url.mayUri()?.scheme ?: url
showToast(true, "can't open browser app for %s", scheme)
}
}
fun Activity.openCustomTab(ta: TootAttachment) =
openCustomTab(ta.getLargeUrl(pref()))
fun openCustomTab(
activity: ActMain,
pos: Int,
url: String,
accessInfo: SavedAccount? = null,
tagList: ArrayList<String>? = null,
allowIntercept: Boolean = true,
whoRef: TootAccountRef? = null,
linkInfo: LinkInfo? = null
) {
try {
log.d("openCustomTab: $url")
val whoAcct = if (whoRef != null) {
accessInfo?.getFullAcct(whoRef.get())
} else {
null
}
if (allowIntercept && accessInfo != null) {
// ハッシュタグはいきなり開くのではなくメニューがある
val tagInfo = url.findHashtagFromUrl()
if (tagInfo != null) {
Action_HashTag.dialog(
activity,
pos,
url,
Host.parse(tagInfo.second),
tagInfo.first,
tagList,
whoAcct
)
return
}
val statusInfo = url.findStatusIdFromUrl()
if (statusInfo != null) {
if (accessInfo.isNA ||
statusInfo.statusId == null ||
!accessInfo.matchHost(statusInfo.host)
) {
Action_Toot.conversationOtherInstance(
activity,
pos,
statusInfo.url,
statusInfo.statusId,
statusInfo.host,
statusInfo.statusId
)
} else {
Action_Toot.conversationLocal(
activity,
pos,
accessInfo,
statusInfo.statusId
)
}
return
}
// opener.linkInfo をチェックしてメンションを判別する
val mention = linkInfo?.mention
if (mention != null) {
val fullAcct = getFullAcctOrNull(mention.acct, mention.url, accessInfo)
if (fullAcct != null) {
if (fullAcct.host != null) {
when (fullAcct.host.ascii) {
"github.com",
"twitter.com" ->
activity.openCustomTab(mention.url)
"gmail.com" ->
activity.openBrowser("mailto:${fullAcct.pretty}")
else ->
Action_User.profile(
activity,
pos,
accessInfo, // FIXME nullが必要なケースがあったっけなかったっけ…
mention.url,
fullAcct.host,
fullAcct.username,
original_url = url
)
}
return
}
}
}
// ユーザページをアプリ内で開く
var m = TootAccount.reAccountUrl.matcher(url)
if (m.find()) {
val host = m.groupEx(1)!!
val user = m.groupEx(2)!!.decodePercent()
val instance = m.groupEx(3)?.decodePercent()?.notEmpty()
// https://misskey.xyz/@tateisu@github.com
// https://misskey.xyz/@tateisu@twitter.com
if (instance != null) {
val instanceHost = Host.parse(instance)
when (instanceHost.ascii) {
"github.com", "twitter.com" -> {
activity.openCustomTab("https://$instance/$user")
}
"gmail.com" -> {
activity.openBrowser("mailto:$user@$instance")
}
else -> {
Action_User.profile(
activity,
pos,
null, // Misskeyだと疑似アカが必要なんだっけ…
"https://$instance/@$user",
instanceHost,
user,
original_url = url
)
}
}
} else {
Action_User.profile(
activity,
pos,
accessInfo,
url,
Host.parse(host),
user
)
}
return
}
m = TootAccount.reAccountUrl2.matcher(url)
if (m.find()) {
val host = m.groupEx(1)!!
val user = m.groupEx(2)!!.decodePercent()
Action_User.profile(
activity,
pos,
accessInfo,
url,
Host.parse(host),
user
)
return
}
}
activity.openCustomTab(url)
} catch (ex: Throwable) {
log.trace(ex)
log.e(ex, "openCustomTab failed. $url")
}
}