一、项目概述
闹钟是移动设备上一项常见且重要的功能,能够在指定时间唤醒用户或提醒重要事项。本项目基于 Android 原生 API 实现一个功能齐全的闹钟应用,具备以下特性:
-
支持 单次闹钟 与 重复闹钟(每天/每周)
-
可在指定时刻通过 Notification+响铃 唤醒用户
-
通过 SQLite 持久化存储闹钟列表,并可查询/删除
-
使用 前台 Service 播放铃声,保证在 Doze 模式下仍能触发
-
提供简单的 UI 界面,可添加、编辑、删除闹钟
二、相关知识
2.1 AlarmManager 与 PendingIntent
-
AlarmManager:系统提供的定时任务调度器,可设置精确(
setExactAndAllowWhileIdle
)或非精确闹钟。 -
PendingIntent:包装
Intent
,在未来某个时刻由系统或其他应用执行。我们通过PendingIntent.getBroadcast()
将闹钟触发包装为广播。
2.2 BroadcastReceiver
-
BroadcastReceiver:静态或动态注册均可。接收到闹钟广播时,负责启动响铃 Service 或唤醒屏幕。
-
注册方式:此处使用 静态 注册(在
AndroidManifest.xml
中),以保证即使应用被杀,也能收到闹钟广播。
2.3 Notification 与铃声播放
-
Notification:在闹钟触发时,创建可点击的通知,跳转到关闭/延迟界面。
-
铃声播放:使用
MediaPlayer
或RingtoneManager
播放系统默认闹铃,支持循环,直到用户关闭。
2.4 时间格式与时区处理
-
Calendar:用于设置年、月、日、时、分、秒。
-
TimeZone:默认使用设备时区,支持切换。
-
重复闹钟:通过
AlarmManager.setRepeating()
或多次setExact()
实现。
2.5 前台 Service 与电量优化
-
前台 Service:播放铃声时启动为前台,以避免被系统杀死,并在通知栏显示控制按钮(关闭/延迟)。
-
Doze 模式:使用
setExactAndAllowWhileIdle()
保证即使在 Doze 下也能唤醒。
三、实现思路
3.1 架构设计
+-------------+ +------------------+ +------------------+
| MainActivity|--设置闹钟-->| AlarmManager |--时间到--> | AlarmReceiver |
+-------------+ +------------------+ +------------------+
|
v
+------------------+
| AlarmService |
+------------------+
|
v
播放铃声+Notification
3.2 数据存储与闹钟调度
-
用户在主界面输入时间、选择重复周期、备注等。
-
点击“保存”后,写入 SQLite(
DBHelper
),并通过AlarmManager
设置对应的闹钟PendingIntent
。
3.3 收到闹钟广播后的唤醒逻辑
-
系统在指定时间发送广播,
AlarmReceiver
接收。 -
在
onReceive()
中启动AlarmService
(前台 Service),播放铃声并发起通知; -
保证屏幕亮起(可选
WindowManager
FLAG 唤醒)。
3.4 通知与界面交互
-
通知提供“关闭闹钟”“延迟 5 分钟”两个操作按钮,点击后由
AlarmService
处理:-
关闭:停止铃声并调用
AlarmManager.cancel()
删除该闹钟。 -
延迟:重新设置一次 5 分钟后的单次闹钟。
-
四、完整代码
/* MainActivity.java */
package com.example.alarmdemo;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.*;
import android.database.Cursor;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import androidx.appcompat.app.AppCompatActivity;
import java.util.*;
public class MainActivity extends AppCompatActivity {
private EditText etHour, etMinute;
private CheckBox cbRepeat;
private Button btnSave, btnList;
private DBHelper dbHelper;
private AlarmManager alarmManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etHour = findViewById(R.id.et_hour);
etMinute = findViewById(R.id.et_minute);
cbRepeat = findViewById(R.id.cb_repeat);
btnSave = findViewById(R.id.btn_save);
btnList = findViewById(R.id.btn_list);
dbHelper = new DBHelper(this);
alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
btnSave.setOnClickListener(v -> saveAlarm());
btnList.setOnClickListener(v -> showAlarmList());
}
private void saveAlarm() {
int hour = Integer.parseInt(etHour.getText().toString());
int minute = Integer.parseInt(etMinute.getText().toString());
boolean repeat = cbRepeat.isChecked();
long id = dbHelper.insertAlarm(hour, minute, repeat);
scheduleAlarm(id, hour, minute, repeat);
Toast.makeText(this, "闹钟已设置", Toast.LENGTH_SHORT).show();
}
private void scheduleAlarm(long id, int hour, int minute, boolean repeat) {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.putExtra("alarm_id", id);
PendingIntent pi = PendingIntent.getBroadcast(
this, (int)id, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, hour);
c.set(Calendar.MINUTE, minute);
c.set(Calendar.SECOND, 0);
if (repeat) {
alarmManager.setRepeating(
AlarmManager.RTC_WAKEUP,
c.getTimeInMillis(),
AlarmManager.INTERVAL_DAY,
pi);
} else {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
c.getTimeInMillis(),
pi);
}
}
private void showAlarmList() {
Cursor c = dbHelper.queryAll();
StringBuilder sb = new StringBuilder();
while (c.moveToNext()) {
sb.append("ID:").append(c.getLong(0))
.append(" ").append(c.getInt(1))
.append(":").append(c.getInt(2))
.append(c.getInt(3)==1?" 重复":" 单次")
.append("\n");
}
new AlertDialog.Builder(this)
.setTitle("闹钟列表")
.setMessage(sb.toString())
.setPositiveButton("确定", null).show();
}
}
/* AlarmReceiver.java */
package com.example.alarmdemo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
long id = intent.getLongExtra("alarm_id", -1);
Intent service = new Intent(ctx, AlarmService.class);
service.putExtra("alarm_id", id);
ctx.startForegroundService(service);
}
}
/* AlarmService.java */
package com.example.alarmdemo;
import android.app.*;
import android.content.Intent;
import android.media.RingtoneManager;
import android.os.*;
import androidx.core.app.NotificationCompat;
public class AlarmService extends Service {
private MediaPlayer player;
private long alarmId;
@Override
public void onCreate() {
super.onCreate();
alarmId = getIntent().getLongExtra("alarm_id", -1);
startForeground(1, buildNotification());
Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
player = MediaPlayer.create(this, uri);
player.setLooping(true);
player.start();
}
private Notification buildNotification() {
Intent stop = new Intent(this, ActionReceiver.class)
.setAction("STOP").putExtra("alarm_id", alarmId);
PendingIntent piStop = PendingIntent.getBroadcast(
this, 0, stop, PendingIntent.FLAG_UPDATE_CURRENT);
return new NotificationCompat.Builder(this, "alarm_chan")
.setContentTitle("闹钟响铃")
.setContentText("点击操作")
.setSmallIcon(R.drawable.ic_alarm)
.addAction(R.drawable.ic_stop, "关闭", piStop)
.setOngoing(true).build();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
}
@Override public IBinder onBind(Intent i){return null;}
@Override public void onDestroy(){
super.onDestroy();
if (player!=null) player.stop();
}
}
/* ActionReceiver.java */
package com.example.alarmdemo;
import android.content.*;
public class ActionReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context ctx, Intent intent) {
long id = intent.getLongExtra("alarm_id", -1);
// 停止服务
ctx.stopService(new Intent(ctx, AlarmService.class));
// 取消闹钟
AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
PendingIntent pi = PendingIntent.getBroadcast(
ctx, (int)id,
new Intent(ctx, AlarmReceiver.class).putExtra("alarm_id", id),
PendingIntent.FLAG_UPDATE_CURRENT);
am.cancel(pi);
// 删除数据库
new DBHelper(ctx).deleteAlarm(id);
}
}
/* DBHelper.java */
package com.example.alarmdemo;
import android.content.*;
import android.database.Cursor;
import android.database.sqlite.*;
public class DBHelper extends SQLiteOpenHelper {
private static final String DB = "alarms.db";
public DBHelper(Context c){ super(c, DB, null, 1);}
@Override public void onCreate(SQLiteDatabase db){
db.execSQL("CREATE TABLE alarm(id INTEGER PRIMARY KEY, hour INTEGER, minute INTEGER, repeat INTEGER)");
}
@Override public void onUpgrade(SQLiteDatabase db,int o,int n){}
public long insertAlarm(int h,int m,boolean r){
SQLiteDatabase db=getWritableDatabase();
ContentValues v=new ContentValues();
v.put("hour",h);v.put("minute",m);v.put("repeat",r?1:0);
return db.insert("alarm",null,v);
}
public Cursor queryAll(){
return getReadableDatabase().query("alarm",null,null,null,null,null,null);
}
public void deleteAlarm(long id){
getWritableDatabase().delete("alarm","id=?",new String[]{String.valueOf(id)});
}
}
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.alarmdemo">
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<application ...>
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver android:name=".AlarmReceiver"/>
<receiver android:name=".ActionReceiver"/>
<service android:name=".AlarmService" android:foregroundServiceType="mediaPlayback"/>
</application>
</manifest>
<!-- res/layout/activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:padding="16dp"
android:layout_width="match_parent" android:layout_height="match_parent">
<EditText android:id="@+id/et_hour" android:hint="小时(0-23)"
android:inputType="number" android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText android:id="@+id/et_minute" android:hint="分钟(0-59)"
android:inputType="number" android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<CheckBox android:id="@+id/cb_repeat" android:text="每天重复"
android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<Button android:id="@+id/btn_save" android:text="保存闹钟"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<Button android:id="@+id/btn_list" android:text="查看闹钟列表"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
</LinearLayout>
五、代码解读
-
MainActivity
-
saveAlarm()
:读取用户输入,存入 SQLite 并调用scheduleAlarm()
设置系统闹钟; -
scheduleAlarm()
:使用AlarmManager.setExactAndAllowWhileIdle()
或setRepeating()
,以PendingIntent
触发AlarmReceiver
; -
showAlarmList()
:从数据库查询所有闹钟并弹窗展示。
-
-
AlarmReceiver
-
静态注册广播,
onReceive()
中取出闹钟 ID,启动AlarmService
(前台服务)播放铃声。
-
-
AlarmService
-
onCreate()
:调用startForeground()
创建频道通知,并使用RingtoneManager
播放系统闹铃; -
通知中添加“关闭”操作,触发
ActionReceiver
。
-
-
ActionReceiver
-
收到“关闭”广播后,停止铃声服务、取消对应
PendingIntent
闹钟,并从数据库删除该条记录。
-
-
DBHelper
-
使用 SQLite 存储闹钟信息,提供增、查、删三种接口。
-
-
布局与权限
-
在
AndroidManifest.xml
中声明WAKE_LOCK
权限与foregroundServiceType="mediaPlayback"
; -
activity_main.xml
提供小时/分钟输入框、重复开关和操作按钮。
-
六、项目总结
本项目使用 AlarmManager 与 BroadcastReceiver 结合 前台 Service,完整实现了 Android 闹钟功能。关键点包括:
-
精确闹钟调度:使用
setExactAndAllowWhileIdle()
确保在 Doze 模式下准时触发。 -
前台 Service 播放铃声:避免系统在播放过程中杀死服务,并在通知栏提供交互。
-
PendingIntent + Broadcast:通过
PendingIntent.getBroadcast()
触发广播,保证闹钟在应用被杀死后仍可生效。 -
持久化管理:以 SQLite 存储闹钟列表,实现查询、删除与多次重启后数据保留。
后续可扩展功能:
-
自定义铃声与震动模式
-
延迟闹钟(Snooze)
-
UI 优化:列表展示与编辑闹钟
-
节假日过滤