新項目的手機需要實現關機狀態下的鬧鐘,早在剛開始接觸 android 的時候都在想為什麼 android 不支持關機狀態下的一些功能呢?像充電或者鬧鐘什麼的,雖然每個平台的驅動不一樣但上層應用是可以提供統一接口的呀,果然在 4.0 的時候支持關機充電了,關機鬧鐘仍然不在默認支持中。市場上的很多品牌手機也都不支持這個功能,讓很多用慣了 Feature Phone 以及擔心輻射的用戶都不習慣。這次做關機鬧鐘在一些思路上借鑒了關機充電的實現方法。
整體思路如下:
在 uboot 中通過 PMU 判斷開機的原因,如果是 RTC 模塊使能開機則在 uboot 中傳遞啟動參數 androidboot.mode=alarm,然後在 init 進程中判斷啟動模式(當前系統有 3 種啟動模式:normal、charger、alarm),如果是 alarm 模式則啟動 alarm 服務,alarm 服務與應用程序 alarm關聯,因此需要編寫應用程序來實現關機鬧鐘的功能。應用程序主要實現以下幾個個方面的功能:1、顯示關機鬧鐘的 UI 以及當前時間;2、播放鬧鈴;3、讀取 input 事件判斷用戶操作;4、用戶可以在 UI 中選擇懶人模式、開機或者關機。下面逐個解析這幾個功能的實現:
1、UI
在 zygote 沒有啟動之前完成 UI 顯示可以參考 charger 的做法,用 android 的 minui 接口,這些接口實現了圖形的描繪以及固定大小的文字顯示,函數介紹如下:
[cpp]
int gr_init(void); /* 初始化圖形顯示,主要是打開設備、分配內存、初始化一些參數 */
void gr_exit(void); /* 注銷圖形顯示,關閉設備並釋放內存 */
int gr_fb_width(void); /* 獲取屏幕的寬度 */
int gr_fb_height(void); /* 獲取屏幕的高度 */
gr_pixel *gr_fb_data(void); /* 獲取顯示數據緩存的地址 */
void gr_flip(void); /* 刷新顯示內容 */
void gr_fb_blank(bool blank); /* 清屏 */
void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a); /* 設置字體顏色 */
void gr_fill(int x, int y, int w, int h); /* 填充矩形區域,參數分別代表起始坐標、矩形區域大小 */
int gr_text(int x, int y, const char *s); /* 顯示字符串 */
int gr_measure(const char *s); /* 獲取字符串在默認字庫中占用的像素長度 */
void gr_font_size(int *x, int *y); /* 獲取當前字庫一個字符所占的長寬 */
void gr_blit(gr_surface source, int sx, int sy, int w, int h, int dx, int dy); /* 填充由source指定的圖片 */
unsigned int gr_get_width(gr_surface surface); /* 獲取圖片寬度 */
unsigned int gr_get_height(gr_surface surface); /* 獲取圖片高度 */
/* 根據圖片創建顯示資源數據,name為圖片在mk文件指定的相對路徑 */
int res_create_surface(const char* name, gr_surface* pSurface);
void res_free_surface(gr_surface surface); /* 釋放資源數據 */
圖片只支持 png 格式,做這個 UI 的圖片資源花了不少時間(沒做過美工),一般圖片的顯示先由 res_create_surface 創建資源數據,然後調用 gr_blit 填充,最後調用 gr_flip 刷新顯示。在關機鬧鐘的界面還需要顯示當前時間,最開始調用 minui 默認的字庫來顯示,但是默認字庫的字體太小了,只支持 10 x 18 ASIC-II 編碼的字符,效果很不好,後來就把時間需要的 10 個數字以及符號以圖片的形式顯示。
2、鬧鈴
在這個階段播放鬧鈴只能選擇 tinyplay,tinyplay 是 android 自帶的一款簡易播放器,只能播放固定格式的 wav 文件。UI 顯示以及播放鬧鈴分別獨占一個線程,以保證各自不被干擾。
3、input evnt
當鬧鐘開始響後,用戶可以通過觸摸屏點擊選擇是否開關機或者進入懶人模式,這裡就需要對用戶操作做出判斷,即在程序中去讀取 /dev/input 下面設備的數據。當進入懶人模式後會停止鬧鈴 5 分鐘再響,這個階段需要關閉 lcd 和 觸摸屏,用戶可以通過按鍵喚醒 lcd。input event 是在進程去循環讀取並處理的,示例代碼如下:
[cpp]
static int event_loop(void)
{
int i;
int ret = 0;
int nfds = ALARM_MAX_DEVICE;
struct input_event event;
const char *device = NULL;
const char *device_path = "/dev/input";
ret = scan_dir(device_path); /* 掃描該目錄下的設備節點,我們只打開觸摸屏和按鍵 */
if(ret < 0) {
printf("scan dir failed for %s.\n", device_path);
return ret;
}
for(;;) {
poll(ufds, nfds, -1); /* 輪詢檢測是否有觸摸屏或者按鍵事件 */
for (i = 0; i < nfds; i++) {
if(ufds[i].revents) { /* have valid value. */
if(ufds[i].revents & POLLIN) {
ret = read(ufds[i].fd, &event, sizeof(event)); /* 讀取事件 */
if (ret < (int)sizeof(event)) {
printf("could not get event.\n");
continue;
}
handle_event(event.type, event.code, event.value); /* 處理事件 */
}
}
}
}
return 0;
}
附:android 權限管理機制
這次在編譯使用 alarm 的時候遇到了一個關於權限的問題:編譯出來的 alarm 可執行程序在 out 目錄下面是擁有可執行權限的,但是在燒錄到機器後發現沒有可執行權限了,最後才發現是 android 的權限管理機制引起的。