Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

WxPusher 是一个使用微信公众号作为通道的,实时信息推送平台。本仓库为 WxPusher 的移动客户端项目,使用 **Kotlin Multiplatform (KMP)** 共享业务逻辑,搭配 **Android (Kotlin)** 和 **iOS (Swift)** 原生 UI 实现。

📖 完整的平台文档请参阅:[WxPusher 官方文档](https://wxpusher.zjiecode.com/docs/) | 📥 [下载最新版本 APP](https://wxpusher.zjiecode.com/docs/download.html)
📖 完整的平台文档请参阅:[WxPusher 官方文档](https://wxpusher.zjiecode.com/docs/) | 📥 [下载最新版本 APP](https://wxpusher.zjiecode.com/download/)

---

Expand Down Expand Up @@ -375,7 +375,7 @@ git push origin feature/your-feature-name

## 下载安装

前往 [WxPusher APP 下载页](https://wxpusher.zjiecode.com/docs/download.html) 获取最新版本:
前往 [WxPusher APP 下载页](https://wxpusher.zjiecode.com/download/) 获取最新版本:

- **Android**:直接下载 APK 安装
- **iOS**:扫码下载或在 App Store 搜索「WxPusher消息推送平台」
Expand All @@ -386,4 +386,4 @@ git push origin feature/your-feature-name

- [WxPusher 官方文档](https://wxpusher.zjiecode.com/docs/)
- [WxPusher 官网](https://wxpusher.zjiecode.com)
- [APP 下载](https://wxpusher.zjiecode.com/docs/download.html)
- [APP 下载](https://wxpusher.zjiecode.com/download/)
10 changes: 8 additions & 2 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ kotlin {
implementation("io.github.scwang90:refresh-layout-kernel:2.1.0")
implementation("io.github.scwang90:refresh-header-classics:2.1.0")
implementation("io.github.scwang90:refresh-footer-classics:2.1.0")
//穿山甲(CSJ/Pangle)标准版广告 SDK,对应 iOS 的 Ads-CN(7.6 系列)。
implementation("com.pangle.cn:ads-sdk-pro:7.6.1.2")
}
commonMain.dependencies {
implementation(libs.androidx.lifecycle.viewmodel)
Expand All @@ -65,8 +67,8 @@ android {
applicationId = "com.smjcco.wxpusher"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 10730
versionName = "1.7.30"
versionCode = 10750
versionName = "1.7.50"
//指定产物名称
setProperty("archivesBaseName", "wxpusher-app-v$versionName")

Expand Down Expand Up @@ -159,7 +161,11 @@ dependencies {
implementation(fileTree("libs") {
include("*.jar")
include("*.aar")
// 穿山甲测量工具仅 offline 包依赖(见下方 offlineImplementation),prod 不打入
exclude("csj_ad_test_tools-release.aar")
})
// 穿山甲测量/预览工具(csj_ad_test_tools-release.aar,doc 5427),仅 offline flavor 打入 APK,正式包不依赖
add("offlineImplementation", files("libs/csj_ad_test_tools-release.aar"))
// implementation(libs.androidx.core)
// AndroidX, The Basics
implementation("androidx.appcompat:appcompat:1.6.1")
Expand Down
Binary file added androidApp/libs/csj_ad_test_tools-release.aar
Binary file not shown.
12 changes: 11 additions & 1 deletion androidApp/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,14 @@
-keep class com.hihonor.push.**{*;}
#oppo
-keep public class * extends android.app.Service
-keep class com.heytap.msp.** { *;}
-keep class com.heytap.msp.** { *;}

#穿山甲(CSJ/Pangle)广告 SDK
-keep class com.bytedance.sdk.openadsdk.** { *; }
-keep interface com.bytedance.sdk.openadsdk.** { *; }
-keep class com.bytedance.sdk.component.** { *; }
-keep class com.bykv.vk.** { *; }
-keep class com.ss.android.** { *; }
-keep class com.pgl.** { *; }
-dontwarn com.bytedance.sdk.openadsdk.**
-dontwarn com.bytedance.sdk.component.**
10 changes: 8 additions & 2 deletions androidApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
tools:node="merge">

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
Expand All @@ -15,7 +16,11 @@
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- 穿山甲(CSJ/Pangle)广告 SDK 推荐权限,提升填充率 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 扫码相册选图改用系统 Photo Picker,无需任何存储/媒体读取权限 -->
<!-- 移除腾讯 Shiply 升级 SDK 带入的 WRITE 权限;移除后 merger 也不会再隐式添加 READ_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />

<!-- 声明需要相机特征 -->
<uses-feature
Expand All @@ -35,7 +40,8 @@
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:node="merge"
tools:remove="android:networkSecurityConfig">
tools:remove="android:networkSecurityConfig"
tools:replace="android:supportsRtl">

<activity
android:name=".page.main.WxpMainActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.smjcco.wxpusher.ad

import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.view.View
import android.widget.FrameLayout
import com.bytedance.sdk.openadsdk.AdSlot
import com.bytedance.sdk.openadsdk.TTAdDislike
import com.bytedance.sdk.openadsdk.TTAdNative
import com.bytedance.sdk.openadsdk.TTAdSdk
import com.bytedance.sdk.openadsdk.TTNativeExpressAd
import com.smjcco.wxpusher.base.common.WxpLogUtils

/**
* 穿山甲 Banner(模版渲染 / Express)广告封装。
*
* 对照 iOS 的 WxpBannerAdView:代码位 ID 写死在本类中(消息详情页底部 Banner),
* 通过 [Callback] 回调渲染结果,由宿主(WebView 页)决定容器展开/收起。
*/
class WxpBannerAdView(context: Context) : FrameLayout(context) {

/** 渲染结果回调,对照 iOS WxpBannerAdViewDelegate */
interface Callback {
/** 模版渲染成功,host 据此将容器展开到 [heightPx] 高度 */
fun onAdRendered(adView: WxpBannerAdView, heightPx: Int)

/** 加载或渲染失败,host 据此保持/收起隐藏 */
fun onAdFailed(adView: WxpBannerAdView)

/** 用户关闭(dislike),host 据此收起隐藏 */
fun onAdClosed(adView: WxpBannerAdView)
}

companion object {
/** 消息详情页底部 Banner 代码位 ID(Android 专属,与 iOS 不同) */
const val messageDetailSlotId = "983728419"

private const val TAG = "WxpAd"

/** Banner 模版比例(宽:高 = 600:150),与 iOS 一致 */
private const val RATIO = 150f / 600f
}

private val activity: Activity = context as? Activity
?: throw IllegalArgumentException("WxpBannerAdView 需要 Activity 上下文")

var callback: Callback? = null

private var expressAd: TTNativeExpressAd? = null

/** 按当前可用宽度(px)加载一条 Banner 广告 */
fun loadAd(slotId: String, widthPx: Int) {
if (widthPx <= 0 || !WxpPangleAdManager.isReady()) {
callback?.onAdFailed(this)
return
}
val widthDp = pxToDp(widthPx.toFloat())
val heightDp = widthDp * RATIO

val adSlot = AdSlot.Builder()
.setCodeId(slotId)
.setAdCount(1)
.setExpressViewAcceptedSize(widthDp, heightDp)
.build()

val adNative = TTAdSdk.getAdManager().createAdNative(activity)
adNative.loadBannerExpressAd(adSlot, object : TTAdNative.NativeExpressAdListener {
override fun onError(code: Int, message: String?) {
WxpLogUtils.d(TAG, "Banner拉取失败 code=$code msg=$message")
callback?.onAdFailed(this@WxpBannerAdView)
}

override fun onNativeExpressAdLoad(ads: List<TTNativeExpressAd>?) {
val ad = ads?.firstOrNull()
if (ad == null) {
callback?.onAdFailed(this@WxpBannerAdView)
return
}
bindAd(ad)
}
})
}

private fun bindAd(ad: TTNativeExpressAd) {
expressAd = ad
ad.setExpressInteractionListener(object : TTNativeExpressAd.ExpressAdInteractionListener {
override fun onAdClicked(view: View?, type: Int) {
// 点击广告,无需额外处理
}

override fun onAdShow(view: View?, type: Int) {
// 广告展示,无需额外处理
}

override fun onRenderFail(view: View?, msg: String?, code: Int) {
WxpLogUtils.d(TAG, "Banner渲染失败 code=$code msg=$msg")
callback?.onAdFailed(this@WxpBannerAdView)
}

override fun onRenderSuccess(view: View?, width: Float, height: Float) {
val adView = view ?: return
removeAllViews()
addView(adView)
val heightPx = dpToPx(height).toInt()
callback?.onAdRendered(this@WxpBannerAdView, heightPx)
}
})
// 用户「不喜欢」关闭回调
ad.setDislikeCallback(activity, object : TTAdDislike.DislikeInteractionCallback {
override fun onShow() {}

override fun onSelected(position: Int, value: String?, enforce: Boolean) {
callback?.onAdClosed(this@WxpBannerAdView)
}

override fun onCancel() {}
})
// 按当前界面深/浅色设置广告主题,使创意与页面背景协调
WxpPangleAdManager.applyAdTheme(isDarkMode())
ad.render()
}

private fun isDarkMode(): Boolean {
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return mode == Configuration.UI_MODE_NIGHT_YES
}

fun destroy() {
expressAd?.destroy()
expressAd = null
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
destroy()
}

private fun pxToDp(px: Float): Float = px / resources.displayMetrics.density

private fun dpToPx(dp: Float): Float = dp * resources.displayMetrics.density
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.smjcco.wxpusher.ad

import android.content.Context
import com.bytedance.sdk.openadsdk.TTAdConfig
import com.bytedance.sdk.openadsdk.TTAdSdk
import com.smjcco.wxpusher.BuildConfig
import com.smjcco.wxpusher.base.common.WxpLogUtils

/**
* 穿山甲(CSJ/Pangle)广告 SDK 初始化封装。
*
* 对照 iOS [com.smjcco.wxpusher] 端的 WxpPangleAdManager:单例 + doInit 幂等模式。
* App ID 与 iOS 一致([APP_ID]),代码位 ID 由各广告 View 自行写死。
*
* 合规要点:必须在用户同意隐私政策之后再调用 [doInit] 初始化 SDK / 加载广告,
* 当前与微信、推送等 SDK 一样在 Application.onCreate 中初始化(启动前已有同意流程)。
*/
object WxpPangleAdManager {
private const val TAG = "WxpAd"

/** 穿山甲「应用 App ID」 */
private const val APP_ID = "5838363"

@Volatile
private var started = false

/** 初始化穿山甲 SDK。需在用户同意隐私政策后调用,重复调用无副作用。 */
fun doInit(context: Context) {
if (started) return
started = true

val config = TTAdConfig.Builder()
.appId(APP_ID)
.appName("WxPusher")
// 线下(offline)环境开启 SDK 调试日志,线上关闭
.debug(!BuildConfig.online)
.allowShowNotify(true)
.supportMultiProcess(false)
.build()

TTAdSdk.init(context.applicationContext, config)
TTAdSdk.start(object : TTAdSdk.Callback {
override fun success() {
WxpLogUtils.d(TAG, "穿山甲SDK初始化成功")
}

override fun fail(code: Int, msg: String?) {
WxpLogUtils.d(TAG, "穿山甲SDK初始化失败 code=$code msg=$msg")
}
})
}

/** SDK 是否已初始化(加载广告前判断),对照 iOS isSupported 的就绪判断。 */
fun isReady(): Boolean = started

/**
* 按当前界面深/浅色设置广告主题,需在加载广告前调用,影响后续渲染的广告创意。
* 对照 iOS applyAdTheme。
*/
fun applyAdTheme(isDark: Boolean) {
if (!started) return
runCatching {
TTAdSdk.getAdManager().setThemeStatus(if (isDark) 1 else 0)
}.onFailure {
WxpLogUtils.w(TAG, "设置广告主题失败", it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.smjcco.wxpusher.app
import android.app.Application
import android.os.Build
import com.smjcco.wxpusher.WxpConfig
import com.smjcco.wxpusher.ad.WxpPangleAdManager
import com.smjcco.wxpusher.base.biz.WxpAppDataService
import com.smjcco.wxpusher.base.biz.WxpAppPageService
import com.smjcco.wxpusher.base.common.ApplicationUtils
Expand Down Expand Up @@ -52,6 +53,8 @@ class WxPusherApplication : Application() {
AppFeVersionManager.refreshOnAppLaunch()
//初始化微信SDK
WxpWeixinOpenManager.init(this)
//初始化穿山甲广告 SDK
WxpPangleAdManager.doInit(this)
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class MessageListFragment : WxpBaseMvpFragment<IWxpMessageListPresenter>(), IWxp
private lateinit var refreshLayout: SmartRefreshLayout
private lateinit var recyclerView: RecyclerView
private lateinit var emptyText: TextView
private lateinit var emptyActionButton: MaterialButton
private lateinit var adapter: MessageListAdapter
private lateinit var searchEditText: EditText
private lateinit var searchCancelBtn: TextView
Expand Down Expand Up @@ -149,6 +150,11 @@ class MessageListFragment : WxpBaseMvpFragment<IWxpMessageListPresenter>(), IWxp
refreshLayout = view.findViewById(R.id.refresh_layout)
recyclerView = view.findViewById(R.id.recycler_view)
emptyText = view.findViewById(R.id.empty_text)
emptyActionButton = view.findViewById(R.id.empty_action_button)
emptyActionButton.setOnClickListener {
// 打开「去体验发送消息」引导页
WxpJumpPageUtils.jumpToWebUrl("${WxpConfig.appFeUrl}/app/#/send-test-guide", activity)
}
searchEditText = view.findViewById(R.id.search_edit_text)
searchCancelBtn = view.findViewById(R.id.search_cancel_btn)
searchBarContainer = view.findViewById(R.id.search_bar_container)
Expand Down Expand Up @@ -310,7 +316,8 @@ class MessageListFragment : WxpBaseMvpFragment<IWxpMessageListPresenter>(), IWxp
// 点击消息项,打开网页
val urlString = message.url.trim()
if (urlString.isNotEmpty()) {
WxpJumpPageUtils.jumpToWebUrl(urlString, activity)
// 消息详情页允许展示广告(最终是否展示由后端开关控制),对照 iOS showAd=true
WxpJumpPageUtils.jumpToWebUrl(urlString, activity, showAd = true)
// 标记消息为已读
message.read = true
adapter.notifyDataSetChanged()
Expand Down Expand Up @@ -359,7 +366,9 @@ class MessageListFragment : WxpBaseMvpFragment<IWxpMessageListPresenter>(), IWxp
private fun loadDataFinish() {
refreshLayout.finishRefresh()
// 显示/隐藏空状态
emptyText.visibility = if (messageList.isEmpty()) View.VISIBLE else View.GONE
val emptyVisibility = if (messageList.isEmpty()) View.VISIBLE else View.GONE
emptyText.visibility = emptyVisibility
emptyActionButton.visibility = emptyVisibility
}

override fun onCheckReason(data: WxpCheckAppMsgReasonResp?) {
Expand Down Expand Up @@ -554,6 +563,15 @@ class MessageListFragment : WxpBaseMvpFragment<IWxpMessageListPresenter>(), IWxp
true
}

R.id.menu_send_test -> {
// 打开体验发送消息页面
WxpJumpPageUtils.jumpToWebUrl(
"${WxpConfig.appFeUrl}/app/#/send-test-guide",
requireActivity()
)
true
}

else -> false
}
}
Expand Down
Loading
Loading