原文作者:铸道的汉子
原文博客: samurai.blog.chinaunix.net
 
--------------------------------------------------------------------------

 1.背景

   转到Android组多日,总有些空虚感,因为之前在WinCE都是做的很底层的驱动,像显示驱动、USB Device驱动、USB Host EHCI、OHCI、2D加速驱动,显示驱动还使用到了NEON机器码!没错是机器码,因为VS2005的ARMASM编译器不支持Cortex-A8才有的NEON指令,所以只好写机器码代替。而在Android这边因为刚刚入手,Framework都不是很熟,只能先做一些简单的任务,这周主要就是完成一个关机的Appwidget。
   Appwidget直译是窗口小部件,类似Win7系统里面桌面中的小闹钟、日历等,在Android中可以自由拖放。下面是一个闹钟的Appwidget。

  好了,废话不多说,我们先分析怎么实现这些功能。
2.分析
2.1Android关机流程
   Android关机流程的介绍网上很多,现在摘抄一段如下:

 
    
  1. 关机动作从按键触发中断,linux kernel层给android framework层返回按键事件进入 framework层,再从 framework层到kernel层执行kernel层关机任务。 
  2.  
  3. 长按键对应的handler代码: 
  4.  
  5. frameworks/policies/base/phone/com/android/internal/policy/impl/PhoneWindowManager.java 
  6. Runnable mPowerLongPress; 
  7.  
  8. private final Runnable mPowerLongPress = new Runnable() { 
  9. public void run() { 
  10. if (!mPowerKeyHandled) { 
  11. mPowerKeyHandled = true
  12. performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false); 
  13. sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS); 
  14. showGlobalActionsDialog(); 
  15. }; 
  16.  
  17. mPowerLongPress 启动关机对话框 
  18.  
  19. (frameworks/policies/base/phone/com/android/internal/policy/impl/GlobalActions.java) 
  20.  
  21. 如果我们选择Power OFF’,会调用 ShutdownThread.shutdown. 启动关机线程执行关机动作。 
  22.  
  23. frameworks/base/core/java/com/android/internal/app/ShutdownThread.java 
  24.  
  25. 真正关机 流程: 
  26.  
  27. (1)广播全局事件, ACTION_SHUTDOWN Intent 
  28.  
  29. (2)shutdown ActivityManager 服务 
  30.  
  31. (3) 停止蓝牙服务 
  32.  
  33. (4) 停止 电话服务 (radio phone service) 
  34.  
  35. (5)停止mount 服务 
  36.  
  37. (6) 调用 Power.shutdown() 进入native 层 
  38.  
  39. frameworks/base/core/java/android/os/Power.java 
  40.  
  41. power的native实现代码: 
  42.  
  43. frameworks/base/core/jni/android_os_Power.cpp 
  44.  
  45. static void android_os_Power_shutdown(JNIEnv *env, jobject clazz) 
  46. sync(); 
  47. #ifdef HAVE_ANDROID_OS 
  48. reboot(RB_POWER_OFF); 
  49. #endif 
  50.  
  51. sync, reboot 为linux系统调用,进入linux内核关机流程。 
  52.  
  53. 完毕。 

仔细按照上面说的流程跟下去,确实是这样的,只不过根据产品的不同,会有一定的修改,例如产品是平板电脑,就会比手机少很多废话对话框,如果是智能电视,则又会有不同,对于这个任务来说,比较重要的ShutdownThread.java这个文件,这个文件启动了关机的对话框,关机对话框效果如图:

PhoneWindowManager中调用ShutdownThread的代码如下:

 
    
  1. mPowerKeyHandled = true
  2. performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false); 
  3. sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS); 
  4. ShutdownThread.shutdown(mContext, true); 

传入第一个参数不解释,第二个参数是是否显示对话框,true就是显示。

2.2应用中关机的方法

  凭借网络这个好老师,我找到了好几种实现关机的方法,一是通过向控制台写shutdown命令完成,Android是建立在Linux基础上的,所以这种方法需要Root。
  二是启动ShutdownThread中的对话框,PhoneWindowManager直接通过一句ShutdownThread.shutdown(mContext, true);就启动了,后面我也试过这种方法,确实可以启动,但是点击确定却一直关不了机。所以靠谱的方法还是通过Intent启动。方法如下:

 
    
  1. Intent shutdown = new Intent(Intent.ACTION_REQUEST_SHUTDOWN); 
  2. shutdown.putExtra(Intent.EXTRA_KEY_CONFIRM, true); 
  3. shutdown.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); 
  4. startActivity(shutdown); 

    这上面的一些Flag值AndroidSDK中并没有公开,所以工程要放在Android源码中才能编译通过,除此之外还需要设置系统权限,需要通过修改AndroidManfast.xml文件实现。

2.3Appwidget的框架
  
个人感觉AppWidget像是嵌入launcher中的View,和AppWidget本身Activity的不是运行在同一个进程中,所以控制Appwidget都需要用RemoteViews控制。
    桌面上有多个不同的AppWidget,如何得知哪一个被激活呢,这主要通过广播完成,广播发出的是在AndroidManfast.xml中定义好的android:name,接收时需要在OnReceive()方法中进行判断。
    AndroidMainfast.xml摘录如下:

 
    
  1. <receiver android:name="AppWidget"> 
  2.             <intent-filter> 
  3.             <action  
  4.                 android:name="android.appwidget.action.APPWIDGET_UPDATE"> 
  5.             </action> 
  6.             </intent-filter> 
  7.                 <meta-data android:name="android.appwidget.provider" 
  8.                             android:resource="@xml/appwidget01" /> 
  9.              <intent-filter> 
  10.                 <action android:name="com.android.shutdownapp"></action> 
  11.             </intent-filter> 
  12. </receiver> 

   Appwidget的处理片段如下:

 
    
  1. @Override 
  2.     public void onReceive(Context context, Intent intent) 
  3.     { 
  4.         if (intent.getAction().equals(broadCastString)) 
  5.         {                 
  6.             RemoteViews remoteViews = new RemoteViews(context.getPackageName(),R.layout.appwidgetlayout); 
  7.             remoteViews.setTextViewText(R.id.btnSend, "shutdown");     
  8.             AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); 
  9.             ComponentName componentName = new ComponentName(context,AppWidget.class); 
  10.             appWidgetManager.updateAppWidget(componentName, remoteViews); 
  11.         } 
  12.         super.onReceive(context, intent); 
  13.     } 


3.实现

3.1建立Appwidget工程
  
同建立普通Activity工程一样,不过要手动添加一些XML文件,这里有一篇写的很不错的博文,推荐一下,可以按照他介绍的方法一步步建立工程:
        http://www.cnblogs.com/qianlifeng/archive/2011/03/26/1996407.html

3.2把Eclipse工程加入到Android工程中
  
建立了Demo之后,需要将其添加到Android工程目录中去编译,因为我们调用了一些SDK中并没有公开的方法,建议拷贝到的目录是\android4.0.1\development\apps下,然后删除一些Eclipse的文件,只留下\res、\src、\AndroidManifast.xml这几个文件和目录。
   接着我们需要一个Android.mk文件,简单,到同级的其他工程中拷贝一个就是,但需要修改LOCAL_PACKAGE_NAME := 为你工程的名字,另外还要有LOCAL_CERTIFICATE := platform这一行存在,我的MK文件如下:

 
    
  1. LOCAL_PATH:= $(call my-dir) 
  2. include $(CLEAR_VARS) 
  3.  
  4. LOCAL_MODULE_TAGS := optional 
  5.  
  6. LOCAL_SRC_FILES := $(call all-subdir-java-files) 
  7.  
  8. LOCAL_PACKAGE_NAME := ShutdownAppWidget 
  9. LOCAL_CERTIFICATE := platform 
  10.  
  11. include $(BUILD_PACKAGE) 

3.3添加工程的系统属性
  
因为调用到了系统对话框,所以整个工程需要系统属性,这通过修改AndroidManifastwen
文件实现,修改后的文件内容如下(注意其中绿色的内容:android:sharedUserId & 两permission):

 
    
  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android" 
  3.       package="com.android.shutdownapp" 
  4.      android:sharedUserId="android.uid.system"> 
  5.  
  6.     <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW "/> 
  7.     <uses-permission android:name="android.permission.BIND_APPWIDGET" /> 
  8.      
  9.     <application android:icon="@drawable/icon" android:label="@string/app_name"> 
  10.         <activity android:name=".ShutdownApp" 
  11.                   android:label="@string/app_name"> 
  12.             <intent-filter> 
  13.                 <action android:name="android.intent.action.MAIN" /> 
  14.                 <category android:name="android.intent.category.LAUNCHER" /> 
  15.             </intent-filter> 
  16.         </activity> 
  17.         <receiver android:name="AppWidget"> 
  18.             <intent-filter> 
  19.                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"></action> 
  20.             </intent-filter> 
  21.             <meta-data android:name="android.appwidget.provider" 
  22.                 android:resource="@xml/appwidget01" /> 
  23.             <intent-filter> 
  24.                 <action android:name="com.android.shutdownapp"></action> 
  25.             </intent-filter> 
  26.         </receiver> 
  27.     </application> 
  28.      
  29. </manifest> 


3.4把Appwidget添加到launcher中
    添加到Launcher中可以实现烧写镜像后AppWidget就存在于界面上,不需要人手工去拖,对应的配置文件在以下的位置:

 
    
  1. /android/packages/apps/Launcher2/res/xml/default_workspace.xml 

    这个文件的摘录如下:

 
    
  1. <favorites xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher"> 
  2.     <!-- Far-left screen [0] --> 
  3.  
  4.     <!-- Left screen [1] --> 
  5.     <appwidget 
  6.         launcher:packageName="com.android.settings" 
  7.         launcher:className="com.android.settings.widget.SettingsAppWidgetProvider" 
  8.         launcher:screen="1" 
  9.         launcher:x="0" 
  10.         launcher:y="3" 
  11.         launcher:spanX="4" 
  12.         launcher:spanY="1" /> 
  13.  
  14.     <!-- Middle screen [2] --> 
  15.     <appwidget 
  16.         launcher:packageName="com.android.deskclock" 
  17.         launcher:className="com.android.alarmclock.AnalogAppWidgetProvider" 
  18.         launcher:screen="2" 
  19.         launcher:x="1" 
  20.         launcher:y="0" 
  21.         launcher:spanX="2" 
  22.         launcher:spanY="2" /> 


   文件中已经有配置的例子,可以按照其中的例子自己去配,需要主要的是其中的坐标不是像素,而是一些定义好的点,在点击屏幕安放AppWidget时就可以看见一些点,这些就是对应的坐标位置。

4.效果
AppWidget显示如下:

按下后效果如下:

还不太完善,显示了“Android系统”的背景还不知如何去掉,另外界面也不太友好。

5.总结
 
和同事沟通确实很重要,之前一直不知道怎样在Launcher中配置Appwidget,结果问了一个阅读过Launcher的同事很快就找到方法了。
   另外发现自己有点太过于依赖网络,这点确实需要改正一下,还是自己思考的进步快。

--------------------------------------------------------------------------

原文作者:铸道的汉子
原文博客: samurai.blog.chinaunix.net