「アプリ設定/挙動/Webブラウザ」を追加

This commit is contained in:
tateisu 2020-12-19 15:46:42 +09:00
parent 70dd21bf41
commit 35deaf985f
9 changed files with 1043 additions and 947 deletions

View File

@ -20,6 +20,18 @@
<category android:name="android.intent.category.BROWSABLE" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
</intent>
<!-- Chrome Custom Tabs の存在確認 -->
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />

View File

@ -2,6 +2,7 @@ package jp.juggler.subwaytooter
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ResolveInfo
import android.graphics.Color
import android.graphics.Typeface
import android.net.Uri
@ -21,13 +22,14 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.dialog.DlgAppPicker
import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.AsyncActivity
import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.cn
import jp.juggler.util.*
import org.apache.commons.io.IOUtils
import java.io.File
@ -1127,7 +1129,7 @@ class ActAppSetting : AsyncActivity(), ColorPickerDialogListener, View.OnClickLi
return list.size
}
override fun getItem(position : Int) : Any? {
override fun getItem(position : Int) : Any {
return list[position]
}
@ -1164,16 +1166,17 @@ class ActAppSetting : AsyncActivity(), ColorPickerDialogListener, View.OnClickLi
}
}
fun openCustomShareChooser(target : CustomShareTarget) {
fun openCustomShareChooser(appSettingItem: AppSettingItem,target : CustomShareTarget) {
try {
val rv = DlgAppPicker(
this,
Intent().apply {
intent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, getString(R.string.content_sample))
}
) { setCustomShare(target, it) }
},
addCopyAction = true
) { setCustomShare(appSettingItem,target, it) }
.show()
if(! rv) showToast(true, "share target app is not installed.")
} catch(ex : Throwable) {
@ -1182,22 +1185,11 @@ class ActAppSetting : AsyncActivity(), ColorPickerDialogListener, View.OnClickLi
}
}
fun setCustomShare(target : CustomShareTarget?, value : String) {
target ?: return
val item = when(target) {
CustomShareTarget.Translate -> AppSettingItem.CUSTOM_TRANSLATE
CustomShareTarget.CustomShare1 -> AppSettingItem.CUSTOM_SHARE_1
CustomShareTarget.CustomShare2 -> AppSettingItem.CUSTOM_SHARE_2
CustomShareTarget.CustomShare3 -> AppSettingItem.CUSTOM_SHARE_3
}
?: error("setCustomShare $target has no setting item.")
val sp : StringPref = item.pref.cast() ?: error("$target: not StringPref")
fun setCustomShare(appSettingItem: AppSettingItem,target : CustomShareTarget, value : String) {
val sp : StringPref = appSettingItem.pref.cast() ?: error("$target: not StringPref")
pref.edit().put(sp, value).apply()
showCustomShareIcon(findItemViewHolder(item)?.textView1, target)
showCustomShareIcon(findItemViewHolder(appSettingItem)?.textView1, target)
}
fun showCustomShareIcon(tv : TextView?, target : CustomShareTarget) {
@ -1208,5 +1200,36 @@ class ActAppSetting : AsyncActivity(), ColorPickerDialogListener, View.OnClickLi
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
tv.compoundDrawablePadding = (resources.displayMetrics.density * 4f + 0.5f).toInt()
}
fun openWebBrowserChooser(appSettingItem: AppSettingItem, intent:Intent, filter: (ResolveInfo) -> Boolean ) {
try {
val rv = DlgAppPicker(
this,
intent=intent,
filter = filter,
addCopyAction = false
) { setWebBrowser( appSettingItem,it) }
.show()
if(! rv) showToast(true, "share target app is not installed.")
} catch(ex : Throwable) {
log.trace(ex)
showToast(ex, "openCustomShareChooser failed.")
}
}
fun setWebBrowser(appSettingItem: AppSettingItem, value : String) {
val sp : StringPref = appSettingItem.pref.cast() ?: error("${getString(appSettingItem.caption)}: not StringPref")
pref.edit().put(sp, value).apply()
showWebBrowser(findItemViewHolder(appSettingItem)?.textView1, value )
}
fun showWebBrowser(tv : TextView?,prefValue:String) {
tv ?: return
val cn =prefValue.cn()
val (label, icon) = CustomShare.getInfo(this, cn)
tv.text = label ?: getString(R.string.not_selected)
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
tv.compoundDrawablePadding = (resources.displayMetrics.density * 4f + 0.5f).toInt()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -591,7 +591,8 @@ object Pref {
val spCustomShare1 = StringPref("CustomShare1", "")
val spCustomShare2 = StringPref("CustomShare2", "")
val spCustomShare3 = StringPref("CustomShare3", "")
val spWebBrowser = StringPref("WebBrowser", "")
val spTimelineSpacing = StringPref("TimelineSpacing", "")
// long

View File

@ -21,8 +21,9 @@ class DlgAppPicker(
val activity: Activity,
val intent: Intent,
val autoSelect: Boolean = false,
val addCopyAction:Boolean= false,
val filter: (ResolveInfo) -> Boolean = { true },
val callback: (String) -> Unit
val callback: (String) -> Unit,
) {
companion object {
@ -55,7 +56,7 @@ class DlgAppPicker(
}
// 自動選択オフの場合、末尾にクリップボード項目を追加する
if (!autoSelect) {
if (addCopyAction && !autoSelect) {
val (label, icon) = CustomShare.getInfo(activity, CustomShare.CN_CLIPBOARD.cn())
add(ListItem(icon, label.toString(), CustomShare.CN_CLIPBOARD))
}

View File

@ -8,6 +8,7 @@ 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
@ -40,15 +41,23 @@ private val log = LogCategory("AppOpener")
// returns true if activity is opened.
// returns false if fallback required
private fun Activity.startActivityExcludeMyApp(
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{
when {
it.activityInfo.packageName == myName -> false
!it.activityInfo.exported -> false
@ -57,7 +66,7 @@ private fun Activity.startActivityExcludeMyApp(
// 標準アプリが設定されていない場合、アプリを選択するためのActivityが出てくる場合がある
it.activityInfo.packageName == "android" -> false
it.activityInfo.javaClass.name.startsWith( "com.android.internal") -> false
it.activityInfo.javaClass.name.startsWith("com.android.internal") -> false
it.activityInfo.javaClass.name.startsWith("com.android.systemui") -> false
// たぶんChromeとかfirefoxとか
@ -89,7 +98,8 @@ private fun Activity.startActivityExcludeMyApp(
this,
intent,
autoSelect = true,
filter = filter
filter = filter,
addCopyAction = false
) {
try {
intent.component = it.cn()
@ -108,16 +118,20 @@ private fun Activity.startActivityExcludeMyApp(
}
}
fun Activity.openBrowser(uri: Uri?) {
fun Activity.openBrowser(uri: Uri? , pref:SharedPreferences = pref()) {
uri ?: return
val rv = startActivityExcludeMyApp(Intent(Intent.ACTION_VIEW, uri))
val rv = startActivityExcludeMyApp(
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?) = openBrowser(url.mayUri())
fun Activity.openBrowser(url: String?, pref:SharedPreferences = pref()) = openBrowser(url.mayUri(),pref)
// Chrome Custom Tab を開く
fun Activity.openCustomTab(url: String?) {
fun Activity.openCustomTab(url: String?, pref:SharedPreferences = pref()) {
url ?: return
if (url.isEmpty()) {
@ -125,9 +139,8 @@ fun Activity.openCustomTab(url: String?) {
return
}
val pref = pref()
if (Pref.bpDontUseCustomTabs(pref)) {
openBrowser(url)
openBrowser(url,pref)
return
}
@ -144,6 +157,7 @@ fun Activity.openCustomTab(url: String?) {
.let {
log.w("startCustomTabIntent ComponentName=$cn")
startActivityExcludeMyApp(
pref,
it.intent.also { intent ->
if (cn != null) intent.component = cn
intent.data = url.toUri()

View File

@ -15,170 +15,179 @@ import jp.juggler.util.showToast
import jp.juggler.util.systemService
enum class CustomShareTarget {
Translate,
CustomShare1,
CustomShare2,
CustomShare3,
Translate,
CustomShare1,
CustomShare2,
CustomShare3,
}
object CustomShare {
val log = LogCategory("CustomShare")
const val CN_CLIPBOARD = "<InApp>/CopyToClipboard"
private const val translate_app_component_default =
"com.google.android.apps.translate/com.google.android.apps.translate.TranslateActivity"
fun getCustomShareComponentName(
pref : SharedPreferences,
target : CustomShareTarget
) : ComponentName? {
val src : String
val defaultComponentName : String?
when(target) {
val log = LogCategory("CustomShare")
const val CN_CLIPBOARD = "<InApp>/CopyToClipboard"
private const val translate_app_component_default =
"com.google.android.apps.translate/com.google.android.apps.translate.TranslateActivity"
fun getCustomShareComponentName(
pref: SharedPreferences,
target: CustomShareTarget
): ComponentName? {
val src: String
val defaultComponentName: String?
when (target) {
CustomShareTarget.Translate -> {
src = Pref.spTranslateAppComponent(pref)
defaultComponentName = translate_app_component_default
}
CustomShareTarget.CustomShare1 -> {
src = Pref.spCustomShare1(pref)
defaultComponentName = null
}
CustomShareTarget.CustomShare2 -> {
src = Pref.spCustomShare2(pref)
defaultComponentName = null
}
CustomShareTarget.CustomShare3 -> {
src = Pref.spCustomShare3(pref)
defaultComponentName = null
}
}
return src.cn() ?: defaultComponentName?.cn()
}
fun getInfo(context : Context, cn : ComponentName?) : Pair<CharSequence?, Drawable?> {
var label : CharSequence? = null
var icon : Drawable? = null
try {
if(cn != null) {
val cnStr = "${cn.packageName}/${cn.className}"
label = cnStr
if(cnStr == CN_CLIPBOARD) {
label =
"${context.getString(R.string.copy_to_clipboard)}(${context.getString(R.string.app_name)})"
icon = ContextCompat.getDrawable(context, R.drawable.ic_copy)?.mutate()?.apply {
setTint(context.getAttributeColor(R.attr.colorVectorDrawable))
setTintMode(PorterDuff.Mode.SRC_IN)
}
} else {
val pm = context.packageManager
val ri = pm.resolveActivity(Intent().apply { component = cn }, 0)
if(ri != null) {
try {
label = ri.loadLabel(pm)
} catch(ex : Throwable) {
log.e(ex, "loadLabel failed.")
}
try {
icon = ri.loadIcon(pm)
} catch(ex : Throwable) {
log.e(ex, "loadIcon failed.")
}
}
}
}
} catch(ex : Throwable) {
log.e(ex, "getInfo failed.")
}
return Pair(label, icon)
}
fun invoke(
context : Context,
text : String,
target : CustomShareTarget
}
return src.cn() ?: defaultComponentName?.cn()
}
fun getInfo(context: Context, cn: ComponentName?): Pair<CharSequence?, Drawable?> {
var label: CharSequence? = null
var icon: Drawable? = null
try {
if (cn != null) {
val cnStr = "${cn.packageName}/${cn.className}"
label = cnStr
if (cnStr == CN_CLIPBOARD) {
label =
"${context.getString(R.string.copy_to_clipboard)}(${context.getString(R.string.app_name)})"
icon = ContextCompat.getDrawable(context, R.drawable.ic_copy)?.mutate()?.apply {
setTint(context.getAttributeColor(R.attr.colorVectorDrawable))
setTintMode(PorterDuff.Mode.SRC_IN)
}
} else {
val pm = context.packageManager
val ri = pm.resolveActivity(Intent().apply { component = cn }, 0)
if (ri != null) {
try {
label = ri.loadLabel(pm)
} catch (ex: Throwable) {
log.e(ex, "loadLabel failed.")
}
try {
icon = ri.loadIcon(pm)
} catch (ex: Throwable) {
log.e(ex, "loadIcon failed.")
}
}
}
}
} catch (ex: Throwable) {
log.e(ex, "getInfo failed.")
}
return Pair(label, icon)
}
fun invoke(
context: Context,
text: String,
target: CustomShareTarget
) {
// convert "pkgName/className" string to ComponentName object.
val cn = getCustomShareComponentName(App1.pref, target)
if(cn == null) {
context.showToast(true, R.string.custom_share_app_not_found)
return
}
val cnStr = "${cn.packageName}/${cn.className}"
if(cnStr == CN_CLIPBOARD) {
try {
val cm : ClipboardManager = systemService(context) !!
cm.setPrimaryClip(ClipData.newPlainText("", text))
context.showToast(false, R.string.copied_to_clipboard)
} catch(ex : Throwable) {
context.showToast(ex, "copy to clipboard failed.")
}
return
}
try {
val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.component = cn
context.startActivity(intent)
} catch(ex : ActivityNotFoundException) {
log.trace(ex)
context.showToast(true, R.string.custom_share_app_not_found)
} catch(ex : Throwable) {
log.trace(ex)
context.showToast(ex, "invoke() failed.")
}
}
fun invoke(
context : Context,
access_info : SavedAccount,
status : TootStatus?,
target : CustomShareTarget
// convert "pkgName/className" string to ComponentName object.
val cn = getCustomShareComponentName(App1.pref, target)
if (cn == null) {
context.showToast(true, R.string.custom_share_app_not_found)
return
}
val cnStr = "${cn.packageName}/${cn.className}"
if (cnStr == CN_CLIPBOARD) {
try {
val cm: ClipboardManager = systemService(context)!!
cm.setPrimaryClip(ClipData.newPlainText("", text))
context.showToast(false, R.string.copied_to_clipboard)
} catch (ex: Throwable) {
context.showToast(ex, "copy to clipboard failed.")
}
return
}
try {
val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.component = cn
context.startActivity(intent)
} catch (ex: ActivityNotFoundException) {
log.trace(ex)
context.showToast(true, R.string.custom_share_app_not_found)
} catch (ex: Throwable) {
log.trace(ex)
context.showToast(ex, "invoke() failed.")
}
}
fun invoke(
context: Context,
access_info: SavedAccount,
status: TootStatus?,
target: CustomShareTarget
) {
status ?: return
try {
// convert "pkgName/className" string to ComponentName object.
val cn = getCustomShareComponentName(App1.pref, target)
if(cn == null) {
context.showToast(true, R.string.custom_share_app_not_found)
return
}
val sv = TootTextEncoder.encodeStatusForTranslate(context, access_info, status)
invoke(context, sv, target)
} catch(ex : Throwable) {
log.trace(ex)
context.showToast(ex, "invoke() failed.")
}
}
private val cache = HashMap<CustomShareTarget, Pair<CharSequence?, Drawable?>>()
fun getCache(target : CustomShareTarget) = cache[target]
fun reloadCache(context : Context, pref : SharedPreferences) {
CustomShareTarget.values().forEach { target ->
val cn = getCustomShareComponentName(pref, target)
val pair = getInfo(context, cn)
cache[target] = pair
}
}
status ?: return
try {
// convert "pkgName/className" string to ComponentName object.
val cn = getCustomShareComponentName(App1.pref, target)
if (cn == null) {
context.showToast(true, R.string.custom_share_app_not_found)
return
}
val sv = TootTextEncoder.encodeStatusForTranslate(context, access_info, status)
invoke(context, sv, target)
} catch (ex: Throwable) {
log.trace(ex)
context.showToast(ex, "invoke() failed.")
}
}
private val cache = HashMap<CustomShareTarget, Pair<CharSequence?, Drawable?>>()
fun getCache(target: CustomShareTarget) = cache[target]
fun reloadCache(context: Context, pref: SharedPreferences) {
CustomShareTarget.values().forEach { target ->
val cn = getCustomShareComponentName(pref, target)
val pair = getInfo(context, cn)
cache[target] = pair
}
}
}
// convert "pkgName/className" string to ComponentName object.
fun String.cn() : ComponentName? {
try {
val idx = indexOf('/')
if(idx >= 1) return ComponentName(substring(0 until idx), substring(idx + 1))
} catch(ex : Throwable) {
CustomShare.log.e(ex, "incorrect component name $this")
}
return null
fun String.cn(): ComponentName? {
try {
val idx = indexOf('/')
if (idx >= 1) return ComponentName(substring(0 until idx), substring(idx + 1))
} catch (ex: Throwable) {
CustomShare.log.e(ex, "incorrect component name $this")
}
return null
}
fun ComponentName.exists(context: Context): Boolean {
return try {
context.packageManager.resolveActivity(Intent().apply { component = this@exists }, 0)
?.activityInfo?.exported ?: false
} catch (_: Throwable) {
false
}
}

View File

@ -1065,5 +1065,6 @@
<string name="username_not_need_atmark">ユーザ名に@や/ を含めることはできません</string>
<string name="speech">読み上げ</string>
<string name="reset_notification_tracking_status">既読状態をリセット</string>
<string name="web_browser">Webブラウザー</string>
</resources>

View File

@ -1076,4 +1076,5 @@
<string name="toot_search_notestock_of">notestock search \"%1$s\"</string>
<string name="update_push_subscription_not_force" translatable="false">Update push subscription(not force)</string>
<string name="reset_notification_tracking_status">Reset notification tracking status</string>
<string name="web_browser">Web browser</string>
</resources>