feat: 增加整点报时,增加闹钟
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
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.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
@@ -8,14 +12,18 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.ttstd.dialer.R;
|
||||
import com.ttstd.dialer.db.alarm.AlarmInfo;
|
||||
import com.zackratos.ultimatebarx.ultimatebarx.java.UltimateBarX;
|
||||
|
||||
public class AlarmAlertActivity extends AppCompatActivity {
|
||||
|
||||
private Ringtone ringtone;
|
||||
private int alarmId = -1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -25,14 +33,44 @@ public class AlarmAlertActivity extends AppCompatActivity {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
setShowWhenLocked(true);
|
||||
setTurnScreenOn(true);
|
||||
KeyguardManager km = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
|
||||
if (km != null) {
|
||||
km.requestDismissKeyguard(this, null);
|
||||
}
|
||||
} else {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
| 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);
|
||||
|
||||
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);
|
||||
btnStop.setOnClickListener(v -> {
|
||||
if (ringtone != null && ringtone.isPlaying()) {
|
||||
@@ -41,11 +79,18 @@ public class AlarmAlertActivity extends AppCompatActivity {
|
||||
finish();
|
||||
});
|
||||
|
||||
playAlarmRingtone();
|
||||
playAlarmRingtone(alarmInfo);
|
||||
}
|
||||
|
||||
private void playAlarmRingtone() {
|
||||
Uri alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
|
||||
private void playAlarmRingtone(AlarmInfo alarmInfo) {
|
||||
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) {
|
||||
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
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (ringtone != null && ringtone.isPlaying()) {
|
||||
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 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.utils.SettingsUtilsActivity;
|
||||
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
|
||||
import com.ttstd.dialer.databinding.ActivitySettingsBinding;
|
||||
import com.ttstd.dialer.tts.sherpa_onnx.SherpaOnnxTtsActivity;
|
||||
|
||||
public class SettingsActivity extends BaseMvvmActivity<SettingsViewModel, ActivitySettingsBinding> {
|
||||
private static final String TAG = "SettingsActivity";
|
||||
@@ -63,6 +65,14 @@ public class SettingsActivity extends BaseMvvmActivity<SettingsViewModel, Activi
|
||||
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) {
|
||||
String url = "https://www.ttstd.com";
|
||||
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
|
||||
@@ -128,6 +141,10 @@ public class SettingsUtilsActivity extends BaseMvvmActivity<SettingsUtilsViewMod
|
||||
boolean defaultLauncher = SystemUtils.isDefaultLauncher(SettingsUtilsActivity.this, MainActivity.class);
|
||||
Logger.e(TAG, "initView: defaultLauncher = " + 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 {
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull ContactInfoHolder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
GlideUtils.clearImage(holder.nv_avatar);
|
||||
}
|
||||
|
||||
|
||||
// 处理拖动交换位置
|
||||
public void onItemMove(int fromPosition, int toPosition) {
|
||||
|
||||
@@ -85,6 +85,12 @@ public class HomeContactAdapter extends RecyclerView.Adapter<HomeContactAdapter.
|
||||
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) {
|
||||
|
||||
@@ -82,6 +82,12 @@ public class HourlyWeatherAdapter extends RecyclerView.Adapter<HourlyWeatherAdap
|
||||
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 {
|
||||
TextView tv_time, tv_temp;
|
||||
ImageView iv_icon;
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -93,6 +94,12 @@ public class WeatherAdapter extends RecyclerView.Adapter<WeatherAdapter.WeatherH
|
||||
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 {
|
||||
|
||||
TextView tv_date, tv_temp_min, tv_temp_max;
|
||||
|
||||
@@ -7,19 +7,57 @@ import android.content.Intent;
|
||||
import android.os.Build;
|
||||
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 {
|
||||
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)
|
||||
*
|
||||
* @param context 上下文
|
||||
* @param alarmId 闹钟ID,用于区分不同的 PendingIntent
|
||||
* @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);
|
||||
if (alarmManager == null) return;
|
||||
|
||||
@@ -35,12 +73,14 @@ public class AlarmManagerHelper {
|
||||
}
|
||||
|
||||
Intent intent = new Intent(context, AlarmReceiver.class);
|
||||
intent.putExtra("ALARM_ID", alarmId);
|
||||
|
||||
// 使用 FLAG_IMMUTABLE 或 FLAG_MUTABLE 增强 Android 12+ 安全性
|
||||
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, ALARM_REQUEST_CODE, intent, flags);
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, alarmId, intent, flags);
|
||||
|
||||
// 核心兼容性定时逻辑
|
||||
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);
|
||||
if (alarmManager != null) {
|
||||
Intent intent = new Intent(context, AlarmReceiver.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, ALARM_REQUEST_CODE, intent, flags);
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, alarmId, intent, flags);
|
||||
alarmManager.cancel(pendingIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ public class AlarmRepeatConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public void setCustomDays(ArrayList<Integer> customDays) {
|
||||
this.customDays = customDays;
|
||||
}
|
||||
|
||||
public int getRepeatType() {
|
||||
return repeatType;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.kongzue.dialogx.DialogX;
|
||||
import com.tencent.bugly.crashreport.CrashReport;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
import com.ttstd.dialer.BuildConfig;
|
||||
import com.ttstd.dialer.alarmclock.AlarmManagerHelper;
|
||||
import com.ttstd.dialer.config.CommonConfig;
|
||||
import com.ttstd.dialer.config.SystemIntentAction;
|
||||
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.push.PushExecutor;
|
||||
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.NativeUtils;
|
||||
import com.ttstd.dialer.utils.SystemUtils;
|
||||
@@ -129,6 +132,13 @@ public class BaseApplication extends Application {
|
||||
|
||||
WeatherManager.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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ public class CommonConfig {
|
||||
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_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 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.os.Bundle;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
||||
import com.ttstd.dialer.R;
|
||||
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.utils.Logger;
|
||||
import com.ttstd.dialer.view.EqualSpaceDecoration;
|
||||
import com.ttstd.dialer.view.NoScrollGridLayoutManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -82,9 +84,19 @@ public class AppFragment extends BaseMvvmFragment<AppViewModel, FragmentAppBindi
|
||||
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.setNestedScrollingEnabled(false);
|
||||
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) {
|
||||
Logger.e(TAG, "initView: mAppInfos size = " + mAppInfos.size());
|
||||
|
||||
@@ -17,6 +17,8 @@ import androidx.lifecycle.Observer;
|
||||
import com.hjq.toast.Toaster;
|
||||
import com.jeremyliao.liveeventbus.LiveEventBus;
|
||||
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.activity.contact.list.ContactListActivity;
|
||||
import com.ttstd.dialer.activity.weather.main.WeatherMainActivity;
|
||||
@@ -110,6 +112,7 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
|
||||
protected void initView(Bundle bundle) {
|
||||
mFestivalUtils = new LunarCalendarFestivalUtils();
|
||||
weatherUpdateManager = WeatherUpdateManager.Companion.getInstance();
|
||||
initWeather();
|
||||
observeWeatherUpdates();
|
||||
WeatherManager.getInstance().refreshweather();
|
||||
setTime();
|
||||
@@ -159,15 +162,45 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
|
||||
}
|
||||
}
|
||||
|
||||
private void initWeather() {
|
||||
WeatherNowResponse nowCache = WeatherManager.getInstance().getWeatherNowCache();
|
||||
if (nowCache != null) {
|
||||
updateWeatherNowUI(nowCache);
|
||||
}
|
||||
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.tvTempCurrent.setText(weatherNowResponse.getNow().getTemp() + "°");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWeatherDailyUI(WeatherDailyResponse weatherDailyResponse) {
|
||||
if (weatherDailyResponse != null && weatherDailyResponse.getDaily() != null && !weatherDailyResponse.getDaily().isEmpty()) {
|
||||
WeatherDaily weatherDaily = weatherDailyResponse.getDaily().get(0);
|
||||
mViewDataBinding.tvTemp.setText(weatherDaily.getTempMin() + "°/" + weatherDaily.getTempMax() + "°");
|
||||
String iconDay = weatherDaily.getIconDay();
|
||||
// 获取资源ID
|
||||
String fileName = "qweather_" + iconDay; // 替换为你的文件名
|
||||
int resId = mContext.getResources().getIdentifier(fileName, "drawable", mContext.getPackageName());
|
||||
if (resId != 0) {
|
||||
mViewDataBinding.ivQweatherIcon.setImageDrawable(mContext.getDrawable(resId));
|
||||
} else {
|
||||
// 处理错误
|
||||
Logger.e("GlideLoad", "Raw resource not found: " + fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void observeWeatherUpdates() {
|
||||
// 观察当前天气更新
|
||||
weatherNowJob = weatherUpdateManager.observeWeatherNow(weatherScope, weatherNowResponse -> {
|
||||
if (weatherNowResponse != null) {
|
||||
Logger.e(TAG, "observeWeatherUpdates", "weatherNowResponse = " + weatherNowResponse);
|
||||
mViewDataBinding.tvWeather.setText(weatherNowResponse.getNow().getText());
|
||||
mViewDataBinding.tvTempCurrent.setText(weatherNowResponse.getNow().getTemp() + "°");
|
||||
}
|
||||
|
||||
updateWeatherNowUI(weatherNowResponse);
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -179,22 +212,7 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
|
||||
});
|
||||
|
||||
weatherDailyJob = weatherUpdateManager.observeWeatherDaily(weatherScope, weatherDailyResponse -> {
|
||||
if (weatherDailyResponse != null) {
|
||||
Logger.e(TAG, "observeWeatherUpdates", "weatherDailyResponse = " + weatherDailyResponse);
|
||||
|
||||
WeatherDaily weatherDaily = weatherDailyResponse.getDaily().get(0);
|
||||
mViewDataBinding.tvTemp.setText(weatherDaily.getTempMin() + "°/" + weatherDaily.getTempMax() + "°");
|
||||
String iconDay = weatherDaily.getIconDay();
|
||||
// 获取资源ID
|
||||
String fileName = "qweather_" + iconDay; // 替换为你的文件名
|
||||
int resId = mContext.getResources().getIdentifier(fileName, "drawable", mContext.getPackageName());
|
||||
if (resId != 0) {
|
||||
mViewDataBinding.ivQweatherIcon.setImageDrawable(mContext.getDrawable(resId));
|
||||
} else {
|
||||
// 处理错误
|
||||
Logger.e("GlideLoad", "Raw resource not found: " + fileName);
|
||||
}
|
||||
}
|
||||
updateWeatherDailyUI(weatherDailyResponse);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,14 +77,20 @@ public class WeatherManager {
|
||||
WeatherNowResponse weatherNowResponse = getWeatherNowCache();
|
||||
if (weatherNowResponse != null) {
|
||||
mWeatherUpdateManager.publishWeatherNowUpdate(weatherNowResponse);
|
||||
} else {
|
||||
|
||||
}
|
||||
WeatherHourlyResponse weatherHourlyResponse = getWeather24hCache();
|
||||
if (weatherHourlyResponse != null) {
|
||||
mWeatherUpdateManager.publishWeatherHourlyUpdate(weatherHourlyResponse);
|
||||
} else {
|
||||
|
||||
}
|
||||
WeatherDailyResponse weatherDailyResponse = getWeather10DCache();
|
||||
if (weatherDailyResponse != null) {
|
||||
mWeatherUpdateManager.publishWeatherDailyUpdate(weatherDailyResponse);
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Parcelable;
|
||||
|
||||
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.AlarmRepeatConfig;
|
||||
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 {
|
||||
private static final String TAG = "AlarmReceiver";
|
||||
private static final String CHANNEL_ID = "alarm_channel";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// 1. 执行原有的响铃和弹出通知/界面的逻辑
|
||||
// (调用上一次回答中创建的通知栏或全屏拉起逻辑)
|
||||
showAlarmNotification(context);
|
||||
int alarmId = intent.getIntExtra("ALARM_ID", -1);
|
||||
Logger.d(TAG, "收到闹钟广播, ID: " + alarmId);
|
||||
|
||||
// 2. 核心:动态轮转,实现重复闹钟
|
||||
scheduleNextRepeatAlarm(context);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (alarmId == -1) {
|
||||
Logger.e(TAG, "收到闹钟广播但 ALARM_ID 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是 每天 或 周一至周五
|
||||
AlarmRepeatConfig config = new AlarmRepeatConfig(repeatType);
|
||||
// 使用同步方法获取闹钟信息
|
||||
AlarmRepository repository = new AlarmRepository(context);
|
||||
AlarmInfo alarmInfo = repository.getAlarmById(alarmId);
|
||||
|
||||
// 重新计算下一次的时间(由于此时已经过了当前闹钟点,计算出的必定是未来的时间)
|
||||
long nextTriggerTime = AlarmTimeCalculator.calculateNextAlarmTime(hour, minute, config);
|
||||
if (alarmInfo == null) {
|
||||
Logger.e(TAG, "未找到对应的闹钟数据, ID: " + alarmId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 再次调用第一步写好的精准闹钟设置工具,埋下新的定时炸弹
|
||||
AlarmManagerHelper.setExactAlarm(context, nextTriggerTime);
|
||||
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 showAlarmNotification(Context context) {
|
||||
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);
|
||||
if (notificationManager == null) return;
|
||||
|
||||
@@ -68,23 +110,27 @@ public class AlarmReceiver extends BroadcastReceiver {
|
||||
|
||||
// 2. 构建点击通知或全屏弹出的 Intent
|
||||
Intent alertIntent = new Intent(context, AlarmAlertActivity.class);
|
||||
alertIntent.putExtra("AlarmInfo", (Parcelable) alarmInfo);
|
||||
alertIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE :
|
||||
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)
|
||||
.setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
|
||||
.setContentTitle("闹钟响了")
|
||||
.setContentText("时间到了,快起床!")
|
||||
.setContentText(alarmInfo.getLabel() != null && !alarmInfo.getLabel().isEmpty() ?
|
||||
alarmInfo.getLabel() : "时间到了,快起床!")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
||||
.setAutoCancel(true)
|
||||
.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.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.widget.ImageView;
|
||||
|
||||
@@ -150,6 +151,7 @@ public class GlideUtils {
|
||||
|
||||
requestManager
|
||||
.load(url)
|
||||
.placeholder(errorId)
|
||||
.error(errorId)
|
||||
.into(imageView);
|
||||
}
|
||||
@@ -165,6 +167,7 @@ public class GlideUtils {
|
||||
|
||||
requestManager
|
||||
.load(id)
|
||||
.placeholder(errorId)
|
||||
.error(errorId)
|
||||
.into(imageView);
|
||||
}
|
||||
@@ -180,6 +183,7 @@ public class GlideUtils {
|
||||
|
||||
requestManager
|
||||
.load(url)
|
||||
.placeholder(drawable)
|
||||
.error(drawable)
|
||||
.into(imageView);
|
||||
}
|
||||
@@ -195,7 +199,8 @@ public class GlideUtils {
|
||||
|
||||
requestManager
|
||||
.load(url)
|
||||
.error(bitmap)
|
||||
.placeholder(new BitmapDrawable(context.getResources(), bitmap))
|
||||
.error(new BitmapDrawable(context.getResources(), bitmap))
|
||||
.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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user