Android实现简易闹钟(附带源码)

一、项目概述

闹钟是移动设备上一项常见且重要的功能,能够在指定时间唤醒用户或提醒重要事项。本项目基于 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:在闹钟触发时,创建可点击的通知,跳转到关闭/延迟界面。

  • 铃声播放:使用 MediaPlayerRingtoneManager 播放系统默认闹铃,支持循环,直到用户关闭。

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 数据存储与闹钟调度

  1. 用户在主界面输入时间、选择重复周期、备注等。

  2. 点击“保存”后,写入 SQLite(DBHelper),并通过 AlarmManager 设置对应的闹钟 PendingIntent

3.3 收到闹钟广播后的唤醒逻辑

  1. 系统在指定时间发送广播,AlarmReceiver 接收。

  2. onReceive() 中启动 AlarmService(前台 Service),播放铃声并发起通知;

  3. 保证屏幕亮起(可选 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>

 

五、代码解读

  1. MainActivity

    • saveAlarm():读取用户输入,存入 SQLite 并调用 scheduleAlarm() 设置系统闹钟;

    • scheduleAlarm():使用 AlarmManager.setExactAndAllowWhileIdle()setRepeating(),以 PendingIntent 触发 AlarmReceiver

    • showAlarmList():从数据库查询所有闹钟并弹窗展示。

  2. AlarmReceiver

    • 静态注册广播,onReceive() 中取出闹钟 ID,启动 AlarmService(前台服务)播放铃声。

  3. AlarmService

    • onCreate():调用 startForeground() 创建频道通知,并使用 RingtoneManager 播放系统闹铃;

    • 通知中添加“关闭”操作,触发 ActionReceiver

  4. ActionReceiver

    • 收到“关闭”广播后,停止铃声服务、取消对应 PendingIntent 闹钟,并从数据库删除该条记录。

  5. DBHelper

    • 使用 SQLite 存储闹钟信息,提供增、查、删三种接口。

  6. 布局与权限

    • AndroidManifest.xml 中声明 WAKE_LOCK 权限与 foregroundServiceType="mediaPlayback"

    • activity_main.xml 提供小时/分钟输入框、重复开关和操作按钮。


六、项目总结

本项目使用 AlarmManagerBroadcastReceiver 结合 前台 Service,完整实现了 Android 闹钟功能。关键点包括:

  1. 精确闹钟调度:使用 setExactAndAllowWhileIdle() 确保在 Doze 模式下准时触发。

  2. 前台 Service 播放铃声:避免系统在播放过程中杀死服务,并在通知栏提供交互。

  3. PendingIntent + Broadcast:通过 PendingIntent.getBroadcast() 触发广播,保证闹钟在应用被杀死后仍可生效。

  4. 持久化管理:以 SQLite 存储闹钟列表,实现查询、删除与多次重启后数据保留。

后续可扩展功能:

  • 自定义铃声与震动模式

  • 延迟闹钟(Snooze)

  • UI 优化:列表展示与编辑闹钟

  • 节假日过滤

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值