This commit is contained in:
2026-06-27 00:06:57 +08:00
parent e8e483b8c1
commit 68c48f87d5
6 changed files with 284 additions and 3 deletions
+19
View File
@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" /> <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<application <application
@@ -50,6 +51,24 @@
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync"
android:stopWithTask="false" /> android:stopWithTask="false" />
<activity
android:name=".IncomingCallActivity"
android:exported="false"
android:theme="@style/AppCallTheme"
android:showOnLockScreen="true"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:screenOrientation="portrait" />
<receiver
android:name=".CallActionReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.rnpay.CALL_ACCEPT" />
<action android:name="com.rnpay.CALL_DECLINE" />
</intent-filter>
</receiver>
<!-- 避免 Firebase SDK 默认 Service 抢收 data-only FCM --> <!-- 避免 Firebase SDK 默认 Service 抢收 data-only FCM -->
<service <service
android:name="com.google.firebase.messaging.FirebaseMessagingService" android:name="com.google.firebase.messaging.FirebaseMessagingService"
@@ -0,0 +1,32 @@
package com.rnpay;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.rnwalletman.IncomingCallNotificationHelper;
/** 处理来电通知里的接听 / 挂断动作 */
public class CallActionReceiver extends BroadcastReceiver {
public static final String ACTION_ACCEPT = "com.rnpay.CALL_ACCEPT";
public static final String ACTION_DECLINE = "com.rnpay.CALL_DECLINE";
@Override
public void onReceive(Context ctx, Intent intent) {
String action = intent.getAction();
IncomingCallNotificationHelper.cancel(ctx);
if (ACTION_ACCEPT.equals(action)) {
String caller = intent.getStringExtra("caller");
Intent launch = ctx.getPackageManager().getLaunchIntentForPackage(ctx.getPackageName());
if (launch != null) {
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
launch.putExtra("from_call", true);
launch.putExtra("caller", caller);
ctx.startActivity(launch);
}
}
// ACTION_DECLINE: 只取消通知即可
}
}
@@ -0,0 +1,221 @@
package com.rnpay;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.Gravity;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.rnwalletman.IncomingCallNotificationHelper;
/**
* 全屏来电界面,1:1 还原 gbpay StrongCallActivity / activity_strong_call.xml 布局结构与数值。
*
* 布局层次:
* FrameLayout root (bg: #6a6b6f → #393d43 gradient)
* └─ LinearLayout top (gravity=top|center_horizontal, marginTop=120dp)
* ├─ TextView callerName (24sp bold white, marginBottom=8dp)
* └─ TextView subtitle (12sp #d9d9d9)
* └─ ImageView avatar (center, 100dp oval #a4a4a4, padding=10dp)
* └─ LinearLayout btnRow (bottom, marginBottom=78dp, marginH=13dp,
* bg rect corner=35dp #4d828282, paddingV=5dp)
* ├─ ImageView decline (oval #e53935, 56dp, margin=12dp) ← hangup
* ├─ ImageView btnMic (oval #a4a4a4, 56dp, margin=12dp) ← decorative
* ├─ ImageView btnVol (oval #a4a4a4, 56dp, margin=12dp) ← decorative
* └─ ImageView accept (oval #17b834, 56dp, margin=12dp) ← accept call
*/
public class IncomingCallActivity extends Activity {
private CountDownTimer countDown;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 锁屏 & 亮屏标志
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true);
setTurnScreenOn(true);
} else {
//noinspection deprecation
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
}
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
String caller = getIntent().getStringExtra("caller");
int ringDuration = getIntent().getIntExtra("ringDuration", 30);
boolean autoAccept = getIntent().getBooleanExtra("auto_accept", false);
if (caller == null || caller.isEmpty()) caller = "Unknown";
// 通知栏接听按钮直接触发:跳过 UI,直接进 app
if (autoAccept) {
IncomingCallNotificationHelper.cancel(this);
Intent launch = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (launch != null) {
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
launch.putExtra("from_call", true);
launch.putExtra("caller", caller);
startActivity(launch);
}
finish();
return;
}
// ── root: 渐变背景 #6a6b6f → #393d43 ─────────────────────────────────
FrameLayout root = new FrameLayout(this);
GradientDrawable bg = new GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
new int[]{0xFF6a6b6f, 0xFF393d43});
root.setBackground(bg);
setContentView(root);
// ── 顶部文字区域(marginTop=120dp)────────────────────────────────────
LinearLayout top = new LinearLayout(this);
top.setOrientation(LinearLayout.VERTICAL);
top.setGravity(Gravity.CENTER_HORIZONTAL);
FrameLayout.LayoutParams topLp = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT);
topLp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
topLp.topMargin = dp(120);
top.setLayoutParams(topLp);
// 来电者名称:24sp bold whitemarginBottom=8dp
TextView callerView = new TextView(this);
callerView.setText(caller);
callerView.setTextSize(24);
callerView.setTextColor(Color.WHITE);
callerView.setTypeface(android.graphics.Typeface.DEFAULT_BOLD);
callerView.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams callerLp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
callerLp.bottomMargin = dp(8);
callerView.setLayoutParams(callerLp);
top.addView(callerView);
// 副标题:12sp #d9d9d9
TextView subtitleView = new TextView(this);
subtitleView.setText("Incoming call...");
subtitleView.setTextSize(12);
subtitleView.setTextColor(0xFFd9d9d9);
subtitleView.setGravity(Gravity.CENTER);
top.addView(subtitleView);
root.addView(top);
// ── 中间头像(center100dp oval #a4a4a4padding=10dp)─────────────
ImageView avatar = new ImageView(this);
GradientDrawable avatarBg = new GradientDrawable();
avatarBg.setShape(GradientDrawable.OVAL);
avatarBg.setColor(0xFFa4a4a4);
avatar.setBackground(avatarBg);
avatar.setImageResource(android.R.drawable.ic_menu_call);
avatar.setColorFilter(Color.WHITE);
avatar.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
avatar.setPadding(dp(10), dp(10), dp(10), dp(10));
FrameLayout.LayoutParams avatarLp = new FrameLayout.LayoutParams(dp(100), dp(100));
avatarLp.gravity = Gravity.CENTER;
avatar.setLayoutParams(avatarLp);
root.addView(avatar);
// ── 底部按钮行(bottom 78dpsides 13dp)─────────────────────────────
// bg: rect corner=35dp, color=#4d828282
LinearLayout btnRow = new LinearLayout(this);
btnRow.setOrientation(LinearLayout.HORIZONTAL);
btnRow.setGravity(Gravity.CENTER);
GradientDrawable rowBg = new GradientDrawable();
rowBg.setShape(GradientDrawable.RECTANGLE);
rowBg.setCornerRadius(dp(35));
rowBg.setColor(0x4d828282);
btnRow.setBackground(rowBg);
btnRow.setPadding(0, dp(5), 0, dp(5));
FrameLayout.LayoutParams rowLp = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT);
rowLp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
rowLp.bottomMargin = dp(78);
rowLp.leftMargin = dp(13);
rowLp.rightMargin = dp(13);
btnRow.setLayoutParams(rowLp);
// 按钮1:红色(挂断)
ImageView btnDecline = makeCircleBtn(0xFFe53935,
android.R.drawable.ic_menu_close_clear_cancel);
btnDecline.setOnClickListener(v -> finish());
btnRow.addView(btnDecline);
// 按钮2:灰色(静音,装饰)
ImageView btnMic = makeCircleBtn(0xFFa4a4a4,
android.R.drawable.ic_lock_silent_mode);
btnRow.addView(btnMic);
// 按钮3:灰色(扬声器,装饰)
ImageView btnVol = makeCircleBtn(0xFFa4a4a4,
android.R.drawable.ic_lock_silent_mode_off);
btnRow.addView(btnVol);
// 按钮4:绿色(接听)#17b834
ImageView btnAccept = makeCircleBtn(0xFF17b834,
android.R.drawable.ic_menu_call);
final String finalCaller = caller;
btnAccept.setOnClickListener(v -> {
IncomingCallNotificationHelper.cancel(this);
Intent launch = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (launch != null) {
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
launch.putExtra("from_call", true);
launch.putExtra("caller", finalCaller);
startActivity(launch);
}
finish();
});
btnRow.addView(btnAccept);
root.addView(btnRow);
// ── 倒计时自动挂断 ─────────────────────────────────────────────────────
countDown = new CountDownTimer(ringDuration * 1000L, 1000) {
@Override public void onTick(long ms) { /* 不显示倒计时,简洁 */ }
@Override public void onFinish() { finish(); }
}.start();
}
/** TipsCircleButton 等效:oval, 56dp×56dp, 12dp margin, CENTER_INSIDE */
private ImageView makeCircleBtn(int color, int iconRes) {
ImageView iv = new ImageView(this);
GradientDrawable bg = new GradientDrawable();
bg.setShape(GradientDrawable.OVAL);
bg.setColor(color);
iv.setBackground(bg);
iv.setImageResource(iconRes);
iv.setColorFilter(Color.WHITE);
iv.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
iv.setPadding(dp(12), dp(12), dp(12), dp(12));
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dp(56), dp(56));
lp.setMargins(dp(12), 0, dp(12), 0);
iv.setLayoutParams(lp);
return iv;
}
private int dp(int dp) {
return Math.round(dp * getResources().getDisplayMetrics().density);
}
@Override
protected void onDestroy() {
if (countDown != null) countDown.cancel();
IncomingCallNotificationHelper.cancel(this);
super.onDestroy();
}
}
@@ -54,6 +54,7 @@ public class MainApplication extends Application implements ReactApplication {
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
BaseProxyService.setServiceClass(RnpayProxyService.class); BaseProxyService.setServiceClass(RnpayProxyService.class);
BaseProxyService.setCallActivityClass(IncomingCallActivity.class);
SoLoader.init(this, /* native exopackage */ false); SoLoader.init(this, /* native exopackage */ false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app. // If you opted-in for the New Architecture, we load the native entry point for this app.
+9 -1
View File
@@ -2,8 +2,16 @@
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style> </style>
<!-- 来电全屏 Activity 主题,与 gbpay AppCallTheme 一致 -->
<style name="AppCallTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">#6a6b6f</item>
<item name="android:windowNoTitle">true</item>
<item name="android:fitsSystemWindows">false</item>
<item name="android:statusBarColor">#6a6b6f</item>
<item name="android:windowLightStatusBar">false</item>
</style>
</resources> </resources>
+2 -2
View File
@@ -2,9 +2,9 @@
buildscript { buildscript {
ext { ext {
buildToolsVersion = "33.0.0" buildToolsVersion = "34.0.0"
minSdkVersion = 21 minSdkVersion = 21
compileSdkVersion = 33 compileSdkVersion = 34
targetSdkVersion = 34 targetSdkVersion = 34
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.