Android's commonly used tool "small plug-in" - Widget mechanism

Widget, commonly known as "small plug-in", is a very commonly used tool in the Android system. For example, we can add a music player widget to the Launcher.

You can add plug-ins on Launcher, so does it mean that only Launcher has this function?

The Android system does not specify who can play the role of "Widget container". It defines a complete set of Widget addition/removal and display mechanisms, so that everyone can be a "Widget provider" and everyone is qualified to be a "Widget container".

Above we mentioned the concepts of "Widget provider" and "Widget container", the former is like a weather plug-in, and the latter is like Launcher. In the Widget mechanism, they all have their own proper nouns (also class names), namely AppWidgetProvider and AppWidgetHost. In addition, we can guess that a global Widget manager is also needed in the system. Similar to the naming method of WindowManagerService and WallpaperManagerService, it is called AppWidgetService.

Insert image description here

"Function provider" - AppWidgetProvider

Since it is called Provider, the implication is "provider of functions". From the Host's perspective, it has no way to know in advance how many Widgets the user will add, nor does it have any way to know what functions these added Widgets implement. So in the "world" of Host, a Widget is just a View - it only needs to be displayed correctly as required, and the specific function implementation is completed by AppWidgetProvider.

Host regards Widget as a "variant" of View.

An effective Provider must provide at least the following aspects.

  • AppWidgetProviderInfo

That is, various information used to describe this Widget, including its layout, refresh frequency, and the AppWidgetProvider mentioned below. This information is expressed in XML format files with Tag tags.

  • AppWidgetProvider

Since the Widget will eventually be displayed in the Host, its functional implementation must be different from that of ordinary applications. AppWidgetProvider mainly uses Broadcast events to "remotely update" Widgets.

  • View layout

AppWidgetProviderInfo is used to describe the overall information of the Widget, and the Layout here is specifically used to describe the "display part" of the Widget (to be precise, the display during initialization).

Provider is a BroadcastReceiver. For example, we can declare the following content in AndroidManifest.xml to define an AppWidgetProvider:

<receiver android:name="ExampleAppWidgetProvider" >
 <intent-filter>
  <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
 </intent-filter>
 <meta-data android:name="android.appwidget.provider"
            android:resource="@xml/example_appwidget_info" />
</receiver>

The only message this receiver needs to receive is APPWIDGET_UPDATE; and it also needs to carry information that clearly indicates that it is an "android.appwidget.provider". The last android:resource is the AppWidgetProviderInfo mentioned earlier. for example:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="294dp"
    android:minHeight="72dp"
    android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/preview"
    android:initialLayout="@layout/example_appwidget"    
</appwidget-provider>

The last attribute of this XML file (android:initialLayout) specifies the initial View layout as example_appwidget, which is the same as the layout syntax we use when writing ordinary applications.

When we write our own Widget Provider, we must first inherit from AppWidgetProvider. The internal implementation of the latter is not complicated. It inherits from BroadcastReceiver and notifies our AppWidgetProvider instance of specific events through the overloaded function in onReceive:

/*frameworks/base/core/java/android/appwidget/AppWidgetProvider.java*/
public class AppWidgetProvider extends BroadcastReceiver {
     …
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
           Bundle extras = intent.getExtras();
           if (extras != null) {
              int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET _IDS);
              if (appWidgetIds != null && appWidgetIds.length > 0) {
                  this.onUpdate(context, AppWidgetManager.getInstance(context), appWid getIds);
              }
          }
       }
     …
    }…

In other words, writing a Widget should override onReceiver (if necessary), onUpdate, onAppWidgetOptionsChanged, onDeleted, onEnabled and onDisabled as needed. They will be called when the Widget is updated, Option changed, deleted, etc.

However, it is important to note that a Widget can have multiple specific instances. For example, if we write a "weather" plug-in for users to use, in theory there is no limit on how many "weather" instances users can add to the Launcher. Therefore, a corresponding WidgetId is needed to uniquely identify each instance.

Insert image description here

AppWidgetHost

In the previous section, we learned about the work that AppWidgetProvider has to do. Next, let’s take a look at how Host cooperates with Provider. In short, the "host" of the Host needs to provide corresponding space for the Widget to display its UI interface. For example, AppWidgetHost is like a showroom, and it doesn't matter whether the cars on display are Volkswagen or Mercedes-Benz brands - it depends on the wishes of the Widget itself.

To become an AppWidgetHost, it needs to solve the following problems.

  • How to display the UI interface of the Widget
  • How to communicate with AppWidgetProvider

The interface between an AppWidgetProvider and the outside world is onReceive, which is then refined into onUpdate, onEnable and other event processing. The root cause of these events, in addition to the system element AppWidgetService, is AppWidgetHost. It's just that the latter also needs to send events through the former.
Insert image description here
Host_Application in the diagram refers to the application that plays the role of Host, such as Launcher. It will only interact with AppWidgetManager in the entire Widget mechanism and will not directly call the interface of AppWidgetService (this is somewhat similar to the role of ServiceManager.java). As you can imagine, AppWidgetManager still needs to be implemented by indirectly calling AppWidgetService. In addition, each Host_Application also holds an AppWidgetHost, which we can think of as the proxy of the Host.

When a Host_Application is created, it needs to register with AppWidgetService to listen for Widget events and provide a callback implementation. This callback actually inherits from IAppWidgetHost.Stub, which is an AIDL-based BinderServer, which ensures that AppWidgetService can call back to the Host when an event occurs. The callback events that need to be received include:


    updateAppWidget updateAppWidgetView@AppWidgetHost
    providerChangedonProviderChanged@AppWidgetHost
    viewDataChangedviewDataChanged @AppWidgetHost

Let's take updateAppWidget as an example to analyze its internal implementation:

/*frameworks/base/core/java/android/appwidget/AppWidgetHost.java*/
    void updateAppWidgetView(int appWidgetId, RemoteViews views, int userId) {
        AppWidgetHostView v;
        synchronized (mViews) {
            v = mViews.get(appWidgetId);
        }
        if (v != null) {
            v.updateAppWidget(views);
        }
    }

When the WidgetProvider wants to update the View display in the Host (for example, the weather plug-in updates the temperature), it will specify the new View style (RemoteViews) through AppWidgetManager.updateAppWidget(int appWidgetId, RemoteViews views). This request is ultimately sent to the corresponding Host by AppWidgetService, that is, updateApp WidgetView.

The mViews in the above code are defined as follows:

HashMap<Integer,AppWidgetHostView> mViews = new HashMap<Integer, 
                                                       AppWidgetHostView>();

It is a collection of AppWidgetHostView. In other words, it is the View object of all Widgets contained in the current Host. For example, every time the user adds a Widget in the Launcher (or the Launcher itself reads the Widgets that need to be loaded and displayed from the saved configuration when the device is first turned on), it will be added to this collection using AppWidgetHost.createView. In addition, because there are many Widgets, they must be assigned a globally unique WidgetId.

The operation process of adding widgets in Launcher

First, the Host gets the Info information of the corresponding WidgetId through AppWidgetManager.getAppWidgetInfo, which is the AppWidgetProviderInfo we talked about in the previous section. Then the Host will generate an AppWidgetHostView through AppWidgetHost.createView - the layout corresponding to this View is specified by the previous initialLayout. Subsequently, the AppWidget Provider will update its Widget display in real time through RemoteViews according to the actual situation.

So, what does createView do?

/*frameworks/base/core/java/android/appwidget/AppWidgetHost.java*/
    public final AppWidgetHostView createView(Context context, int appWidgetId,
                                   AppWidgetProviderInfo appWidget) {
        final int userId = mContext.getUserId();
        AppWidgetHostView view = onCreateView(mContext, appWidgetId, appWidget);
        //本地的View对象
        view.setUserId(userId);
        view.setOnClickHandler(mOnClickHandler);
        view.setAppWidget(appWidgetId, appWidget);
        synchronized (mViews) {
            mViews.put(appWidgetId, view);
        }
        RemoteViews views;
        try {
            views = sService.getAppWidgetViews(appWidgetId, userId);//得到该Widget的RemoteViews
            if (views != null) {
                views.setUser(new UserHandle(mContext.getUserId()));
            }
        } catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
        view.updateAppWidget(views);//通过RemoteViews“搭建”本地的View
        return view;
    }

The first step is to generate an AppWidgetHostView. By default, onCreateView just creates a new AppWidgetHostView object and returns it directly. If readers have special needs, they can overload this function:

/*frameworks/base/core/java/android/appwidget/AppWidgetHostView.java*/
public class AppWidgetHostView extends FrameLayout {
…

It can be seen that AppWidgetHostView is actually an extended subclass of FrameLayout. On the one hand, setAppWidget associates the widgetId with this AppWidgetHostView, and on the other hand, sets the padding value of the widget to be displayed. We can also overload this implementation.

Next is the focus of widget display, that is, how we display the interface defined by Widget Provider into Host_Application.

Before analyzing the source code, let's make an analogy first. Zhang San built a villa in Beijing. Li Si liked it very much after seeing it, so he also wanted to build an identical one in Shanghai. what to do? Obviously it is impossible to move Zhang San's villa directly to Shanghai, because they are in different places and belong to two different "process spaces". A feasible way is to submit the completed architectural drawings of Zhang San to Li Si, and then Li Si can build an identical villa in his own "process space". Although the bricks, tiles and cement may not be from the same brand, this will not affect everyone's belief that "the style of the two villas is exactly the same."

The display of Widget is also similar. If we need to display our View in another process space (i.e. Host_Application), we can also hand over the "drawing" of the View to the other party - in this way, the other party only needs to "follow the gourd" and it will not be difficult to "restore" it. The "true face" of Widget is revealed. And this "drawing" is RemoteViews:

/*RemoteViews.java*/
public class RemoteViews implements Parcelable, Filter {…

Although it also has "Views" in its name, there is actually no shadow of View. It inherits from the Parcelable class that can be passed across processes and the Filter class that constrains data.

With these foundations in mind, let's go back and look at the previous createView:

views = sService.getAppWidgetViews(appWidgetId);

The above code gets a RemoteViews based on WidgetId, which is implemented with the help of sService, the interface provided by AppWidgetService. In fact, it simply fills in the LayoutId and PackageName of the Widget, and the "drawing" will be fetched later when the UI interface of the Widget is actually constructed.

The last call to updateAppWidget is where the widget interface is actually built (read in sections):

public void updateAppWidget(RemoteViews remoteViews) {…
        boolean recycled = false;
        View content = null;
        Exception exception = null;
        …
        if (remoteViews == null) {…
        } else {
            mRemoteContext = getRemoteContext(remoteViews);
            int layoutId = remoteViews.getLayoutId();/*Step1. 描述Widget的LayoutId*/
            …
            if (content == null) {
                try {
                   content = remoteViews.apply(mContext, this, mOnClickHandler);/*Step2.
                    创建Widget的View*/
                } catch (RuntimeException e) {
                    exception = e;
                }
            }
            mLayoutId = layoutId;
            mViewMode = VIEW_MODE_CONTENT;
        }
        …
        if (!recycled) {
            prepareView(content);
   addView(content);/*添加Widget的View到全局管理中*/
        }
        …
    }

To summarize this function, simply speaking it does two things.

  • Generate a View (content variable).
    It is speculated that this View is generated from the "drawing" of the Widget, and therefore represents the UI interface of the Widget.

  • Add the above View to AppWidgetHostView.
    AppWidgetHostView is a FrameLayout, which adds content as a sub-View. In this way, when the entire View is redrawn, the Widget's interface will naturally appear.

Let’s take a look at the implementation of the apply function in the above code snippet:

  /*frameworks/base/core/java/android/widget/RemoteViews.java*/
    public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);
        View result;
        Context c = prepareContext(context);
        LayoutInflater inflater = (LayoutInflater)c.
                                   getSystemService(Context.LAYOUT_ INFLATER_ SERVICE);
        …
        result=inflater.inflate(rvToApply.getLayoutId(), parent, false);
        …
        return result;
    }

In this way, the program successfully constructs a local View object in the host process according to the "drawing" provided by the Widget - it will be finally displayed on the screen together with other Views in Host_Application after being processed by SurfaceFlinger.

Guess you like

Origin blog.csdn.net/jxq1994/article/details/132741262