Relearning the basic part of android -- AIDL

AIDL is similar to other IDL languages. You need to do some work. It allows you to define the program interface agreed between the client and the server, and use interprocess communication to communicate with each other. On Android, one process cannot normally access the memory of another process. Therefore, they need to decompose their objects into basic units that the operating system can understand, and then write these objects across the process boundary in order. This code is monotonous and lengthy, so Android uses AIDL to deal with this problem for you.

Note: using AIDL is only necessary if you allow clients from different applications to access your service through cross process communication and want to handle multithreading in your service. If you don't need to execute IPC concurrency between different applications, you should establish your interface by implementing Binder, or if you want to execute IPC but don't need to handle multithreading. Then use Messenger to implement your interface. Anyway, make sure you understand the Bound Service before implementing an AIDL

Before you design your AIDL interface, please note that calling an AIDL interface is a direct function call. You should not assume that the occurrence of a thread in which call is related to whether the dependent call comes from a thread in a local process or a thread in a remote process, especially:

  • The call from the local process is executed in the same thread as the caller. If this is your main UI thread, the thread continues to execute in the AIDL interface. If it is another thread, it is a thread that executes your code in the service. In this way, if only the local thread accesses the service, You can completely control which threads execute in it (but if so, you should not use AIDL at all, but establish the interface by implementing Binder)
  • The platform internally maintains the call of a remote process allocated in a thread pool in your own process. You must be prepared for the upcoming call from an unknown thread and be accompanied by multiple calls at the same time. In other words, the implementation of AIDL interface must be completely thread safe
  • One way keyword defines the behavior of remote call. When used, a remote call will not be blocked; It simply sends the transmission data and immediately returns to the implementation of the final interface. It receives it as a regular call from the Binder thread pool and an ordinary remote call. If the local call uses one-way, it will not have an impact, and the call is still different

Define an AIDL interface

You must be in one Use the java programming language syntax to define your AIDL interface in the AIDL file, and then save it in the source code of the application providing the service and any application bound to the service (in the src directory)

When you compile contains When applying aidl files, Android SDK tools are based on this The aidl file generates an IBinder interface and saves it to the gen directory of the project. The service must properly implement the IBinder interface. After that, the client application can bind to the service, and then call the method from IBinder to execute IPC

To establish an adjacent service using AIDL, you need to follow the following steps

  1. 1. Establishment aidl file, which defines the language interface using method signatures
  2. 2. Implement this interface. Android SDk tool is based on your AIDL file uses java language to generate an interface. This interface has an internal abstract class called Stub. It inherits Binder and implements your AIDL interface. You must inherit this Stub class and implement these methods
  3. 3. Expose this interface to the client to implement a service and override the onBind() method to return your Stub implementation class

Warning: any changes after you first released aidl must be backward compatible to avoid damaging other applications using your service, that is, because of your aidl files must be copied to other applications to allow them to access the interface of your service. You must maintain the support of the original interface.

1. Establishment aidl file

AIDL uses a simple syntax that allows you to declare an interface with one or more methods with parameters and return values. Parameters and return values can be of any type, even AIDL generated interfaces

You must use the java language to build aidl file each The aidl file must define a simple interface and require only interface declarations and method signatures

By default, AIDL supports the following data types:

  • All basic data types in ava language (such as int, long, char, boolean, etc.)
  • String
  • CharSequence
  • List All elements in the List must be one of the types supported by AIDL, or an interface generated by other AIDL, or the parseable List you define can use the paradigm (for example, List). The actual class at the receiving end is often an ArrayList, although the method is generated using the List interface
  • Map All elements in the map must be one of the types supported by AIDL, or an interface generated by other AIDL, or the parseable template map you define is not supported (such as this form of map). The actual class at the receiving end is often a HashMap, although the method is generated using the map interface

For types other than the above types, you must declare import, even within the same package.

When defining your service interface, note:

  • Method can receive 0 or more parameters and have a return value or return void
  • All non basic data types require a directional tag to specify which direction the data is going. Whether it is input, output, or input / output (see the following example), the basic data type is supported by default and cannot be other. Warning: you should limit the direction to what you really need, because the cost of sorting parameters is very expensive.
  • All code comments in the. aidl file are in the generated IBinder interface (except the comments before import and package declaration)
  • Only methods are supported. You cannot expose static domains in AIDL

Here's a Example of aidl file:

// IRemoteService.aidl
package com.example.android;

// Declare any non-default types here with import statements

/** Example service interface */
interface IRemoteService {
    /** Request the process ID of this service, to do evil things with it. */
    int getPid();

    /** Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

Simply save your The Aidl file is in the src directory of your project. When you build your application, the SDK tool generates the IBinder interface file in the gen directory of your project. The generated file name and Aidl name matches, but it is based on Java is the extension (for example, IRemoteService.aidl corresponds to IRemoteService.java)

If you use Eclipse, incremental compilation generates the binder class almost immediately. If you don't use Eclipse, the Ant tool generates the binder class the next time you compile your application (you should compile your project with ant debug or ant release). Once you've written it aidl file, your code can be linked to the generated class.

2. Implement the interface

When you compile your application, the Android SDK tool generates one java interface files use your The generated interface named by aidl file contains a subclass named stub (such as YourInterface.Stub), which is an abstract implementation of its parent class and declares All methods in aidl

Note: Stub also defines some auxiliary methods, most notably asInterface(), which is used to receive an IBinder (usually the onServiceConnected() callback method passed by the IBinder to the client) and return an instance of the Stub interface. For more details, please refer to the Calling an IPC Method section.

In order to realize from The interface generated from the aidl file needs to inherit the Binder interface (for example, YourInterface.Stub) and implement the interface from Methods inherited from the aidl file.

Here is an example of using anonymous instances to implement an interface called IRemoteService (defined in IRemoteService.aidl, as shown above)

private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
    public int getPid(){
        return Process.myPid();
    }
    public void basicTypes(int anInt, long aLong, boolean aBoolean,
        float aFloat, double aDouble, String aString) {
        // Does nothing
    }
};

mBinder is an instance of the Stub class

There are many rules to pay attention to when implementing your AIDL interface

  • It is not necessary to ensure that your main thread is safe for you to start the service
  • By default, RPC calls are synchronous. If you know that service takes some time to complete the request, you should not call it from the main thread of activity, because it may make the application unresponsive (Android may display a ANR dialog box), usually you should call it on a separate thread in the client.
  • Thrown exceptions are not returned to the caller

3. Expose the interface to the client

Once you implement the interface for the service, you need to expose it to the client so that they can bind to it. In order to expose the interface for your service, inherit the service and implement the onBind() method to return a Stub class generated by your implementation (as we discussed in the previous conclusion). Here is an example of a service exposing the IRemoteService interface to the client

public class RemoteService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Return the interface
        return mBinder;
    }

    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        public int getPid(){
            return Process.myPid();
        }
        public void basicTypes(int anInt, long aLong, boolean aBoolean,
            float aFloat, double aDouble, String aString) {
            // Does nothing
        }
    };
}

Now, when a client (such as an activity) calls bindService() to connect to the service, the onServiceConnected() callback function of the client receives the mBinder instance returned by the onBind() method in the service

The client must be able to access the interface class, so if the client and server are in different applications, the application where the client is located must have one The copy of AIDL file is in its src directory (the android.os.Binder interface is generated, and the AIDL methods provided by the client are all in this directory)

When the client receives the IBinder in the onServiceConnected() callback method, it must call your ServiceInterface Stub. Asinterface (service) to map the return parameters to your ServiceInterface type. For example:

IRemoteService mIRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the example above for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service
        mIRemoteService = IRemoteService.Stub.asInterface(service);
    }

    // Called when the connection with the service disconnects unexpectedly
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        mIRemoteService = null;
    }
};

For more sample codes, see remoteservice in ApiDemos java

Transfer objects across processes

If you want to pass a class from one process to another through the IPC interface, you can. However, you must ensure that the code written for your class is also available at the other end of the IPC channel, and your class must support the Parcelable interface. Supporting the Parcelable interface is very important because it allows the Android system to decompose objects into basic units that can be transferred across processes by the organization

In order to create a class that supports the Parcelable protocol, you must follow the following rules:

  1. To implement the Parcelable interface
  2. Implement writeToParcel, which is used to write the current state of the object into a Parcel object.
  3. Add a static field called CREATOR to your class, which implements Parcelable CREATOR interface
  4. Finally, establish a The Aidl file declares your parcelable class (as shown in Rect.aidl below). If you use a custom build process, do not build Aidl file. Similar to the header file in C language Aidl files are not compiled

AIDL uses these fields and methods in the code to encapsulate, transmit and interpret your objects

For example, there is a Rect The Aidl file class creates a Rect class, which is parcelable

package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;

Here is an example of how the Rect class implements the Parcelable protocol

import android.os.Parcel;
import android.os.Parcelable;

public final class Rect implements Parcelable {
    public int left;
    public int top;
    public int right;
    public int bottom;

    public static final Parcelable.Creator<Rect> CREATOR = new
Parcelable.Creator<Rect>() {
        public Rect createFromParcel(Parcel in) {
            return new Rect(in);
        }

        public Rect[] newArray(int size) {
            return new Rect[size];
        }
    };

    public Rect() {
    }

    private Rect(Parcel in) {
        readFromParcel(in);
    }

    public void writeToParcel(Parcel out) {
        out.writeInt(left);
        out.writeInt(top);
        out.writeInt(right);
        out.writeInt(bottom);
    }

    public void readFromParcel(Parcel in) {
        left = in.readInt();
        top = in.readInt();
        right = in.readInt();
        bottom = in.readInt();
    }
}

Organizing data transfer in Rect class is very simple. Take a look at other functions above Parcel to see how you can write other types of values into a Parcel.

Warning: don't forget the security nature of receiving data from other processes. In this case, Rect reads four numbers from Parcel, but it depends on ensuring that they are within the acceptable range, regardless of what the caller is trying to do. For more information on how to keep away from malicious programs and ensure the security of your application, see Security and Permissions

Call an IPC method

The following is the calling step. The caller must call a remote interface defined by AIDL

  1. Import under src directory in the project aidl file
  2. Declare an instance of the IBinder interface (generated based on AIDL)
  3. Implement ServiceConnection
  4. Call context Bindservice() is passed to your ServiceConnection implementation.
  5. In your onServiceConnected() implementation, you will receive an IBinder instance (called the server) calling yourinterfacename Stub. Asinterface ((IBinder) service) maps the return value to your interface type
  6. Call the method defined in your interface. You should catch the DeadObjectException thrown when the connection is broken. This is the only exception thrown by the remote method
  7. Use the instance of your interface to call context Unbindservice() to disconnect

Some comments on calling IPC server:

  • Objects are reference counted across processes
  • You can pass an anonymous object as a method parameter

For more information about binding services, please read the Bound Services documentation

Call some sample codes of the server established by an AIDL, from the Remote Service sample in the ApiDemos project.

public static class Binding extends Activity {
    /** The primary interface we will be calling on the service. */
    IRemoteService mService = null;
    /** Another interface we use on the service. */
    ISecondary mSecondaryService = null;

    Button mKillButton;
    TextView mCallbackText;

    private boolean mIsBound;

    /**
     * Standard initialization of this activity.  Set up the UI, then wait
     * for the user to poke it before doing anything.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.remote_service_binding);

        // Watch for button clicks.
        Button button = (Button)findViewById(R.id.bind);
        button.setOnClickListener(mBindListener);
        button = (Button)findViewById(R.id.unbind);
        button.setOnClickListener(mUnbindListener);
        mKillButton = (Button)findViewById(R.id.kill);
        mKillButton.setOnClickListener(mKillListener);
        mKillButton.setEnabled(false);

        mCallbackText = (TextView)findViewById(R.id.callback);
        mCallbackText.setText("Not attached.");
    }

    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // This is called when the connection with the service has been
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service);
            mKillButton.setEnabled(true);
            mCallbackText.setText("Attached.");

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService.registerCallback(mCallback);
            } catch (RemoteException e) {
                // In this case the service has crashed before we could even
                // do anything with it; we can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_connected,
                    Toast.LENGTH_SHORT).show();
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            mService = null;
            mKillButton.setEnabled(false);
            mCallbackText.setText("Disconnected.");

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private ServiceConnection mSecondaryConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            mSecondaryService = ISecondary.Stub.asInterface(service);
            mKillButton.setEnabled(true);
        }

        public void onServiceDisconnected(ComponentName className) {
            mSecondaryService = null;
            mKillButton.setEnabled(false);
        }
    };

    private OnClickListener mBindListener = new OnClickListener() {
        public void onClick(View v) {
            // Establish a couple connections with the service, binding
            // by interface names.  This allows other applications to be
            // installed that replace the remote service by implementing
            // the same interface.
            bindService(new Intent(IRemoteService.class.getName()),
                    mConnection, Context.BIND_AUTO_CREATE);
            bindService(new Intent(ISecondary.class.getName()),
                    mSecondaryConnection, Context.BIND_AUTO_CREATE);
            mIsBound = true;
            mCallbackText.setText("Binding.");
        }
    };

    private OnClickListener mUnbindListener = new OnClickListener() {
        public void onClick(View v) {
            if (mIsBound) {
                // If we have received the service, and hence registered with
                // it, then now is the time to unregister.
                if (mService != null) {
                    try {
                        mService.unregisterCallback(mCallback);
                    } catch (RemoteException e) {
                        // There is nothing special we need to do if the service
                        // has crashed.
                    }
                }

                // Detach our existing connection.
                unbindService(mConnection);
                unbindService(mSecondaryConnection);
                mKillButton.setEnabled(false);
                mIsBound = false;
                mCallbackText.setText("Unbinding.");
            }
        }
    };

    private OnClickListener mKillListener = new OnClickListener() {
        public void onClick(View v) {
            // To kill the process hosting our service, we need to know its
            // PID.  Conveniently our service has a call that will return
            // to us that information.
            if (mSecondaryService != null) {
                try {
                    int pid = mSecondaryService.getPid();
                    // Note that, though this API allows us to request to
                    // kill any process based on its PID, the kernel will
                    // still impose standard restrictions on which PIDs you
                    // are actually able to kill.  Typically this means only
                    // the process running your application and any additional
                    // processes created by that app as shown here; packages
                    // sharing a common UID will also be able to kill each
                    // other's processes.
                    Process.killProcess(pid);
                    mCallbackText.setText("Killed service process.");
                } catch (RemoteException ex) {
                    // Recover gracefully from the process hosting the
                    // server dying.
                    // Just for purposes of the sample, put up a notification.
                    Toast.makeText(Binding.this,
                            R.string.remote_call_failed,
                            Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here will
         * NOT be running in our main thread like most other things -- so,
         * to update the UI, we need to use a Handler to hop over there.
         */
        public void valueChanged(int value) {
            mHandler.sendMessage(mHandler.obtainMessage(BUMP_MSG, value, 0));
        }
    };

    private static final int BUMP_MSG = 1;

    private Handler mHandler = new Handler() {
        @Override public void handleMessage(Message msg) {
            switch (msg.what) {
                case BUMP_MSG:
                    mCallbackText.setText("Received from service: " + msg.arg1);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }

    };
}
1.Based on the above aidl Use this time to prepare for the study ActivityManager Frame, right aidl More in-depth research has been carried out because android The framework uses a lot of process communication mechanism, so it is under research android framework Study it carefully before AIDL The implementation mechanism of is very necessary. 2.As mentioned earlier aidl yes Android Interface definition language It is a description of a process communication interface sdk The interpreter compiles the interpreter and compiles it into java Code in gen Directory, classpath and aidl The classpath of the file is the same. three.aidl Interface
package com.cao.android.demos.binder.aidl;  
import com.cao.android.demos.binder.aidl.AIDLActivity;
interface AIDLService {   
    void registerTestCall(AIDLActivity cb);   
    void invokCallBack();
}It is generated after compilation java The documents are as follows AIDLService.java Detailed description aidl For the implementation of the interface, see the figure above, AIDLActivity.aidl Compiled into an interface AIDLActivity´╝îA stub class Stub,A proxy class Proxy
public interface AIDLService extends android.os.IInterface//With aidlactivity java interface implementation corresponding to the interface defined in Aidl
public static abstract class Stub extends android.os.Binder implements com.cao.android.demos.binder.aidl.AIDLService
//Inherit Android os. Binder receives the communication data in onTransact, calls the AIDLService interface method through different communication parameter code s, and writes back the call result. The AIDLService interface method needs to
//Server implementation
private static class Proxy implements com.cao.android.demos.binder.aidl.AIDLService
//Implement the AIDLService interface method, but the method only performs the proxy remote call operation. The specific method operation is implemented in the stub stub class at the remote end. In general, AIDLActivity Aidl compilation will generate an AIDLActivity interface, a stub stub abstraction class and a proxy proxy class. This implementation is consistent with the idea of compiling and generating the wsdl file of the root axis,
stub The stub extraction class needs to be implemented on the server, proxy The proxy class is used by the client through stub,proxy The encapsulation of, which shields the details of process communication, is just a problem for users AIDLActivity Interface call 4.Use according to the above ideas aidl Take another look AIDLService Call implementation code
--1.On the server side AIDLService.Stub Abstract class, on the server side onBind Method
--2.Client binding service Shi Zai ServiceConnection.onServiceConnected obtain onBind Returned IBinder object
        private ServiceConnection mConnection = new ServiceConnection() {
                public void onServiceConnected(ComponentName className, IBinder service) {
                        Log("connect service");
                        mService = AIDLService.Stub.asInterface(service);
                        try {
                                mService.registerTestCall(mCallback);
                        } catch (RemoteException e) {                        }
                }
        be careful mConnection stay bindservice As call parameters: bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
--3.AIDLService.Stub.asInterface(service);
public static com.cao.android.demos.binder.aidl.AIDLService asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
//If bindService binds a service of the same process and returns the home province of the Stub object on the server side, the Stub object is directly operated on the client side and process communication is not carried out
if (((iin!=null)&&(iin instanceof com.cao.android.demos.binder.aidl.AIDLService))) {
return ((com.cao.android.demos.binder.aidl.AIDLService)iin);
}
//bindService does not bind to a service of the same process, but returns a proxy object, obj = = Android os. Binderproxy object, wrapped as an aidlservice Stub. Proxy proxy object
//But aidlservice Stub. The communication between proxy processes is through Android os. Binderproxy implementation
return new com.cao.android.demos.binder.aidl.AIDLService.Stub.Proxy(obj);
}
--4.call AIDLService Interface method. If it is the same process, AIDLService namely service of Stub Object, equivalent to direct call Stub Object implemented AIDLService Interface method
 If it's a proxy Object, which is called between processes. Let's look at an example of client call:
                        public void onClick(View v) {
                                Log("AIDLTestActivity.btnCallBack");
                                try {
                                        mService.invokCallBack();
                                } catch (RemoteException e) {
                                        // TODO Auto-generated catch block
                                        e.printStackTrace();
                                }
        --mService.invokCallBack()Equivalent call Proxy.invokCallBack,When we call this method between processes, we look at it
public void invokCallBack() throws android.os.RemoteException
{
//Construct a Parcel object that can be transferred between processes
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
//DESCRIPTOR = "com.cao.android.demos.binder.aidl.AIDLService", describes which Stub object to call
_data.writeInterfaceToken(DESCRIPTOR);
//Stub.TRANSACTION_invokCallBack identifies which interface method in the stub is called. mRemote is the parameter obj of the Proxy object, that is, public void onserviceconnected (componentname, classname, ibinder service)
//The service parameter in, which is a BinderProxy object and is responsible for transmitting inter process data.
mRemote.transact(Stub.TRANSACTION_invokCallBack, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
--5.BinderProxy.transact The method is implemented locally
   public native boolean transact(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException;
        //Corresponding localization code / frameworks / base / core / JNI / Android_ util_ Binder. cpp->static jboolean android_ os_ BinderProxy_ transact(JNIEnv* env, jobject obj,
                                                jint code, jobject dataObj,
                                                jobject replyObj, jint flags)
  //How to implement the specific process communication in c code will be further studied in the future.
--6.Server process data receiving
        --call Stack 
        ##AIDLService.Stub.onTransact
        ##AIDLService.Stub(Binder).execTransact
        ##NativeStart.run
        --AIDLService.Stub.onTransact
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_registerTestCall:
{
data.enforceInterface(DESCRIPTOR);
com.cao.android.demos.binder.aidl.AIDLActivity _arg0;
_arg0 = com.cao.android.demos.binder.aidl.AIDLActivity.Stub.asInterface(data.readStrongBinder());
this.registerTestCall(_arg0);
reply.writeNoException();
return true;
}
//TRANSACTION_invokCallBack is determined by the transact method parameter when the client calls, code==TRANSACTION_invokCallBack, execute
//invokCallBack method, which is implemented by the server stub class inheriting study.
case TRANSACTION_invokCallBack:
{
data.enforceInterface(DESCRIPTOR);
this.invokCallBack();
reply.writeNoException();
return true;
}5.Local settings inside C I didn't study the code call in depth, as I later android I will send a message if the framework is deepened blog Further, the bottom of the people C How does the code realize process communication AIDL Process communication, temporarily studied here. Original text: http://blog.csdn.net/stonecao/article/details/6579333

Added by patelp7 on Mon, 07 Mar 2022 08:54:28 +0200