I finally found a way by redefining EOSIO_ABI.
The "transfer" function in my contract get called when an EOS transfer is done through eosio.token contract.
#undef EOSIO_ABI
#define EOSIO_ABI( TYPE, MEMBERS ) \
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
if( action == N(onerror)) { \
/* onerror is only valid if it is for the "eosio" code account and authorized by "eosio"'s "active permission */ \
eosio_assert(code == N(eosio), "onerror action's are only valid from the \"eosio\" system account"); \
} \
auto self = receiver; \
if( code == self || code == N(eosio.token) || action == N(onerror) ) { \
TYPE thiscontract( self ); \
switch( action ) { \
EOSIO_API( TYPE, MEMBERS ) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \
} \
}
EOSIO_ABI(ping_contract, (ping) (transfer) )
I used something like this for the permission:
cleos set account permission CONTRACT_ACCOUNT active '{"threshold": 1,"keys": [{"key":"CONTRACT_ACCOUNT_PUBKEY","weight":1}],"accounts": [{"permission":{"actor":"CONTRACT_ACCOUNT","permission":"eosio.code"},"weight":1}]}' owner -p CONTRACT_ACCOUNT@active