通过前面的《什么是桌面插件》的讲解,估计你对桌面插件应用有了一定的了解,接着那这篇文章,我们继续讲解在一个桌面上如何创建一个桌面插件实例,以及它是如何显示在我们的桌面上的,如何被删除的,这些都是这篇文章要解答的问题。
用过Android原生Launcher的都知道,长按桌面空白处会弹出一个对话框,这个对话框就对应CreateShortcut类,手上有原生launcher代码的同学可以找到对应的代码看一下,但这不是我重点要关注的,我重点要关注的从你点击对话框中的桌面插件项开始了,你点击这个选项后它就会弹出一个Activity,里面列出了所有的系统安装的桌面插件应用,等等,在跳出这个桌面插件应用列表Activity前有一件重要的事情值得我们关注,那就是,分配桌面插件实例id,为什么还没有选择桌面插件应用就要分配桌面插件实例id了呢?因为一个桌面插件实例它不仅与桌面插件应用有关,而且与一个桌面有关,具体点说与一个AppWidgetHost有关系,AppWidgetHost我称之为桌面插件宿主,每一个桌面程序,或者一个app,它上面能容纳桌面插件实例首先得有一个桌面插件宿主对象,这个AppWidgetHost对象怎么来的我在后续文章中再继续作介绍。回到分配桌面插件实例id分配,桌面插件宿主通过函数allocateAppWidgetId来分配一个与自身有绑定关系的桌面插件实例Id,实际分配是通过AppWidgetService这个服务来实现的:
public int allocateAppWidgetId(String packageName, int hostId);
packageName是桌面插件宿主应用包名,hostId是代表桌面插件宿主对象的一个id,一旦成功分配一个桌面插件实例id后,这个桌面插件实例id就与hostId代表的桌面插件宿主对象绑定了,但这时这个桌面插件实例id只是一个数字,需要绑定一个具体桌面插件应用实例它才有意义,而弹出的一个桌面插件应用列表Activity就是让你选择这个实例的桌面插件应用。
桌面插件应用列表Activity名字是AppWidgetPickActivity,它在源生应用settings中,AppWidgetPickActivity中的桌面插件应用列表通过AppWidgetService获取,实际内容就是前面一篇文章中提到的mInstalledProviders。当我们选中一个桌面插件应用它会执行什么动作呢?看代码:
//绑定一个桌面组件 mAppWidgetManager.bindAppWidgetId(mAppWidgetId, intent.getComponent()); result = RESULT_OK;
对这段代码进行说明,mAppWidgetManager实质就是一个AppWidgetService的代理,mAppWidgetId是前面桌面插件宿主分配的桌面插件实例id,intent.getComponent()是一个桌面插件应用,它正真进行的桌面插件实例id与桌面插件应用绑定并生成一个桌面插件实例的程序在AppWidgetService中进行:
/**
* 绑定一个appWidgetId到一个provider * @param appWidgetId 桌面插件实例id * @param provider 桌面插件应用<包名,provider类名> */ public void bindAppWidgetId(int appWidgetId, ComponentName provider) { mContext.enforceCallingPermission(android.Manifest.permission.BIND_APPWIDGET, "bindGagetId appWidgetId=" + appWidgetId + " provider=" + provider); synchronized (mAppWidgetIds) { //根据桌面插件实例id找到桌面插件AppWidgetId对象,AppWidgetId对象在分配桌面插件实例id时生成的 AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); if (id == null) { throw new IllegalArgumentException("bad appWidgetId"); } //id.provider未设置目的是保证该桌面插件实例id是未绑定到桌面插件应用的 if (id.provider != null) { throw new IllegalArgumentException("appWidgetId " + appWidgetId + " already bound to " + id.provider.info.provider); } //根据桌面插件应用包名,provider类名,找到桌面插件应用provider实例,一个Provider对象代表一个桌面插件应用 Provider p = lookupProviderLocked(provider); if (p == null) { throw new IllegalArgumentException("not a appwidget provider: " + provider); } if (p.zombie) { throw new IllegalArgumentException("can't bind to a 3rd party provider in" + " safe mode: " + provider); } //桌面插件实例与桌面插件应用相互关联 id.provider = p; p.instances.add(id); int instancesSize = p.instances.size(); if (instancesSize == 1) { //第一次添加系统插件实例,发送ACTION_APPWIDGET_ENABLED消息,让桌面插件应用做好准备 sendEnableIntentLocked(p); } // send an update now -- We need this update now, and just for this appWidgetId. // It's less critical when the next one happens, so when we schdule the next one, // we add updatePeriodMillis to its start time. That time will have some slop, // but that's okay. //发送更新系统插件消息ACTION_APPWIDGET_UPDATE sendUpdateIntentLocked(p, new int[] { appWidgetId }); //注册定时更新消息ACTION_APPWIDGET_UPDATE广播,如果没有注册的话. registerForBroadcastsLocked(p, getAppWidgetIds(p)); //保存数据到持久化文件 saveStateLocked(); } }
这个函数我全拷过来了,因为它确实很重要,读懂它,估计你以前很多的关于桌面插件的疑问都会得以解答,同学如果你有时间的话建议你对函数中调用到的函数比如lookupAppWidgetIdLocked,registerForBroadcastsLocked,saveStateLocked等都深入去阅读,我这里不深入进去,只是作一个解读帮助你理解桌面插件的机制:lookupAppWidgetIdLocked,lookupProviderLocked分别是根据桌面插件实例id找到桌面插件AppWidgetId对象,根据桌面插件应用包名,provider类名,找到桌面插件应用provider实例,然后作一些验证,保证桌面插件实例id是已经分配好的,并且没有被绑定到一个桌面插件应用上的,保证桌面插件应用是存在的。AppWidgetId对象就代表这一个桌面插件实例,provider对象就代表一个桌面插件应用,id.provider = p; p.instances.add(id);把两者进行关联,就建立了两者的绑定关系,其实到这里,桌面插件实例id与桌面插件应用绑定就已经完成了,那接下来的代码是需要做什么呢? sendEnableIntentLocked实际就是发送一个ACTION_APPWIDGET_ENABLED广播消息,当该桌面插件实例是桌面插件应用的第一个桌面插件实例时,就会发送该消息,对应会执行AppWidgetProvider onEnabled方法,提示桌面插件应用你已经启动了,做好相关准备。接下来执行sendUpdateIntentLocked方法,就是发送ACTION_APPWIDGET_UPDATE广播消息到桌面插件应用,对应AppWidgetProvider onUpdate方法就会执行,所以写过桌面插件应用的同学你可能知道,每次添加一个桌面插件实例到桌面,onUpdate方法就会被调用,而registerForBroadcastsLocked方法就与桌面插件应用配置项android:updatePeriodMillis:指定桌面组件的更新周期有关了,它指定系统周期发送ACTION_APPWIDGET_UPDATE广播消息到桌面插件应用,注意是系统自动的,它的目的也不是更新桌面插件实例,我更倾向于是系统让桌面插件应用保持活动状态而不至于让系统注销这个桌面插件应用,因为这个桌面插件应用一旦被注销,那那些绑定该桌面插件应用的桌面插件实例就再也不会更新了。最后一个saveStateLocked其实就是把AppWidgetService的数据结构保存到文件中,防止手机重启这些数据没有了,那样的话你一重启手机,你手机上的那些桌面插件实例就都不见了,又得重新添加一遍,这样的话估计就没人用这玩意了。
似乎已经说了不少,不过现在还只是停留在数据结构层面的添加桌面插件实例,按MVC模型我们现在还是在M阶段,要到V还得回到桌面来,因为你要了解,所有的桌面插件实例都是再桌面程序中给画出来的。再回到添加桌面插件实例操作场景中来,选择一个桌面插件后AppWidgetPickActivity会返回到Launcher,返回码是REQUEST_PICK_APPWIDGET
case REQUEST_PICK_APPWIDGET: addAppWidget(data); break; case REQUEST_CREATE_APPWIDGET: completeAddAppWidget(data, mAddItemCellInfo); break;
void addAppWidget(Intent data) { // 获取插件的配置信息AppWidgetProviderInfo int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); AppWidgetProviderInfo appWidget = mAppWidgetManager.getAppWidgetInfo(appWidgetId); //若桌面组件配置属性不为空,启动桌面组件配置activity if (appWidget.configure != null) { // Launch over to configure widget, if needed Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); intent.setComponent(appWidget.configure); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); startActivityForResultSafely(intent, REQUEST_CREATE_APPWIDGET); } else { // 否则直接添加该桌面插件实例 onActivityResult(REQUEST_CREATE_APPWIDGET, Activity.RESULT_OK, data); } }
appWidget.configure实际就对应我前一篇文章讲到的一个桌面插件应用android:configure,这个函数所做的就是当有配置android:configure时打开配置Acitivity,否则就直接添加该桌面插件实例,也就是回到onActivityResult的REQUEST_CREATE_APPWIDGET,代码中可以明显的看出这一点,不管有没有配置界面,它最终都会调用completeAddAppWidget,它才是最终添加桌面插件视图的实现代码,为了尽量能讲清楚,我还是把代码给贴出来:
/** * Add a widget to the workspace. * 添加一个桌面组件实例到桌面上 * * @param data The intent describing the appWidgetId. * 桌面插件实例相关信息 * @param cellInfo The position on screen where to create the widget. * 添加桌面插件实例的位置信息 */ private void completeAddAppWidget(Intent data, CellLayout.CellInfo cellInfo) { Bundle extras = data.getExtras(); //获取桌面插件实例id和配置信息 int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); if (LOGD) Log.d(TAG, "dumping extras content=" + extras.toString()); AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); //计算该桌面插件实例添加的位置并验证该位置是否有效,若位置已经被占用,删除桌面组件实例 CellLayout layout = (CellLayout) mWorkspace.getChildAt(cellInfo.screen); int[] spans = layout.rectToCell(appWidgetInfo.minWidth, appWidgetInfo.minHeight); final int[] xy = mCellCoordinates; if (!findSlot(cellInfo, xy, spans[0], spans[1])) { if (appWidgetId != -1) mAppWidgetHost.deleteAppWidgetId(appWidgetId); return; } //创建一个LauncherAppWidgetInfo对象并保存到数据库中 LauncherAppWidgetInfo launcherInfo = new LauncherAppWidgetInfo(appWidgetId); launcherInfo.spanX = spans[0]; launcherInfo.spanY = spans[1]; LauncherModel.addItemToDatabase(this, launcherInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, mWorkspace.getCurrentScreen(), xy[0], xy[1], false); if (!mRestoring) { //添加一个桌面插件项 mDesktopItems.add(launcherInfo); // Perform actual inflation because we're live //创建桌面插件实例view launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo); launcherInfo.hostView.setTag(launcherInfo); //添加该桌面插件实例view到桌面 mWorkspace.addInCurrentScreen(launcherInfo.hostView, xy[0], xy[1], launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); } }
整个函数的函数我在注释中已经大致给讲清楚了,主要做的事情就是根据插件实例信息以及要添加到桌面的位置信息创建LauncherAppWidgetInfo对象并保存到数据库中,然后调用桌面插件宿主mAppWidgetHost创建桌面插件实例view(该桌面插件实例在launcher中的view对象,我称之为桌面插件实例view),最后把该桌面插件实例view添加到整个桌面的视图中,这就完成了桌面插件实例view的添加。不过在这里有两处我要展开讲一下,一个是创建桌面插件实例view的过程,另一个是添加桌面插件实例view到桌面的过程。
桌面插件实例view的创建是通过调用桌面插件宿主mAppWidgetHost的createView成员函数来实现的,它有三个参数,我们关注后面两个,一个是appWidgetId,它表示一个桌面插件实例id,一个是appWidgetInfo它里面包含桌面插件应用的配置信息,我们看函数实现:
public final AppWidgetHostView createView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget) { //创建一个AppWidgetHostView AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget); //设置桌面组件实例id和配置信息 view.setAppWidget(appWidgetId, appWidget); //缓存appWidgetId 与桌面插件实例映射表 synchronized (mViews) { mViews.put(appWidgetId, view); } //通过桌面插件服务代理从桌面插件服务中获取该appWidgetId的RemoteViews对象 RemoteViews views; try { views = sService.getAppWidgetViews(appWidgetId); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); } //把RemoteViews对象的操作应用到该系统插件实例view上 view.updateAppWidget(views); return view; }
第一步是通过接口方法onCreateView来创建AppWidgeHostView对象,LauncherAppWidgetHost类中实现了该方法,如若需要,你也可继承该方法,做一些个性化的定制;
//构建RemoteViews对象来对桌面插件进行更新
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider);
//更新文本内容,指定布局的组件
views.setTextViewText(R.id.appwidget_text, titlePrefix+mUpdateCount);
//将RemoteViews的更新传入AppWidget进行更新
appWidgetManager.updateAppWidget(appWidgetId, views);
appWidgetMananger为桌面插件服务的代理,调用updateAppWidget方法并吧RemoteViews对象传到了桌面插件服务,这个时候桌面插件实例view还没有创建,那这个
RemoteViews对象是保存在哪呢?它实际保存在桌面插件服务中mAppWidgetIds列表中appWidgetId对应的桌面插件实例下面有一个views成员,我们可以看一下AppWidgetId类的结构:
/**一个系统插件实例*/ static class AppWidgetId { int appWidgetId; Provider provider; RemoteViews views; Host host; }该步骤就是通过 appWidgetId从桌面插件服务中找到 mAppWidgetIds列表中对应的AppWidgetId对象,获取对象中的views成员返回。
第五步就是通过RemoteViews对象来构建AppWidgeHostView对象。前面步骤创建的AppWidgeHostView对象只是一个没有任何内容的view,如何根据桌面插件应用中的布局文件创建与之一致的view对象就是在这一步实现的,通过调用AppWidgeHostView的updateAppWidget方法实现的,那下面我们就深入该函数一探究竟:
public void updateAppWidget(RemoteViews remoteViews) { ... if (remoteViews == null) { ... } else { //获取桌面组件应用上下文(克隆对象) mRemoteContext = getRemoteContext(remoteViews); //获取桌面插件布局id int layoutId = remoteViews.getLayoutId(); //当系统插件已经创建了,进入下面逻辑进行处理 if (content == null && layoutId == mLayoutId) { //更新桌面组件 remoteViews.reapply(mContext, mView); } //首次update content为空,通过remoteViews构建content if (content == null) { //创建系统插件view content = remoteViews.apply(mContext, this); } //设置布局id mLayoutId = layoutId; mViewMode = VIEW_MODE_CONTENT; } //创建view失败,设置一个error view if (content == null) { if (mViewMode == VIEW_MODE_ERROR) { // We've already done this -- nothing to do. return ; } Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception); content = getErrorView(); mViewMode = VIEW_MODE_ERROR; } if (!recycled) { //调整系统插件view的布局参数 prepareView(content); //添加系统插件view到hostview addView(content); } //设置mview为新的系统插件view if (mView != content) { //删除旧的系统插件view removeView(mView); //设置mView为新的系统插件view mView = content; } }
这个过程首先是通过remoteviews获取桌面插件应用的上下文,然后获取桌面插件的布局文件id,这样就可以获取该桌面插件实例创建view所需要的布局以及图片,文件资源了;然后调用remoteViews.apply方法来构建一个view,若构建失败获取一个默认的错误view作为桌面插件实例的view,主要流程就是这样的,实现view的构造主要还是在apply方法中:
public View apply(Context context, ViewGroup parent) { View result; //创建一个系统插件应用的上下文 Context c = prepareContext(context); //获取系统插件的LayoutInflater LayoutInflater inflater = (LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE); //复制inflater inflater = inflater.cloneInContext(c); inflater.setFilter(this); //创建一个系统插件的view result = inflater.inflate(mLayoutId, parent, false); //应用remoteview中所有的更新action performApply(result); return result; }函数比较简短,我也作了详细的注释,这里再梳理一下,context函数参数是AppWidgetHostView所在桌面的上下文,通过prepareContext函数创建了一个系统插件应用对应的上下文件c,这样c就可以获取系统插件应用的资源文件和布局文件,然后获取系统布局器服务,通过布局器LayoutInflater以及桌面插件应用上下文对象c来实例化出来一个view,该view的父view正是即将要添加到桌面的AppWidgetHostView实例,AppWidgetHostView实例是真正系统插件view的容器,最后,有些通过代码设置的action通过调用performApply方法加以应用,例如设置系统插件textview的不同文字,performApply方法实现的原理就是反射调用action中定义的类方法,达到对系统插件view内容进行控制的目的。
完成创建AppWidgetHostView对象后再回到方法completeAddAppWidget,通过调用mWorkspace.addInCurrentScreen方法把桌面插件view实例添加到桌面的,xy[0], xy[1],
launcherInfo.spanX, launcherInfo.spanY分别指定桌面插件在桌面左上角的起始位置以及横向和纵向占据的位置范围,这些位置是桌面划分的一个一个空格,一般一个app占据一个1*1的空格,当然,不同桌面可以自己具体实现。
到这里,桌面launcher添加一个桌面插件实例算是完成了,接着再看删除桌面插件实例,这个过程在手机上直观的体现就是拖动一个桌面插件实例到桌面垃圾箱放手,然后桌面插件实例就被删除了。launcher中有一个DeleteZone类,该类的对象就是我们所看到了桌面上的垃圾箱,它可以接收拖放对象,因此它实现了DropTarget接口,然后在onDrop方法中实现对拖放到垃圾箱的对象的处理,这其中就包括桌面插件实例的处理:
if (item instanceof UserFolderInfo) { final UserFolderInfo userFolderInfo = (UserFolderInfo)item; LauncherModel.deleteUserFolderContentsFromDatabase(mLauncher, userFolderInfo); mLauncher.removeFolder(userFolderInfo); } else if (item instanceof LauncherAppWidgetInfo) { final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); if (appWidgetHost != null) { final int appWidgetId = launcherAppWidgetInfo.appWidgetId; // Deleting an app widget ID is a void call but writes to disk before returning // to the caller... new Thread("deleteAppWidgetId") { public void run() { appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId); } }.start(); } } LauncherModel.deleteItemFromDatabase(mLauncher, item);这里 把关键代码标记成了红色,判断拖放对象类型为LauncherAppWidgetInfo后,就把它当作桌面插件处理,处理流程大致是获取桌面插件宿主appWidgetHost对象,然后根据桌面插件实例的appWidgetId来删除桌面插件实例,最后对桌面数据库中的数据进行删除处理,这其中删除桌面插件实例主要实现是在appWidgetHost.deleteAppWidgetId函数中,appWidgetHost则是通过AppWidgetService服务来远程实现该过程的,看一下AppWidgetService中该函数的实现:
/**删除系统插件实例appWidgetId,清理该实例的所有数据结构,并把状态数据保存到文件*/
public void deleteAppWidgetId(int appWidgetId) {
synchronized (mAppWidgetIds) {
AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId);
if (id != null) {
deleteAppWidgetLocked(id);//清理所有该系统插件实例的数据
saveStateLocked(); //把状态数据持久化到/data/system/appwidgets.xml
}
}
}
/**删除一个系统插件实例*/ void deleteAppWidgetLocked(AppWidgetId id) { Host host = id.host; host.instances.remove(id); pruneHostLocked(host); mAppWidgetIds.remove(id); Provider p = id.provider; if (p != null) { p.instances.remove(id); if (!p.zombie) { // send the broacast saying that this appWidgetId has been deleted Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_DELETED); intent.setComponent(p.info.provider); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id.appWidgetId); mContext.sendBroadcast(intent);//发送删除系统插件广播 if (p.instances.size() == 0) {//若系统插件个数为0 发送DISABLED广播 // cancel the future updates cancelBroadcasts(p);//注销定时刷新系统插件广播 // send the broacast saying that the provider is not in use any more intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_DISABLED); intent.setComponent(p.info.provider); mContext.sendBroadcast(intent); } } } }整个删除过程可以分为两个部分,一是清理桌面插件服务中的相关内存数据结构,另一个是把修改后的数据持久化到appWidgets.xml文件中。对于内存数据结构的修改,主要包括所有桌面插件实例列表mAppWidgetIds中数据的清理,以及该桌面插件应用下桌面插件应用实例对象列表p.instances的数据清理工作,同时发送ACTION_APPWIDGET_DELETED广播通知桌面插件应用进行处理,AppWidgetProvider中onDelete方法将对该广播进行响应。最后,若该桌面插件应用的所有桌面插件实例都删除时,需要发送ACTION_APPWIDGET_DISABLED广播来通知桌面插件应用进行相应处理,AppWidgetProvider中onDisabled方法将对该广播进行响应,同时要注销原来在系统中注册的定时更新广播,避免定时更新广播激活桌面插件应用。