AppWidget开发实例

在下面的例子中,笔者新建一个AppWidget,通过在桌面点击AppWidget中的“Send”按钮,然后通过刷新TextView来验证AppWidget。

        代码结构如下图所示:


一、创建工程

        首先,新建Android 工程“com.devdiv.test.appwidgettest”。在res文件夹下新建一个名字为xml的文件夹,然后在xml目录下创建一个名为appwidgetprovider_info.xml的xml文件。

        appwidgetprovider_info.xml的内容如下:  

[代码]xml代码:

  1.     xmlns:android="http://schemas.android.com/apk/res/android"
  2.     android:minWidth="240dp"
  3.         android:minHeight="70dp"
  4.         android:updatePeriodMillis="86400000"
  5.         android:initialLayout="@layout/widget_layout"
  6.         >
复制代码
下面,简单介绍一下appwidget-provider中的各个属性。

        minWidth 和 minHeight 属性的值规定了 Widget 布局所需要的最小区域大小。
        updatePeriodMillis 属性定义了 Widget 框架通过调用 onUpdate() 回调方法从 AppWidgetProvider 中请求更新的频率。
        initialLayout 属性指向定义 widget 的布局资源。
        configure 属性定义加载 Activity 。为了在用户添加 Widget 时,让用户可以配置Widget 属性。 这是可选的。
        previewImage 属性表明配置后的 widget 的缩略图,当用户选择widget 时可以看到。如果没有提供, 用户看到的是 应用程序图标。 该属性在 AndroidManifest.xml文件的元素中对应android:previewImage 属性。
        autoAdvanceViewId 属性表明应该在 widget 宿主中被自动优化的 widget 子视图的 ID。
        resizeMode 属性表明widget 改变大小的规则。 用此属性让主界面的 widget 大小可调,水平的,垂直的, 或同时两轴。用户按住一个weidget来显示其调节手柄, 然后在宫格布局中拖拽水平 和/或 垂直手柄来改变大小。 resizeMode 属性的值包含 "horizontal", "vertical", 和 "none". 要声明widget 是水平和垂直方向可调的, 设置为"horizontal|vertical"。

二、创建AppWidget 布局

        必须在XML中为widget定义一个初始布局,并且把它保存在工程的 res/layout/ 目录。 你可以在widget中使用的控件对象。

        需要注意的是,widget 布局是基于 RemoteViews 的, 他不支持所有的布局或view 控件。

        RemoteViews 对象能支持如下布局类:FrameLayout、LinearLayout、RelativeLayout。支持一下控件类:AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper。这些类的子类是不被支持的。

        在layout文件夹下面新建一个widget_layout.xml文件,在这个文件中描述了AppWidget控件的布局信息。

        widget_layout.xml的内容如下:

[代码]xml代码:

  1.     android:layout_width="fill_parent"
  2.     android:layout_height="fill_parent"
  3.     android:orientation="horizontal"
  4.     >
  5.    
  6.             android:id="@+id/btnSend"
  7.             android:layout_width="wrap_content"
  8.             android:layout_height="wrap_content"
  9.             android:text="Send">
  10.    
  11.    
  12.             android:id="@+id/txtapp"
  13.             android:text="test"
  14.             android:layout_width="wrap_content"
  15.                 android:layout_height="wrap_content"
  16.                 android:background="#ffffff">
  17.        
复制代码
三、创建AppWidgetProvider类

        AppWidgetProvider 继承自 BroadcastReceiver。AppWidgetProvider 只接收关于Widget 的广播, 比如widget 的被更新,被删除, 被激活,和被禁用。

[代码]java代码:
  1. package com.devdiv.test.appwidgettest;

  2. import android.app.PendingIntent;
  3. import android.appwidget.AppWidgetManager;
  4. import android.appwidget.AppWidgetProvider;
  5. import android.content.ComponentName;
  6. import android.content.Context;
  7. import android.content.Intent;
  8. import android.util.Log;
  9. import android.widget.RemoteViews;

  10. public class MyAppWidgetProvider extends AppWidgetProvider {

  11.         //删除一个AppWidget组件时被调用
  12.         @Override
  13.         public void onDeleted(Context context, int[] appWidgetIds) {
  14.                 // TODO Auto-generated method stub
  15.                 super.onDeleted(context, appWidgetIds);
  16.         }

  17.         //最后一个AppWidget组件被删除时调用
  18.         @Override
  19.         public void onDisabled(Context context) {
  20.                 // TODO Auto-generated method stub
  21.                 super.onDisabled(context);
  22.         }

  23.         //AppWidget的实例第一次被创建时调用
  24.         @Override
  25.         public void onEnabled(Context context) {
  26.                 // TODO Auto-generated method stub
  27.                 super.onEnabled(context);
  28.         }

  29.         //接受广播事件
  30.         @Override
  31.         public void onReceive(Context context, Intent intent) {
  32.                 // TODO Auto-generated method stub
  33.                 super.onReceive(context, intent);
  34.         }

  35.         //组件被放置到桌面上时,或者刷新时间到达时调用
  36.         @Override
  37.         public void onUpdate(Context context, AppWidgetManager appWidgetManager,
  38.                         int[] appWidgetIds) {
  39.                 // TODO Auto-generated method stub
  40.                
  41.                 //打印出每次调用onUpdate()方法的appWidgetIds的长度
  42.                 Log.d("WIDGET","onUpdate()  "+"appWidgetIds.length:" + appWidgetIds.length);
  43.                
  44.                 RemoteViews mRemoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
  45.                 Intent mIntent = new Intent(context.getApplicationContext(), UpdateWidgetService.class);
  46.                
  47. //                //把每次调用onUpdate()方法时传入的appWidgetIds通过putExtra()方法放入mIntent
  48. //                onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
  49.                
  50.                 //通过appWidgetManager.getAppWidgetIds()方法,取到所有桌面上绑定到MyAppWidgetProvider的AppWidget的Id
  51.                 //Get the list of appWidgetIds that have been bound to the given AppWidget provider.
  52.                 int[] totalAppWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, getClass()));
  53.                
  54.                 //把totalAppWidgetIds放入mIntent中,以键值对的形式
  55.                 mIntent.putExtra("totalAppWidgetIds", totalAppWidgetIds);
  56.                
  57.                 //通过PendingIntent和mRemoteViews.setOnClickPendingIntent为AppWidget中的Button设置监听器
  58.                 //具体用法,参考文档
  59.                 //注意生成PendingIntent时的第四个参数
  60.                 PendingIntent mPendingIntent = PendingIntent.getService(context, 0, mIntent, PendingIntent.FLAG_UPDATE_CURRENT);
  61.                 mRemoteViews.setOnClickPendingIntent(R.id.btnSend, mPendingIntent);
  62.                
  63.                 appWidgetManager.updateAppWidget(appWidgetIds, mRemoteViews);
  64.                
  65.         }


  66. }
复制代码
关于AppWidgetProvider中的几个回调方法的调用时机,之前我们已经讲解过了。我在代码中添加了详细的注释,方便大家理解。

四、创建AppWidget Service

        创建一个新的继承于Service的UpdateWidgetService类,并在AndroidManifest.xml文件中属性中声明。

        UpdateWidgetService类的内容如下:

[代码]java代码:
  1. package com.devdiv.test.appwidgettest;

  2. import java.util.Random;

  3. import android.app.Service;
  4. import android.appwidget.AppWidgetManager;
  5. import android.content.Intent;
  6. import android.os.IBinder;
  7. import android.util.Log;
  8. import android.widget.RemoteViews;

  9. public class UpdateWidgetService extends Service {

  10.         @Override
  11.         public void onStart(Intent intent, int startId) {
  12.                 // TODO Auto-generated method stub
  13.                
  14.                 Log.d("WIDGET", "onStart_Called");
  15.                
  16.                 String mString = null;
  17.                 Random mRandom = new Random();
  18.                
  19.                 //获得AppWidgetManager实例
  20.                 AppWidgetManager mAppWidgetManager = AppWidgetManager.getInstance(this.getApplicationContext());
  21.                
  22. //                //通过getIntArrayExtra()方法取到appWidgetIds(桌面上所有的)
  23. //                int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
  24.                
  25.                 //通过getIntArrayExtra()方法取到appWidgetIds(桌面上所有的)
  26.                 int[] totalAppWidgetIds = intent.getIntArrayExtra("totalAppWidgetIds");
  27.                 //分别为所有的AppWidget产生并设置随机数字
  28.                 if(totalAppWidgetIds.length > 0) {
  29.                         for (int appWidgetId : totalAppWidgetIds) {
  30.                                 Log.d("WIDGET", "appWidgetId:  "+String.valueOf(appWidgetId));
  31.                                
  32.                                 int randomInt = mRandom.nextInt(100);
  33.                                 mString = "Random: " + String.valueOf(randomInt);
  34.                                
  35.                                 RemoteViews mRemoteViews = new RemoteViews(this.getPackageName(), R.layout.widget_layout);
  36. //                                RemoteViews mRemoteViews = new RemoteViews(getPackageName(), R.layout.widget_layout);
  37.                                
  38.                                 mRemoteViews.setTextViewText(R.id.txtapp, mString);
  39.                                
  40.                                 mAppWidgetManager.updateAppWidget(appWidgetId, mRemoteViews);
  41.                         }
  42.                 }
  43.                
  44.                 //完成随机数字的设置后,结束Service
  45.                 stopSelf();               
  46.                
  47.                 super.onStart(intent, startId);
  48.         }

  49.         @Override
  50.         public IBinder onBind(Intent intent) {
  51.                 // TODO Auto-generated method stub
  52.                 return null;
  53.         }

  54. }
复制代码
五、manifest.xml文件的配置

        Manifest.xml文件的内容如下:

[代码]xml代码:

  1.     package="com.devdiv.test.appwidgettest"
  2.     android:versionCode="1"
  3.     android:versionName="1.0" >

  4.    

  5.    
  6.         android:icon="@drawable/ic_launcher"
  7.         android:label="@string/app_name" >
  8.         
  9.             >
  10.             
  11.                
  12.             
  13.             
  14.                 android:resource="@xml/appwidgetprovider_info"/>   
  15.         
  16.         
  17.         
  18.    

复制代码
注意标签中,的设置,其中各部分的设置都是必须的。具体的用法参考,manifest.xml文件的介绍,网址如下: http://developer.android.com/gui ... manifest-intro.html。也可不必深究原因,按照上面的代码设置。

六、运行效果

        首先,在桌面上添加三个AppWidgetTest桌面组件,添加方式参考第一节“AppWidget简介”中的讲解。效果如下图所示:

Demo运行效果图1

点击任一Button后,所有Widget的随机数字都将变化,效果如下图所示:

Demo运行效果图2

再次点击任一Button后,效果如下图所示:
Demo运行效果图3

代码:AppWidgetTest.rar(150 KB, 下载次数: 108)






使用时遇到了一些问题,在reinstall时,按button出错(android4.4):

W/ActivityManager(  884): Permission Denial: Accessing service ComponentInfo{com.devdiv.test.appwidgettest/com.devdiv.test.appwidgettest.UpdateWidgetService}
from pid=-1, uid=10059 that is not exported from uid 10060
W/ActivityManager(  884): Permission Denial: Accessing service ComponentInfo{com.devdiv.test.appwidgettest/com.devdiv.test.appwidgettest.UpdateWidgetService}
from pid=-1, uid=10059 that is not exported from uid 10060
W/ActivityManager(  884): Permission Denial: Accessing service ComponentInfo{com.devdiv.test.appwidgettest/com.devdiv.test.appwidgettest.UpdateWidgetService}
from pid=-1, uid=10059 that is not exported from uid 10060
W/ActivityManager(  884): Permission Denial: Accessing service ComponentInfo{com.devdiv.test.appwidgettest/com.devdiv.test.appwidgettest.UpdateWidgetService}
from pid=-1, uid=10059 that is not exported from uid 10060


5 down vote favorite

I'm attempting to use Google's Activity Recognition Service. Several days ago everything worked like a charm, i.e. I could connect use the service to obtain activity information. But today I found that I couldn't receive any anymore. After looking at the log I found this error:

 05-15 21:19:27.196: W/ActivityManager(765): Permission Denial: Accessing service
 ComponentInfo{edu.umich.si.inteco.captureprobe/edu.umich.si.inteco.captureprobe.
 contextmanager.ActivityRecognitionService} from pid=-1, uid=10220 that is not exported   
 from uid 10223

I rebooted the phone and then it worked again. However, after I reinstall the application, the same problem appeared again. Could anyone point out what the "real" problem would be? Is it something related to "pid=-1"? I do have the permission in the Manifest file

 android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION"/>

I looked for answers on Google, but most of the issues are that they didn't place permission in the manifest file. This seems to me just a different issue...Could anyone help me? Thanks!

UPDATE:The issue can always be solved by rebooting the phone. However, it always reappears when I uninstall the app and reinstall it through Eclipse. A consistent yet weird pattern (at least to me). I'm wondering whether the phone remember the app and stop it to access Google Play service after I uninstall it (or for some reason Google Play service just doesn't allow my app to access it). Any ideas?

share improve this question
 
 
Yes I did. And so it used to work before. –  Stanley Chang May 17 '14 at 4:48

2 Answers

active oldest votes
up vote 7 down vote

I figured out the solution. The reason is due to the combination of two things:

  1. Reinstallation generates a new different uid of the same application (note, by reinstallation I meant uninstall the app from the phone and then use Eclipse to reinstall).

  2. By default, in the service tag, the value of "exported" is false, as described here

Whether or not components of other applications can invoke the service or interact with it — "true" if they can, and "false" if not. When the value is "false", only components of the same application or applications with the same user ID can start the service or bind to it.The default value depends on whether the service contains intent filters. The absence of any filters means that it can be invoked only by specifying its exact class name. This implies that the service is intended only for application-internal use (since others would not know the class name). So in this case, the default value is "false". On the other hand, the presence of at least one filter implies that the service is intended for external use, so the default value is "true".

So I solved my problem simply by setting the flag to "true." (the sample code of Google Activity Recognition use the value "false" instead of "true.")

     
        android:name="edu.umich.si.inteco.captureprobe.contextmanager.ActivityRecognitionService"
        android:enabled="true"
        android:exported="true"
        >
    

The interesting thing is, the same code works on Android 4.3 or below. I tested my code on four different phones with different Android version, and the uninstallation/reinstallation problem only occurred on the phone of Android 4.4. So that's why the I've been puzzled by the problem. Why didn't it happen before? Anyway, if you run into the same problem (i.e. the same application cannot use a service after you reinstall it), check the "exported" flag.

share improve this answer
 
 
The only potential problem with exporting a service is it can then be accessed by a malicious app. It might be best to make an ActivityRecognitionService that validates and forwards any recognition results to an internal service. –  Sofi Software LLC Aug 12 '14 at 18:58
up vote 1 down vote

This is not specific to Activity Recognition; it's apparently a security improvement in 4.4, PendingIntents are registered in the system and a better solution appears to be to cancel the current pending intent (using PendingIntent.FLAG_CANCEL_CURRENT).

https://code.google.com/p/android/issues/detail?id=61850




你可能感兴趣的:(android研究)