feat: 增加整点报时,增加闹钟

This commit is contained in:
2026-06-01 01:23:35 +08:00
parent 1c89943459
commit 1f6832ac17
60 changed files with 71963 additions and 77 deletions

View File

@@ -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);
}
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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();

View File

@@ -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");
}
}

View 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);
}
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -24,6 +24,10 @@ public class AlarmRepeatConfig {
}
}
public void setCustomDays(ArrayList<Integer> customDays) {
this.customDays = customDays;
}
public int getRepeatType() {
return repeatType;
}

View File

@@ -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();
}
}

View File

@@ -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;

View 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);
}

View File

@@ -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;
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());

View File

@@ -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;
});
}

View File

@@ -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 {
}
}

View File

@@ -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());
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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) {
}
}

View File

@@ -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;
}
}

View 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, "", "", "", "", "", "", ""
);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}