Compare commits
7 Commits
contacts-v
...
contacts-v
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ef6141c9d | |||
| fb79b83705 | |||
| de89a326c0 | |||
| dd95e1ea6c | |||
| 0237e72c62 | |||
| 9771f5d8e0 | |||
| 4d0049f66f |
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Thu Apr 09 02:36:26 HKT 2026
|
||||
stageCount=10
|
||||
#Sat Apr 18 21:14:59 HKT 2026
|
||||
stageCount=13
|
||||
libraryProject=
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.9
|
||||
publishVersion=15.14.12
|
||||
buildCount=0
|
||||
baseBetaVersion=15.14.10
|
||||
baseBetaVersion=15.14.13
|
||||
|
||||
@@ -9,54 +9,73 @@
|
||||
<!-- 拨打电话 -->
|
||||
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||
|
||||
<!-- 读取手机状态和身份(API 30+ 需细化权限) -->
|
||||
<!-- 读取手机状态和身份 -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||
|
||||
<!-- 读取电话号码 -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
|
||||
|
||||
<!-- 修改系统设置(移除无效的 protectionLevel 声明,该属性由系统定义) -->
|
||||
<!-- 修改系统设置 -->
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||
|
||||
<!-- 联系人权限(适配 Android 13+ 细分权限) -->
|
||||
<!-- 读取联系人 -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
|
||||
<!-- 修改您的通讯录 -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
|
||||
<!-- GET_CONTACTS -->
|
||||
<uses-permission android:name="android.permission.GET_CONTACTS"/>
|
||||
|
||||
<!-- 悬浮窗权限(需动态申请) -->
|
||||
<!-- 此应用可显示在其他应用上方 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<!-- 更改音频设置 -->
|
||||
<!-- 更改您的音频设置 -->
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
|
||||
<!-- 通话记录权限(适配 Android 13+ 细分权限) -->
|
||||
<!-- 读取通话记录 -->
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
|
||||
|
||||
<!-- 新建/修改/删除通话记录 -->
|
||||
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
|
||||
|
||||
<!-- GET_CALL_LOG -->
|
||||
<uses-permission android:name="android.permission.GET_CALL_LOG"/>
|
||||
|
||||
<!-- 录音权限 -->
|
||||
<!-- 录音 -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
|
||||
<!-- 前台服务权限(按业务类型声明) -->
|
||||
<!-- 运行前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<!-- 运行“dataSync”类型的前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
|
||||
<!-- 运行“phoneCall”类型的前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
|
||||
|
||||
<!-- 运行“microphone”类型的前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||
|
||||
<!-- API 30+ 通话筛选服务权限(替代 PROCESS_OUTGOING_CALLS) -->
|
||||
<!-- BIND_CALL_SCREENING_SERVICE -->
|
||||
<uses-permission android:name="android.permission.BIND_CALL_SCREENING_SERVICE"/>
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- MANAGE_EXTERNAL_STORAGE -->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_winboll"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/MyAppTheme"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
@@ -66,8 +85,11 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
@@ -77,6 +99,7 @@
|
||||
android:label="CallActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -85,53 +108,60 @@
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.DIAL"/>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="tel"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.DIAL"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="cc.winboll.studio.contacts.activities.SettingsActivity"/>
|
||||
|
||||
<!-- 主服务:仅 dataSync 类型(与代码中 0x00000008 匹配) -->
|
||||
<service
|
||||
android:name=".services.MainService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<!-- 辅助服务:dataSync 类型 -->
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<!-- 通话UI服务(系统绑定) -->
|
||||
<service
|
||||
android:name=".phonecallui.PhoneCallService"
|
||||
android:permission="android.permission.BIND_INCALL_SERVICE"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false">
|
||||
android:stopWithTask="false">
|
||||
|
||||
<meta-data
|
||||
android:name="android.telecom.IN_CALL_SERVICE_UI"
|
||||
android:value="true"/>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.telecom.InCallService"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<!-- 通话监听服务:phoneCall 类型(与代码中 0x00000020 匹配) -->
|
||||
<service
|
||||
android:name=".listenphonecall.CallListenerService"
|
||||
android:enabled="true"
|
||||
@@ -139,37 +169,52 @@
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
|
||||
<action android:name=".service.CallShowService"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<!-- API 30+ 通话筛选服务(替代 PROCESS_OUTGOING_CALLS 权限) -->
|
||||
<service
|
||||
android:name=".services.MyCallScreeningService"
|
||||
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||
<service
|
||||
android:name=".services.MyCallScreeningService"
|
||||
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||
android:exported="true"
|
||||
android:stopWithTask="false">
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.telecom.CallScreeningService"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receivers.MainReceiver"
|
||||
android:stopWithTask="false">
|
||||
<receiver
|
||||
android:name=".receivers.MainReceiver"
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.receivers.MainReceiver"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.APPStatusWidget"
|
||||
android:exported="true"
|
||||
android:stopWithTask="false">
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_ACTIVE"/>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_NOACTIVE"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
@@ -178,11 +223,16 @@
|
||||
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener"
|
||||
android:stopWithTask="false">
|
||||
<receiver
|
||||
android:name=".widgets.APPStatusWidgetClickListener"
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidgetClickListener.ACTION_APPICON_CLICK"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
@@ -201,7 +251,8 @@
|
||||
|
||||
<activity android:name="cc.winboll.studio.contacts.activities.AboutActivity"/>
|
||||
|
||||
<service android:name="cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.activities.UnitTestActivity;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService;
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
@@ -116,6 +118,13 @@ public class UnitTestActivity extends WinBollActivity implements IWinBoLLActivit
|
||||
LogUtils.d(TAG, String.format("onTestMain: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
|
||||
}
|
||||
LogUtils.d(TAG, "onTestMain: 批量号码规则测试完成");
|
||||
|
||||
new Thread(new Runnable(){
|
||||
@Override
|
||||
public void run() {
|
||||
LimitedTimeSpecialChannelService.unitTest(UnitTestActivity.this);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
// ====================== 私有工具函数区 ======================
|
||||
|
||||
@@ -5,6 +5,7 @@ import cc.winboll.studio.contacts.activities.SettingsActivity;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||
import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||
@@ -170,6 +171,14 @@ public class Rules {
|
||||
isConnect = false;
|
||||
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
|
||||
}
|
||||
|
||||
// 限时特殊通道打开时返回连接
|
||||
if (!isDefend && LimitedTimeSpecialChannelService.isServiceRunning()) {
|
||||
LogUtils.d(TAG, String.format("PhoneNumber %s\n and Limited Time Special Channel Service Is Running.", phoneNumber));
|
||||
isDefend = true;
|
||||
isConnect = true;
|
||||
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
|
||||
}
|
||||
|
||||
// 检验拨不通号码群
|
||||
if (!isDefend && MainService.isPhoneInBoBullToon(phoneNumber)) {
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/04/18 15:00:00 (GMT+8)
|
||||
* @LastEditTime 2026/04/22 17:20:00 (GMT+8)
|
||||
* @Describe 限时特殊通道服务
|
||||
* 提供安全的服务启动、令牌校验及循环任务管理
|
||||
* 新增功能:
|
||||
* 1. 计时过程中实时输出剩余秒数
|
||||
* 2. 服务销毁前,发送本地广播通知应用内其他组件,并携带剩余秒数信息
|
||||
* 3. 每秒发送一次倒计时状态的本地广播
|
||||
*/
|
||||
public class LimitedTimeSpecialChannelService extends Service {
|
||||
|
||||
// ========================= 常量定义 =========================
|
||||
public static final String TAG = "LimitedTimeSpecialChannelService";
|
||||
public static final String EXTRA_DELAY_MILLIS = "EXTRA_DELAY_MILLIS";
|
||||
private static final String EXTRA_SECURITY_TOKEN = "EXTRA_SECURITY_TOKEN";
|
||||
|
||||
// 本地广播 Action 常量
|
||||
public static final String ACTION_SERVICE_DESTROYED = "cc.winboll.studio.contacts.services.ACTION_SERVICE_DESTROYED";
|
||||
|
||||
// 倒计时心跳广播 Action
|
||||
public static final String ACTION_COUNTDOWN_TICK = "cc.winboll.studio.contacts.services.ACTION_COUNTDOWN_TICK";
|
||||
// 广播携带的额外参数:剩余秒数
|
||||
public static final String EXTRA_REMAINING_SECONDS = "EXTRA_REMAINING_SECONDS";
|
||||
// 广播携带的额外参数:总定时秒数
|
||||
public static final String EXTRA_TOTAL_SECONDS = "EXTRA_TOTAL_SECONDS";
|
||||
|
||||
// ========================= 静态变量 =========================
|
||||
/**
|
||||
* 公共静态私有字段:安全校验令牌
|
||||
* 确保全局可访问且值不可变更
|
||||
*/
|
||||
public static final String mValidToken = "VALID_TOKEN_" + System.currentTimeMillis();
|
||||
|
||||
private static volatile LimitedTimeSpecialChannelService sInstance = null;
|
||||
private static volatile boolean sIsServiceRunning = false;
|
||||
|
||||
// ========================= 成员变量 =========================
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
private long mTotalMillis = 0; // 总定时时长
|
||||
private long mRemainingMillis = 0; // 剩余时长
|
||||
private LocalBroadcastManager mLocalBroadcastManager; // 本地广播管理器实例
|
||||
|
||||
// ========================= 公共静态方法 =========================
|
||||
/**
|
||||
* 公共静态方法:启动服务
|
||||
* @param context 上下文
|
||||
* @param delayMillis 定时时长(毫秒)
|
||||
*/
|
||||
public static void startService(Context context, long delayMillis) {
|
||||
LogUtils.i(TAG, "调用静态入口方法 startService");
|
||||
LogUtils.i(TAG, "入参 - context: " + context + ", delayMillis: " + delayMillis);
|
||||
|
||||
if (context == null) {
|
||||
LogUtils.w(TAG, "启动失败,上下文为null");
|
||||
return;
|
||||
}
|
||||
if (isServiceRunning()) {
|
||||
LogUtils.i(TAG, "服务已运行,忽略重复启动请求");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建Intent传递参数
|
||||
Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class);
|
||||
intent.putExtra(EXTRA_SECURITY_TOKEN, mValidToken);
|
||||
intent.putExtra(EXTRA_DELAY_MILLIS, delayMillis);
|
||||
|
||||
context.startService(intent);
|
||||
LogUtils.i(TAG, "服务启动命令已发出");
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共静态方法:停止服务
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void stopService(Context context) {
|
||||
LogUtils.i(TAG, "调用静态入口方法 stopService");
|
||||
LogUtils.i(TAG, "入参 - context: " + context);
|
||||
|
||||
if (context == null) {
|
||||
LogUtils.w(TAG, "停止失败,上下文为null");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class);
|
||||
context.stopService(intent);
|
||||
LogUtils.i(TAG, "服务停止命令已发出");
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共静态方法:查询服务运行状态
|
||||
* @return true if running
|
||||
*/
|
||||
public static boolean isServiceRunning() {
|
||||
return sIsServiceRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* 【核心单元测试方法】
|
||||
* 执行完整的单元测试流程
|
||||
* @param context 上下文
|
||||
*/
|
||||
public static void unitTest(Context context) {
|
||||
LogUtils.i(TAG, "=== 开始执行单元测试 ===");
|
||||
|
||||
// 测试1: 初始状态应为未运行
|
||||
boolean initialState = isServiceRunning();
|
||||
LogUtils.i(TAG, "测试1 - 初始状态检查: " + (initialState ? "失败(不应运行)" : "成功(未运行)"));
|
||||
|
||||
// 启动服务,设置5秒定时
|
||||
startService(context, 5000L);
|
||||
|
||||
// 核心修复:同步等待服务启动完成
|
||||
try {
|
||||
Thread.sleep(1200);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, "单元测试等待被中断", e);
|
||||
}
|
||||
|
||||
// 测试3: 验证服务已启动
|
||||
boolean runningState = isServiceRunning();
|
||||
LogUtils.i(TAG, "测试3 - 运行状态检查: " + (runningState ? "成功(运行中)" : "失败(未运行)"));
|
||||
|
||||
// 测试4: 尝试重复启动
|
||||
LogUtils.i(TAG, "测试4 - 尝试重复启动(预期忽略)");
|
||||
startService(context, 5000L);
|
||||
|
||||
// 测试5: 等待服务自动停止
|
||||
LogUtils.i(TAG, "测试5 - 等待服务自动停止(5秒)...");
|
||||
try {
|
||||
// 等待超过服务的5秒运行时间,确保能观测到销毁状态
|
||||
Thread.sleep(6000);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, "测试等待被中断", e);
|
||||
}
|
||||
|
||||
// 验证最终状态
|
||||
boolean finalState = isServiceRunning();
|
||||
LogUtils.i(TAG, "测试5 - 最终状态检查: " + (!finalState ? "成功(已销毁)" : "失败(仍运行)"));
|
||||
LogUtils.i(TAG, "=== 单元测试执行完成 ===");
|
||||
}
|
||||
|
||||
// ========================= 生命周期 =========================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.i(TAG, "服务 onCreate,创建实例");
|
||||
sInstance = this;
|
||||
sIsServiceRunning = true;
|
||||
|
||||
// 初始化本地广播管理器
|
||||
mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
LogUtils.i(TAG, "本地广播管理器初始化完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.i(TAG, "服务 onStartCommand,处理启动请求");
|
||||
LogUtils.i(TAG, "入参 - intent: " + intent + ", flags: " + flags + ", startId: " + startId);
|
||||
|
||||
if (!isValidToken(intent)) {
|
||||
LogUtils.w(TAG, "安全校验失败,拒绝启动服务");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
long delayMillis = intent.getLongExtra(EXTRA_DELAY_MILLIS, 0);
|
||||
if (delayMillis <= 0) {
|
||||
LogUtils.w(TAG, "无效的定时时长: " + delayMillis + ",服务将退出");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// 初始化总时长和剩余时长
|
||||
mTotalMillis = delayMillis;
|
||||
mRemainingMillis = delayMillis;
|
||||
LogUtils.i(TAG, "初始化定时时长: " + (mTotalMillis / 1000) + " 秒");
|
||||
|
||||
startLoopTask();
|
||||
// 设置自动停止定时器
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.i(TAG, "定时时长结束,准备停止服务");
|
||||
stopSelf();
|
||||
}
|
||||
}, delayMillis);
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
LogUtils.i(TAG, "服务 onDestroy,准备销毁");
|
||||
|
||||
// 发送本地广播,通知应用内其他组件服务即将销毁
|
||||
// 此处携带剩余秒数信息,方便接收端知晓具体的结束状态
|
||||
sendServiceDestroyedBroadcast();
|
||||
|
||||
// 重置运行状态
|
||||
sIsServiceRunning = false;
|
||||
sInstance = null;
|
||||
|
||||
// 清理所有回调
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
// 执行父类销毁
|
||||
super.onDestroy();
|
||||
|
||||
LogUtils.i(TAG, "服务销毁流程完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送服务销毁的本地广播
|
||||
* 确保应用内所有注册了该广播接收器的组件都能收到通知
|
||||
* [新增] 携带当前剩余秒数信息
|
||||
*/
|
||||
private void sendServiceDestroyedBroadcast() {
|
||||
LogUtils.i(TAG, "准备发送服务销毁本地广播");
|
||||
try {
|
||||
Intent intent = new Intent(ACTION_SERVICE_DESTROYED);
|
||||
// 新增:将当前剩余秒数放入广播附加数据中
|
||||
intent.putExtra(EXTRA_REMAINING_SECONDS, mRemainingMillis / 1000);
|
||||
|
||||
mLocalBroadcastManager.sendBroadcast(intent);
|
||||
LogUtils.i(TAG, "服务销毁广播发送成功,Action: " + ACTION_SERVICE_DESTROYED);
|
||||
LogUtils.i(TAG, "广播附加数据 - 剩余秒数: " + (mRemainingMillis / 1000));
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "发送服务销毁广播失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送倒计时心跳的本地广播
|
||||
* 每秒执行一次,携带当前剩余秒数和总时长
|
||||
*/
|
||||
private void sendCountdownTickBroadcast() {
|
||||
try {
|
||||
Intent intent = new Intent(ACTION_COUNTDOWN_TICK);
|
||||
intent.putExtra(EXTRA_REMAINING_SECONDS, mRemainingMillis / 1000);
|
||||
intent.putExtra(EXTRA_TOTAL_SECONDS, mTotalMillis / 1000);
|
||||
|
||||
mLocalBroadcastManager.sendBroadcast(intent);
|
||||
// 日志已精简,只在关键节点打印
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "发送倒计时广播失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.i(TAG, "服务 onBind,不支持绑定操作");
|
||||
// 不支持绑定
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================= 私有辅助方法 =========================
|
||||
/**
|
||||
* 校验令牌有效性
|
||||
*/
|
||||
private boolean isValidToken(Intent intent) {
|
||||
LogUtils.i(TAG, "调用 isValidToken 方法进行安全校验");
|
||||
LogUtils.i(TAG, "入参 - intent: " + intent);
|
||||
|
||||
if (intent == null) {
|
||||
return false;
|
||||
}
|
||||
String incomingToken = intent.getStringExtra(EXTRA_SECURITY_TOKEN);
|
||||
LogUtils.i(TAG, "接收到外部传入令牌: " + incomingToken);
|
||||
LogUtils.i(TAG, "本地校验令牌: " + mValidToken);
|
||||
return mValidToken.equals(incomingToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动循环任务
|
||||
*/
|
||||
private void startLoopTask() {
|
||||
LogUtils.i(TAG, "启动倒计时循环任务");
|
||||
mHandler.removeCallbacks(mLoopTaskRunnable);
|
||||
mHandler.postDelayed(mLoopTaskRunnable, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环任务心跳
|
||||
* 核心逻辑:先递减时长,再发送广播,确保0秒能被正确发送
|
||||
*/
|
||||
private final Runnable mLoopTaskRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (sIsServiceRunning) {
|
||||
// 核心修复:先递减剩余时长
|
||||
if (mRemainingMillis >= 1000) {
|
||||
mRemainingMillis -= 1000;
|
||||
} else {
|
||||
// 兜底:防止负数,直接置为0
|
||||
mRemainingMillis = 0;
|
||||
}
|
||||
|
||||
// 计算并发送当前剩余秒数
|
||||
long remainingSeconds = mRemainingMillis / 1000;
|
||||
// 日志已精简,只在非0状态打印,避免日志刷屏
|
||||
if (remainingSeconds > 0) {
|
||||
LogUtils.i(TAG, "循环心跳:剩余 " + remainingSeconds + " 秒");
|
||||
}
|
||||
|
||||
// 发送倒计时广播
|
||||
sendCountdownTickBroadcast();
|
||||
|
||||
// 递归调用,形成循环
|
||||
startLoopTask();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Switch;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/04/18 15:05:00 (GMT+8)
|
||||
* @LastEditTime 2026/04/22 17:30:00 (GMT+8)
|
||||
* @Describe 限时特殊通道视图
|
||||
* 功能说明:
|
||||
* 1. Switch 打开时,EditText 不可编辑,布局背景变为绿色,加减按钮不可用
|
||||
* 2. Switch 关闭时,EditText 可编辑,布局背景恢复为白色,加减按钮恢复可用
|
||||
* 3. 监听服务销毁本地广播,到达后自动关闭 Switch 并恢复背景色与按钮状态
|
||||
* 4. 监听倒计时心跳广播,实时更新EditText显示剩余秒数
|
||||
* 5. 加减按钮控制秒数增减,最小值限制为0
|
||||
* 6. 新增快速设置按钮(btn_thisSeconds),点击直接设置为当前量级3600秒
|
||||
* 7. [新增] 服务销毁时,恢复EditText显示为倒计时剩余秒数
|
||||
*/
|
||||
public class LimitedTimeSpecialChannelView extends LinearLayout {
|
||||
|
||||
// ====================== 常量定义 =========================
|
||||
public static final String TAG = "LimitedTimeSpecialChannelView";
|
||||
// 定义秒数增量的量级
|
||||
private static final long SECONDS_INCREMENT = 3600;
|
||||
|
||||
// ====================== 成员变量 =========================
|
||||
private Context mContext;
|
||||
private EditText mEtSeconds;
|
||||
private Switch mSwEnable;
|
||||
private LinearLayout mLlMain;
|
||||
private Button mBtnAddSeconds;
|
||||
private Button mBtnSubSeconds;
|
||||
// [新增] 快速设置量级按钮
|
||||
private Button mBtnThisSeconds;
|
||||
private android.content.BroadcastReceiver mCountdownReceiver;
|
||||
|
||||
// ====================== 构造函数 =========================
|
||||
public LimitedTimeSpecialChannelView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public LimitedTimeSpecialChannelView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public LimitedTimeSpecialChannelView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
// ====================== 初始化方法 =========================
|
||||
private void initView(Context context) {
|
||||
LogUtils.i(TAG, "开始初始化视图...");
|
||||
this.mContext = context;
|
||||
|
||||
// 加载布局文件
|
||||
LayoutInflater.from(context).inflate(R.layout.view_limitedtimespecialchannel, this, true);
|
||||
LogUtils.i(TAG, "布局文件加载完成");
|
||||
|
||||
// 绑定控件ID
|
||||
bindViews();
|
||||
// 初始化所有按钮点击事件
|
||||
initButtons();
|
||||
// 初始化开关状态
|
||||
initSwitchState();
|
||||
// 注册广播监听
|
||||
registerServiceDestroyedListener();
|
||||
registerCountdownTickListener();
|
||||
|
||||
LogUtils.i(TAG, "视图初始化流程结束");
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定布局中的所有控件ID
|
||||
*/
|
||||
private void bindViews() {
|
||||
LogUtils.i(TAG, "开始绑定控件ID");
|
||||
mEtSeconds = (EditText) findViewById(R.id.et_seconds);
|
||||
mSwEnable = (Switch) findViewById(R.id.sw_enable);
|
||||
mLlMain = (LinearLayout) findViewById(R.id.ll_main);
|
||||
mBtnAddSeconds = (Button) findViewById(R.id.btn_addSeconds);
|
||||
mBtnSubSeconds = (Button) findViewById(R.id.btn_subSeconds);
|
||||
// [新增] 绑定快速设置按钮ID
|
||||
mBtnThisSeconds = (Button) findViewById(R.id.btn_thisSeconds);
|
||||
|
||||
// 健壮性检查
|
||||
if (mEtSeconds == null) LogUtils.e(TAG, "未找到ID为 et_seconds 的EditText");
|
||||
if (mSwEnable == null) LogUtils.e(TAG, "未找到ID为 sw_enable 的Switch");
|
||||
if (mLlMain == null) LogUtils.w(TAG, "未找到ID为 ll_main 的LinearLayout,将无法设置背景色");
|
||||
if (mBtnAddSeconds == null) LogUtils.e(TAG, "未找到ID为 btn_addSeconds 的Button");
|
||||
if (mBtnSubSeconds == null) LogUtils.e(TAG, "未找到ID为 btn_subSeconds 的Button");
|
||||
if (mBtnThisSeconds == null) LogUtils.e(TAG, "未找到ID为 btn_thisSeconds 的Button");
|
||||
|
||||
LogUtils.i(TAG, "控件ID绑定完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有按钮的点击事件
|
||||
* 包含:加号、减号、快速设置量级按钮
|
||||
*/
|
||||
private void initButtons() {
|
||||
LogUtils.i(TAG, "初始化按钮点击事件");
|
||||
|
||||
// 1. 加号按钮:增加一个量级
|
||||
if (mBtnAddSeconds != null) {
|
||||
mBtnAddSeconds.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.i(TAG, "点击加号按钮,准备增加秒数");
|
||||
try {
|
||||
long current = Long.parseLong(mEtSeconds.getText().toString().trim());
|
||||
long newValue = current + SECONDS_INCREMENT;
|
||||
mEtSeconds.setText(String.valueOf(newValue));
|
||||
LogUtils.i(TAG, "增加成功,当前秒数: " + newValue);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, "当前输入格式错误,无法增加", e);
|
||||
mEtSeconds.setText("0");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 减号按钮:减少一个量级,最低为0
|
||||
if (mBtnSubSeconds != null) {
|
||||
mBtnSubSeconds.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.i(TAG, "点击减号按钮,准备减少秒数");
|
||||
try {
|
||||
long current = Long.parseLong(mEtSeconds.getText().toString().trim());
|
||||
long newValue = Math.max(0, current - SECONDS_INCREMENT);
|
||||
mEtSeconds.setText(String.valueOf(newValue));
|
||||
LogUtils.i(TAG, "减少成功,当前秒数: " + newValue);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, "当前输入格式错误,无法减少", e);
|
||||
mEtSeconds.setText("0");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. [新增] 快速设置按钮:点击直接设置为量级数值 3600
|
||||
if (mBtnThisSeconds != null) {
|
||||
mBtnThisSeconds.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.i(TAG, "点击快速设置按钮,设置秒数为: " + SECONDS_INCREMENT);
|
||||
// 直接设置为常量数值
|
||||
mEtSeconds.setText(String.valueOf(SECONDS_INCREMENT));
|
||||
LogUtils.i(TAG, "快速设置成功,当前秒数: " + SECONDS_INCREMENT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化开关状态
|
||||
* 设置Switch的监听事件,以及背景色、按钮可点击状态
|
||||
*/
|
||||
private void initSwitchState() {
|
||||
LogUtils.i(TAG, "初始化开关状态");
|
||||
boolean isChecked = mSwEnable.isChecked();
|
||||
|
||||
// 设置初始UI状态
|
||||
updateUIState(!isChecked);
|
||||
|
||||
LogUtils.i(TAG, "初始状态 - 开关: " + (isChecked ? "开启" : "关闭") + ", 背景色: " + (isChecked ? "绿色" : "白色") + ", 按钮可用: " + (!isChecked ? "是" : "否"));
|
||||
|
||||
// 设置开关切换监听
|
||||
mSwEnable.setOnCheckedChangeListener(new Switch.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
LogUtils.i(TAG, "开关状态变更为: " + (isChecked ? "开启" : "关闭"));
|
||||
// 根据开关状态更新UI可操作状态
|
||||
updateUIState(!isChecked);
|
||||
|
||||
if (isChecked) {
|
||||
startLimitedTimeSpecialChannelService();
|
||||
} else {
|
||||
LimitedTimeSpecialChannelService.stopService(mContext);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一更新UI可操作状态
|
||||
* @param enabled 可用状态
|
||||
*/
|
||||
private void updateUIState(boolean enabled) {
|
||||
if (mEtSeconds != null) mEtSeconds.setEnabled(enabled);
|
||||
if (mBtnAddSeconds != null) mBtnAddSeconds.setEnabled(enabled);
|
||||
if (mBtnSubSeconds != null) mBtnSubSeconds.setEnabled(enabled);
|
||||
// [新增] 同步快速设置按钮状态
|
||||
if (mBtnThisSeconds != null) mBtnThisSeconds.setEnabled(enabled);
|
||||
|
||||
if (mLlMain != null) {
|
||||
mLlMain.setBackgroundColor(enabled ? android.graphics.Color.WHITE : android.graphics.Color.GREEN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册服务销毁本地广播监听
|
||||
* [修改] 接收广播时检索倒计时剩余秒数参数并设置到EditText
|
||||
*/
|
||||
private void registerServiceDestroyedListener() {
|
||||
LogUtils.i(TAG, "注册服务销毁广播监听");
|
||||
IntentFilter filter = new IntentFilter(LimitedTimeSpecialChannelService.ACTION_SERVICE_DESTROYED);
|
||||
LocalBroadcastManager.getInstance(mContext).registerReceiver(new android.content.BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (LimitedTimeSpecialChannelService.ACTION_SERVICE_DESTROYED.equals(intent.getAction())) {
|
||||
LogUtils.i(TAG, "收到服务销毁广播,执行恢复操作");
|
||||
|
||||
// 1. 恢复开关状态
|
||||
if (mSwEnable != null && mSwEnable.isChecked()) {
|
||||
mSwEnable.setChecked(false);
|
||||
}
|
||||
|
||||
// 2. [核心修改] 检索并设置倒计时剩余秒数参数
|
||||
// 从广播附加数据中获取之前服务发送的剩余秒数
|
||||
long remainingSeconds = intent.getLongExtra(LimitedTimeSpecialChannelService.EXTRA_REMAINING_SECONDS, 0);
|
||||
// 实际倒计时消耗冲正
|
||||
remainingSeconds -= 1;
|
||||
// 倒计时秒数0值保底
|
||||
remainingSeconds = remainingSeconds < 0? 0 : remainingSeconds;
|
||||
LogUtils.i(TAG, "从销毁广播中检索到剩余秒数参数: " + remainingSeconds + " 秒");
|
||||
//ToastUtils.show("从销毁广播中检索到剩余秒数参数: " + remainingSeconds + " 秒");
|
||||
|
||||
// 3. 恢复所有UI状态为可用
|
||||
updateUIState(true);
|
||||
// 4. 设置EditText显示为倒计时剩余秒数
|
||||
if (mEtSeconds != null) {
|
||||
mEtSeconds.setText(String.valueOf(remainingSeconds));
|
||||
LogUtils.i(TAG, "已将EditText设置为剩余秒数: " + remainingSeconds);
|
||||
}
|
||||
|
||||
//ToastUtils.show("服务已销毁,恢复倒计时剩余状态");
|
||||
}
|
||||
}
|
||||
}, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册倒计时心跳广播监听
|
||||
*/
|
||||
private void registerCountdownTickListener() {
|
||||
LogUtils.i(TAG, "注册倒计时心跳广播监听");
|
||||
mCountdownReceiver = new android.content.BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (LimitedTimeSpecialChannelService.ACTION_COUNTDOWN_TICK.equals(intent.getAction())) {
|
||||
long remainingSeconds = intent.getLongExtra(LimitedTimeSpecialChannelService.EXTRA_REMAINING_SECONDS, 0);
|
||||
if (mEtSeconds != null) {
|
||||
mEtSeconds.setText(String.valueOf(remainingSeconds));
|
||||
}
|
||||
if (mSwEnable != null) {
|
||||
mSwEnable.setChecked(remainingSeconds>0);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter filter = new IntentFilter(LimitedTimeSpecialChannelService.ACTION_COUNTDOWN_TICK);
|
||||
LocalBroadcastManager.getInstance(mContext).registerReceiver(mCountdownReceiver, filter);
|
||||
LogUtils.i(TAG, "倒计时广播注册成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动限时特殊通道服务
|
||||
*/
|
||||
private void startLimitedTimeSpecialChannelService() {
|
||||
LogUtils.i(TAG, "准备启动限时特殊通道服务");
|
||||
String secondsStr = mEtSeconds.getText().toString().trim();
|
||||
if (secondsStr.isEmpty()) {
|
||||
LogUtils.w(TAG, "输入框为空,使用默认值 3600 秒");
|
||||
secondsStr = "3600";
|
||||
}
|
||||
|
||||
try {
|
||||
long seconds = Long.parseLong(secondsStr);
|
||||
long delayMillis = seconds * 1000;
|
||||
|
||||
LogUtils.i(TAG, "启动参数 - 输入秒数: " + seconds + ", 转换毫秒: " + delayMillis);
|
||||
//ToastUtils.show("调用 LimitedTimeSpecialChannelService 服务");
|
||||
LimitedTimeSpecialChannelService.startService(mContext, delayMillis);
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.e(TAG, "输入的秒数格式错误: " + secondsStr, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
LogUtils.i(TAG, "视图销毁,反注册所有广播接收器");
|
||||
|
||||
if (mCountdownReceiver != null) {
|
||||
LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mCountdownReceiver);
|
||||
LogUtils.i(TAG, "反注册倒计时心跳广播接收器");
|
||||
}
|
||||
|
||||
LogUtils.i(TAG, "所有广播反注册完成");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,24 @@
|
||||
android:id="@+id/tv_DunInfo"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="特殊通道设置:"/>
|
||||
|
||||
<cc.winboll.studio.contacts.views.LimitedTimeSpecialChannelView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/view_LimitedTimeSpecialChannel"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical|center_horizontal"
|
||||
android:id="@+id/ll_main">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="总倒计时:"/>
|
||||
|
||||
<EditText
|
||||
android:layout_width="85dp"
|
||||
android:inputType="number"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:text="3600"
|
||||
android:id="@+id/et_seconds"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="秒"/>
|
||||
|
||||
<Switch
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/sw_enable"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="-"
|
||||
android:id="@+id/btn_subSeconds"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:text="3600秒"
|
||||
android:id="@+id/btn_thisSeconds"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="+"
|
||||
android:id="@+id/btn_addSeconds"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
Reference in New Issue
Block a user