feat: 插件系统(BETA) & 查询发言插件

This commit is contained in:
HuanChengFly 2021-08-11 17:10:16 +08:00
parent 0e48bfe809
commit c306846e7e
12 changed files with 352 additions and 17 deletions

View File

@ -251,4 +251,6 @@
-keep class com.alibaba.android.vlayout.ExposeLinearLayoutManagerEx { *; }
-keep class androidx.recyclerview.widget.RecyclerView$LayoutParams { *; }
-keep class androidx.recyclerview.widget.RecyclerView$ViewHolder { *; }
-keep class androidx.recyclerview.widget.RecyclerView$LayoutManager { *; }
-keep class androidx.recyclerview.widget.RecyclerView$LayoutManager { *; }
-keep class com.huanchengfly.tieba.post.plugins.** { *; }

View File

@ -0,0 +1,9 @@
[
{
"id": "CommentLookup",
"name": "发言查询",
"desc": "使用第三方工具箱查询用户过往发言",
"version": "1.0",
"mainClass": "com.huanchengfly.tieba.post.plugins.PluginCommentLookup"
}
]

View File

@ -19,6 +19,8 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import com.flurry.android.FlurryAgent
import com.huanchengfly.tieba.post.api.interfaces.CommonCallback
import com.huanchengfly.tieba.post.plugins.PluginManager
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
import com.huanchengfly.tieba.post.ui.theme.interfaces.ThemeSwitcher
import com.huanchengfly.tieba.post.ui.theme.utils.ThemeUtils
import com.huanchengfly.tieba.post.utils.*
@ -32,13 +34,14 @@ import org.litepal.LitePal
import java.util.*
import java.util.regex.Pattern
class BaseApplication : Application() {
class BaseApplication : Application(), IApp {
private val mActivityList: MutableList<Activity> = mutableListOf()
override fun onCreate() {
instance = this
super.onCreate()
ThemeUtils.init(ThemeDelegate)
PluginManager.init(this)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
LitePal.initialize(this)
FlurryAgent.Builder()
@ -441,13 +444,33 @@ class BaseApplication : Application() {
R.color.default_color_shadow -> return getColorByAttr(context, R.attr.shadow_color)
R.color.default_color_unselected -> return getColorByAttr(context, R.attr.colorUnselected)
R.color.default_color_text -> return getColorByAttr(context, R.attr.colorText)
R.color.default_color_text_on_primary -> return getColorByAttr(context, R.attr.colorTextOnPrimary)
R.color.default_color_text_secondary -> return getColorByAttr(context, R.attr.colorTextSecondary)
R.color.default_color_text_disabled -> return getColorByAttr(context, R.attr.color_text_disabled)
R.color.default_color_text_on_primary -> return getColorByAttr(
context,
R.attr.colorTextOnPrimary
)
R.color.default_color_text_secondary -> return getColorByAttr(
context,
R.attr.colorTextSecondary
)
R.color.default_color_text_disabled -> return getColorByAttr(
context,
R.attr.color_text_disabled
)
R.color.default_color_divider -> return getColorByAttr(context, R.attr.colorDivider)
R.color.default_color_swipe_refresh_view_background -> return getColorByAttr(context, R.attr.color_swipe_refresh_layout_background)
R.color.default_color_swipe_refresh_view_background -> return getColorByAttr(
context,
R.attr.color_swipe_refresh_layout_background
)
}
return context.getColorCompat(colorId)
}
}
override fun getAppContext(): Context {
return this
}
override fun launchUrl(url: String) {
launchUrl(this, url)
}
}

View File

@ -11,6 +11,7 @@ import androidx.annotation.NonNull;
import com.huanchengfly.tieba.post.R;
import com.huanchengfly.tieba.post.ui.theme.utils.ThemeUtils;
import com.huanchengfly.tieba.post.utils.NavigationHelper;
import com.huanchengfly.tieba.post.utils.UtilsKt;
public class MyURLSpan extends ClickableSpan {
public String url;
@ -34,6 +35,6 @@ public class MyURLSpan extends ClickableSpan {
@Override
public void onClick(@NonNull View view) {
navigationHelper.navigationByData(NavigationHelper.ACTION_URL, this.url);
UtilsKt.launchUrl(context, url);
}
}

View File

@ -0,0 +1,43 @@
package com.huanchengfly.tieba.post.plugins
import android.content.Context
import com.huanchengfly.tieba.post.plugins.PluginMenuItem.ClickCallback
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
import com.huanchengfly.tieba.post.plugins.models.PluginManifest
abstract class IPlugin(
val app: IApp,
val manifest: PluginManifest
) {
val context: Context
get() = app.getAppContext()
open fun onCreate() {}
open fun onEnable() {}
open fun onDisable() {}
open fun onDestroy() {}
}
inline fun <reified Data> IPlugin.registerMenuItem(
id: String,
title: String,
callback: ClickCallback<Data>? = null
) {
val menu = getMenuByData(Data::class)
PluginManager.registerMenuItem(this, PluginMenuItem(id, menu, title, callback))
}
inline fun <reified Data> IPlugin.registerMenuItem(
id: String,
title: String,
crossinline callback: (Data) -> Unit
) {
registerMenuItem(id, title, object : ClickCallback<Data> {
override fun onClick(data: Data) {
callback.invoke(data)
}
})
}

View File

@ -0,0 +1,18 @@
package com.huanchengfly.tieba.post.plugins
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.ProfileBean
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
import com.huanchengfly.tieba.post.plugins.models.PluginManifest
class PluginCommentLookup(app: IApp, manifest: PluginManifest) : IPlugin(app, manifest) {
override fun onEnable() {
super.onEnable()
registerMenuItem<ProfileBean>(
"lookup_comment",
context.getString(R.string.plugin_comment_lookup_menu)
) {
app.launchUrl("https://www.82cat.com/tieba/reply/${it.user?.name}/1")
}
}
}

View File

@ -0,0 +1,168 @@
package com.huanchengfly.tieba.post.plugins
import android.content.Context
import android.content.SharedPreferences
import com.huanchengfly.tieba.post.api.models.ProfileBean
import com.huanchengfly.tieba.post.fromJson
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
import com.huanchengfly.tieba.post.plugins.models.PluginManifest
import com.huanchengfly.tieba.post.utils.AssetUtil
import com.huanchengfly.tieba.post.utils.SharedPreferencesUtil
import com.huanchengfly.tieba.post.utils.SharedPreferencesUtil.SP_PLUGINS
import kotlin.reflect.KClass
object PluginManager {
const val MENU_USER_ACTIVITY = "user_activity"
const val MENU_NONE = "none"
lateinit var appInstance: IApp
val pluginManifests: MutableList<PluginManifest> = mutableListOf()
val pluginInstances: MutableList<IPlugin> = mutableListOf()
val registeredPluginMenuItems: MutableMap<String, MutableMap<String, PluginMenuItem<*>>> =
mutableMapOf()
val context: Context
get() = appInstance.getAppContext()
val preferences: SharedPreferences
get() = SharedPreferencesUtil.get(SP_PLUGINS)
init {
listOf(
MENU_USER_ACTIVITY,
MENU_NONE
).forEach {
registeredPluginMenuItems[it] = mutableMapOf()
}
}
fun <Data> registerMenuItem(pluginInstance: IPlugin, menuItem: PluginMenuItem<Data>) {
registeredPluginMenuItems[menuItem.menuId]!!["${pluginInstance.manifest.id}_${menuItem.id}"] =
menuItem
}
fun init(app: IApp) {
appInstance = app
reloadPlugins()
}
fun enablePlugin(id: String) {
pluginInstances.filter { it.manifest.id == id }.forEach { enablePlugin(it) }
}
fun disablePlugin(id: String) {
pluginInstances.filter { it.manifest.id == id }.forEach { disablePlugin(it) }
}
private fun createPlugin(pluginManifest: PluginManifest): IPlugin? {
try {
val mainClazz = Class.forName(pluginManifest.mainClass)
val constructor =
mainClazz.getDeclaredConstructor(IApp::class.java, PluginManifest::class.java)
constructor.isAccessible = true
return constructor.newInstance(appInstance, pluginManifest) as IPlugin
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
private fun enablePlugin(pluginInstance: IPlugin) {
try {
pluginInstance.onEnable()
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun disablePlugin(pluginInstance: IPlugin) {
try {
pluginInstance.onDisable()
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun destroyPlugin(pluginInstance: IPlugin) {
try {
pluginInstance.onDestroy()
} catch (e: Exception) {
e.printStackTrace()
}
}
fun reloadPluginManifests() {
pluginManifests.clear()
pluginManifests.addAll(
AssetUtil.getStringFromAsset(context, "plugins.json").fromJson<List<PluginManifest>>()
)
}
fun reloadPlugins() {
pluginInstances.forEach {
disablePlugin(it)
destroyPlugin(it)
}
pluginInstances.clear()
reloadPluginManifests()
pluginManifests.forEach {
try {
if (it.pluginCreated && preferences.getBoolean("${it.id}_enabled", true)) {
val pluginInstance = createPlugin(it)
if (pluginInstance != null) {
pluginInstances.add(pluginInstance)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
pluginInstances.forEach {
enablePlugin(it)
}
}
private val PluginManifest.pluginCreated: Boolean
get() {
return pluginInstances.firstOrNull { it.manifest.id == id } != null
}
}
class PluginMenuItem<Data>(
val id: String,
val menuId: String,
val title: String,
val callback: ClickCallback<Data>? = null
) {
interface ClickCallback<Data> {
fun onClick(data: Data)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PluginMenuItem<*>) return false
if (id != other.id) return false
if (menuId != other.menuId) return false
if (title != other.title) return false
if (callback != other.callback) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + menuId.hashCode()
result = 31 * result + title.hashCode()
return result
}
}
fun getMenuByData(dataClass: KClass<*>): String = getMenuByData(dataClass.java)
fun getMenuByData(dataClass: Class<*>): String {
return when (dataClass.canonicalName) {
ProfileBean::class.java.canonicalName -> PluginManager.MENU_USER_ACTIVITY
else -> "none"
}
}

View File

@ -0,0 +1,9 @@
package com.huanchengfly.tieba.post.plugins.interfaces
import android.content.Context
interface IApp {
fun getAppContext(): Context
fun launchUrl(url: String)
}

View File

@ -0,0 +1,12 @@
package com.huanchengfly.tieba.post.plugins.models
import com.google.gson.annotations.SerializedName
data class PluginManifest(
val id: String,
val name: String,
val desc: String,
val version: String,
@SerializedName("main_class")
val mainClass: String
)

View File

@ -17,6 +17,7 @@ public class SharedPreferencesUtil {
public static final String SP_PERMISSION = "permission";
public static final String SP_IGNORE_VERSIONS = "ignore_version";
public static final String SP_WEBVIEW_INFO = "webview_info";
public static final String SP_PLUGINS = "plugins";
public static SharedPreferences get(@Preferences String name) {
return get(BaseApplication.getInstance(), name);
@ -50,7 +51,7 @@ public class SharedPreferencesUtil {
return put(get(context, preference), key, value);
}
@StringDef({SP_APP_DATA, SP_IGNORE_VERSIONS, SP_PERMISSION, SP_SETTINGS, SP_WEBVIEW_INFO, SP_DRAFT})
@StringDef({SP_APP_DATA, SP_IGNORE_VERSIONS, SP_PERMISSION, SP_SETTINGS, SP_WEBVIEW_INFO, SP_DRAFT, SP_PLUGINS})
@Retention(RetentionPolicy.SOURCE)
public @interface Preferences {
}

View File

@ -1,15 +1,22 @@
package com.huanchengfly.tieba.post.utils
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import com.huanchengfly.tieba.post.BaseApplication
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.WebViewActivity
import com.huanchengfly.tieba.post.dpToPxFloat
import com.huanchengfly.tieba.post.ui.theme.utils.ColorStateListUtils
import com.huanchengfly.tieba.post.ui.theme.utils.ThemeUtils
@JvmOverloads
fun getItemBackgroundDrawable(
@ -101,13 +108,54 @@ fun getIntermixedColorBackground(
context,
position,
itemCount,
positionOffset,
radius,
if (context.appPreferences.listItemsBackgroundIntermixed) {
colors
} else {
intArrayOf(colors[0])
},
ripple
positionOffset,
radius,
if (context.appPreferences.listItemsBackgroundIntermixed) {
colors
} else {
intArrayOf(colors[0])
},
ripple
)
}
}
fun launchUrl(context: Context, url: String) {
val uri = Uri.parse(url)
val host = uri.host
val path = uri.path
val scheme = uri.scheme
if (host == null || scheme == null || path == null) {
return
}
if (!path.contains("android_asset")) {
val isTiebaLink =
host.contains("tieba.baidu.com") || host.contains("wappass.baidu.com") || host.contains(
"ufosdk.baidu.com"
) || host.contains("m.help.baidu.com")
if (isTiebaLink || context.appPreferences.useWebView) {
WebViewActivity.launch(context, url)
} else {
if (context.appPreferences.useCustomTabs) {
val intentBuilder = CustomTabsIntent.Builder()
.setShowTitle(true)
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(
ThemeUtils.getColorByAttr(
context,
R.attr.colorToolbar
)
)
.build()
)
try {
intentBuilder.build().launchUrl(context, uri)
} catch (e: ActivityNotFoundException) {
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
}
} else {
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
}
}
}
}

View File

@ -465,4 +465,5 @@
<string name="bubble_not_completed">功能未完成开发,敬请期待</string>
<string name="title_theme_translucent">透明主题</string>
<string name="title_theme_custom">自定义</string>
<string name="plugin_comment_lookup_menu">查询发言</string>
</resources>