feat: 增加整点报时,增加闹钟
This commit is contained in:
@@ -197,6 +197,7 @@ android {
|
|||||||
|
|
||||||
ext {
|
ext {
|
||||||
jpush = [version: "5.7.0"]
|
jpush = [version: "5.7.0"]
|
||||||
|
AndroidPicker = [version: "4.1.15"]
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -207,6 +208,9 @@ dependencies {
|
|||||||
implementation files('libs/QWeather_Public_Android_V5.1.2.jar')
|
implementation files('libs/QWeather_Public_Android_V5.1.2.jar')
|
||||||
implementation 'net.i2p.crypto:eddsa:0.3.0'
|
implementation 'net.i2p.crypto:eddsa:0.3.0'
|
||||||
|
|
||||||
|
//sherpa-onnx https://github.com/k2-fsa/sherpa-onnx/releases
|
||||||
|
implementation files('libs/sherpa-onnx-1.13.2.aar')
|
||||||
|
|
||||||
implementation project(path: ':niceimageview')
|
implementation project(path: ':niceimageview')
|
||||||
implementation project(path: ':iconloader')
|
implementation project(path: ':iconloader')
|
||||||
|
|
||||||
@@ -387,6 +391,22 @@ dependencies {
|
|||||||
// 自定义相机 (按需引入)
|
// 自定义相机 (按需引入)
|
||||||
implementation 'io.github.lucksiege:camerax:v3.11.2'
|
implementation 'io.github.lucksiege:camerax:v3.11.2'
|
||||||
|
|
||||||
|
//所有选择器的基础窗体(用于自定义弹窗)
|
||||||
|
implementation "com.github.gzu-liyujiang.AndroidPicker:Common:$AndroidPicker.version"
|
||||||
|
//滚轮选择器的滚轮控件(用于自定义滚轮选择器)
|
||||||
|
implementation "com.github.gzu-liyujiang.AndroidPicker:WheelView:$AndroidPicker.version"
|
||||||
|
//单项/数字、二三级联动、日期/时间等滚轮选择器
|
||||||
|
implementation "com.github.gzu-liyujiang.AndroidPicker:WheelPicker:$AndroidPicker.version"
|
||||||
|
//省市区地址选择器
|
||||||
|
implementation "com.github.gzu-liyujiang.AndroidPicker:AddressPicker:$AndroidPicker.version"
|
||||||
|
//文件/目录选择器
|
||||||
|
implementation "com.github.gzu-liyujiang.AndroidPicker:FilePicker:$AndroidPicker.version"
|
||||||
|
//颜色选择器
|
||||||
|
implementation "com.github.gzu-liyujiang.AndroidPicker:ColorPicker:$AndroidPicker.version"
|
||||||
|
//日历日期选择器
|
||||||
|
implementation "com.github.gzu-liyujiang.AndroidPicker:CalendarPicker:$AndroidPicker.version"
|
||||||
|
//图片选择器
|
||||||
|
implementation "com.github.gzu-liyujiang.AndroidPicker:ImagePicker:$AndroidPicker.version"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在 dependencies 之后添加
|
// 在 dependencies 之后添加
|
||||||
|
|||||||
BIN
app/libs/sherpa-onnx-1.13.2.aar
Normal file
BIN
app/libs/sherpa-onnx-1.13.2.aar
Normal file
Binary file not shown.
@@ -30,6 +30,7 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
|
|
||||||
|
|
||||||
<!--baidumap start-->
|
<!--baidumap start-->
|
||||||
@@ -114,10 +115,27 @@
|
|||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.alarm.AlarmAlertActivity"
|
android:name=".activity.alarm.AlarmAlertActivity"
|
||||||
android:showOnLockScreen="true"
|
android:exported="false"
|
||||||
android:turnScreenOn="true"
|
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:exported="false" />
|
android:showOnLockScreen="true"
|
||||||
|
android:turnScreenOn="true" />
|
||||||
|
<activity
|
||||||
|
android:name=".tts.sherpa_onnx.SherpaOnnxTtsActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name=".activity.alarm.add.AlarmAddActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@style/MaterialTheme"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name=".activity.alarm.edit.AlarmEditActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name=".activity.alarm.list.AlarmListActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.main.MainService"
|
android:name=".service.main.MainService"
|
||||||
@@ -162,6 +180,14 @@
|
|||||||
android:name=".receiver.AlarmReceiver"
|
android:name=".receiver.AlarmReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.HourlyChimeReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.FileProvider"
|
android:authorities="${applicationId}.FileProvider"
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Model card for 小雅 (Xiǎo Yǎ) (medium)
|
||||||
|
|
||||||
|
* Language: zh_CN (Chinese, China)
|
||||||
|
* Speakers: 1
|
||||||
|
* Quality: medium
|
||||||
|
* Samplerate: 22,050Hz
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Only works on the Python version of Piper 1.4+ due to a dependency on [g2pW](https://github.com/GitYCC/g2pW/)
|
||||||
|
|
||||||
|
## Dataset
|
||||||
|
|
||||||
|
* URL: https://huggingface.co/openspeech/BZNSYP
|
||||||
|
* License: Non-commercial use (see https://www.data-baker.com/data/index/TNtts/)
|
||||||
|
|
||||||
|
## Training
|
||||||
|
|
||||||
|
Trained from scratch
|
||||||
BIN
app/src/main/assets/vits-piper-zh_CN-xiao_ya-medium/date.fst
Normal file
BIN
app/src/main/assets/vits-piper-zh_CN-xiao_ya-medium/date.fst
Normal file
Binary file not shown.
68012
app/src/main/assets/vits-piper-zh_CN-xiao_ya-medium/lexicon.txt
Normal file
68012
app/src/main/assets/vits-piper-zh_CN-xiao_ya-medium/lexicon.txt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
app/src/main/assets/vits-piper-zh_CN-xiao_ya-medium/number.fst
Normal file
BIN
app/src/main/assets/vits-piper-zh_CN-xiao_ya-medium/number.fst
Normal file
Binary file not shown.
BIN
app/src/main/assets/vits-piper-zh_CN-xiao_ya-medium/phone.fst
Normal file
BIN
app/src/main/assets/vits-piper-zh_CN-xiao_ya-medium/phone.fst
Normal file
Binary file not shown.
@@ -0,0 +1,85 @@
|
|||||||
|
_ 0
|
||||||
|
^ 1
|
||||||
|
$ 2
|
||||||
|
Ø 3
|
||||||
|
b 4
|
||||||
|
p 5
|
||||||
|
m 6
|
||||||
|
f 7
|
||||||
|
d 8
|
||||||
|
t 9
|
||||||
|
n 10
|
||||||
|
l 11
|
||||||
|
g 12
|
||||||
|
k 13
|
||||||
|
h 14
|
||||||
|
j 15
|
||||||
|
q 16
|
||||||
|
x 17
|
||||||
|
zh 18
|
||||||
|
ch 19
|
||||||
|
sh 20
|
||||||
|
r 21
|
||||||
|
z 22
|
||||||
|
c 23
|
||||||
|
s 24
|
||||||
|
y 25
|
||||||
|
w 26
|
||||||
|
a 27
|
||||||
|
o 28
|
||||||
|
e 29
|
||||||
|
ai 30
|
||||||
|
ei 31
|
||||||
|
ao 32
|
||||||
|
ou 33
|
||||||
|
an 34
|
||||||
|
en 35
|
||||||
|
ang 36
|
||||||
|
eng 37
|
||||||
|
ong 38
|
||||||
|
i 39
|
||||||
|
ia 40
|
||||||
|
ie 41
|
||||||
|
iao 42
|
||||||
|
iu 43
|
||||||
|
ian 44
|
||||||
|
in 45
|
||||||
|
iang 46
|
||||||
|
ing 47
|
||||||
|
iong 48
|
||||||
|
u 49
|
||||||
|
ua 50
|
||||||
|
uo 51
|
||||||
|
uai 52
|
||||||
|
ui 53
|
||||||
|
uan 54
|
||||||
|
un 55
|
||||||
|
uang 56
|
||||||
|
ueng 57
|
||||||
|
v 58
|
||||||
|
ve 59
|
||||||
|
van 60
|
||||||
|
vn 61
|
||||||
|
er 62
|
||||||
|
ue 63
|
||||||
|
1 64
|
||||||
|
2 65
|
||||||
|
3 66
|
||||||
|
4 67
|
||||||
|
5 68
|
||||||
|
。 69
|
||||||
|
. 69
|
||||||
|
? 70
|
||||||
|
? 70
|
||||||
|
! 71
|
||||||
|
! 71
|
||||||
|
— 72
|
||||||
|
… 72
|
||||||
|
、 72
|
||||||
|
, 72
|
||||||
|
, 72
|
||||||
|
: 72
|
||||||
|
: 72
|
||||||
|
; 72
|
||||||
|
; 72
|
||||||
|
72
|
||||||
Binary file not shown.
@@ -0,0 +1,286 @@
|
|||||||
|
{
|
||||||
|
"audio": {
|
||||||
|
"sample_rate": 22050,
|
||||||
|
"quality": "medium"
|
||||||
|
},
|
||||||
|
"espeak": {
|
||||||
|
"voice": "zh"
|
||||||
|
},
|
||||||
|
"phoneme_type": "pinyin",
|
||||||
|
"num_symbols": 256,
|
||||||
|
"num_speakers": 1,
|
||||||
|
"inference": {
|
||||||
|
"noise_scale": 0.667,
|
||||||
|
"length_scale": 1.0,
|
||||||
|
"noise_w": 0.8
|
||||||
|
},
|
||||||
|
"phoneme_id_map": {
|
||||||
|
"_": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"^": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"$": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"Ø": [
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"b": [
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"p": [
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"m": [
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"f": [
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"d": [
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"t": [
|
||||||
|
9
|
||||||
|
],
|
||||||
|
"n": [
|
||||||
|
10
|
||||||
|
],
|
||||||
|
"l": [
|
||||||
|
11
|
||||||
|
],
|
||||||
|
"g": [
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"k": [
|
||||||
|
13
|
||||||
|
],
|
||||||
|
"h": [
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"j": [
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"q": [
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"x": [
|
||||||
|
17
|
||||||
|
],
|
||||||
|
"zh": [
|
||||||
|
18
|
||||||
|
],
|
||||||
|
"ch": [
|
||||||
|
19
|
||||||
|
],
|
||||||
|
"sh": [
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"r": [
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"z": [
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"c": [
|
||||||
|
23
|
||||||
|
],
|
||||||
|
"s": [
|
||||||
|
24
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
25
|
||||||
|
],
|
||||||
|
"w": [
|
||||||
|
26
|
||||||
|
],
|
||||||
|
"a": [
|
||||||
|
27
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
28
|
||||||
|
],
|
||||||
|
"e": [
|
||||||
|
29
|
||||||
|
],
|
||||||
|
"ai": [
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"ei": [
|
||||||
|
31
|
||||||
|
],
|
||||||
|
"ao": [
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"ou": [
|
||||||
|
33
|
||||||
|
],
|
||||||
|
"an": [
|
||||||
|
34
|
||||||
|
],
|
||||||
|
"en": [
|
||||||
|
35
|
||||||
|
],
|
||||||
|
"ang": [
|
||||||
|
36
|
||||||
|
],
|
||||||
|
"eng": [
|
||||||
|
37
|
||||||
|
],
|
||||||
|
"ong": [
|
||||||
|
38
|
||||||
|
],
|
||||||
|
"i": [
|
||||||
|
39
|
||||||
|
],
|
||||||
|
"ia": [
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"ie": [
|
||||||
|
41
|
||||||
|
],
|
||||||
|
"iao": [
|
||||||
|
42
|
||||||
|
],
|
||||||
|
"iu": [
|
||||||
|
43
|
||||||
|
],
|
||||||
|
"ian": [
|
||||||
|
44
|
||||||
|
],
|
||||||
|
"in": [
|
||||||
|
45
|
||||||
|
],
|
||||||
|
"iang": [
|
||||||
|
46
|
||||||
|
],
|
||||||
|
"ing": [
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"iong": [
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"u": [
|
||||||
|
49
|
||||||
|
],
|
||||||
|
"ua": [
|
||||||
|
50
|
||||||
|
],
|
||||||
|
"uo": [
|
||||||
|
51
|
||||||
|
],
|
||||||
|
"uai": [
|
||||||
|
52
|
||||||
|
],
|
||||||
|
"ui": [
|
||||||
|
53
|
||||||
|
],
|
||||||
|
"uan": [
|
||||||
|
54
|
||||||
|
],
|
||||||
|
"un": [
|
||||||
|
55
|
||||||
|
],
|
||||||
|
"uang": [
|
||||||
|
56
|
||||||
|
],
|
||||||
|
"ueng": [
|
||||||
|
57
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
58
|
||||||
|
],
|
||||||
|
"ve": [
|
||||||
|
59
|
||||||
|
],
|
||||||
|
"van": [
|
||||||
|
60
|
||||||
|
],
|
||||||
|
"vn": [
|
||||||
|
61
|
||||||
|
],
|
||||||
|
"er": [
|
||||||
|
62
|
||||||
|
],
|
||||||
|
"ue": [
|
||||||
|
63
|
||||||
|
],
|
||||||
|
"1": [
|
||||||
|
64
|
||||||
|
],
|
||||||
|
"2": [
|
||||||
|
65
|
||||||
|
],
|
||||||
|
"3": [
|
||||||
|
66
|
||||||
|
],
|
||||||
|
"4": [
|
||||||
|
67
|
||||||
|
],
|
||||||
|
"5": [
|
||||||
|
68
|
||||||
|
],
|
||||||
|
"。": [
|
||||||
|
69
|
||||||
|
],
|
||||||
|
".": [
|
||||||
|
69
|
||||||
|
],
|
||||||
|
"?": [
|
||||||
|
70
|
||||||
|
],
|
||||||
|
"?": [
|
||||||
|
70
|
||||||
|
],
|
||||||
|
"!": [
|
||||||
|
71
|
||||||
|
],
|
||||||
|
"!": [
|
||||||
|
71
|
||||||
|
],
|
||||||
|
"—": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
"…": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
"、": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
",": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
",": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
":": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
":": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
";": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
";": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
" ": [
|
||||||
|
72
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"speaker_id_map": {},
|
||||||
|
"hop_length": 256,
|
||||||
|
"piper_version": "1.3.0",
|
||||||
|
"language": {
|
||||||
|
"code": "zh_CN",
|
||||||
|
"family": "zh",
|
||||||
|
"region": "CN",
|
||||||
|
"name_native": "简体中文",
|
||||||
|
"name_english": "Chinese",
|
||||||
|
"country_english": "China"
|
||||||
|
},
|
||||||
|
"dataset": "xiao_ya"
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.ttstd.dialer.activity.alarm;
|
package com.ttstd.dialer.activity.alarm;
|
||||||
|
|
||||||
|
import android.app.KeyguardManager;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.media.AudioAttributes;
|
import android.media.AudioAttributes;
|
||||||
import android.media.Ringtone;
|
import android.media.Ringtone;
|
||||||
import android.media.RingtoneManager;
|
import android.media.RingtoneManager;
|
||||||
@@ -8,14 +12,18 @@ import android.os.Build;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import com.ttstd.dialer.R;
|
import com.ttstd.dialer.R;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||||
|
import com.zackratos.ultimatebarx.ultimatebarx.java.UltimateBarX;
|
||||||
|
|
||||||
public class AlarmAlertActivity extends AppCompatActivity {
|
public class AlarmAlertActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private Ringtone ringtone;
|
private Ringtone ringtone;
|
||||||
|
private int alarmId = -1;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -25,14 +33,44 @@ public class AlarmAlertActivity extends AppCompatActivity {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
setShowWhenLocked(true);
|
setShowWhenLocked(true);
|
||||||
setTurnScreenOn(true);
|
setTurnScreenOn(true);
|
||||||
|
KeyguardManager km = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
|
||||||
|
if (km != null) {
|
||||||
|
km.requestDismissKeyguard(this, null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||||
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||||
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||||
|
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UltimateBarX.statusBar(this)
|
||||||
|
.transparent()
|
||||||
|
.light(false)
|
||||||
|
.apply();
|
||||||
|
UltimateBarX.navigationBar(this)
|
||||||
|
.transparent()
|
||||||
|
.light(false)
|
||||||
|
.apply();
|
||||||
|
|
||||||
setContentView(R.layout.activity_alarm_alert);
|
setContentView(R.layout.activity_alarm_alert);
|
||||||
|
|
||||||
|
AlarmInfo alarmInfo;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
alarmInfo = getIntent().getParcelableExtra("AlarmInfo", AlarmInfo.class);
|
||||||
|
} else {
|
||||||
|
alarmInfo = getIntent().getParcelableExtra("AlarmInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alarmInfo != null) {
|
||||||
|
alarmId = alarmInfo.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView tvLabel = findViewById(R.id.tv_alarm_label);
|
||||||
|
if (alarmInfo != null && alarmInfo.getLabel() != null && !alarmInfo.getLabel().isEmpty()) {
|
||||||
|
tvLabel.setText(alarmInfo.getLabel());
|
||||||
|
}
|
||||||
|
|
||||||
Button btnStop = findViewById(R.id.btn_stop_alarm);
|
Button btnStop = findViewById(R.id.btn_stop_alarm);
|
||||||
btnStop.setOnClickListener(v -> {
|
btnStop.setOnClickListener(v -> {
|
||||||
if (ringtone != null && ringtone.isPlaying()) {
|
if (ringtone != null && ringtone.isPlaying()) {
|
||||||
@@ -41,11 +79,18 @@ public class AlarmAlertActivity extends AppCompatActivity {
|
|||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
|
|
||||||
playAlarmRingtone();
|
playAlarmRingtone(alarmInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void playAlarmRingtone() {
|
private void playAlarmRingtone(AlarmInfo alarmInfo) {
|
||||||
Uri alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
|
Uri alarmUri = null;
|
||||||
|
if (alarmInfo != null && alarmInfo.getRingtoneUri() != null) {
|
||||||
|
alarmUri = Uri.parse(alarmInfo.getRingtoneUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alarmUri == null) {
|
||||||
|
alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
|
||||||
|
}
|
||||||
if (alarmUri == null) {
|
if (alarmUri == null) {
|
||||||
alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
|
alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
|
||||||
}
|
}
|
||||||
@@ -63,11 +108,48 @@ public class AlarmAlertActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
|
||||||
|
AlarmInfo alarmInfo;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
alarmInfo = intent.getParcelableExtra("AlarmInfo", AlarmInfo.class);
|
||||||
|
} else {
|
||||||
|
alarmInfo = intent.getParcelableExtra("AlarmInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alarmInfo != null) {
|
||||||
|
alarmId = alarmInfo.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView tvLabel = findViewById(R.id.tv_alarm_label);
|
||||||
|
if (alarmInfo != null && alarmInfo.getLabel() != null && !alarmInfo.getLabel().isEmpty()) {
|
||||||
|
tvLabel.setText(alarmInfo.getLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ringtone != null && ringtone.isPlaying()) {
|
||||||
|
ringtone.stop();
|
||||||
|
}
|
||||||
|
playAlarmRingtone(alarmInfo);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (ringtone != null && ringtone.isPlaying()) {
|
if (ringtone != null && ringtone.isPlaying()) {
|
||||||
ringtone.stop();
|
ringtone.stop();
|
||||||
}
|
}
|
||||||
|
cancelNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelNotification() {
|
||||||
|
if (alarmId != -1) {
|
||||||
|
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
if (notificationManager != null) {
|
||||||
|
notificationManager.cancel(alarmId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.ttstd.dialer.activity.alarm.add;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.google.android.material.timepicker.MaterialTimePicker;
|
||||||
|
import com.google.android.material.timepicker.TimeFormat;
|
||||||
|
import com.kongzue.dialogx.dialogs.TipDialog;
|
||||||
|
import com.kongzue.dialogx.dialogs.WaitDialog;
|
||||||
|
import com.ttstd.dialer.R;
|
||||||
|
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
||||||
|
import com.ttstd.dialer.databinding.ActivityAlarmAddBinding;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
public class AlarmAddActivity extends BaseMvvmActivity<AlarmAddViewModel, ActivityAlarmAddBinding> {
|
||||||
|
|
||||||
|
private static final String TAG = "AlarmAddActivity";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setNightMode() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setfitWindow() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayoutId() {
|
||||||
|
return R.layout.activity_alarm_add;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initDataBinding() {
|
||||||
|
mViewModel.setContext(this);
|
||||||
|
mViewModel.setVDBinding(mViewDataBinding);
|
||||||
|
mViewModel.setLifecycle(getLifecycleSubject());
|
||||||
|
mViewDataBinding.setClick(new BtnClick());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initView() {
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
mViewDataBinding.timePicker.setHour(calendar.get(Calendar.HOUR_OF_DAY));
|
||||||
|
mViewDataBinding.timePicker.setMinute(calendar.get(Calendar.MINUTE));
|
||||||
|
mViewDataBinding.timePicker.setIs24HourView(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initData() {
|
||||||
|
mViewModel.mSaveResult.observe(this, success -> {
|
||||||
|
if (success) {
|
||||||
|
TipDialog.show("添加成功", WaitDialog.TYPE.SUCCESS);
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
TipDialog.show("添加失败", WaitDialog.TYPE.ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BtnClick {
|
||||||
|
public void exit(View view) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveAlarm(View view) {
|
||||||
|
int hour = mViewDataBinding.timePicker.getHour();
|
||||||
|
int minute = mViewDataBinding.timePicker.getMinute();
|
||||||
|
String label = mViewDataBinding.etLabel.getText().toString();
|
||||||
|
boolean vibration = mViewDataBinding.switchVibration.isChecked();
|
||||||
|
int repeatType = com.ttstd.dialer.alarmclock.AlarmRepeatConfig.REPEAT_EVERYDAY;
|
||||||
|
if (mViewDataBinding.rbOnce.isChecked()) {
|
||||||
|
repeatType = com.ttstd.dialer.alarmclock.AlarmRepeatConfig.REPEAT_ONCE;
|
||||||
|
} else if (mViewDataBinding.rbEveryday.isChecked()) {
|
||||||
|
repeatType = com.ttstd.dialer.alarmclock.AlarmRepeatConfig.REPEAT_EVERYDAY;
|
||||||
|
} else if (mViewDataBinding.rbWeekdays.isChecked()) {
|
||||||
|
repeatType = com.ttstd.dialer.alarmclock.AlarmRepeatConfig.REPEAT_WEEKDAYS;
|
||||||
|
} else if (mViewDataBinding.rbCustom.isChecked()) {
|
||||||
|
repeatType = com.ttstd.dialer.alarmclock.AlarmRepeatConfig.REPEAT_CUSTOM;
|
||||||
|
}
|
||||||
|
mViewModel.saveAlarm(hour, minute, label, vibration, repeatType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showTimePicker(View view) {
|
||||||
|
MaterialTimePicker picker = new MaterialTimePicker.Builder()
|
||||||
|
.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
|
||||||
|
.setTimeFormat(TimeFormat.CLOCK_24H)
|
||||||
|
.setHour(mViewDataBinding.timePicker.getHour())
|
||||||
|
.setMinute(mViewDataBinding.timePicker.getMinute())
|
||||||
|
.setTitleText("选择时间")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
picker.addOnPositiveButtonClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
mViewDataBinding.timePicker.setHour(picker.getHour());
|
||||||
|
mViewDataBinding.timePicker.setMinute(picker.getMinute());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
picker.show(getSupportFragmentManager(), "time_picker");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.ttstd.dialer.activity.alarm.add;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
|
import com.trello.rxlifecycle4.RxLifecycle;
|
||||||
|
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||||
|
import com.ttstd.dialer.alarmclock.AlarmManagerHelper;
|
||||||
|
import com.ttstd.dialer.base.mvvm.BaseViewModel;
|
||||||
|
import com.ttstd.dialer.databinding.ActivityAlarmAddBinding;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmRepository;
|
||||||
|
import com.ttstd.dialer.utils.Logger;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.annotations.NonNull;
|
||||||
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
import io.reactivex.rxjava3.core.SingleObserver;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
public class AlarmAddViewModel extends BaseViewModel<ActivityAlarmAddBinding, ActivityEvent> {
|
||||||
|
private static final String TAG = "AlarmAddViewModel";
|
||||||
|
private AlarmRepository mRepository;
|
||||||
|
|
||||||
|
private int mVolume = 80;
|
||||||
|
private int mSnoozeInterval = 5;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContext(Context context) {
|
||||||
|
super.setContext(context);
|
||||||
|
mRepository = new AlarmRepository(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<Boolean> mSaveResult = new MutableLiveData<>();
|
||||||
|
|
||||||
|
public void saveAlarm(int hour, int minute, String label, boolean vibration, int repeatType) {
|
||||||
|
AlarmInfo alarmInfo = new AlarmInfo();
|
||||||
|
alarmInfo.setHour(hour);
|
||||||
|
alarmInfo.setMinute(minute);
|
||||||
|
alarmInfo.setLabel(label);
|
||||||
|
alarmInfo.setRepeatType(repeatType);
|
||||||
|
alarmInfo.setEnabled(true);
|
||||||
|
alarmInfo.setVibration(vibration);
|
||||||
|
alarmInfo.setVolume(mVolume);
|
||||||
|
alarmInfo.setSnoozeInterval(mSnoozeInterval);
|
||||||
|
|
||||||
|
Single.fromCallable(() -> mRepository.insertAlarm(alarmInfo))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
|
||||||
|
.subscribe(new SingleObserver<Long>() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(@NonNull Disposable d) {
|
||||||
|
Logger.d(TAG, "开始保存闹钟");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Long aLong) {
|
||||||
|
Logger.d(TAG, "闹钟保存成功,ID: " + aLong);
|
||||||
|
alarmInfo.setId(aLong.intValue());
|
||||||
|
|
||||||
|
// 计算并设置系统闹钟
|
||||||
|
long nextTime = AlarmManagerHelper.calculateNextTime(alarmInfo);
|
||||||
|
alarmInfo.setNextTriggerTime(nextTime);
|
||||||
|
|
||||||
|
// 更新数据库中的下次触发时间
|
||||||
|
Completable.fromAction(() -> mRepository.updateNextTriggerTime(alarmInfo.getId(), nextTime))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe(() -> {
|
||||||
|
AlarmManagerHelper.setExactAlarm(getSafeContext(), alarmInfo.getId(), nextTime);
|
||||||
|
mSaveResult.postValue(true);
|
||||||
|
}, throwable -> {
|
||||||
|
Logger.e(TAG, "更新下次触发时间失败: " + throwable.getMessage());
|
||||||
|
mSaveResult.postValue(true); // 依然认为保存成功,只是系统闹钟可能没设好
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull Throwable e) {
|
||||||
|
Logger.e(TAG, "闹钟保存失败: " + e.getMessage());
|
||||||
|
mSaveResult.setValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.ttstd.dialer.activity.alarm.edit;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.kongzue.dialogx.dialogs.TipDialog;
|
||||||
|
import com.kongzue.dialogx.dialogs.WaitDialog;
|
||||||
|
import com.ttstd.dialer.R;
|
||||||
|
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
||||||
|
import com.ttstd.dialer.databinding.ActivityAlarmEditBinding;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||||
|
|
||||||
|
public class AlarmEditActivity extends BaseMvvmActivity<AlarmEditViewModel, ActivityAlarmEditBinding> {
|
||||||
|
|
||||||
|
private static final String TAG = "AlarmEditActivity";
|
||||||
|
private AlarmInfo mAlarmInfo;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setNightMode() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setfitWindow() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayoutId() {
|
||||||
|
return R.layout.activity_alarm_edit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initDataBinding() {
|
||||||
|
mViewModel.setContext(this);
|
||||||
|
mViewModel.setVDBinding(mViewDataBinding);
|
||||||
|
mViewModel.setLifecycle(getLifecycleSubject());
|
||||||
|
mViewDataBinding.setClick(new BtnClick());
|
||||||
|
|
||||||
|
mAlarmInfo = (AlarmInfo) getIntent().getSerializableExtra("AlarmInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initView() {
|
||||||
|
mViewDataBinding.timePicker.setIs24HourView(true);
|
||||||
|
|
||||||
|
if (mAlarmInfo != null) {
|
||||||
|
mViewModel.loadAlarmInfo(mAlarmInfo);
|
||||||
|
mViewDataBinding.timePicker.setHour(mAlarmInfo.getHour());
|
||||||
|
mViewDataBinding.timePicker.setMinute(mAlarmInfo.getMinute());
|
||||||
|
mViewDataBinding.etLabel.setText(mAlarmInfo.getLabel() != null ? mAlarmInfo.getLabel() : "");
|
||||||
|
mViewDataBinding.switchEnabled.setChecked(mAlarmInfo.isEnabled());
|
||||||
|
mViewDataBinding.switchVibration.setChecked(mAlarmInfo.isVibration());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initData() {
|
||||||
|
mViewModel.mUpdateResult.observe(this, success -> {
|
||||||
|
if (success) {
|
||||||
|
TipDialog.show("保存成功", WaitDialog.TYPE.SUCCESS);
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
TipDialog.show("保存失败", WaitDialog.TYPE.ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mViewModel.mDeleteResult.observe(this, success -> {
|
||||||
|
if (success) {
|
||||||
|
TipDialog.show("删除成功", WaitDialog.TYPE.SUCCESS);
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
TipDialog.show("删除失败", WaitDialog.TYPE.ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BtnClick {
|
||||||
|
public void exit(View view) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveAlarm(View view) {
|
||||||
|
if (mAlarmInfo != null) {
|
||||||
|
int hour = mViewDataBinding.timePicker.getHour();
|
||||||
|
int minute = mViewDataBinding.timePicker.getMinute();
|
||||||
|
String label = mViewDataBinding.etLabel.getText().toString();
|
||||||
|
boolean enabled = mViewDataBinding.switchEnabled.isChecked();
|
||||||
|
boolean vibration = mViewDataBinding.switchVibration.isChecked();
|
||||||
|
mViewModel.updateAlarm(hour, minute, label, enabled, vibration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAlarm(View view) {
|
||||||
|
if (mAlarmInfo != null) {
|
||||||
|
mViewModel.deleteAlarm(mAlarmInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package com.ttstd.dialer.activity.alarm.edit;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
|
import com.trello.rxlifecycle4.RxLifecycle;
|
||||||
|
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||||
|
import com.ttstd.dialer.alarmclock.AlarmManagerHelper;
|
||||||
|
import com.ttstd.dialer.base.mvvm.BaseViewModel;
|
||||||
|
import com.ttstd.dialer.databinding.ActivityAlarmEditBinding;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmRepository;
|
||||||
|
import com.ttstd.dialer.utils.Logger;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.annotations.NonNull;
|
||||||
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
|
import io.reactivex.rxjava3.core.CompletableObserver;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
public class AlarmEditViewModel extends BaseViewModel<ActivityAlarmEditBinding, ActivityEvent> {
|
||||||
|
private static final String TAG = "AlarmEditViewModel";
|
||||||
|
private AlarmRepository mRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContext(Context context) {
|
||||||
|
super.setContext(context);
|
||||||
|
mRepository = new AlarmRepository(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<Boolean> mUpdateResult = new MutableLiveData<>();
|
||||||
|
public MutableLiveData<Boolean> mDeleteResult = new MutableLiveData<>();
|
||||||
|
|
||||||
|
private AlarmInfo mOriginalAlarm;
|
||||||
|
|
||||||
|
public void loadAlarmInfo(AlarmInfo alarmInfo) {
|
||||||
|
this.mOriginalAlarm = alarmInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAlarm(int hour, int minute, String label, boolean enabled, boolean vibration) {
|
||||||
|
if (mOriginalAlarm == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlarmInfo alarmInfo = mOriginalAlarm;
|
||||||
|
alarmInfo.setHour(hour);
|
||||||
|
alarmInfo.setMinute(minute);
|
||||||
|
alarmInfo.setLabel(label);
|
||||||
|
alarmInfo.setEnabled(enabled);
|
||||||
|
alarmInfo.setVibration(vibration);
|
||||||
|
|
||||||
|
// 如果启用,计算下次触发时间
|
||||||
|
if (alarmInfo.isEnabled()) {
|
||||||
|
long nextTime = AlarmManagerHelper.calculateNextTime(alarmInfo);
|
||||||
|
alarmInfo.setNextTriggerTime(nextTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
Completable.fromAction(() -> mRepository.updateAlarm(alarmInfo))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
|
||||||
|
.subscribe(new CompletableObserver() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(@NonNull Disposable d) {
|
||||||
|
Logger.d(TAG, "开始更新闹钟");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull Throwable e) {
|
||||||
|
Logger.e(TAG, "闹钟更新失败: " + e.getMessage());
|
||||||
|
mUpdateResult.setValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
Logger.d(TAG, "更新闹钟完成");
|
||||||
|
if (alarmInfo.isEnabled()) {
|
||||||
|
AlarmManagerHelper.setExactAlarm(getSafeContext(), alarmInfo.getId(), alarmInfo.getNextTriggerTime());
|
||||||
|
} else {
|
||||||
|
AlarmManagerHelper.cancelAlarm(getSafeContext(), alarmInfo.getId());
|
||||||
|
}
|
||||||
|
mUpdateResult.setValue(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAlarm(AlarmInfo alarmInfo) {
|
||||||
|
Completable.fromAction(() -> mRepository.deleteAlarm(alarmInfo))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
|
||||||
|
.subscribe(new CompletableObserver() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(@NonNull Disposable d) {
|
||||||
|
Logger.d(TAG, "开始删除闹钟");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull Throwable e) {
|
||||||
|
Logger.e(TAG, "闹钟删除失败: " + e.getMessage());
|
||||||
|
mDeleteResult.setValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
Logger.d(TAG, "删除闹钟完成");
|
||||||
|
AlarmManagerHelper.cancelAlarm(getSafeContext(), alarmInfo.getId());
|
||||||
|
mDeleteResult.setValue(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package com.ttstd.dialer.activity.alarm.list;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
|
import com.kongzue.dialogx.dialogs.MessageDialog;
|
||||||
|
import com.kongzue.dialogx.dialogs.TipDialog;
|
||||||
|
import com.kongzue.dialogx.dialogs.WaitDialog;
|
||||||
|
import com.ttstd.dialer.R;
|
||||||
|
import com.ttstd.dialer.activity.alarm.add.AlarmAddActivity;
|
||||||
|
import com.ttstd.dialer.activity.alarm.edit.AlarmEditActivity;
|
||||||
|
import com.ttstd.dialer.adapter.AlarmInfoAdapter;
|
||||||
|
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
||||||
|
import com.ttstd.dialer.databinding.ActivityAlarmListBinding;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||||
|
import com.ttstd.dialer.view.ListDividerItemDecoration;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AlarmListActivity extends BaseMvvmActivity<AlarmListViewModel, ActivityAlarmListBinding> {
|
||||||
|
|
||||||
|
private static final String TAG = "AlarmListActivity";
|
||||||
|
|
||||||
|
private AlarmInfoAdapter mAlarmInfoAdapter;
|
||||||
|
private List<AlarmInfo> mAlarmInfos;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setNightMode() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setfitWindow() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getLayoutId() {
|
||||||
|
return R.layout.activity_alarm_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initDataBinding() {
|
||||||
|
mViewModel.setContext(this);
|
||||||
|
mViewModel.setVDBinding(mViewDataBinding);
|
||||||
|
mViewModel.setLifecycle(getLifecycleSubject());
|
||||||
|
mViewDataBinding.setClick(new BtnClick());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initView() {
|
||||||
|
mViewDataBinding.swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
|
||||||
|
@Override
|
||||||
|
public void onRefresh() {
|
||||||
|
mViewDataBinding.swipeRefreshLayout.setRefreshing(true);
|
||||||
|
mViewModel.getAllAlarms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mAlarmInfoAdapter = new AlarmInfoAdapter();
|
||||||
|
mAlarmInfoAdapter.setOnClickListener(new AlarmInfoAdapter.ClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(AlarmInfo alarmInfo) {
|
||||||
|
Intent intent = new Intent(AlarmListActivity.this, AlarmEditActivity.class);
|
||||||
|
intent.putExtra("AlarmInfo", (Serializable) alarmInfo);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick(AlarmInfo alarmInfo) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMoreOperationClick(AlarmInfo alarmInfo) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeleteClick(AlarmInfo alarmInfo) {
|
||||||
|
MessageDialog.show("删除闹钟", "确定要删除该闹钟吗?", "确定", "取消")
|
||||||
|
.setOkButton((dialog, v) -> {
|
||||||
|
mViewModel.deleteAlarm(alarmInfo);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnabledClick(AlarmInfo alarmInfo, boolean enabled) {
|
||||||
|
mViewModel.updateAlarmEnabled(alarmInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
|
||||||
|
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||||
|
mViewDataBinding.recyclerView.setLayoutManager(linearLayoutManager);
|
||||||
|
mViewDataBinding.recyclerView.setAdapter(mAlarmInfoAdapter);
|
||||||
|
mViewDataBinding.recyclerView.addItemDecoration(new ListDividerItemDecoration(this, 16));
|
||||||
|
|
||||||
|
mViewDataBinding.recyclerView.addOnScrollListener(new androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(androidx.recyclerview.widget.RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
mViewDataBinding.swipeRefreshLayout.setEnabled(recyclerView.getChildCount() == 0 || recyclerView.getChildAt(0).getTop() >= 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initData() {
|
||||||
|
mViewModel.mAlarmListData.observe(this, new Observer<List<AlarmInfo>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<AlarmInfo> alarmInfos) {
|
||||||
|
mViewDataBinding.swipeRefreshLayout.setRefreshing(false);
|
||||||
|
if (alarmInfos != null) {
|
||||||
|
mAlarmInfos = alarmInfos;
|
||||||
|
mAlarmInfoAdapter.setAlarmInfos(mAlarmInfos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mViewModel.mDeleteLiveData.observe(this, new Observer<Boolean>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(Boolean success) {
|
||||||
|
if (success) {
|
||||||
|
TipDialog.show("删除成功", WaitDialog.TYPE.SUCCESS);
|
||||||
|
mViewModel.getAllAlarms();
|
||||||
|
} else {
|
||||||
|
TipDialog.show("删除失败", WaitDialog.TYPE.ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
mViewModel.getAllAlarms();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BtnClick {
|
||||||
|
public void exit(View view) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAlarm(View view) {
|
||||||
|
startActivity(new Intent(AlarmListActivity.this, AlarmAddActivity.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.ttstd.dialer.activity.alarm.list;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
|
import com.trello.rxlifecycle4.RxLifecycle;
|
||||||
|
import com.trello.rxlifecycle4.android.ActivityEvent;
|
||||||
|
import com.ttstd.dialer.alarmclock.AlarmManagerHelper;
|
||||||
|
import com.ttstd.dialer.base.mvvm.BaseViewModel;
|
||||||
|
import com.ttstd.dialer.databinding.ActivityAlarmListBinding;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmRepository;
|
||||||
|
import com.ttstd.dialer.utils.Logger;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.annotations.NonNull;
|
||||||
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
|
import io.reactivex.rxjava3.core.CompletableObserver;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
import io.reactivex.rxjava3.core.SingleObserver;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
public class AlarmListViewModel extends BaseViewModel<ActivityAlarmListBinding, ActivityEvent> {
|
||||||
|
private static final String TAG = "AlarmListViewModel";
|
||||||
|
private AlarmRepository mRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContext(Context context) {
|
||||||
|
super.setContext(context);
|
||||||
|
mRepository = new AlarmRepository(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<List<AlarmInfo>> mAlarmListData = new MutableLiveData<>();
|
||||||
|
|
||||||
|
public void getAllAlarms() {
|
||||||
|
Single.fromCallable(() -> mRepository.getAllAlarms())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
|
||||||
|
.subscribe(new SingleObserver<List<AlarmInfo>>() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(@NonNull Disposable d) {
|
||||||
|
Logger.d(TAG, "开始加载闹钟列表");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<AlarmInfo> alarmInfos) {
|
||||||
|
Logger.d(TAG, "加载闹钟列表成功,数量: " + alarmInfos.size());
|
||||||
|
mAlarmListData.setValue(alarmInfos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull Throwable e) {
|
||||||
|
Logger.e(TAG, "加载闹钟列表失败: " + e.getMessage());
|
||||||
|
mAlarmListData.setValue(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAlarmEnabled(AlarmInfo alarmInfo) {
|
||||||
|
Completable.fromAction(() -> {
|
||||||
|
mRepository.updateAlarmEnabled(alarmInfo.getId(), alarmInfo.isEnabled());
|
||||||
|
if (alarmInfo.isEnabled()) {
|
||||||
|
long nextTime = AlarmManagerHelper.calculateNextTime(alarmInfo);
|
||||||
|
alarmInfo.setNextTriggerTime(nextTime);
|
||||||
|
mRepository.updateNextTriggerTime(alarmInfo.getId(), nextTime);
|
||||||
|
AlarmManagerHelper.setExactAlarm(getSafeContext(), alarmInfo.getId(), nextTime);
|
||||||
|
Logger.d(TAG, "updateAlarmEnabled: 闹钟状态开启");
|
||||||
|
} else {
|
||||||
|
AlarmManagerHelper.cancelAlarm(getSafeContext(), alarmInfo.getId());
|
||||||
|
Logger.d(TAG, "updateAlarmEnabled: 闹钟状态关闭");
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
|
||||||
|
.subscribe(new CompletableObserver() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(@NonNull Disposable d) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
Logger.d(TAG, "updateAlarmEnabled: 更新闹钟状态成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull Throwable e) {
|
||||||
|
Logger.e(TAG, "updateAlarmEnabled: 更新闹钟状态失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<Boolean> mDeleteLiveData = new MutableLiveData<>();
|
||||||
|
|
||||||
|
public void deleteAlarm(AlarmInfo alarmInfo) {
|
||||||
|
Completable.fromAction(() -> mRepository.deleteAlarm(alarmInfo))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
|
||||||
|
.subscribe(new CompletableObserver() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(@NonNull Disposable d) {
|
||||||
|
Logger.d(TAG, "开始删除闹钟");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull Throwable e) {
|
||||||
|
Logger.e(TAG, "删除闹钟失败: " + e.getMessage());
|
||||||
|
mDeleteLiveData.setValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
Logger.d(TAG, "删除闹钟完成");
|
||||||
|
AlarmManagerHelper.cancelAlarm(getSafeContext(), alarmInfo.getId());
|
||||||
|
mDeleteLiveData.setValue(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,12 @@ import androidx.browser.customtabs.CustomTabsIntent;
|
|||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.ttstd.dialer.R;
|
import com.ttstd.dialer.R;
|
||||||
|
import com.ttstd.dialer.activity.alarm.list.AlarmListActivity;
|
||||||
import com.ttstd.dialer.activity.settings.call.SettingsCallActivity;
|
import com.ttstd.dialer.activity.settings.call.SettingsCallActivity;
|
||||||
import com.ttstd.dialer.activity.settings.utils.SettingsUtilsActivity;
|
import com.ttstd.dialer.activity.settings.utils.SettingsUtilsActivity;
|
||||||
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
||||||
import com.ttstd.dialer.databinding.ActivitySettingsBinding;
|
import com.ttstd.dialer.databinding.ActivitySettingsBinding;
|
||||||
|
import com.ttstd.dialer.tts.sherpa_onnx.SherpaOnnxTtsActivity;
|
||||||
|
|
||||||
public class SettingsActivity extends BaseMvvmActivity<SettingsViewModel, ActivitySettingsBinding> {
|
public class SettingsActivity extends BaseMvvmActivity<SettingsViewModel, ActivitySettingsBinding> {
|
||||||
private static final String TAG = "SettingsActivity";
|
private static final String TAG = "SettingsActivity";
|
||||||
@@ -63,6 +65,14 @@ public class SettingsActivity extends BaseMvvmActivity<SettingsViewModel, Activi
|
|||||||
startActivity(new Intent(SettingsActivity.this, SettingsUtilsActivity.class));
|
startActivity(new Intent(SettingsActivity.this, SettingsUtilsActivity.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void openTts(View view) {
|
||||||
|
startActivity(new Intent(SettingsActivity.this, SherpaOnnxTtsActivity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openAlarmClock(View view) {
|
||||||
|
startActivity(new Intent(SettingsActivity.this, AlarmListActivity.class));
|
||||||
|
}
|
||||||
|
|
||||||
public void aboutUs(View view) {
|
public void aboutUs(View view) {
|
||||||
String url = "https://www.ttstd.com";
|
String url = "https://www.ttstd.com";
|
||||||
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
||||||
|
|||||||
@@ -107,6 +107,19 @@ public class SettingsUtilsActivity extends BaseMvvmActivity<SettingsUtilsViewMod
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mViewDataBinding.siHourlyChime.setOnToggleChanged(new ToggleButton.OnToggleChanged() {
|
||||||
|
@Override
|
||||||
|
public void onToggle(boolean on) {
|
||||||
|
if (on) {
|
||||||
|
mMMKV.encode(CommonConfig.HOURLY_CHIME_ENABLE, 1);
|
||||||
|
com.ttstd.dialer.receiver.HourlyChimeManager.startChime(SettingsUtilsActivity.this);
|
||||||
|
} else {
|
||||||
|
mMMKV.encode(CommonConfig.HOURLY_CHIME_ENABLE, 0);
|
||||||
|
com.ttstd.dialer.receiver.HourlyChimeManager.stopChime(SettingsUtilsActivity.this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -128,6 +141,10 @@ public class SettingsUtilsActivity extends BaseMvvmActivity<SettingsUtilsViewMod
|
|||||||
boolean defaultLauncher = SystemUtils.isDefaultLauncher(SettingsUtilsActivity.this, MainActivity.class);
|
boolean defaultLauncher = SystemUtils.isDefaultLauncher(SettingsUtilsActivity.this, MainActivity.class);
|
||||||
Logger.e(TAG, "initView: defaultLauncher = " + defaultLauncher);
|
Logger.e(TAG, "initView: defaultLauncher = " + defaultLauncher);
|
||||||
mViewDataBinding.siDefaultLauncher.setToggleStatu(defaultLauncher);
|
mViewDataBinding.siDefaultLauncher.setToggleStatu(defaultLauncher);
|
||||||
|
|
||||||
|
int enableHourlyChime = mMMKV.decodeInt(CommonConfig.HOURLY_CHIME_ENABLE, 0);
|
||||||
|
Logger.e(TAG, "initView: enableHourlyChime = " + enableHourlyChime);
|
||||||
|
mViewDataBinding.siHourlyChime.setToggleStatu(enableHourlyChime == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BtnClick {
|
public class BtnClick {
|
||||||
@@ -200,7 +217,7 @@ public class SettingsUtilsActivity extends BaseMvvmActivity<SettingsUtilsViewMod
|
|||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cameraUtil.startTakePicture(getExternalCacheDir().getAbsolutePath() + File.separator + createTime + ".jpg");
|
cameraUtil.startTakePicture(getExternalCacheDir().getAbsolutePath() + "cameracapture_" + File.separator + createTime + ".jpg");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
148
app/src/main/java/com/ttstd/dialer/adapter/AlarmInfoAdapter.java
Normal file
148
app/src/main/java/com/ttstd/dialer/adapter/AlarmInfoAdapter.java
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package com.ttstd.dialer.adapter;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.HorizontalScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.ttstd.dialer.R;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AlarmInfoAdapter extends RecyclerView.Adapter<AlarmInfoAdapter.AlarmInfoHolder> {
|
||||||
|
private static final String TAG = "AlarmInfoAdapter";
|
||||||
|
|
||||||
|
private FragmentActivity mContext;
|
||||||
|
private List<AlarmInfo> mAlarmInfos;
|
||||||
|
|
||||||
|
public void setAlarmInfos(List<AlarmInfo> alarmInfos) {
|
||||||
|
mAlarmInfos = alarmInfos;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ClickListener {
|
||||||
|
void onClick(AlarmInfo alarmInfo);
|
||||||
|
|
||||||
|
void onLongClick(AlarmInfo alarmInfo);
|
||||||
|
|
||||||
|
void onMoreOperationClick(AlarmInfo alarmInfo);
|
||||||
|
|
||||||
|
void onDeleteClick(AlarmInfo alarmInfo);
|
||||||
|
|
||||||
|
void onEnabledClick(AlarmInfo alarmInfo, boolean enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClickListener mClickListener;
|
||||||
|
|
||||||
|
public void setOnClickListener(ClickListener clickListener) {
|
||||||
|
mClickListener = clickListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public AlarmInfoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
mContext = (FragmentActivity) parent.getContext();
|
||||||
|
return new AlarmInfoHolder(LayoutInflater.from(mContext).inflate(R.layout.item_alarm, parent, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull AlarmInfoHolder holder, int position) {
|
||||||
|
AlarmInfo alarmInfo = mAlarmInfos.get(position);
|
||||||
|
|
||||||
|
// 设置内容区域宽度为屏幕宽度,确保删除按钮在屏幕外
|
||||||
|
holder.root.getLayoutParams().width = mContext.getResources().getDisplayMetrics().widthPixels;
|
||||||
|
holder.hsv.scrollTo(0, 0); // 重置滚动位置
|
||||||
|
|
||||||
|
String time = String.format("%02d:%02d", alarmInfo.getHour(), alarmInfo.getMinute());
|
||||||
|
holder.tvTime.setText(time);
|
||||||
|
|
||||||
|
if (alarmInfo.getLabel() != null && !alarmInfo.getLabel().isEmpty()) {
|
||||||
|
holder.tvLabel.setText(alarmInfo.getLabel());
|
||||||
|
} else {
|
||||||
|
holder.tvLabel.setText("闹钟");
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.tvRepeat.setText(getRepeatText(alarmInfo));
|
||||||
|
holder.switchEnabled.setOnCheckedChangeListener(null);
|
||||||
|
holder.switchEnabled.setChecked(alarmInfo.isEnabled());
|
||||||
|
|
||||||
|
holder.root.setOnClickListener(v -> {
|
||||||
|
if (mClickListener != null) {
|
||||||
|
mClickListener.onClick(alarmInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.root.setOnLongClickListener(v -> {
|
||||||
|
if (mClickListener != null) {
|
||||||
|
mClickListener.onLongClick(alarmInfo);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.clOperation.setOnClickListener(v -> {
|
||||||
|
if (mClickListener != null) {
|
||||||
|
mClickListener.onMoreOperationClick(alarmInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.llDelete.setOnClickListener(v -> {
|
||||||
|
if (mClickListener != null) {
|
||||||
|
mClickListener.onDeleteClick(alarmInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.switchEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
|
alarmInfo.setEnabled(isChecked);
|
||||||
|
if (mClickListener != null) {
|
||||||
|
mClickListener.onEnabledClick(alarmInfo, isChecked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRepeatText(AlarmInfo alarmInfo) {
|
||||||
|
switch (alarmInfo.getRepeatType()) {
|
||||||
|
case 0:
|
||||||
|
return "只响一次";
|
||||||
|
case 1:
|
||||||
|
return "每天";
|
||||||
|
case 2:
|
||||||
|
return "周一至周五";
|
||||||
|
case 3:
|
||||||
|
return "自定义";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return mAlarmInfos == null ? 0 : mAlarmInfos.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AlarmInfoHolder extends RecyclerView.ViewHolder {
|
||||||
|
public ConstraintLayout root, clOperation;
|
||||||
|
public View llDelete;
|
||||||
|
public HorizontalScrollView hsv;
|
||||||
|
public TextView tvTime, tvLabel, tvRepeat;
|
||||||
|
public androidx.appcompat.widget.SwitchCompat switchEnabled;
|
||||||
|
|
||||||
|
public AlarmInfoHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
hsv = itemView.findViewById(R.id.hsv);
|
||||||
|
root = itemView.findViewById(R.id.root);
|
||||||
|
clOperation = itemView.findViewById(R.id.cl_operation);
|
||||||
|
llDelete = itemView.findViewById(R.id.ll_delete);
|
||||||
|
tvTime = itemView.findViewById(R.id.tv_time);
|
||||||
|
tvLabel = itemView.findViewById(R.id.tv_label);
|
||||||
|
tvRepeat = itemView.findViewById(R.id.tv_repeat);
|
||||||
|
switchEnabled = itemView.findViewById(R.id.switch_enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,6 +106,12 @@ public class ContactInfoAdapter extends RecyclerView.Adapter<ContactInfoAdapter.
|
|||||||
return mContactInfos == null ? 0 : mContactInfos.size();
|
return mContactInfos == null ? 0 : mContactInfos.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewRecycled(@NonNull ContactInfoHolder holder) {
|
||||||
|
super.onViewRecycled(holder);
|
||||||
|
GlideUtils.clearImage(holder.nv_avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 处理拖动交换位置
|
// 处理拖动交换位置
|
||||||
public void onItemMove(int fromPosition, int toPosition) {
|
public void onItemMove(int fromPosition, int toPosition) {
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ public class HomeContactAdapter extends RecyclerView.Adapter<HomeContactAdapter.
|
|||||||
return mContactInfos == null ? 0 : mContactInfos.size();
|
return mContactInfos == null ? 0 : mContactInfos.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewRecycled(@NonNull ContactInfoHolder holder) {
|
||||||
|
super.onViewRecycled(holder);
|
||||||
|
GlideUtils.clearImage(holder.nv_avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 处理拖动交换位置
|
// 处理拖动交换位置
|
||||||
public void onItemMove(int fromPosition, int toPosition) {
|
public void onItemMove(int fromPosition, int toPosition) {
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ public class HourlyWeatherAdapter extends RecyclerView.Adapter<HourlyWeatherAdap
|
|||||||
return mWeatherHourlyList == null ? 27 : mWeatherHourlyList.size();
|
return mWeatherHourlyList == null ? 27 : mWeatherHourlyList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewRecycled(@NonNull HourlyWeather holder) {
|
||||||
|
super.onViewRecycled(holder);
|
||||||
|
GlideUtils.clearImage(holder.iv_icon);
|
||||||
|
}
|
||||||
|
|
||||||
public class HourlyWeather extends RecyclerView.ViewHolder {
|
public class HourlyWeather extends RecyclerView.ViewHolder {
|
||||||
TextView tv_time, tv_temp;
|
TextView tv_time, tv_temp;
|
||||||
ImageView iv_icon;
|
ImageView iv_icon;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
@@ -93,6 +94,12 @@ public class WeatherAdapter extends RecyclerView.Adapter<WeatherAdapter.WeatherH
|
|||||||
return mWeatherDailyList == null ? 10 : mWeatherDailyList.size();
|
return mWeatherDailyList == null ? 10 : mWeatherDailyList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewRecycled(@NonNull WeatherHolder holder) {
|
||||||
|
super.onViewRecycled(holder);
|
||||||
|
GlideUtils.clearImage(holder.iv_icon);
|
||||||
|
}
|
||||||
|
|
||||||
public class WeatherHolder extends RecyclerView.ViewHolder {
|
public class WeatherHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
TextView tv_date, tv_temp_min, tv_temp_max;
|
TextView tv_date, tv_temp_min, tv_temp_max;
|
||||||
|
|||||||
@@ -7,19 +7,57 @@ import android.content.Intent;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
|
||||||
import cn.jpush.android.service.AlarmReceiver;
|
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmRepository;
|
||||||
|
import com.ttstd.dialer.db.alarm.IntegerListConverter;
|
||||||
|
import com.ttstd.dialer.receiver.AlarmReceiver;
|
||||||
|
import com.ttstd.dialer.utils.Logger;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class AlarmManagerHelper {
|
public class AlarmManagerHelper {
|
||||||
|
private static final String TAG = "AlarmManagerHelper";
|
||||||
|
|
||||||
public static final int ALARM_REQUEST_CODE = 1001;
|
/**
|
||||||
|
* 重新调度所有已启用的闹钟
|
||||||
|
*
|
||||||
|
* @param context 上下文
|
||||||
|
*/
|
||||||
|
public static void rescheduleAllAlarms(Context context) {
|
||||||
|
AlarmRepository repository = new AlarmRepository(context);
|
||||||
|
Completable.fromAction(() -> {
|
||||||
|
List<AlarmInfo> enabledAlarms = repository.getEnabledAlarms();
|
||||||
|
for (AlarmInfo alarm : enabledAlarms) {
|
||||||
|
long nextTime = calculateNextTime(alarm);
|
||||||
|
alarm.setNextTriggerTime(nextTime);
|
||||||
|
repository.updateNextTriggerTime(alarm.getId(), nextTime);
|
||||||
|
setExactAlarm(context, alarm.getId(), nextTime);
|
||||||
|
Logger.d(TAG, "已重新调度闹钟 ID: " + alarm.getId() + ",下次触发时间: " + nextTime);
|
||||||
|
}
|
||||||
|
}).subscribeOn(Schedulers.io()).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long calculateNextTime(AlarmInfo info) {
|
||||||
|
AlarmRepeatConfig config = new AlarmRepeatConfig(info.getRepeatType());
|
||||||
|
if (info.getRepeatType() == AlarmRepeatConfig.REPEAT_CUSTOM) {
|
||||||
|
List<Integer> days = IntegerListConverter.toIntegerList(info.getCustomDays());
|
||||||
|
config.setCustomDays(new ArrayList<>(days));
|
||||||
|
}
|
||||||
|
return AlarmTimeCalculator.calculateNextAlarmTime(info.getHour(), info.getMinute(), config);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置一个精准闹钟(兼容 Android 5.0 到 Android 16)
|
* 设置一个精准闹钟(兼容 Android 5.0 到 Android 16)
|
||||||
*
|
*
|
||||||
* @param context 上下文
|
* @param context 上下文
|
||||||
|
* @param alarmId 闹钟ID,用于区分不同的 PendingIntent
|
||||||
* @param triggerAtMillis 触发的时间戳(毫秒)
|
* @param triggerAtMillis 触发的时间戳(毫秒)
|
||||||
*/
|
*/
|
||||||
public static void setExactAlarm(Context context, long triggerAtMillis) {
|
public static void setExactAlarm(Context context, int alarmId, long triggerAtMillis) {
|
||||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
if (alarmManager == null) return;
|
if (alarmManager == null) return;
|
||||||
|
|
||||||
@@ -35,12 +73,14 @@ public class AlarmManagerHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Intent intent = new Intent(context, AlarmReceiver.class);
|
Intent intent = new Intent(context, AlarmReceiver.class);
|
||||||
|
intent.putExtra("ALARM_ID", alarmId);
|
||||||
|
|
||||||
// 使用 FLAG_IMMUTABLE 或 FLAG_MUTABLE 增强 Android 12+ 安全性
|
// 使用 FLAG_IMMUTABLE 或 FLAG_MUTABLE 增强 Android 12+ 安全性
|
||||||
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
|
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT;
|
PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, ALARM_REQUEST_CODE, intent, flags);
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, alarmId, intent, flags);
|
||||||
|
|
||||||
// 核心兼容性定时逻辑
|
// 核心兼容性定时逻辑
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
@@ -57,15 +97,18 @@ public class AlarmManagerHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消闹钟
|
* 取消闹钟
|
||||||
|
*
|
||||||
|
* @param context 上下文
|
||||||
|
* @param alarmId 闹钟ID
|
||||||
*/
|
*/
|
||||||
public static void cancelAlarm(Context context) {
|
public static void cancelAlarm(Context context, int alarmId) {
|
||||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
if (alarmManager != null) {
|
if (alarmManager != null) {
|
||||||
Intent intent = new Intent(context, AlarmReceiver.class);
|
Intent intent = new Intent(context, AlarmReceiver.class);
|
||||||
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
|
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT;
|
PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, ALARM_REQUEST_CODE, intent, flags);
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, alarmId, intent, flags);
|
||||||
alarmManager.cancel(pendingIntent);
|
alarmManager.cancel(pendingIntent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ public class AlarmRepeatConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setCustomDays(ArrayList<Integer> customDays) {
|
||||||
|
this.customDays = customDays;
|
||||||
|
}
|
||||||
|
|
||||||
public int getRepeatType() {
|
public int getRepeatType() {
|
||||||
return repeatType;
|
return repeatType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.kongzue.dialogx.DialogX;
|
|||||||
import com.tencent.bugly.crashreport.CrashReport;
|
import com.tencent.bugly.crashreport.CrashReport;
|
||||||
import com.tencent.mmkv.MMKV;
|
import com.tencent.mmkv.MMKV;
|
||||||
import com.ttstd.dialer.BuildConfig;
|
import com.ttstd.dialer.BuildConfig;
|
||||||
|
import com.ttstd.dialer.alarmclock.AlarmManagerHelper;
|
||||||
import com.ttstd.dialer.config.CommonConfig;
|
import com.ttstd.dialer.config.CommonConfig;
|
||||||
import com.ttstd.dialer.config.SystemIntentAction;
|
import com.ttstd.dialer.config.SystemIntentAction;
|
||||||
import com.ttstd.dialer.manager.AppManager;
|
import com.ttstd.dialer.manager.AppManager;
|
||||||
@@ -27,6 +28,8 @@ import com.ttstd.dialer.mdm.DeviceManagerService;
|
|||||||
import com.ttstd.dialer.network.OkHttpManager;
|
import com.ttstd.dialer.network.OkHttpManager;
|
||||||
import com.ttstd.dialer.push.PushExecutor;
|
import com.ttstd.dialer.push.PushExecutor;
|
||||||
import com.ttstd.dialer.receiver.AppChangedReceiver;
|
import com.ttstd.dialer.receiver.AppChangedReceiver;
|
||||||
|
import com.ttstd.dialer.receiver.HourlyChimeManager;
|
||||||
|
import com.ttstd.dialer.tts.sherpa_onnx.SherpaOnnxTtsManager;
|
||||||
import com.ttstd.dialer.utils.Logger;
|
import com.ttstd.dialer.utils.Logger;
|
||||||
import com.ttstd.dialer.utils.NativeUtils;
|
import com.ttstd.dialer.utils.NativeUtils;
|
||||||
import com.ttstd.dialer.utils.SystemUtils;
|
import com.ttstd.dialer.utils.SystemUtils;
|
||||||
@@ -129,6 +132,13 @@ public class BaseApplication extends Application {
|
|||||||
|
|
||||||
WeatherManager.init(this);
|
WeatherManager.init(this);
|
||||||
IconCacheManager.init(this);
|
IconCacheManager.init(this);
|
||||||
|
SherpaOnnxTtsManager.getInstance().init(this);
|
||||||
|
AlarmManagerHelper.rescheduleAllAlarms(this);
|
||||||
|
|
||||||
|
if (mMMKV.decodeInt(CommonConfig.HOURLY_CHIME_ENABLE, 0) == 1) {
|
||||||
|
HourlyChimeManager.startChime(this);
|
||||||
|
}
|
||||||
|
|
||||||
registerReceivers();
|
registerReceivers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public class CommonConfig {
|
|||||||
public static final String FLOAT_WINDOW_Y = "float_window_y_key";
|
public static final String FLOAT_WINDOW_Y = "float_window_y_key";
|
||||||
public static final String FLOAT_WINDOW_ENABLE = "float_window_enable_key";
|
public static final String FLOAT_WINDOW_ENABLE = "float_window_enable_key";
|
||||||
public static final String FLOAT_WINDOW_KILL_APP = "float_window_kill_app_key";
|
public static final String FLOAT_WINDOW_KILL_APP = "float_window_kill_app_key";
|
||||||
|
public static final String HOURLY_CHIME_ENABLE = "hourly_chime_enable_key";
|
||||||
|
|
||||||
public static final String FRAGMENT_APP_ROW_KEY = "fragment_app_row_key";
|
public static final String FRAGMENT_APP_ROW_KEY = "fragment_app_row_key";
|
||||||
public static final int FRAGMENT_APP_ROW_DEFAULT_VALUE = 2;
|
public static final int FRAGMENT_APP_ROW_DEFAULT_VALUE = 2;
|
||||||
|
|||||||
61
app/src/main/java/com/ttstd/dialer/db/alarm/AlarmDao.java
Normal file
61
app/src/main/java/com/ttstd/dialer/db/alarm/AlarmDao.java
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package com.ttstd.dialer.db.alarm;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Delete;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Update;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface AlarmDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
long insertAlarm(AlarmInfo alarmInfo);
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
void insertAlarms(List<AlarmInfo> alarmInfos);
|
||||||
|
|
||||||
|
@Update
|
||||||
|
void updateAlarm(AlarmInfo alarmInfo);
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
void deleteAlarm(AlarmInfo alarmInfo);
|
||||||
|
|
||||||
|
@Query("DELETE FROM alarm_clock WHERE id = :alarmId")
|
||||||
|
void deleteAlarmById(int alarmId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM alarm_clock ORDER BY hour ASC, minute ASC")
|
||||||
|
LiveData<List<AlarmInfo>> getAllAlarmsLive();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM alarm_clock ORDER BY hour ASC, minute ASC")
|
||||||
|
List<AlarmInfo> getAllAlarms();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM alarm_clock WHERE id = :alarmId")
|
||||||
|
AlarmInfo getAlarmById(int alarmId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM alarm_clock WHERE is_enabled = 1 ORDER BY hour ASC, minute ASC")
|
||||||
|
List<AlarmInfo> getEnabledAlarms();
|
||||||
|
|
||||||
|
@Query("UPDATE alarm_clock SET is_enabled = :enabled WHERE id = :alarmId")
|
||||||
|
void updateAlarmEnabled(int alarmId, boolean enabled);
|
||||||
|
|
||||||
|
@Query("UPDATE alarm_clock SET next_trigger_time = :nextTime WHERE id = :alarmId")
|
||||||
|
void updateNextTriggerTime(int alarmId, long nextTime);
|
||||||
|
|
||||||
|
@Query("UPDATE alarm_clock SET label = :label WHERE id = :alarmId")
|
||||||
|
void updateAlarmLabel(int alarmId, String label);
|
||||||
|
|
||||||
|
@Query("DELETE FROM alarm_clock")
|
||||||
|
void deleteAllAlarms();
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM alarm_clock")
|
||||||
|
int getAlarmCount();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM alarm_clock WHERE next_trigger_time > :currentTime ORDER BY next_trigger_time ASC LIMIT 1")
|
||||||
|
AlarmInfo getNextAlarm(long currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.ttstd.dialer.db.alarm;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.room.Database;
|
||||||
|
import androidx.room.Room;
|
||||||
|
import androidx.room.RoomDatabase;
|
||||||
|
import androidx.room.TypeConverters;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
@Database(entities = {AlarmInfo.class, AlarmMedia.class}, version = 1, exportSchema = false)
|
||||||
|
@TypeConverters({IntegerListConverter.class})
|
||||||
|
public abstract class AlarmDatabase extends RoomDatabase {
|
||||||
|
public abstract AlarmDao alarmDao();
|
||||||
|
|
||||||
|
public abstract AlarmMediaDao alarmMediaDao();
|
||||||
|
|
||||||
|
private static volatile AlarmDatabase INSTANCE;
|
||||||
|
|
||||||
|
public static AlarmDatabase getDatabase(final Context context) {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
synchronized (AlarmDatabase.class) {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
|
||||||
|
AlarmDatabase.class,
|
||||||
|
context.getExternalFilesDir("db") + File.separator + "alarm" + File.separator + "alarm_db")
|
||||||
|
.allowMainThreadQueries()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void destroyInstance() {
|
||||||
|
if (INSTANCE != null && INSTANCE.isOpen()) {
|
||||||
|
INSTANCE.close();
|
||||||
|
}
|
||||||
|
INSTANCE = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
251
app/src/main/java/com/ttstd/dialer/db/alarm/AlarmInfo.java
Normal file
251
app/src/main/java/com/ttstd/dialer/db/alarm/AlarmInfo.java
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
package com.ttstd.dialer.db.alarm;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.ColumnInfo;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import androidx.room.TypeConverters;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Entity(tableName = "alarm_clock")
|
||||||
|
@TypeConverters({IntegerListConverter.class})
|
||||||
|
public class AlarmInfo implements Serializable, Parcelable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
private int id;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "hour")
|
||||||
|
private int hour;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "minute")
|
||||||
|
private int minute;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "label")
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "repeat_type")
|
||||||
|
private int repeatType;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "custom_days")
|
||||||
|
private String customDays;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "is_enabled")
|
||||||
|
private boolean isEnabled;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "vibration")
|
||||||
|
private boolean vibration;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "volume")
|
||||||
|
private int volume;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "ringtone_uri")
|
||||||
|
private String ringtoneUri;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "snooze_interval")
|
||||||
|
private int snoozeInterval;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "next_trigger_time")
|
||||||
|
private long nextTriggerTime;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "is_snooze")
|
||||||
|
private boolean isSnooze;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "created_at")
|
||||||
|
private long createdAt;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "updated_at")
|
||||||
|
private long updatedAt;
|
||||||
|
|
||||||
|
public AlarmInfo() {
|
||||||
|
this.createdAt = System.currentTimeMillis();
|
||||||
|
this.updatedAt = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AlarmInfo(Parcel in) {
|
||||||
|
id = in.readInt();
|
||||||
|
hour = in.readInt();
|
||||||
|
minute = in.readInt();
|
||||||
|
label = in.readString();
|
||||||
|
repeatType = in.readInt();
|
||||||
|
customDays = in.readString();
|
||||||
|
isEnabled = in.readByte() != 0;
|
||||||
|
vibration = in.readByte() != 0;
|
||||||
|
volume = in.readInt();
|
||||||
|
ringtoneUri = in.readString();
|
||||||
|
snoozeInterval = in.readInt();
|
||||||
|
nextTriggerTime = in.readLong();
|
||||||
|
isSnooze = in.readByte() != 0;
|
||||||
|
createdAt = in.readLong();
|
||||||
|
updatedAt = in.readLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<AlarmInfo> CREATOR = new Creator<AlarmInfo>() {
|
||||||
|
@Override
|
||||||
|
public AlarmInfo createFromParcel(Parcel in) {
|
||||||
|
return new AlarmInfo(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlarmInfo[] newArray(int size) {
|
||||||
|
return new AlarmInfo[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHour() {
|
||||||
|
return hour;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHour(int hour) {
|
||||||
|
this.hour = hour;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinute() {
|
||||||
|
return minute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMinute(int minute) {
|
||||||
|
this.minute = minute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRepeatType() {
|
||||||
|
return repeatType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRepeatType(int repeatType) {
|
||||||
|
this.repeatType = repeatType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomDays() {
|
||||||
|
return customDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomDays(String customDays) {
|
||||||
|
this.customDays = customDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
isEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVibration() {
|
||||||
|
return vibration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVibration(boolean vibration) {
|
||||||
|
this.vibration = vibration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVolume() {
|
||||||
|
return volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVolume(int volume) {
|
||||||
|
this.volume = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRingtoneUri() {
|
||||||
|
return ringtoneUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRingtoneUri(String ringtoneUri) {
|
||||||
|
this.ringtoneUri = ringtoneUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSnoozeInterval() {
|
||||||
|
return snoozeInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSnoozeInterval(int snoozeInterval) {
|
||||||
|
this.snoozeInterval = snoozeInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getNextTriggerTime() {
|
||||||
|
return nextTriggerTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNextTriggerTime(long nextTriggerTime) {
|
||||||
|
this.nextTriggerTime = nextTriggerTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSnooze() {
|
||||||
|
return isSnooze;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSnooze(boolean snooze) {
|
||||||
|
isSnooze = snooze;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(long createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(long updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeInt(id);
|
||||||
|
dest.writeInt(hour);
|
||||||
|
dest.writeInt(minute);
|
||||||
|
dest.writeString(label);
|
||||||
|
dest.writeInt(repeatType);
|
||||||
|
dest.writeString(customDays);
|
||||||
|
dest.writeByte((byte) (isEnabled ? 1 : 0));
|
||||||
|
dest.writeByte((byte) (vibration ? 1 : 0));
|
||||||
|
dest.writeInt(volume);
|
||||||
|
dest.writeString(ringtoneUri);
|
||||||
|
dest.writeInt(snoozeInterval);
|
||||||
|
dest.writeLong(nextTriggerTime);
|
||||||
|
dest.writeByte((byte) (isSnooze ? 1 : 0));
|
||||||
|
dest.writeLong(createdAt);
|
||||||
|
dest.writeLong(updatedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
174
app/src/main/java/com/ttstd/dialer/db/alarm/AlarmMedia.java
Normal file
174
app/src/main/java/com/ttstd/dialer/db/alarm/AlarmMedia.java
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package com.ttstd.dialer.db.alarm;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.ColumnInfo;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "alarm_media",
|
||||||
|
foreignKeys = @ForeignKey(
|
||||||
|
entity = AlarmInfo.class,
|
||||||
|
parentColumns = "id",
|
||||||
|
childColumns = "alarm_id",
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
),
|
||||||
|
indices = {@Index("alarm_id")}
|
||||||
|
)
|
||||||
|
public class AlarmMedia implements Serializable, Parcelable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public static final int MEDIA_TYPE_IMAGE = 1;
|
||||||
|
public static final int MEDIA_TYPE_VIDEO = 2;
|
||||||
|
public static final int MEDIA_TYPE_GIF = 3;
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
private int id;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "alarm_id")
|
||||||
|
private int alarmId;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "media_type")
|
||||||
|
private int mediaType;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "media_path")
|
||||||
|
private String mediaPath;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "thumbnail_path")
|
||||||
|
private String thumbnailPath;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "display_duration")
|
||||||
|
private int displayDuration;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "sort_order")
|
||||||
|
private int sortOrder;
|
||||||
|
|
||||||
|
@ColumnInfo(name = "created_at")
|
||||||
|
private long createdAt;
|
||||||
|
|
||||||
|
public AlarmMedia() {
|
||||||
|
this.createdAt = System.currentTimeMillis();
|
||||||
|
this.displayDuration = 5000;
|
||||||
|
this.sortOrder = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AlarmMedia(Parcel in) {
|
||||||
|
id = in.readInt();
|
||||||
|
alarmId = in.readInt();
|
||||||
|
mediaType = in.readInt();
|
||||||
|
mediaPath = in.readString();
|
||||||
|
thumbnailPath = in.readString();
|
||||||
|
displayDuration = in.readInt();
|
||||||
|
sortOrder = in.readInt();
|
||||||
|
createdAt = in.readLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<AlarmMedia> CREATOR = new Creator<AlarmMedia>() {
|
||||||
|
@Override
|
||||||
|
public AlarmMedia createFromParcel(Parcel in) {
|
||||||
|
return new AlarmMedia(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlarmMedia[] newArray(int size) {
|
||||||
|
return new AlarmMedia[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAlarmId() {
|
||||||
|
return alarmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlarmId(int alarmId) {
|
||||||
|
this.alarmId = alarmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMediaType() {
|
||||||
|
return mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMediaType(int mediaType) {
|
||||||
|
this.mediaType = mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMediaPath() {
|
||||||
|
return mediaPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMediaPath(String mediaPath) {
|
||||||
|
this.mediaPath = mediaPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getThumbnailPath() {
|
||||||
|
return thumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThumbnailPath(String thumbnailPath) {
|
||||||
|
this.thumbnailPath = thumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDisplayDuration() {
|
||||||
|
return displayDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayDuration(int displayDuration) {
|
||||||
|
this.displayDuration = displayDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSortOrder() {
|
||||||
|
return sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSortOrder(int sortOrder) {
|
||||||
|
this.sortOrder = sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(long createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return JsonParser.parseString(new Gson().toJson(this)).getAsJsonObject().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeInt(id);
|
||||||
|
dest.writeInt(alarmId);
|
||||||
|
dest.writeInt(mediaType);
|
||||||
|
dest.writeString(mediaPath);
|
||||||
|
dest.writeString(thumbnailPath);
|
||||||
|
dest.writeInt(displayDuration);
|
||||||
|
dest.writeInt(sortOrder);
|
||||||
|
dest.writeLong(createdAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.ttstd.dialer.db.alarm;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Delete;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Update;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface AlarmMediaDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
long insertMedia(AlarmMedia media);
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
void insertMediaList(List<AlarmMedia> mediaList);
|
||||||
|
|
||||||
|
@Update
|
||||||
|
void updateMedia(AlarmMedia media);
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
void deleteMedia(AlarmMedia media);
|
||||||
|
|
||||||
|
@Query("DELETE FROM alarm_media WHERE alarm_id = :alarmId")
|
||||||
|
void deleteMediaByAlarmId(int alarmId);
|
||||||
|
|
||||||
|
@Query("DELETE FROM alarm_media WHERE id = :mediaId")
|
||||||
|
void deleteMediaById(int mediaId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM alarm_media WHERE alarm_id = :alarmId ORDER BY sort_order ASC")
|
||||||
|
LiveData<List<AlarmMedia>> getMediaForAlarmLive(int alarmId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM alarm_media WHERE alarm_id = :alarmId ORDER BY sort_order ASC")
|
||||||
|
List<AlarmMedia> getMediaForAlarm(int alarmId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM alarm_media WHERE id = :mediaId")
|
||||||
|
AlarmMedia getMediaById(int mediaId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM alarm_media WHERE alarm_id = :alarmId AND media_type = :mediaType ORDER BY sort_order ASC")
|
||||||
|
List<AlarmMedia> getMediaByType(int alarmId, int mediaType);
|
||||||
|
|
||||||
|
@Query("UPDATE alarm_media SET sort_order = :order WHERE id = :mediaId")
|
||||||
|
void updateMediaSortOrder(int mediaId, int order);
|
||||||
|
|
||||||
|
@Query("DELETE FROM alarm_media")
|
||||||
|
void deleteAllMedia();
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ttstd.dialer.db.alarm;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AlarmRepository {
|
||||||
|
private final AlarmDao alarmDao;
|
||||||
|
private final AlarmMediaDao alarmMediaDao;
|
||||||
|
|
||||||
|
public AlarmRepository(Context context) {
|
||||||
|
AlarmDatabase database = AlarmDatabase.getDatabase(context);
|
||||||
|
this.alarmDao = database.alarmDao();
|
||||||
|
this.alarmMediaDao = database.alarmMediaDao();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long insertAlarm(AlarmInfo alarmInfo) {
|
||||||
|
return alarmDao.insertAlarm(alarmInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertAlarms(List<AlarmInfo> alarmInfos) {
|
||||||
|
alarmDao.insertAlarms(alarmInfos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAlarm(AlarmInfo alarmInfo) {
|
||||||
|
alarmDao.updateAlarm(alarmInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAlarm(AlarmInfo alarmInfo) {
|
||||||
|
alarmDao.deleteAlarm(alarmInfo);
|
||||||
|
alarmMediaDao.deleteMediaByAlarmId(alarmInfo.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AlarmInfo> getAllAlarms() {
|
||||||
|
return alarmDao.getAllAlarms();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlarmInfo getAlarmById(int alarmId) {
|
||||||
|
return alarmDao.getAlarmById(alarmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AlarmInfo> getEnabledAlarms() {
|
||||||
|
return alarmDao.getEnabledAlarms();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAlarmEnabled(int alarmId, boolean enabled) {
|
||||||
|
alarmDao.updateAlarmEnabled(alarmId, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateNextTriggerTime(int alarmId, long nextTime) {
|
||||||
|
alarmDao.updateNextTriggerTime(alarmId, nextTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlarmInfo getNextAlarm(long currentTime) {
|
||||||
|
return alarmDao.getNextAlarm(currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long insertMedia(AlarmMedia media) {
|
||||||
|
return alarmMediaDao.insertMedia(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertMediaList(List<AlarmMedia> mediaList) {
|
||||||
|
alarmMediaDao.insertMediaList(mediaList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AlarmMedia> getMediaForAlarm(int alarmId) {
|
||||||
|
return alarmMediaDao.getMediaForAlarm(alarmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteMediaByAlarmId(int alarmId) {
|
||||||
|
alarmMediaDao.deleteMediaByAlarmId(alarmId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.ttstd.dialer.db.alarm;
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class IntegerListConverter {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static String fromIntegerList(List<Integer> list) {
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
sb.append(list.get(i));
|
||||||
|
if (i < list.size() - 1) {
|
||||||
|
sb.append(",");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static List<Integer> toIntegerList(String data) {
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
String[] parts = data.split(",");
|
||||||
|
List<Integer> result = new ArrayList<>();
|
||||||
|
for (String part : parts) {
|
||||||
|
try {
|
||||||
|
result.add(Integer.parseInt(part.trim()));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,10 +2,11 @@ package com.ttstd.dialer.fragment.app;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
|
|
||||||
import com.ttstd.dialer.R;
|
import com.ttstd.dialer.R;
|
||||||
import com.ttstd.dialer.adapter.AppAdapter;
|
import com.ttstd.dialer.adapter.AppAdapter;
|
||||||
@@ -15,6 +16,7 @@ import com.ttstd.dialer.databinding.FragmentAppBinding;
|
|||||||
import com.ttstd.dialer.db.app.AppInfo;
|
import com.ttstd.dialer.db.app.AppInfo;
|
||||||
import com.ttstd.dialer.utils.Logger;
|
import com.ttstd.dialer.utils.Logger;
|
||||||
import com.ttstd.dialer.view.EqualSpaceDecoration;
|
import com.ttstd.dialer.view.EqualSpaceDecoration;
|
||||||
|
import com.ttstd.dialer.view.NoScrollGridLayoutManager;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -82,9 +84,19 @@ public class AppFragment extends BaseMvvmFragment<AppViewModel, FragmentAppBindi
|
|||||||
mViewModel.updateAppInfo(appInfo);
|
mViewModel.updateAppInfo(appInfo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mViewDataBinding.recyclerView.setLayoutManager(new GridLayoutManager(mContext, CommonConfig.FRAGMENT_APP_ROW_DEFAULT_VALUE));
|
mViewDataBinding.recyclerView.setLayoutManager(new NoScrollGridLayoutManager(mContext, CommonConfig.FRAGMENT_APP_ROW_DEFAULT_VALUE));
|
||||||
mViewDataBinding.recyclerView.addItemDecoration(new EqualSpaceDecoration(CommonConfig.FRAGMENT_APP_COLUMN_DEFAULT_VALUE, 4));
|
mViewDataBinding.recyclerView.addItemDecoration(new EqualSpaceDecoration(CommonConfig.FRAGMENT_APP_COLUMN_DEFAULT_VALUE, 4));
|
||||||
|
mViewDataBinding.recyclerView.setNestedScrollingEnabled(false);
|
||||||
mViewDataBinding.recyclerView.setAdapter(mAppAdapter);
|
mViewDataBinding.recyclerView.setAdapter(mAppAdapter);
|
||||||
|
mViewDataBinding.recyclerView.setOnTouchListener(new View.OnTouchListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||||
|
v.getParent().requestDisallowInterceptTouchEvent(false);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (mAppInfos != null) {
|
if (mAppInfos != null) {
|
||||||
Logger.e(TAG, "initView: mAppInfos size = " + mAppInfos.size());
|
Logger.e(TAG, "initView: mAppInfos size = " + mAppInfos.size());
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import androidx.lifecycle.Observer;
|
|||||||
import com.hjq.toast.Toaster;
|
import com.hjq.toast.Toaster;
|
||||||
import com.jeremyliao.liveeventbus.LiveEventBus;
|
import com.jeremyliao.liveeventbus.LiveEventBus;
|
||||||
import com.qweather.sdk.response.weather.WeatherDaily;
|
import com.qweather.sdk.response.weather.WeatherDaily;
|
||||||
|
import com.qweather.sdk.response.weather.WeatherDailyResponse;
|
||||||
|
import com.qweather.sdk.response.weather.WeatherNowResponse;
|
||||||
import com.ttstd.dialer.R;
|
import com.ttstd.dialer.R;
|
||||||
import com.ttstd.dialer.activity.contact.list.ContactListActivity;
|
import com.ttstd.dialer.activity.contact.list.ContactListActivity;
|
||||||
import com.ttstd.dialer.activity.weather.main.WeatherMainActivity;
|
import com.ttstd.dialer.activity.weather.main.WeatherMainActivity;
|
||||||
@@ -110,6 +112,7 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
|
|||||||
protected void initView(Bundle bundle) {
|
protected void initView(Bundle bundle) {
|
||||||
mFestivalUtils = new LunarCalendarFestivalUtils();
|
mFestivalUtils = new LunarCalendarFestivalUtils();
|
||||||
weatherUpdateManager = WeatherUpdateManager.Companion.getInstance();
|
weatherUpdateManager = WeatherUpdateManager.Companion.getInstance();
|
||||||
|
initWeather();
|
||||||
observeWeatherUpdates();
|
observeWeatherUpdates();
|
||||||
WeatherManager.getInstance().refreshweather();
|
WeatherManager.getInstance().refreshweather();
|
||||||
setTime();
|
setTime();
|
||||||
@@ -159,29 +162,26 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void observeWeatherUpdates() {
|
private void initWeather() {
|
||||||
// 观察当前天气更新
|
WeatherNowResponse nowCache = WeatherManager.getInstance().getWeatherNowCache();
|
||||||
weatherNowJob = weatherUpdateManager.observeWeatherNow(weatherScope, weatherNowResponse -> {
|
if (nowCache != null) {
|
||||||
if (weatherNowResponse != null) {
|
updateWeatherNowUI(nowCache);
|
||||||
Logger.e(TAG, "observeWeatherUpdates", "weatherNowResponse = " + weatherNowResponse);
|
}
|
||||||
|
WeatherDailyResponse dailyCache = WeatherManager.getInstance().getWeather10DCache();
|
||||||
|
if (dailyCache != null) {
|
||||||
|
updateWeatherDailyUI(dailyCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWeatherNowUI(WeatherNowResponse weatherNowResponse) {
|
||||||
|
if (weatherNowResponse != null && weatherNowResponse.getNow() != null) {
|
||||||
mViewDataBinding.tvWeather.setText(weatherNowResponse.getNow().getText());
|
mViewDataBinding.tvWeather.setText(weatherNowResponse.getNow().getText());
|
||||||
mViewDataBinding.tvTempCurrent.setText(weatherNowResponse.getNow().getTemp() + "°");
|
mViewDataBinding.tvTempCurrent.setText(weatherNowResponse.getNow().getTemp() + "°");
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
weatherHourlyJob = weatherUpdateManager.observeWeatherHourly(weatherScope, weatherHourlyResponse -> {
|
|
||||||
if (weatherHourlyResponse != null) {
|
|
||||||
Logger.e(TAG, "observeWeatherUpdates", "weatherHourlyResponse = " + weatherHourlyResponse);
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
weatherDailyJob = weatherUpdateManager.observeWeatherDaily(weatherScope, weatherDailyResponse -> {
|
|
||||||
if (weatherDailyResponse != null) {
|
|
||||||
Logger.e(TAG, "observeWeatherUpdates", "weatherDailyResponse = " + weatherDailyResponse);
|
|
||||||
|
|
||||||
|
private void updateWeatherDailyUI(WeatherDailyResponse weatherDailyResponse) {
|
||||||
|
if (weatherDailyResponse != null && weatherDailyResponse.getDaily() != null && !weatherDailyResponse.getDaily().isEmpty()) {
|
||||||
WeatherDaily weatherDaily = weatherDailyResponse.getDaily().get(0);
|
WeatherDaily weatherDaily = weatherDailyResponse.getDaily().get(0);
|
||||||
mViewDataBinding.tvTemp.setText(weatherDaily.getTempMin() + "°/" + weatherDaily.getTempMax() + "°");
|
mViewDataBinding.tvTemp.setText(weatherDaily.getTempMin() + "°/" + weatherDaily.getTempMax() + "°");
|
||||||
String iconDay = weatherDaily.getIconDay();
|
String iconDay = weatherDaily.getIconDay();
|
||||||
@@ -195,6 +195,24 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
|
|||||||
Logger.e("GlideLoad", "Raw resource not found: " + fileName);
|
Logger.e("GlideLoad", "Raw resource not found: " + fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void observeWeatherUpdates() {
|
||||||
|
// 观察当前天气更新
|
||||||
|
weatherNowJob = weatherUpdateManager.observeWeatherNow(weatherScope, weatherNowResponse -> {
|
||||||
|
updateWeatherNowUI(weatherNowResponse);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
weatherHourlyJob = weatherUpdateManager.observeWeatherHourly(weatherScope, weatherHourlyResponse -> {
|
||||||
|
if (weatherHourlyResponse != null) {
|
||||||
|
Logger.e(TAG, "observeWeatherUpdates", "weatherHourlyResponse = " + weatherHourlyResponse);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
weatherDailyJob = weatherUpdateManager.observeWeatherDaily(weatherScope, weatherDailyResponse -> {
|
||||||
|
updateWeatherDailyUI(weatherDailyResponse);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,14 +77,20 @@ public class WeatherManager {
|
|||||||
WeatherNowResponse weatherNowResponse = getWeatherNowCache();
|
WeatherNowResponse weatherNowResponse = getWeatherNowCache();
|
||||||
if (weatherNowResponse != null) {
|
if (weatherNowResponse != null) {
|
||||||
mWeatherUpdateManager.publishWeatherNowUpdate(weatherNowResponse);
|
mWeatherUpdateManager.publishWeatherNowUpdate(weatherNowResponse);
|
||||||
|
} else {
|
||||||
|
|
||||||
}
|
}
|
||||||
WeatherHourlyResponse weatherHourlyResponse = getWeather24hCache();
|
WeatherHourlyResponse weatherHourlyResponse = getWeather24hCache();
|
||||||
if (weatherHourlyResponse != null) {
|
if (weatherHourlyResponse != null) {
|
||||||
mWeatherUpdateManager.publishWeatherHourlyUpdate(weatherHourlyResponse);
|
mWeatherUpdateManager.publishWeatherHourlyUpdate(weatherHourlyResponse);
|
||||||
|
} else {
|
||||||
|
|
||||||
}
|
}
|
||||||
WeatherDailyResponse weatherDailyResponse = getWeather10DCache();
|
WeatherDailyResponse weatherDailyResponse = getWeather10DCache();
|
||||||
if (weatherDailyResponse != null) {
|
if (weatherDailyResponse != null) {
|
||||||
mWeatherUpdateManager.publishWeatherDailyUpdate(weatherDailyResponse);
|
mWeatherUpdateManager.publishWeatherDailyUpdate(weatherDailyResponse);
|
||||||
|
} else {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import android.app.PendingIntent;
|
|||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
@@ -15,44 +15,86 @@ import com.ttstd.dialer.activity.alarm.AlarmAlertActivity;
|
|||||||
import com.ttstd.dialer.alarmclock.AlarmManagerHelper;
|
import com.ttstd.dialer.alarmclock.AlarmManagerHelper;
|
||||||
import com.ttstd.dialer.alarmclock.AlarmRepeatConfig;
|
import com.ttstd.dialer.alarmclock.AlarmRepeatConfig;
|
||||||
import com.ttstd.dialer.alarmclock.AlarmTimeCalculator;
|
import com.ttstd.dialer.alarmclock.AlarmTimeCalculator;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||||
|
import com.ttstd.dialer.db.alarm.AlarmRepository;
|
||||||
|
import com.ttstd.dialer.db.alarm.IntegerListConverter;
|
||||||
|
import com.ttstd.dialer.utils.Logger;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class AlarmReceiver extends BroadcastReceiver {
|
public class AlarmReceiver extends BroadcastReceiver {
|
||||||
|
private static final String TAG = "AlarmReceiver";
|
||||||
private static final String CHANNEL_ID = "alarm_channel";
|
private static final String CHANNEL_ID = "alarm_channel";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
// 1. 执行原有的响铃和弹出通知/界面的逻辑
|
int alarmId = intent.getIntExtra("ALARM_ID", -1);
|
||||||
// (调用上一次回答中创建的通知栏或全屏拉起逻辑)
|
Logger.d(TAG, "收到闹钟广播, ID: " + alarmId);
|
||||||
showAlarmNotification(context);
|
|
||||||
|
|
||||||
// 2. 核心:动态轮转,实现重复闹钟
|
if (alarmId == -1) {
|
||||||
scheduleNextRepeatAlarm(context);
|
Logger.e(TAG, "收到闹钟广播但 ALARM_ID 为空");
|
||||||
}
|
|
||||||
|
|
||||||
private void scheduleNextRepeatAlarm(Context context) {
|
|
||||||
// 从 SharedPreferences 中取出用户之前保存的闹钟时间和重复设置
|
|
||||||
// 实际开发中此处可以替换为 SQLite 数据库
|
|
||||||
SharedPreferences sp = context.getSharedPreferences("AlarmPrefs", Context.MODE_PRIVATE);
|
|
||||||
int hour = sp.getInt("alarm_hour", 8);
|
|
||||||
int minute = sp.getInt("alarm_minute", 0);
|
|
||||||
int repeatType = sp.getInt("repeat_type", AlarmRepeatConfig.REPEAT_ONCE);
|
|
||||||
|
|
||||||
// 如果是“只响一次”,响完就结束了,不需要再设置
|
|
||||||
if (repeatType == AlarmRepeatConfig.REPEAT_ONCE) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是 每天 或 周一至周五
|
// 使用同步方法获取闹钟信息
|
||||||
AlarmRepeatConfig config = new AlarmRepeatConfig(repeatType);
|
AlarmRepository repository = new AlarmRepository(context);
|
||||||
|
AlarmInfo alarmInfo = repository.getAlarmById(alarmId);
|
||||||
|
|
||||||
// 重新计算下一次的时间(由于此时已经过了当前闹钟点,计算出的必定是未来的时间)
|
if (alarmInfo == null) {
|
||||||
long nextTriggerTime = AlarmTimeCalculator.calculateNextAlarmTime(hour, minute, config);
|
Logger.e(TAG, "未找到对应的闹钟数据, ID: " + alarmId);
|
||||||
|
return;
|
||||||
// 再次调用第一步写好的精准闹钟设置工具,埋下新的定时炸弹
|
|
||||||
AlarmManagerHelper.setExactAlarm(context, nextTriggerTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showAlarmNotification(Context context) {
|
if (!alarmInfo.isEnabled()) {
|
||||||
|
Logger.w(TAG, "闹钟已被禁用,不再处理: " + alarmId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 执行响铃和弹出通知逻辑
|
||||||
|
showAlarmNotification(context, alarmInfo);
|
||||||
|
|
||||||
|
// 尝试直接启动 Activity
|
||||||
|
try {
|
||||||
|
Intent alertIntent = new Intent(context, AlarmAlertActivity.class);
|
||||||
|
alertIntent.putExtra("AlarmInfo", (Parcelable) alarmInfo);
|
||||||
|
alertIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
context.startActivity(alertIntent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.e(TAG, "直接启动 AlarmAlertActivity 失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 核心:动态轮转,实现重复闹钟
|
||||||
|
scheduleNextRepeatAlarm(context, repository, alarmInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleNextRepeatAlarm(Context context, AlarmRepository repository, AlarmInfo alarmInfo) {
|
||||||
|
// 如果是“只响一次”,响完就将其置为禁用状态
|
||||||
|
if (alarmInfo.getRepeatType() == AlarmRepeatConfig.REPEAT_ONCE) {
|
||||||
|
alarmInfo.setEnabled(false);
|
||||||
|
repository.updateAlarm(alarmInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新计算下一次的时间
|
||||||
|
AlarmRepeatConfig config = new AlarmRepeatConfig(alarmInfo.getRepeatType());
|
||||||
|
if (alarmInfo.getRepeatType() == AlarmRepeatConfig.REPEAT_CUSTOM) {
|
||||||
|
List<Integer> days = IntegerListConverter.toIntegerList(alarmInfo.getCustomDays());
|
||||||
|
config.setCustomDays(new ArrayList<>(days));
|
||||||
|
}
|
||||||
|
|
||||||
|
long nextTriggerTime = AlarmTimeCalculator.calculateNextAlarmTime(
|
||||||
|
alarmInfo.getHour(), alarmInfo.getMinute(), config);
|
||||||
|
|
||||||
|
// 更新数据库中的下次触发时间
|
||||||
|
alarmInfo.setNextTriggerTime(nextTriggerTime);
|
||||||
|
repository.updateAlarm(alarmInfo);
|
||||||
|
|
||||||
|
// 再次调用精准闹钟设置工具,埋下新的定时炸弹
|
||||||
|
AlarmManagerHelper.setExactAlarm(context, alarmInfo.getId(), nextTriggerTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showAlarmNotification(Context context, AlarmInfo alarmInfo) {
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
if (notificationManager == null) return;
|
if (notificationManager == null) return;
|
||||||
|
|
||||||
@@ -68,23 +110,27 @@ public class AlarmReceiver extends BroadcastReceiver {
|
|||||||
|
|
||||||
// 2. 构建点击通知或全屏弹出的 Intent
|
// 2. 构建点击通知或全屏弹出的 Intent
|
||||||
Intent alertIntent = new Intent(context, AlarmAlertActivity.class);
|
Intent alertIntent = new Intent(context, AlarmAlertActivity.class);
|
||||||
|
alertIntent.putExtra("AlarmInfo", (Parcelable) alarmInfo);
|
||||||
alertIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
alertIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
|
||||||
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
|
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT;
|
PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, alertIntent, flags);
|
|
||||||
|
|
||||||
// 3. 构建高优先级通知(兼容 Android 10+ 后台全屏拉起)
|
// 使用 alarmId 作为 requestCode 区分不同的闹钟通知
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, alarmInfo.getId(), alertIntent, flags);
|
||||||
|
|
||||||
|
// 3. 构建高优先级通知
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
|
.setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
|
||||||
.setContentTitle("闹钟响了")
|
.setContentTitle("闹钟响了")
|
||||||
.setContentText("时间到了,快起床!")
|
.setContentText(alarmInfo.getLabel() != null && !alarmInfo.getLabel().isEmpty() ?
|
||||||
|
alarmInfo.getLabel() : "时间到了,快起床!")
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setFullScreenIntent(pendingIntent, true); // 核心:锁屏时直接拉起 Activity
|
.setFullScreenIntent(pendingIntent, true); // 核心:锁屏时直接拉起 Activity
|
||||||
|
|
||||||
notificationManager.notify(1, builder.build());
|
notificationManager.notify(alarmInfo.getId(), builder.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.ttstd.dialer.receiver;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import com.ttstd.dialer.utils.Logger;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
public class HourlyChimeManager {
|
||||||
|
private static final String TAG = "HourlyChimeManager";
|
||||||
|
private static final int REQUEST_CODE = 1001;
|
||||||
|
|
||||||
|
public static void startChime(Context context) {
|
||||||
|
Logger.d(TAG, "启动整点报时");
|
||||||
|
scheduleNextChime(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void stopChime(Context context) {
|
||||||
|
Logger.d(TAG, "停止整点报时");
|
||||||
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
if (alarmManager == null) return;
|
||||||
|
|
||||||
|
Intent intent = new Intent(context, HourlyChimeReceiver.class);
|
||||||
|
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flags);
|
||||||
|
alarmManager.cancel(pendingIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void scheduleNextChime(Context context) {
|
||||||
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
if (alarmManager == null) return;
|
||||||
|
|
||||||
|
// 检查 Android 12+ 的精准闹钟权限
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if (!alarmManager.canScheduleExactAlarms()) {
|
||||||
|
Logger.w(TAG, "没有精准闹钟权限,无法安排报时");
|
||||||
|
// 暂时不主动跳转,避免打扰用户
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.add(Calendar.HOUR_OF_DAY, 1);
|
||||||
|
calendar.set(Calendar.MINUTE, 0);
|
||||||
|
calendar.set(Calendar.SECOND, 0);
|
||||||
|
calendar.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
long triggerAtMillis = calendar.getTimeInMillis();
|
||||||
|
|
||||||
|
Intent intent = new Intent(context, HourlyChimeReceiver.class);
|
||||||
|
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flags);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
|
||||||
|
} else {
|
||||||
|
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
|
||||||
|
}
|
||||||
|
Logger.d(TAG, "已安排下一次报时: " + calendar.getTime().toString());
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
Logger.e(TAG, "由于安全权限无法设置闹钟: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.ttstd.dialer.receiver;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import com.tencent.mmkv.MMKV;
|
||||||
|
import com.ttstd.dialer.config.CommonConfig;
|
||||||
|
import com.ttstd.dialer.tts.sherpa_onnx.SherpaOnnxTtsManager;
|
||||||
|
import com.ttstd.dialer.utils.Logger;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
public class HourlyChimeReceiver extends BroadcastReceiver {
|
||||||
|
private static final String TAG = "HourlyChimeReceiver";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
Logger.d(TAG, "收到广播: " + intent.getAction());
|
||||||
|
|
||||||
|
MMKV mmkv = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
|
||||||
|
boolean enabled = mmkv.decodeInt(CommonConfig.HOURLY_CHIME_ENABLE, 0) == 1;
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
Logger.d(TAG, "整点报时未开启,跳过");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
||||||
|
Logger.d(TAG, "开机完成,重新调度整点报时");
|
||||||
|
HourlyChimeManager.scheduleNextChime(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
int hour = calendar.get(Calendar.HOUR_OF_DAY);
|
||||||
|
String text = "现在是北京时间 " + hour + " 点整";
|
||||||
|
|
||||||
|
Logger.d(TAG, "开始播报: " + text);
|
||||||
|
SherpaOnnxTtsManager.getInstance().init(context);
|
||||||
|
SherpaOnnxTtsManager.getInstance().speak(text);
|
||||||
|
|
||||||
|
// 安排下一个小时的报时
|
||||||
|
HourlyChimeManager.scheduleNextChime(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
// Copyright (c) 2023 Xiaomi Corporation
|
||||||
|
package com.ttstd.dialer.tts.sherpa_onnx;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.media.MediaPlayer;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTts;
|
||||||
|
import com.ttstd.dialer.R;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/k2-fsa/sherpa-onnx/tree/master/android/SherpaOnnxTts
|
||||||
|
*/
|
||||||
|
public class SherpaOnnxTtsActivity extends AppCompatActivity {
|
||||||
|
private static final String TAG = "sherpa-onnx";
|
||||||
|
|
||||||
|
private OfflineTts tts;
|
||||||
|
private EditText text;
|
||||||
|
private EditText sid;
|
||||||
|
private EditText speed;
|
||||||
|
private Button generate;
|
||||||
|
private Button play;
|
||||||
|
private Button stop;
|
||||||
|
private Button save;
|
||||||
|
private Button share;
|
||||||
|
private boolean stopped = false;
|
||||||
|
private MediaPlayer mediaPlayer = null;
|
||||||
|
|
||||||
|
private final ActivityResultLauncher<String> saveLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.CreateDocument("audio/wav"),
|
||||||
|
uri -> {
|
||||||
|
if (uri != null) {
|
||||||
|
copyGeneratedWavToUri(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_sherpa_onnx_tts);
|
||||||
|
|
||||||
|
Log.i(TAG, "Start to initialize TTS Manager");
|
||||||
|
SherpaOnnxTtsManager.getInstance().init(this);
|
||||||
|
|
||||||
|
text = findViewById(R.id.text);
|
||||||
|
sid = findViewById(R.id.sid);
|
||||||
|
speed = findViewById(R.id.speed);
|
||||||
|
|
||||||
|
generate = findViewById(R.id.generate);
|
||||||
|
play = findViewById(R.id.play);
|
||||||
|
stop = findViewById(R.id.stop);
|
||||||
|
save = findViewById(R.id.save);
|
||||||
|
share = findViewById(R.id.share);
|
||||||
|
|
||||||
|
generate.setOnClickListener(v -> onClickGenerate());
|
||||||
|
play.setOnClickListener(v -> onClickPlay());
|
||||||
|
stop.setOnClickListener(v -> onClickStop());
|
||||||
|
save.setOnClickListener(v -> onClickSave());
|
||||||
|
share.setOnClickListener(v -> onClickShare());
|
||||||
|
|
||||||
|
sid.setText("0");
|
||||||
|
speed.setText("1.0");
|
||||||
|
|
||||||
|
// we will change sampleText here in the CI
|
||||||
|
String sampleText = "";
|
||||||
|
text.setText(sampleText);
|
||||||
|
|
||||||
|
play.setEnabled(false);
|
||||||
|
save.setEnabled(false);
|
||||||
|
share.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClickGenerate() {
|
||||||
|
String sidStr = sid.getText().toString();
|
||||||
|
Integer sidInt = null;
|
||||||
|
try {
|
||||||
|
sidInt = Integer.parseInt(sidStr);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidInt == null || sidInt < 0) {
|
||||||
|
Toast.makeText(
|
||||||
|
getApplicationContext(),
|
||||||
|
"Please input a non-negative integer for speaker ID!",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String speedStr = speed.getText().toString();
|
||||||
|
Float speedFloat = null;
|
||||||
|
try {
|
||||||
|
speedFloat = Float.parseFloat(speedStr);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
if (speedFloat == null || speedFloat <= 0) {
|
||||||
|
Toast.makeText(
|
||||||
|
getApplicationContext(),
|
||||||
|
"Please input a positive number for speech speed!",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String textStr = text.getText().toString().trim();
|
||||||
|
if (textStr.isEmpty()) {
|
||||||
|
Toast.makeText(getApplicationContext(), "Please input a non-empty text!", Toast.LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SherpaOnnxTtsManager.getInstance().speak(textStr, sidInt, speedFloat, new SherpaOnnxTtsManager.TtsCallback() {
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
play.setEnabled(false);
|
||||||
|
save.setEnabled(false);
|
||||||
|
share.setEnabled(false);
|
||||||
|
generate.setEnabled(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompleted(String filePath) {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
play.setEnabled(true);
|
||||||
|
save.setEnabled(true);
|
||||||
|
share.setEnabled(true);
|
||||||
|
generate.setEnabled(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String message) {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
|
||||||
|
generate.setEnabled(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClickPlay() {
|
||||||
|
String filename = getApplication().getExternalFilesDir("cache").getAbsolutePath() + "generate/generated.wav";
|
||||||
|
if (mediaPlayer != null) {
|
||||||
|
mediaPlayer.stop();
|
||||||
|
mediaPlayer.release();
|
||||||
|
}
|
||||||
|
mediaPlayer = MediaPlayer.create(
|
||||||
|
getApplicationContext(),
|
||||||
|
Uri.fromFile(new File(filename))
|
||||||
|
);
|
||||||
|
if (mediaPlayer != null) {
|
||||||
|
mediaPlayer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClickStop() {
|
||||||
|
SherpaOnnxTtsManager.getInstance().stop();
|
||||||
|
play.setEnabled(true);
|
||||||
|
save.setEnabled(true);
|
||||||
|
share.setEnabled(true);
|
||||||
|
generate.setEnabled(true);
|
||||||
|
if (mediaPlayer != null) {
|
||||||
|
mediaPlayer.stop();
|
||||||
|
mediaPlayer.release();
|
||||||
|
mediaPlayer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClickSave() {
|
||||||
|
saveLauncher.launch("generated.wav");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClickShare() {
|
||||||
|
File file = new File(getApplication().getExternalFilesDir("cache").getAbsolutePath() + "/generated.wav");
|
||||||
|
if (!file.exists()) {
|
||||||
|
Toast.makeText(getApplicationContext(), "No audio to share", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Uri uri = FileProvider.getUriForFile(
|
||||||
|
this,
|
||||||
|
"com.k2fsa.sherpa.onnx.fileprovider",
|
||||||
|
file
|
||||||
|
);
|
||||||
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
|
intent.setType("audio/wav");
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
startActivity(Intent.createChooser(intent, "Share audio"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyGeneratedWavToUri(Uri destUri) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
package com.ttstd.dialer.tts.sherpa_onnx;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
import android.media.AudioAttributes;
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.media.AudioTrack;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.k2fsa.sherpa.onnx.GeneratedAudio;
|
||||||
|
import com.k2fsa.sherpa.onnx.GenerationConfig;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTts;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsConfig;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class SherpaOnnxTtsManager {
|
||||||
|
private static final String TAG = "SherpaOnnxTtsManager";
|
||||||
|
|
||||||
|
private static final String MODEL_DIR = "vits-piper-zh_CN-xiao_ya-medium";
|
||||||
|
private static final String MODEL_NAME = "zh_CN-xiao_ya-medium.onnx";
|
||||||
|
private static final String LEXICON = "lexicon.txt";
|
||||||
|
|
||||||
|
private static volatile SherpaOnnxTtsManager instance;
|
||||||
|
|
||||||
|
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
private OfflineTts mOfflineTts;
|
||||||
|
private AudioTrack mAudioTrack;
|
||||||
|
private boolean mStopped = false;
|
||||||
|
private boolean mInitialized = false;
|
||||||
|
|
||||||
|
private boolean mSupertonic = false;
|
||||||
|
private String supertonicLang = "en";
|
||||||
|
private String filesDir;
|
||||||
|
|
||||||
|
private SherpaOnnxTtsManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SherpaOnnxTtsManager getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (SherpaOnnxTtsManager.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new SherpaOnnxTtsManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(Context context) {
|
||||||
|
if (mInitialized) return;
|
||||||
|
this.filesDir = context.getApplicationContext().getExternalFilesDir("cache").getAbsolutePath();
|
||||||
|
executorService.execute(() -> {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
initTts(context.getApplicationContext());
|
||||||
|
initAudioTrack();
|
||||||
|
mInitialized = true;
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
Log.i(TAG, "TTS Initialized successfully in " + (endTime - startTime) + " ms");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "TTS Initialization failed", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initTts(Context context) {
|
||||||
|
String dataDir = null;
|
||||||
|
String ruleFsts = null;
|
||||||
|
String ruleFars = null;
|
||||||
|
|
||||||
|
AssetManager assets = context.getAssets();
|
||||||
|
boolean isKitten = false;
|
||||||
|
boolean isSupertonic = false;
|
||||||
|
String durationPredictor = "";
|
||||||
|
String textEncoder = "";
|
||||||
|
String vectorEstimator = "";
|
||||||
|
String supertonicVocoder = "";
|
||||||
|
String ttsJson = "";
|
||||||
|
String unicodeIndexer = "";
|
||||||
|
String voiceStyle = "";
|
||||||
|
String supertonicLang = "en";
|
||||||
|
|
||||||
|
this.mSupertonic = isSupertonic;
|
||||||
|
this.supertonicLang = supertonicLang;
|
||||||
|
|
||||||
|
if (dataDir != null) {
|
||||||
|
String newDir = copyDataDir(context, dataDir);
|
||||||
|
dataDir = newDir + "/" + dataDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
OfflineTtsConfig config = Tts.getOfflineTtsConfig(
|
||||||
|
MODEL_DIR,
|
||||||
|
MODEL_NAME,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
LEXICON,
|
||||||
|
dataDir != null ? dataDir : "",
|
||||||
|
"",
|
||||||
|
ruleFsts != null ? ruleFsts : "vits-piper-zh_CN-xiao_ya-medium/phone.fst,vits-piper-zh_CN-xiao_ya-medium/date.fst,vits-piper-zh_CN-xiao_ya-medium/number.fst",
|
||||||
|
ruleFars != null ? ruleFars : "",
|
||||||
|
null,
|
||||||
|
isKitten,
|
||||||
|
isSupertonic,
|
||||||
|
durationPredictor,
|
||||||
|
textEncoder,
|
||||||
|
vectorEstimator,
|
||||||
|
supertonicVocoder,
|
||||||
|
ttsJson,
|
||||||
|
unicodeIndexer,
|
||||||
|
voiceStyle
|
||||||
|
);
|
||||||
|
|
||||||
|
mOfflineTts = new OfflineTts(assets, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initAudioTrack() {
|
||||||
|
int sampleRate = mOfflineTts.sampleRate();
|
||||||
|
int bufLength = AudioTrack.getMinBufferSize(
|
||||||
|
sampleRate,
|
||||||
|
AudioFormat.CHANNEL_OUT_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_FLOAT
|
||||||
|
);
|
||||||
|
Log.i(TAG, "sampleRate: " + sampleRate + ", buffLength: " + bufLength);
|
||||||
|
|
||||||
|
AudioAttributes attr = new AudioAttributes.Builder()
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
|
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AudioFormat format = new AudioFormat.Builder()
|
||||||
|
.setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
|
||||||
|
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
||||||
|
.setSampleRate(sampleRate)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
mAudioTrack = new AudioTrack(
|
||||||
|
attr, format, bufLength, AudioTrack.MODE_STREAM,
|
||||||
|
AudioManager.AUDIO_SESSION_ID_GENERATE
|
||||||
|
);
|
||||||
|
mAudioTrack.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String copyDataDir(Context context, String dataDir) {
|
||||||
|
Log.i(TAG, "data dir is " + dataDir);
|
||||||
|
copyAssets(context, dataDir);
|
||||||
|
|
||||||
|
File externalFilesDir = context.getExternalFilesDir(null);
|
||||||
|
String newDataDir = externalFilesDir != null ? externalFilesDir.getAbsolutePath() : "";
|
||||||
|
Log.i(TAG, "newDataDir: " + newDataDir);
|
||||||
|
return newDataDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyAssets(Context context, String path) {
|
||||||
|
String[] assets;
|
||||||
|
try {
|
||||||
|
assets = context.getAssets().list(path);
|
||||||
|
if (assets == null || assets.length == 0) {
|
||||||
|
copyFile(context, path);
|
||||||
|
} else {
|
||||||
|
File externalFilesDir = context.getExternalFilesDir(null);
|
||||||
|
String fullPath = externalFilesDir + "/" + path;
|
||||||
|
File dir = new File(fullPath);
|
||||||
|
dir.mkdirs();
|
||||||
|
for (String asset : assets) {
|
||||||
|
String p = path.isEmpty() ? "" : path + "/";
|
||||||
|
copyAssets(context, p + asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Log.e(TAG, "Failed to copy " + path + ". " + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyFile(Context context, String filename) {
|
||||||
|
try (InputStream istream = context.getAssets().open(filename)) {
|
||||||
|
File externalFilesDir = context.getExternalFilesDir(null);
|
||||||
|
String newFilename = externalFilesDir + "/" + filename;
|
||||||
|
try (FileOutputStream ostream = new FileOutputStream(newFilename)) {
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int read;
|
||||||
|
while ((read = istream.read(buffer)) != -1) {
|
||||||
|
ostream.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
ostream.flush();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.e(TAG, "Failed to copy " + filename + ", " + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface TtsCallback {
|
||||||
|
void onStart();
|
||||||
|
|
||||||
|
void onCompleted(String filePath);
|
||||||
|
|
||||||
|
void onError(String message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void speak(String text) {
|
||||||
|
speak(text, 0, 1.0f, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void speak(String text, int sid, float speed, TtsCallback callback) {
|
||||||
|
if (!mInitialized) {
|
||||||
|
Log.e(TAG, "TTS not initialized");
|
||||||
|
if (callback != null) callback.onError("TTS not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executorService.execute(() -> {
|
||||||
|
if (callback != null) callback.onStart();
|
||||||
|
mStopped = false;
|
||||||
|
mAudioTrack.pause();
|
||||||
|
mAudioTrack.flush();
|
||||||
|
mAudioTrack.play();
|
||||||
|
|
||||||
|
GenerationConfig genConfig = new GenerationConfig(sid, speed, 0, null, 0, null, 5, null);
|
||||||
|
if (mSupertonic) {
|
||||||
|
Map<String, String> extra = new HashMap<>();
|
||||||
|
extra.put("lang", supertonicLang);
|
||||||
|
genConfig.setExtra(extra);
|
||||||
|
}
|
||||||
|
GeneratedAudio audio = mOfflineTts.generateWithConfigAndCallback(
|
||||||
|
text,
|
||||||
|
genConfig,
|
||||||
|
new kotlin.jvm.functions.Function1<float[], Integer>() {
|
||||||
|
@Override
|
||||||
|
public Integer invoke(float[] samples) {
|
||||||
|
if (!mStopped) {
|
||||||
|
mAudioTrack.write(samples, 0, samples.length, AudioTrack.WRITE_BLOCKING);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
mAudioTrack.stop();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
mAudioTrack.stop();
|
||||||
|
|
||||||
|
String filename = filesDir + "/generated.wav";
|
||||||
|
boolean ok = audio.getSamples().length > 0 && audio.save(filename);
|
||||||
|
if (ok) {
|
||||||
|
if (callback != null) callback.onCompleted(filename);
|
||||||
|
} else {
|
||||||
|
if (callback != null) callback.onError("Failed to save audio");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
mStopped = true;
|
||||||
|
if (mAudioTrack != null) {
|
||||||
|
mAudioTrack.pause();
|
||||||
|
mAudioTrack.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return mInitialized;
|
||||||
|
}
|
||||||
|
}
|
||||||
148
app/src/main/java/com/ttstd/dialer/tts/sherpa_onnx/Tts.java
Normal file
148
app/src/main/java/com/ttstd/dialer/tts/sherpa_onnx/Tts.java
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// Copyright (c) 2023 Xiaomi Corporation
|
||||||
|
package com.ttstd.dialer.tts.sherpa_onnx;
|
||||||
|
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsConfig;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsKittenModelConfig;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsKokoroModelConfig;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsMatchaModelConfig;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsModelConfig;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsPocketModelConfig;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsSupertonicModelConfig;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsVitsModelConfig;
|
||||||
|
import com.k2fsa.sherpa.onnx.OfflineTtsZipVoiceModelConfig;
|
||||||
|
|
||||||
|
public class Tts {
|
||||||
|
public static OfflineTtsConfig getOfflineTtsConfig(
|
||||||
|
String modelDir,
|
||||||
|
String modelName,
|
||||||
|
String acousticModelName,
|
||||||
|
String vocoder,
|
||||||
|
String voices,
|
||||||
|
String lexicon,
|
||||||
|
String dataDir,
|
||||||
|
String dictDir,
|
||||||
|
String ruleFsts,
|
||||||
|
String ruleFars,
|
||||||
|
Integer numThreads,
|
||||||
|
boolean isKitten,
|
||||||
|
boolean isSupertonic,
|
||||||
|
String durationPredictor,
|
||||||
|
String textEncoder,
|
||||||
|
String vectorEstimator,
|
||||||
|
String supertonicVocoder,
|
||||||
|
String ttsJson,
|
||||||
|
String unicodeIndexer,
|
||||||
|
String voiceStyle
|
||||||
|
) {
|
||||||
|
int numberOfThreads;
|
||||||
|
if (numThreads != null) {
|
||||||
|
numberOfThreads = numThreads;
|
||||||
|
} else if (voices != null && !voices.isEmpty()) {
|
||||||
|
numberOfThreads = 4;
|
||||||
|
} else {
|
||||||
|
numberOfThreads = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSupertonic && (modelName == null || modelName.isEmpty()) && (acousticModelName == null || acousticModelName.isEmpty())) {
|
||||||
|
throw new IllegalArgumentException("Please specify a TTS model");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelName != null && !modelName.isEmpty() && acousticModelName != null && !acousticModelName.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Please specify either a VITS or a Matcha model, but not both");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acousticModelName != null && !acousticModelName.isEmpty() && (vocoder == null || vocoder.isEmpty())) {
|
||||||
|
throw new IllegalArgumentException("Please provide vocoder for Matcha TTS");
|
||||||
|
}
|
||||||
|
|
||||||
|
OfflineTtsVitsModelConfig vits;
|
||||||
|
if (modelName != null && !modelName.isEmpty() && (voices == null || voices.isEmpty()) && !isSupertonic) {
|
||||||
|
vits = new OfflineTtsVitsModelConfig();
|
||||||
|
vits.setModel(modelDir + "/" + modelName);
|
||||||
|
vits.setLexicon(modelDir + "/" + lexicon);
|
||||||
|
vits.setTokens(modelDir + "/tokens.txt");
|
||||||
|
vits.setDataDir(dataDir);
|
||||||
|
} else {
|
||||||
|
vits = new OfflineTtsVitsModelConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
OfflineTtsMatchaModelConfig matcha;
|
||||||
|
if (acousticModelName != null && !acousticModelName.isEmpty()) {
|
||||||
|
matcha = new OfflineTtsMatchaModelConfig();
|
||||||
|
matcha.setAcousticModel(modelDir + "/" + acousticModelName);
|
||||||
|
matcha.setVocoder(vocoder);
|
||||||
|
matcha.setLexicon(modelDir + "/" + lexicon);
|
||||||
|
matcha.setTokens(modelDir + "/tokens.txt");
|
||||||
|
matcha.setDataDir(dataDir);
|
||||||
|
} else {
|
||||||
|
matcha = new OfflineTtsMatchaModelConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
OfflineTtsKokoroModelConfig kokoro;
|
||||||
|
if (voices != null && !voices.isEmpty() && !isKitten && !isSupertonic) {
|
||||||
|
kokoro = new OfflineTtsKokoroModelConfig();
|
||||||
|
kokoro.setModel(modelDir + "/" + modelName);
|
||||||
|
kokoro.setVoices(modelDir + "/" + voices);
|
||||||
|
kokoro.setTokens(modelDir + "/tokens.txt");
|
||||||
|
kokoro.setDataDir(dataDir);
|
||||||
|
if (lexicon == null || lexicon.isEmpty()) {
|
||||||
|
kokoro.setLexicon(lexicon);
|
||||||
|
} else if (lexicon.contains(",")) {
|
||||||
|
kokoro.setLexicon(lexicon);
|
||||||
|
} else {
|
||||||
|
kokoro.setLexicon(modelDir + "/" + lexicon);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
kokoro = new OfflineTtsKokoroModelConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
OfflineTtsZipVoiceModelConfig zipvoice = new OfflineTtsZipVoiceModelConfig();
|
||||||
|
|
||||||
|
OfflineTtsKittenModelConfig kitten;
|
||||||
|
if (isKitten) {
|
||||||
|
kitten = new OfflineTtsKittenModelConfig();
|
||||||
|
kitten.setModel(modelDir + "/" + modelName);
|
||||||
|
kitten.setVoices(modelDir + "/" + voices);
|
||||||
|
kitten.setTokens(modelDir + "/tokens.txt");
|
||||||
|
kitten.setDataDir(dataDir);
|
||||||
|
} else {
|
||||||
|
kitten = new OfflineTtsKittenModelConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
OfflineTtsPocketModelConfig pocket = new OfflineTtsPocketModelConfig();
|
||||||
|
|
||||||
|
OfflineTtsSupertonicModelConfig supertonic;
|
||||||
|
if (isSupertonic) {
|
||||||
|
supertonic = new OfflineTtsSupertonicModelConfig();
|
||||||
|
supertonic.setDurationPredictor(modelDir + "/" + durationPredictor);
|
||||||
|
supertonic.setTextEncoder(modelDir + "/" + textEncoder);
|
||||||
|
supertonic.setVectorEstimator(modelDir + "/" + vectorEstimator);
|
||||||
|
supertonic.setVocoder(modelDir + "/" + supertonicVocoder);
|
||||||
|
supertonic.setTtsJson(modelDir + "/" + ttsJson);
|
||||||
|
supertonic.setUnicodeIndexer(modelDir + "/" + unicodeIndexer);
|
||||||
|
supertonic.setVoiceStyle(modelDir + "/" + voiceStyle);
|
||||||
|
} else {
|
||||||
|
supertonic = new OfflineTtsSupertonicModelConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
OfflineTtsModelConfig modelConfig = new OfflineTtsModelConfig(
|
||||||
|
vits, matcha, kokoro, zipvoice, kitten, pocket, supertonic, numberOfThreads, true, "cpu"
|
||||||
|
);
|
||||||
|
|
||||||
|
return new OfflineTtsConfig(modelConfig, ruleFsts, ruleFars, 1, 0.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OfflineTtsConfig getOfflineTtsConfig(
|
||||||
|
String modelDir,
|
||||||
|
String modelName,
|
||||||
|
String lexicon,
|
||||||
|
String dataDir,
|
||||||
|
String ruleFsts,
|
||||||
|
String ruleFars
|
||||||
|
) {
|
||||||
|
return getOfflineTtsConfig(
|
||||||
|
modelDir, modelName, "", "", "", lexicon, dataDir, "", ruleFsts, ruleFars,
|
||||||
|
null, false, false, "", "", "", "", "", "", ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.ttstd.dialer.utils;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
@@ -150,6 +151,7 @@ public class GlideUtils {
|
|||||||
|
|
||||||
requestManager
|
requestManager
|
||||||
.load(url)
|
.load(url)
|
||||||
|
.placeholder(errorId)
|
||||||
.error(errorId)
|
.error(errorId)
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
}
|
}
|
||||||
@@ -165,6 +167,7 @@ public class GlideUtils {
|
|||||||
|
|
||||||
requestManager
|
requestManager
|
||||||
.load(id)
|
.load(id)
|
||||||
|
.placeholder(errorId)
|
||||||
.error(errorId)
|
.error(errorId)
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
}
|
}
|
||||||
@@ -180,6 +183,7 @@ public class GlideUtils {
|
|||||||
|
|
||||||
requestManager
|
requestManager
|
||||||
.load(url)
|
.load(url)
|
||||||
|
.placeholder(drawable)
|
||||||
.error(drawable)
|
.error(drawable)
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
}
|
}
|
||||||
@@ -195,7 +199,8 @@ public class GlideUtils {
|
|||||||
|
|
||||||
requestManager
|
requestManager
|
||||||
.load(url)
|
.load(url)
|
||||||
.error(bitmap)
|
.placeholder(new BitmapDrawable(context.getResources(), bitmap))
|
||||||
|
.error(new BitmapDrawable(context.getResources(), bitmap))
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.ttstd.dialer.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.ttstd.dialer.utils.ScreenUtils;
|
||||||
|
|
||||||
|
public class ListDividerItemDecoration extends RecyclerView.ItemDecoration {
|
||||||
|
private final Drawable mDivider;
|
||||||
|
private int mMarginLeft = 0;
|
||||||
|
private int mMarginRight = 0;
|
||||||
|
|
||||||
|
public ListDividerItemDecoration(Context context) {
|
||||||
|
int[] attrs = new int[]{android.R.attr.listDivider};
|
||||||
|
mDivider = context.obtainStyledAttributes(attrs).getDrawable(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListDividerItemDecoration(Context context, int marginDp) {
|
||||||
|
this(context);
|
||||||
|
int marginPx = ScreenUtils.dp2px(context.getResources(), marginDp);
|
||||||
|
this.mMarginLeft = marginPx;
|
||||||
|
this.mMarginRight = marginPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListDividerItemDecoration(Context context, int marginLeftDp, int marginRightDp) {
|
||||||
|
this(context);
|
||||||
|
this.mMarginLeft = ScreenUtils.dp2px(context.getResources(), marginLeftDp);
|
||||||
|
this.mMarginRight = ScreenUtils.dp2px(context.getResources(), marginRightDp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListDividerItemDecoration(Drawable divider) {
|
||||||
|
mDivider = divider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
|
||||||
|
if (mDivider == null) {
|
||||||
|
super.getItemOffsets(outRect, view, parent, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int position = parent.getChildAdapterPosition(view);
|
||||||
|
int dividerHeight = mDivider.getIntrinsicHeight();
|
||||||
|
|
||||||
|
// 顶部第一项上方增加偏移,每一项下方都增加偏移
|
||||||
|
int top = (position == 0) ? dividerHeight : 0;
|
||||||
|
outRect.set(0, top, 0, dividerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
|
||||||
|
if (mDivider == null || parent.getLayoutManager() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int left = parent.getPaddingLeft() + mMarginLeft;
|
||||||
|
int right = parent.getWidth() - parent.getPaddingRight() - mMarginRight;
|
||||||
|
|
||||||
|
int childCount = parent.getChildCount();
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
View child = parent.getChildAt(i);
|
||||||
|
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
|
||||||
|
int position = parent.getChildAdapterPosition(child);
|
||||||
|
|
||||||
|
// 第一项上方画线
|
||||||
|
if (position == 0) {
|
||||||
|
int bottom = child.getTop() - params.topMargin;
|
||||||
|
int top = bottom - mDivider.getIntrinsicHeight();
|
||||||
|
mDivider.setBounds(left, top, right, bottom);
|
||||||
|
mDivider.draw(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每一项下方画线
|
||||||
|
int top = child.getBottom() + params.bottomMargin;
|
||||||
|
int bottom = top + mDivider.getIntrinsicHeight();
|
||||||
|
mDivider.setBounds(left, top, right, bottom);
|
||||||
|
mDivider.draw(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ttstd.dialer.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
|
||||||
|
public class NoScrollGridLayoutManager extends GridLayoutManager {
|
||||||
|
|
||||||
|
public NoScrollGridLayoutManager(Context context, int spanCount) {
|
||||||
|
super(context, spanCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canScrollVertically() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canScrollHorizontally() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
175
app/src/main/res/layout/activity_alarm_add.xml
Normal file
175
app/src/main/res/layout/activity_alarm_add.xml
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context=".activity.alarm.add.AlarmAddActivity">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="click"
|
||||||
|
type="com.ttstd.dialer.activity.alarm.add.AlarmAddActivity.BtnClick" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/dp_48"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/default_return_icon_size"
|
||||||
|
android:layout_height="@dimen/default_return_icon_size"
|
||||||
|
android:layout_marginStart="@dimen/dp_8"
|
||||||
|
android:onClick="@{click::exit}"
|
||||||
|
android:src="@drawable/ic_return"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:onClick="@{click::showTimePicker}"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="添加闹钟"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="@dimen/sp_22"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/dp_16"
|
||||||
|
android:onClick="@{click::saveAlarm}"
|
||||||
|
android:text="保存"
|
||||||
|
android:textColor="@color/blue"
|
||||||
|
android:textSize="@dimen/sp_18"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/bar">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="@dimen/dp_16">
|
||||||
|
|
||||||
|
<TimePicker
|
||||||
|
android:id="@+id/timePicker"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:format24Hour="HH:mm"
|
||||||
|
android:timePickerMode="spinner"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etLabel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_16"
|
||||||
|
android:hint="标签(可选)"
|
||||||
|
android:padding="@dimen/dp_12"
|
||||||
|
android:textSize="@dimen/sp_16"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/timePicker" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvRepeatTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_16"
|
||||||
|
android:text="重复"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="@dimen/sp_16"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/etLabel" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/rgRepeat"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_8"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tvRepeatTitle">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rbOnce"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="只响一次"
|
||||||
|
android:textSize="@dimen/sp_14" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rbEveryday"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="每天"
|
||||||
|
android:textSize="@dimen/sp_14" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rbWeekdays"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="周一至周五"
|
||||||
|
android:textSize="@dimen/sp_14" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rbCustom"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="自定义"
|
||||||
|
android:textSize="@dimen/sp_14" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvVibrationTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_16"
|
||||||
|
android:text="振动"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="@dimen/sp_16"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/rgRepeat" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/switchVibration"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/tvVibrationTitle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/tvVibrationTitle" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</layout>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/tv_alarm_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="闹钟响铃中..."
|
android:text="闹钟响铃中..."
|
||||||
|
|||||||
157
app/src/main/res/layout/activity_alarm_edit.xml
Normal file
157
app/src/main/res/layout/activity_alarm_edit.xml
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context=".activity.alarm.edit.AlarmEditActivity">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="click"
|
||||||
|
type="com.ttstd.dialer.activity.alarm.edit.AlarmEditActivity.BtnClick" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="alarmInfo"
|
||||||
|
type="com.ttstd.dialer.db.alarm.AlarmInfo" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/dp_48"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/default_return_icon_size"
|
||||||
|
android:layout_height="@dimen/default_return_icon_size"
|
||||||
|
android:layout_marginStart="@dimen/dp_8"
|
||||||
|
android:onClick="@{click::exit}"
|
||||||
|
android:src="@drawable/ic_return"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="编辑闹钟"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="@dimen/sp_22"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/dp_16"
|
||||||
|
android:onClick="@{click::saveAlarm}"
|
||||||
|
android:text="保存"
|
||||||
|
android:textColor="@color/blue"
|
||||||
|
android:textSize="@dimen/sp_18"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/bar">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="@dimen/dp_16">
|
||||||
|
|
||||||
|
<TimePicker
|
||||||
|
android:id="@+id/timePicker"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:format24Hour="HH:mm"
|
||||||
|
android:timePickerMode="spinner"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etLabel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_16"
|
||||||
|
android:hint="标签(可选)"
|
||||||
|
android:padding="@dimen/dp_12"
|
||||||
|
android:textSize="@dimen/sp_16"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/timePicker" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEnabledTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_16"
|
||||||
|
android:text="启用"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="@dimen/sp_16"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/etLabel" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/switchEnabled"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/tvEnabledTitle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/tvEnabledTitle" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvVibrationTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_16"
|
||||||
|
android:text="振动"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="@dimen/sp_16"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tvEnabledTitle" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/switchVibration"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/tvVibrationTitle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/tvVibrationTitle" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnDelete"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_32"
|
||||||
|
android:backgroundTint="@color/red"
|
||||||
|
android:onClick="@{click::deleteAlarm}"
|
||||||
|
android:text="删除闹钟"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tvVibrationTitle" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</layout>
|
||||||
84
app/src/main/res/layout/activity_alarm_list.xml
Normal file
84
app/src/main/res/layout/activity_alarm_list.xml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context=".activity.alarm.list.AlarmListActivity">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="click"
|
||||||
|
type="com.ttstd.dialer.activity.alarm.list.AlarmListActivity.BtnClick" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/dp_48"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/default_return_icon_size"
|
||||||
|
android:layout_height="@dimen/default_return_icon_size"
|
||||||
|
android:layout_marginStart="@dimen/dp_8"
|
||||||
|
android:onClick="@{click::exit}"
|
||||||
|
android:src="@drawable/ic_return"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="闹钟"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="@dimen/sp_22"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/bar">
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipeRefreshLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:layout_width="@dimen/dp_64"
|
||||||
|
android:layout_height="@dimen/dp_64"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="@dimen/dp_32"
|
||||||
|
android:layout_marginBottom="@dimen/dp_32"
|
||||||
|
android:onClick="@{click::addAlarm}"
|
||||||
|
android:src="@drawable/ic_add"
|
||||||
|
app:fabCustomSize="@dimen/dp_64"
|
||||||
|
app:layout_behavior="com.ttstd.dialer.view.ScrollAwareFABBehavior" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</layout>
|
||||||
@@ -148,11 +148,110 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="@dimen/dp_100"
|
android:layout_width="@dimen/dp_100"
|
||||||
android:layout_height="@dimen/dp_100"
|
android:layout_height="@dimen/dp_100"
|
||||||
|
android:onClick="@{click::openTts}"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="语音设置"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/dp_100"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="@dimen/dp_100"
|
||||||
|
android:layout_height="@dimen/dp_100"
|
||||||
|
android:onClick="@{click::openAlarmClock}"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="闹钟设置"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/dp_100"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="@dimen/dp_100"
|
||||||
|
android:layout_height="@dimen/dp_100"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/dp_100"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="@dimen/dp_100"
|
||||||
|
android:layout_height="@dimen/dp_100"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,14 @@
|
|||||||
app:enableText="@string/enable_text_default_launcher"
|
app:enableText="@string/enable_text_default_launcher"
|
||||||
app:optionsText="@string/options_text_default_launcher" />
|
app:optionsText="@string/options_text_default_launcher" />
|
||||||
|
|
||||||
|
<com.ttstd.dialer.view.SettingItem
|
||||||
|
android:id="@+id/si_hourly_chime"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:disableText="@string/disable_text_hourly_chime"
|
||||||
|
app:enableText="@string/enable_text_hourly_chime"
|
||||||
|
app:optionsText="@string/options_text_hourly_chime" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/dp_100">
|
android:layout_height="@dimen/dp_100">
|
||||||
|
|||||||
130
app/src/main/res/layout/activity_sherpa_onnx_tts.xml
Normal file
130
app/src/main/res/layout/activity_sherpa_onnx_tts.xml
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".tts.sherpa_onnx.SherpaOnnxTtsActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sid_label_hint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/sid_label"
|
||||||
|
android:gravity="center"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/sid"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:hint="@string/sid_hint"
|
||||||
|
android:gravity="center"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/sid_label_hint" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/speed_label_hint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="3dp"
|
||||||
|
android:text="@string/speed_label"
|
||||||
|
android:gravity="center"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/sid" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/speed"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:hint="@string/speed_hint"
|
||||||
|
android:gravity="center"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/speed_label_hint" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:lines="8"
|
||||||
|
android:minLines="10"
|
||||||
|
android:gravity="top|start"
|
||||||
|
android:maxLines="30"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:hint="@string/text_hint"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/speed" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/generate"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/generate"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/play"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/play"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/generate" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/stop"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/stop"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/play" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/save_share_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/stop">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/save"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="2dp"
|
||||||
|
android:text="@string/save" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/share"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="2dp"
|
||||||
|
android:text="@string/share" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context=".fragment.app.AppFragment">
|
tools:context=".fragment.app.AppFragment">
|
||||||
|
|
||||||
@@ -18,7 +17,8 @@
|
|||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"
|
||||||
|
android:nestedScrollingEnabled="false" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|||||||
90
app/src/main/res/layout/item_alarm.xml
Normal file
90
app/src/main/res/layout/item_alarm.xml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/hsv"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:scrollbars="none">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/card_background_color"
|
||||||
|
android:padding="@dimen/dp_16">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="@dimen/sp_36"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_4"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="@dimen/sp_14"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tv_time" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_repeat"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/dp_2"
|
||||||
|
android:textColor="@color/gray"
|
||||||
|
android:textSize="@dimen/sp_12"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tv_label" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/cl_operation"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/switch_enabled"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_delete"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/red"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="删除"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="@dimen/sp_16"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</HorizontalScrollView>
|
||||||
@@ -44,6 +44,10 @@
|
|||||||
<string name="options_text_default_launcher">设置为默认桌面</string>
|
<string name="options_text_default_launcher">设置为默认桌面</string>
|
||||||
<string name="enable_text_default_launcher">已设置为默认桌面</string>
|
<string name="enable_text_default_launcher">已设置为默认桌面</string>
|
||||||
<string name="disable_text_default_launcher">未设置为默认桌面</string>
|
<string name="disable_text_default_launcher">未设置为默认桌面</string>
|
||||||
|
|
||||||
|
<string name="options_text_hourly_chime">开启整点报时</string>
|
||||||
|
<string name="enable_text_hourly_chime">已开启,整点时将自动语音报时</string>
|
||||||
|
<string name="disable_text_hourly_chime">未开启,整点时将自动语音报时</string>
|
||||||
<!--utils end-->
|
<!--utils end-->
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
13
app/src/main/res/values/strings_onnx.xml
Normal file
13
app/src/main/res/values/strings_onnx.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<resources>
|
||||||
|
<!-- <string name="app_name">TTS</string>-->
|
||||||
|
<string name="sid_label">Speaker ID</string>
|
||||||
|
<string name="sid_hint">0</string>
|
||||||
|
<string name="speed_label">Speech speed (large->fast)</string>
|
||||||
|
<string name="speed_hint">1.0</string>
|
||||||
|
<string name="text_hint">Please input your text here</string>
|
||||||
|
<string name="generate">Generate</string>
|
||||||
|
<string name="play">Play</string>
|
||||||
|
<string name="stop">Stop</string>
|
||||||
|
<string name="save">Save</string>
|
||||||
|
<string name="share">Share</string>
|
||||||
|
</resources>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<!-- <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">-->
|
<!-- <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">-->
|
||||||
<!-- <!– Customize your theme here. –>-->
|
<!-- <!– Customize your theme here. –>-->
|
||||||
<!-- <item name="colorPrimary">@color/colorPrimary</item>-->
|
<!-- <item name="colorPrimary">@color/colorPrimary</item>-->
|
||||||
<!-- <item name="colorPrimaryDark">@color/colorPrimaryDark</item>-->
|
<!-- <item name="colorPrimaryDark">@color/colorPrimaryDark</item>-->
|
||||||
<!-- <item name="colorAccent">@color/colorAccent</item>-->
|
<!-- <item name="colorAccent">@color/colorAccent</item>-->
|
||||||
<!-- </style>-->
|
<!-- </style>-->
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
@@ -17,6 +17,13 @@
|
|||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="MaterialTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
<item name="backgroundColor">@color/default_background_color</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="AppThemeFitsSystemWindows" parent="Theme.AppCompat.Light.NoActionBar">
|
<style name="AppThemeFitsSystemWindows" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user