2024-01-16 12:18:56 +03:00
|
|
|
/**
|
|
|
|
* @file expansion_test.c
|
|
|
|
* @brief Expansion module support testing application.
|
|
|
|
*
|
|
|
|
* Before running, connect pins using the following scheme:
|
|
|
|
* 13 -> 16 (USART TX to LPUART RX)
|
|
|
|
* 14 -> 15 (USART RX to LPUART TX)
|
|
|
|
*
|
|
|
|
* What this application does:
|
|
|
|
*
|
|
|
|
* - Enables module support and emulates the module on a single device
|
|
|
|
* (hence the above connection),
|
|
|
|
* - Connects to the expansion module service, sets baud rate,
|
|
|
|
* - Starts the RPC session,
|
|
|
|
* - Creates a directory at `/ext/ExpansionTest` and writes a file
|
|
|
|
* named `test.txt` under it,
|
|
|
|
* - Plays an audiovisual alert (sound and blinking display),
|
|
|
|
* - Waits 10 cycles of idle loop,
|
|
|
|
* - Stops the RPC session,
|
|
|
|
* - Waits another 10 cycles of idle loop,
|
|
|
|
* - Exits (plays a sound if any of the above steps failed).
|
|
|
|
*/
|
|
|
|
#include <furi.h>
|
|
|
|
|
|
|
|
#include <furi_hal_resources.h>
|
|
|
|
|
|
|
|
#include <furi_hal_serial.h>
|
|
|
|
#include <furi_hal_serial_control.h>
|
|
|
|
|
|
|
|
#include <pb.h>
|
|
|
|
#include <pb_decode.h>
|
|
|
|
#include <pb_encode.h>
|
|
|
|
|
|
|
|
#include <flipper.pb.h>
|
|
|
|
|
|
|
|
#include <storage/storage.h>
|
|
|
|
#include <expansion/expansion.h>
|
|
|
|
#include <notification/notification_messages.h>
|
|
|
|
#include <expansion/expansion_protocol.h>
|
|
|
|
|
|
|
|
#define TAG "ExpansionTest"
|
|
|
|
|
|
|
|
#define TEST_DIR_PATH EXT_PATH(TAG)
|
|
|
|
#define TEST_FILE_NAME "test.txt"
|
|
|
|
#define TEST_FILE_PATH EXT_PATH(TAG "/" TEST_FILE_NAME)
|
|
|
|
|
|
|
|
#define HOST_SERIAL_ID (FuriHalSerialIdLpuart)
|
|
|
|
#define MODULE_SERIAL_ID (FuriHalSerialIdUsart)
|
|
|
|
|
|
|
|
#define RECEIVE_BUFFER_SIZE (sizeof(ExpansionFrame) + sizeof(ExpansionFrameChecksum))
|
|
|
|
|
|
|
|
typedef enum {
|
|
|
|
ExpansionTestAppFlagData = 1U << 0,
|
|
|
|
ExpansionTestAppFlagExit = 1U << 1,
|
|
|
|
} ExpansionTestAppFlag;
|
|
|
|
|
|
|
|
#define EXPANSION_TEST_APP_ALL_FLAGS (ExpansionTestAppFlagData | ExpansionTestAppFlagExit)
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
FuriThreadId thread_id;
|
|
|
|
Expansion* expansion;
|
|
|
|
FuriHalSerialHandle* handle;
|
|
|
|
FuriStreamBuffer* buf;
|
|
|
|
ExpansionFrame frame;
|
|
|
|
PB_Main msg;
|
|
|
|
Storage* storage;
|
|
|
|
} ExpansionTestApp;
|
|
|
|
|
|
|
|
static void expansion_test_app_serial_rx_callback(
|
|
|
|
FuriHalSerialHandle* handle,
|
|
|
|
FuriHalSerialRxEvent event,
|
|
|
|
void* context) {
|
|
|
|
furi_assert(handle);
|
|
|
|
furi_assert(context);
|
|
|
|
ExpansionTestApp* app = context;
|
|
|
|
|
|
|
|
if(event == FuriHalSerialRxEventData) {
|
|
|
|
const uint8_t data = furi_hal_serial_async_rx(handle);
|
|
|
|
furi_stream_buffer_send(app->buf, &data, sizeof(data), 0);
|
|
|
|
furi_thread_flags_set(app->thread_id, ExpansionTestAppFlagData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static ExpansionTestApp* expansion_test_app_alloc() {
|
|
|
|
ExpansionTestApp* instance = malloc(sizeof(ExpansionTestApp));
|
|
|
|
instance->buf = furi_stream_buffer_alloc(RECEIVE_BUFFER_SIZE, 1);
|
|
|
|
return instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void expansion_test_app_free(ExpansionTestApp* instance) {
|
|
|
|
furi_stream_buffer_free(instance->buf);
|
|
|
|
free(instance);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void expansion_test_app_start(ExpansionTestApp* instance) {
|
|
|
|
instance->thread_id = furi_thread_get_current_id();
|
|
|
|
instance->expansion = furi_record_open(RECORD_EXPANSION);
|
|
|
|
instance->handle = furi_hal_serial_control_acquire(MODULE_SERIAL_ID);
|
2024-01-29 15:35:46 +03:00
|
|
|
furi_check(instance->handle);
|
2024-01-16 12:18:56 +03:00
|
|
|
// Configure the serial port
|
|
|
|
furi_hal_serial_init(instance->handle, EXPANSION_PROTOCOL_DEFAULT_BAUD_RATE);
|
|
|
|
// Start waiting for the initial pulse
|
2024-01-30 16:54:25 +03:00
|
|
|
expansion_set_listen_serial(instance->expansion, HOST_SERIAL_ID);
|
2024-01-16 12:18:56 +03:00
|
|
|
|
|
|
|
furi_hal_serial_async_rx_start(
|
|
|
|
instance->handle, expansion_test_app_serial_rx_callback, instance, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void expansion_test_app_stop(ExpansionTestApp* instance) {
|
2024-01-30 16:54:25 +03:00
|
|
|
// Disable expansion module support
|
|
|
|
expansion_disable(instance->expansion);
|
2024-01-16 12:18:56 +03:00
|
|
|
// Give back the module handle
|
|
|
|
furi_hal_serial_control_release(instance->handle);
|
2024-01-30 16:54:25 +03:00
|
|
|
// Restore expansion user settings
|
|
|
|
expansion_enable(instance->expansion);
|
2024-01-16 12:18:56 +03:00
|
|
|
furi_record_close(RECORD_EXPANSION);
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline bool expansion_test_app_is_success_response(const ExpansionFrame* response) {
|
|
|
|
return response->header.type == ExpansionFrameTypeStatus &&
|
|
|
|
response->content.status.error == ExpansionFrameErrorNone;
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline bool expansion_test_app_is_success_rpc_message(const PB_Main* message) {
|
|
|
|
return (message->command_status == PB_CommandStatus_OK ||
|
|
|
|
message->command_status == PB_CommandStatus_ERROR_STORAGE_EXIST) &&
|
|
|
|
(message->which_content == PB_Main_empty_tag);
|
|
|
|
}
|
|
|
|
|
|
|
|
static size_t expansion_test_app_receive_callback(uint8_t* data, size_t data_size, void* context) {
|
|
|
|
ExpansionTestApp* instance = context;
|
|
|
|
|
|
|
|
size_t received_size = 0;
|
|
|
|
|
|
|
|
while(true) {
|
|
|
|
received_size += furi_stream_buffer_receive(
|
|
|
|
instance->buf, data + received_size, data_size - received_size, 0);
|
|
|
|
if(received_size == data_size) break;
|
|
|
|
|
|
|
|
const uint32_t flags = furi_thread_flags_wait(
|
|
|
|
EXPANSION_TEST_APP_ALL_FLAGS, FuriFlagWaitAny, EXPANSION_PROTOCOL_TIMEOUT_MS);
|
|
|
|
|
|
|
|
// Exit on any error
|
|
|
|
if(flags & FuriFlagError) break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return received_size;
|
|
|
|
}
|
|
|
|
|
|
|
|
static size_t
|
|
|
|
expansion_test_app_send_callback(const uint8_t* data, size_t data_size, void* context) {
|
|
|
|
ExpansionTestApp* instance = context;
|
|
|
|
|
|
|
|
furi_hal_serial_tx(instance->handle, data, data_size);
|
|
|
|
furi_hal_serial_tx_wait_complete(instance->handle);
|
|
|
|
|
|
|
|
return data_size;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_receive_frame(ExpansionTestApp* instance, ExpansionFrame* frame) {
|
|
|
|
return expansion_protocol_decode(frame, expansion_test_app_receive_callback, instance) ==
|
|
|
|
ExpansionProtocolStatusOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
expansion_test_app_send_status_response(ExpansionTestApp* instance, ExpansionFrameError error) {
|
|
|
|
ExpansionFrame frame = {
|
|
|
|
.header.type = ExpansionFrameTypeStatus,
|
|
|
|
.content.status.error = error,
|
|
|
|
};
|
|
|
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
|
|
|
ExpansionProtocolStatusOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_send_heartbeat(ExpansionTestApp* instance) {
|
|
|
|
ExpansionFrame frame = {
|
|
|
|
.header.type = ExpansionFrameTypeHeartbeat,
|
|
|
|
.content.heartbeat = {},
|
|
|
|
};
|
|
|
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
|
|
|
ExpansionProtocolStatusOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
expansion_test_app_send_baud_rate_request(ExpansionTestApp* instance, uint32_t baud_rate) {
|
|
|
|
ExpansionFrame frame = {
|
|
|
|
.header.type = ExpansionFrameTypeBaudRate,
|
|
|
|
.content.baud_rate.baud = baud_rate,
|
|
|
|
};
|
|
|
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
|
|
|
ExpansionProtocolStatusOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_send_control_request(
|
|
|
|
ExpansionTestApp* instance,
|
|
|
|
ExpansionFrameControlCommand command) {
|
|
|
|
ExpansionFrame frame = {
|
|
|
|
.header.type = ExpansionFrameTypeControl,
|
|
|
|
.content.control.command = command,
|
|
|
|
};
|
|
|
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
|
|
|
ExpansionProtocolStatusOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_send_data_request(
|
|
|
|
ExpansionTestApp* instance,
|
|
|
|
const uint8_t* data,
|
|
|
|
size_t data_size) {
|
|
|
|
furi_assert(data_size <= EXPANSION_PROTOCOL_MAX_DATA_SIZE);
|
|
|
|
|
|
|
|
ExpansionFrame frame = {
|
|
|
|
.header.type = ExpansionFrameTypeData,
|
|
|
|
.content.data.size = data_size,
|
|
|
|
};
|
|
|
|
|
|
|
|
memcpy(frame.content.data.bytes, data, data_size);
|
|
|
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
|
|
|
ExpansionProtocolStatusOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_rpc_encode_callback(
|
|
|
|
pb_ostream_t* stream,
|
|
|
|
const pb_byte_t* data,
|
|
|
|
size_t data_size) {
|
|
|
|
ExpansionTestApp* instance = stream->state;
|
|
|
|
|
|
|
|
size_t size_sent = 0;
|
|
|
|
|
|
|
|
while(size_sent < data_size) {
|
|
|
|
const size_t current_size = MIN(data_size - size_sent, EXPANSION_PROTOCOL_MAX_DATA_SIZE);
|
|
|
|
if(!expansion_test_app_send_data_request(instance, data + size_sent, current_size)) break;
|
|
|
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
|
|
|
if(!expansion_test_app_is_success_response(&instance->frame)) break;
|
|
|
|
size_sent += current_size;
|
|
|
|
}
|
|
|
|
|
|
|
|
return size_sent == data_size;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_send_rpc_request(ExpansionTestApp* instance, PB_Main* message) {
|
|
|
|
pb_ostream_t stream = {
|
|
|
|
.callback = expansion_test_app_rpc_encode_callback,
|
|
|
|
.state = instance,
|
|
|
|
.max_size = SIZE_MAX,
|
|
|
|
.bytes_written = 0,
|
|
|
|
.errmsg = NULL,
|
|
|
|
};
|
|
|
|
|
|
|
|
const bool success = pb_encode_ex(&stream, &PB_Main_msg, message, PB_ENCODE_DELIMITED);
|
|
|
|
pb_release(&PB_Main_msg, message);
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_receive_rpc_request(ExpansionTestApp* instance, PB_Main* message) {
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
|
|
|
if(!expansion_test_app_send_status_response(instance, ExpansionFrameErrorNone)) break;
|
|
|
|
if(instance->frame.header.type != ExpansionFrameTypeData) break;
|
|
|
|
pb_istream_t stream = pb_istream_from_buffer(
|
|
|
|
instance->frame.content.data.bytes, instance->frame.content.data.size);
|
|
|
|
if(!pb_decode_ex(&stream, &PB_Main_msg, message, PB_DECODE_DELIMITED)) break;
|
|
|
|
success = true;
|
|
|
|
} while(false);
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_send_presence(ExpansionTestApp* instance) {
|
|
|
|
// Send pulses to emulate module insertion
|
|
|
|
const uint8_t init = 0xAA;
|
|
|
|
furi_hal_serial_tx(instance->handle, &init, sizeof(init));
|
|
|
|
furi_hal_serial_tx_wait_complete(instance->handle);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_wait_ready(ExpansionTestApp* instance) {
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
|
|
|
if(instance->frame.header.type != ExpansionFrameTypeHeartbeat) break;
|
|
|
|
success = true;
|
|
|
|
} while(false);
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_handshake(ExpansionTestApp* instance) {
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(!expansion_test_app_send_baud_rate_request(instance, 230400)) break;
|
|
|
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
|
|
|
if(!expansion_test_app_is_success_response(&instance->frame)) break;
|
|
|
|
furi_hal_serial_set_br(instance->handle, 230400);
|
|
|
|
furi_delay_ms(EXPANSION_PROTOCOL_BAUD_CHANGE_DT_MS);
|
|
|
|
success = true;
|
|
|
|
} while(false);
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_start_rpc(ExpansionTestApp* instance) {
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(!expansion_test_app_send_control_request(instance, ExpansionFrameControlCommandStartRpc))
|
|
|
|
break;
|
|
|
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
|
|
|
if(!expansion_test_app_is_success_response(&instance->frame)) break;
|
|
|
|
success = true;
|
|
|
|
} while(false);
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_rpc_mkdir(ExpansionTestApp* instance) {
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
instance->msg.command_id++;
|
|
|
|
instance->msg.command_status = PB_CommandStatus_OK;
|
|
|
|
instance->msg.which_content = PB_Main_storage_mkdir_request_tag;
|
|
|
|
instance->msg.has_next = false;
|
|
|
|
instance->msg.content.storage_mkdir_request.path = TEST_DIR_PATH;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(!expansion_test_app_send_rpc_request(instance, &instance->msg)) break;
|
|
|
|
if(!expansion_test_app_receive_rpc_request(instance, &instance->msg)) break;
|
|
|
|
if(!expansion_test_app_is_success_rpc_message(&instance->msg)) break;
|
|
|
|
success = true;
|
|
|
|
} while(false);
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_rpc_write(ExpansionTestApp* instance) {
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
|
|
File* file = storage_file_alloc(storage);
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(!storage_file_open(file, APP_ASSETS_PATH(TEST_FILE_NAME), FSAM_READ, FSOM_OPEN_EXISTING))
|
|
|
|
break;
|
|
|
|
|
|
|
|
const uint64_t file_size = storage_file_size(file);
|
|
|
|
|
|
|
|
instance->msg.command_id++;
|
|
|
|
instance->msg.command_status = PB_CommandStatus_OK;
|
|
|
|
instance->msg.which_content = PB_Main_storage_write_request_tag;
|
|
|
|
instance->msg.has_next = false;
|
|
|
|
instance->msg.content.storage_write_request.path = TEST_FILE_PATH;
|
|
|
|
instance->msg.content.storage_write_request.has_file = true;
|
|
|
|
instance->msg.content.storage_write_request.file.data =
|
|
|
|
malloc(PB_BYTES_ARRAY_T_ALLOCSIZE(file_size));
|
|
|
|
instance->msg.content.storage_write_request.file.data->size = file_size;
|
|
|
|
|
|
|
|
const size_t bytes_read = storage_file_read(
|
|
|
|
file, instance->msg.content.storage_write_request.file.data->bytes, file_size);
|
|
|
|
|
|
|
|
if(bytes_read != file_size) {
|
|
|
|
pb_release(&PB_Main_msg, &instance->msg);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!expansion_test_app_send_rpc_request(instance, &instance->msg)) break;
|
|
|
|
if(!expansion_test_app_receive_rpc_request(instance, &instance->msg)) break;
|
|
|
|
if(!expansion_test_app_is_success_rpc_message(&instance->msg)) break;
|
|
|
|
success = true;
|
|
|
|
} while(false);
|
|
|
|
|
|
|
|
storage_file_free(file);
|
|
|
|
furi_record_close(RECORD_STORAGE);
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_rpc_alert(ExpansionTestApp* instance) {
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
instance->msg.command_id++;
|
|
|
|
instance->msg.command_status = PB_CommandStatus_OK;
|
|
|
|
instance->msg.which_content = PB_Main_system_play_audiovisual_alert_request_tag;
|
|
|
|
instance->msg.has_next = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(!expansion_test_app_send_rpc_request(instance, &instance->msg)) break;
|
|
|
|
if(!expansion_test_app_receive_rpc_request(instance, &instance->msg)) break;
|
|
|
|
if(instance->msg.which_content != PB_Main_empty_tag) break;
|
|
|
|
if(instance->msg.command_status != PB_CommandStatus_OK) break;
|
|
|
|
success = true;
|
|
|
|
} while(false);
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_idle(ExpansionTestApp* instance, uint32_t num_cycles) {
|
|
|
|
uint32_t num_cycles_done;
|
|
|
|
for(num_cycles_done = 0; num_cycles_done < num_cycles; ++num_cycles_done) {
|
|
|
|
if(!expansion_test_app_send_heartbeat(instance)) break;
|
|
|
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
|
|
|
if(instance->frame.header.type != ExpansionFrameTypeHeartbeat) break;
|
|
|
|
furi_delay_ms(EXPANSION_PROTOCOL_TIMEOUT_MS - 50);
|
|
|
|
}
|
|
|
|
|
|
|
|
return num_cycles_done == num_cycles;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool expansion_test_app_stop_rpc(ExpansionTestApp* instance) {
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(!expansion_test_app_send_control_request(instance, ExpansionFrameControlCommandStopRpc))
|
|
|
|
break;
|
|
|
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
|
|
|
if(!expansion_test_app_is_success_response(&instance->frame)) break;
|
|
|
|
success = true;
|
|
|
|
} while(false);
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
int32_t expansion_test_app(void* p) {
|
|
|
|
UNUSED(p);
|
|
|
|
|
|
|
|
ExpansionTestApp* instance = expansion_test_app_alloc();
|
|
|
|
expansion_test_app_start(instance);
|
|
|
|
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(!expansion_test_app_send_presence(instance)) break;
|
|
|
|
if(!expansion_test_app_wait_ready(instance)) break;
|
|
|
|
if(!expansion_test_app_handshake(instance)) break;
|
|
|
|
if(!expansion_test_app_start_rpc(instance)) break;
|
|
|
|
if(!expansion_test_app_rpc_mkdir(instance)) break;
|
|
|
|
if(!expansion_test_app_rpc_write(instance)) break;
|
|
|
|
if(!expansion_test_app_rpc_alert(instance)) break;
|
|
|
|
if(!expansion_test_app_idle(instance, 10)) break;
|
|
|
|
if(!expansion_test_app_stop_rpc(instance)) break;
|
|
|
|
if(!expansion_test_app_idle(instance, 10)) break;
|
|
|
|
success = true;
|
|
|
|
} while(false);
|
|
|
|
|
|
|
|
expansion_test_app_stop(instance);
|
|
|
|
expansion_test_app_free(instance);
|
|
|
|
|
|
|
|
if(!success) {
|
|
|
|
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
|
|
|
notification_message(notification, &sequence_error);
|
|
|
|
furi_record_close(RECORD_NOTIFICATION);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|