mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Initial implementation of Bluetooth file transfer
This commit is contained in:
parent
3ce11f14ce
commit
26b2dc27fa
321
src/activities/bluetooth/BluetoothActivity.cpp
Normal file
321
src/activities/bluetooth/BluetoothActivity.cpp
Normal file
@ -0,0 +1,321 @@
|
||||
#include "BluetoothActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <BLE2902.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
#define DEVICE_NAME "EPaper"
|
||||
#define SERVICE_UUID "4ae29d01-499a-480a-8c41-a82192105125"
|
||||
#define REQUEST_CHARACTERISTIC_UUID "a00e530d-b48b-48c8-aadb-d062a1b91792"
|
||||
#define RESPONSE_CHARACTERISTIC_UUID "0c656023-dee6-47c5-9afb-e601dfbdaa1d"
|
||||
|
||||
#define OUTPUT_DIRECTORY "/bt"
|
||||
|
||||
#define PROTOCOL_ASSERT(cond, fmt, ...) \
|
||||
do { \
|
||||
if (!(cond)) \
|
||||
{ \
|
||||
snprintf(errorMessage, sizeof(errorMessage), fmt, ##__VA_ARGS__); \
|
||||
intoState(STATE_ERROR); \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
void BluetoothActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<BluetoothActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void BluetoothActivity::startAdvertising() {
|
||||
BLEDevice::startAdvertising();
|
||||
}
|
||||
|
||||
void BluetoothActivity::stopAdvertising() {
|
||||
BLEDevice::stopAdvertising();
|
||||
}
|
||||
|
||||
void BluetoothActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
BLEDevice::init(DEVICE_NAME);
|
||||
pServer = BLEDevice::createServer();
|
||||
pServer->setCallbacks(&serverCallbacks);
|
||||
pService = pServer->createService(SERVICE_UUID);
|
||||
pRequestChar = pService->createCharacteristic(
|
||||
REQUEST_CHARACTERISTIC_UUID,
|
||||
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR
|
||||
);
|
||||
pRequestChar->setCallbacks(&requestCallbacks);
|
||||
pResponseChar = pService->createCharacteristic(
|
||||
RESPONSE_CHARACTERISTIC_UUID,
|
||||
BLECharacteristic::PROPERTY_INDICATE
|
||||
);
|
||||
pResponseChar->addDescriptor(new BLE2902());
|
||||
pService->start();
|
||||
|
||||
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
||||
pAdvertising->addServiceUUID(SERVICE_UUID);
|
||||
pAdvertising->setScanResponse(true);
|
||||
pAdvertising->setMinPreferred(0x06);
|
||||
pAdvertising->setMinPreferred(0x12);
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
state = STATE_INITIALIZING;
|
||||
intoState(STATE_WAITING);
|
||||
|
||||
xTaskCreate(&BluetoothActivity::taskTrampoline, "BluetoothTask",
|
||||
// TODO: figure out how much stack we actually need
|
||||
4096, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothActivity::intoState(State newState) {
|
||||
if (state == newState) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (newState) {
|
||||
case STATE_WAITING:
|
||||
file.close();
|
||||
startAdvertising();
|
||||
txnId = 0;
|
||||
break;
|
||||
case STATE_OFFERED:
|
||||
// caller sets filename, totalBytes, file, txnId
|
||||
receivedBytes = 0;
|
||||
break;
|
||||
case STATE_ERROR:
|
||||
{
|
||||
// caller sets errorMessage
|
||||
file.close();
|
||||
auto connId = pServer->getConnId();
|
||||
if (connId != ESP_GATT_IF_NONE) {
|
||||
// TODO: send back a response over BLE?
|
||||
pServer->disconnect(connId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
state = newState;
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void BluetoothActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
file.close();
|
||||
|
||||
stopAdvertising();
|
||||
|
||||
pService->stop();
|
||||
BLEDevice::deinit();
|
||||
|
||||
// Wait until not rendering to delete task
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void BluetoothActivity::loop() {
|
||||
// Handle back button - cancel
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == STATE_ERROR) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
// restart
|
||||
intoState(STATE_WAITING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothActivity::render() const {
|
||||
renderer.clearScreen();
|
||||
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Bluetooth", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 50, "Use the Longform app to transfer files.");
|
||||
|
||||
std::string stateText;
|
||||
switch (state) {
|
||||
case STATE_WAITING:
|
||||
stateText = "Waiting for a connection.";
|
||||
break;
|
||||
case STATE_CONNECTED:
|
||||
stateText = "Connected.";
|
||||
break;
|
||||
case STATE_OFFERED:
|
||||
stateText = "Ready to receive.";
|
||||
break;
|
||||
case STATE_RECEIVING:
|
||||
stateText = "Receiving.";
|
||||
break;
|
||||
case STATE_DONE:
|
||||
stateText = "Transfer complete.";
|
||||
break;
|
||||
case STATE_ERROR:
|
||||
stateText = "An error occurred.";
|
||||
break;
|
||||
default:
|
||||
stateText = "UNKNOWN STATE.";
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 75, stateText.c_str());
|
||||
|
||||
if (state == STATE_OFFERED || state == STATE_RECEIVING || state == STATE_DONE) {
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 110, filename);
|
||||
} else if (state == STATE_ERROR) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 110, errorMessage);
|
||||
}
|
||||
|
||||
if (state == STATE_RECEIVING) {
|
||||
const int percent = (totalBytes > 0) ? (receivedBytes * 100) / totalBytes : 0;
|
||||
|
||||
const int barWidth = renderer.getScreenWidth() * 3 / 4;
|
||||
const int barHeight = 20;
|
||||
const int boxX = (renderer.getScreenWidth() - barWidth) / 2;
|
||||
const int boxY = 160;
|
||||
renderer.drawRect(boxX, boxY, barWidth, barHeight);
|
||||
const int fillWidth = (barWidth - 2) * percent / 100;
|
||||
renderer.fillRect(boxX + 1, boxY + 1, fillWidth, barHeight - 2);
|
||||
|
||||
char dynamicText[64];
|
||||
snprintf(dynamicText, sizeof(dynamicText), "Received %zu / %zu bytes (%d%%)", receivedBytes, totalBytes, percent);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 200, dynamicText);
|
||||
}
|
||||
|
||||
// Draw help text at bottom
|
||||
const auto labels = mappedInput.mapLabels(
|
||||
"« Back",
|
||||
(state == STATE_ERROR) ? "Restart" : "",
|
||||
"",
|
||||
""
|
||||
);
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
void BluetoothActivity::ServerCallbacks::onConnect(BLEServer* pServer) {
|
||||
Serial.printf("BLE connected\n");
|
||||
activity->onConnected(true);
|
||||
}
|
||||
|
||||
void BluetoothActivity::ServerCallbacks::onDisconnect(BLEServer* pServer) {
|
||||
Serial.printf("BLE disconnected\n");
|
||||
activity->onConnected(false);
|
||||
}
|
||||
|
||||
void BluetoothActivity::onConnected(bool isConnected) {
|
||||
if (state == STATE_ERROR) {
|
||||
// stay in error state so the user can read the error message even after disconnect
|
||||
return;
|
||||
}
|
||||
|
||||
intoState(isConnected ? STATE_CONNECTED : STATE_WAITING);
|
||||
}
|
||||
|
||||
void BluetoothActivity::onRequest(lfbt_message* msg, size_t msg_len) {
|
||||
if (state == STATE_ERROR) {
|
||||
// ignore further messages in error state
|
||||
return;
|
||||
}
|
||||
|
||||
PROTOCOL_ASSERT((txnId == 0) || (txnId == msg->txnId), "Multiple transfers happening at once (%x != %x)", txnId, msg->txnId);
|
||||
|
||||
switch (msg->type) {
|
||||
case 0: // client_offer
|
||||
{
|
||||
PROTOCOL_ASSERT(state == STATE_CONNECTED, "Invalid state for client_offer: %d", state);
|
||||
PROTOCOL_ASSERT(msg->body.clientOffer.version == 1, "Unsupported protocol version: %u", msg->body.clientOffer.version);
|
||||
|
||||
totalBytes = msg->body.clientOffer.bodyLength;
|
||||
|
||||
size_t filenameLen = msg_len - 8 - sizeof(lfbt_msg_client_offer);
|
||||
if (filenameLen > MAX_FILENAME) {
|
||||
filenameLen = MAX_FILENAME;
|
||||
}
|
||||
|
||||
memcpy(filename, msg->body.clientOffer.name, filenameLen);
|
||||
filename[filenameLen] = 0;
|
||||
|
||||
// sanitize filename
|
||||
for (char *p = filename; *p; p++) {
|
||||
if (*p == '/' || *p == '\\' || *p == ':') {
|
||||
*p = '_';
|
||||
}
|
||||
}
|
||||
|
||||
PROTOCOL_ASSERT(SdMan.ensureDirectoryExists(OUTPUT_DIRECTORY), "Couldn't create output directory %s", OUTPUT_DIRECTORY);
|
||||
char filepath[MAX_FILENAME + strlen(OUTPUT_DIRECTORY) + 2];
|
||||
snprintf(filepath, sizeof(filepath), "%s/%s", OUTPUT_DIRECTORY, filename);
|
||||
// TODO: we could check if file already exists and append a number to filename to avoid overwriting
|
||||
PROTOCOL_ASSERT(SdMan.openFileForWrite("BT", filepath, file), "Couldn't open file %s for writing", filepath);
|
||||
// TODO: would be neat to check if we have enough space, but SDCardManager doesn't seem to expose that info currently
|
||||
|
||||
txnId = msg->txnId;
|
||||
|
||||
intoState(STATE_OFFERED);
|
||||
|
||||
lfbt_message response = {
|
||||
.type = 1, // server_response
|
||||
.txnId = txnId,
|
||||
.body = {.serverResponse = {.status = 0}}
|
||||
};
|
||||
pResponseChar->setValue((uint8_t*)&response, 8 + sizeof(lfbt_msg_server_response));
|
||||
pResponseChar->indicate(); // TODO: indicate should not be called in a callback, this ends up timing out
|
||||
|
||||
updateRequired = true;
|
||||
break;
|
||||
}
|
||||
case 2: // client_chunk
|
||||
{
|
||||
Serial.printf("Received client_chunk, offset %u, length %zu\n", msg->body.clientChunk.offset, msg_len - 8 - sizeof(lfbt_msg_client_chunk));
|
||||
PROTOCOL_ASSERT(state == STATE_OFFERED || state == STATE_RECEIVING, "Invalid state for client_chunk: %d", state);
|
||||
PROTOCOL_ASSERT(msg->body.clientChunk.offset == receivedBytes, "Expected chunk %d, got %d", receivedBytes, msg->body.clientChunk.offset);
|
||||
|
||||
size_t written = file.write((uint8_t*)msg->body.clientChunk.body, msg_len - 8 - sizeof(lfbt_msg_client_chunk));
|
||||
PROTOCOL_ASSERT(written > 0, "Couldn't write to file");
|
||||
receivedBytes += msg_len - 8 - sizeof(lfbt_msg_client_chunk);
|
||||
if (receivedBytes >= totalBytes) {
|
||||
PROTOCOL_ASSERT(receivedBytes == totalBytes, "Got more bytes than expected: %zu > %zu", receivedBytes, totalBytes);
|
||||
PROTOCOL_ASSERT(file.close(), "Couldn't finalize writing the file");
|
||||
// TODO: automatically open file in reader
|
||||
intoState(STATE_DONE);
|
||||
} else {
|
||||
intoState(STATE_RECEIVING);
|
||||
}
|
||||
updateRequired = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothActivity::RequestCallbacks::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gatts_cb_param_t* param) {
|
||||
lfbt_message *msg = (lfbt_message*) param->write.value;
|
||||
Serial.printf("Received BLE message of type %u, txnId %x, length %d\n", msg->type, msg->txnId, param->write.len);
|
||||
activity->onRequest(msg, param->write.len);
|
||||
}
|
||||
124
src/activities/bluetooth/BluetoothActivity.h
Normal file
124
src/activities/bluetooth/BluetoothActivity.h
Normal file
@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <BLEServer.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include "../Activity.h"
|
||||
|
||||
#define MAX_FILENAME 200
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t version;
|
||||
uint32_t bodyLength;
|
||||
uint32_t nameLength;
|
||||
char name[];
|
||||
} lfbt_msg_client_offer; // msg type 0
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t status;
|
||||
} lfbt_msg_server_response; // msg type 1
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t offset;
|
||||
char body[];
|
||||
} lfbt_msg_client_chunk; // msg type 2
|
||||
|
||||
typedef union {
|
||||
lfbt_msg_client_offer clientOffer;
|
||||
lfbt_msg_server_response serverResponse;
|
||||
lfbt_msg_client_chunk clientChunk;
|
||||
} lfbt_message_body;
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t type;
|
||||
uint32_t txnId;
|
||||
lfbt_message_body body;
|
||||
} lfbt_message;
|
||||
|
||||
/**
|
||||
* BluetoothActivity receives files over a custom BLE protocol and stores them on the SD card.
|
||||
*
|
||||
* The onCancel callback is called if the user presses back.
|
||||
*/
|
||||
class BluetoothActivity final : public Activity {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
const std::function<void()> onCancel;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
void onConnected(bool isConnected);
|
||||
void onRequest(lfbt_message *msg, size_t msg_len);
|
||||
|
||||
class ServerCallbacks : public BLEServerCallbacks {
|
||||
friend class BluetoothActivity;
|
||||
BluetoothActivity *activity;
|
||||
|
||||
void onConnect(BLEServer* pServer);
|
||||
void onDisconnect(BLEServer* pServer);
|
||||
|
||||
protected:
|
||||
explicit ServerCallbacks(BluetoothActivity *activity) : activity(activity) {}
|
||||
};
|
||||
|
||||
ServerCallbacks serverCallbacks;
|
||||
|
||||
class RequestCallbacks : public BLECharacteristicCallbacks {
|
||||
friend class BluetoothActivity;
|
||||
BluetoothActivity *activity;
|
||||
|
||||
void onWrite(BLECharacteristic* pCharacteristic, esp_ble_gatts_cb_param_t* param);
|
||||
|
||||
protected:
|
||||
explicit RequestCallbacks(BluetoothActivity *activity) : activity(activity) {}
|
||||
};
|
||||
|
||||
RequestCallbacks requestCallbacks;
|
||||
|
||||
BLEServer *pServer;
|
||||
BLEService *pService;
|
||||
BLECharacteristic *pRequestChar, *pResponseChar;
|
||||
BLEAdvertising *pAdvertising;
|
||||
void startAdvertising();
|
||||
void stopAdvertising();
|
||||
|
||||
typedef enum {
|
||||
STATE_INITIALIZING,
|
||||
STATE_WAITING,
|
||||
STATE_CONNECTED,
|
||||
STATE_OFFERED,
|
||||
STATE_RECEIVING,
|
||||
STATE_DONE,
|
||||
STATE_ERROR
|
||||
} State;
|
||||
|
||||
State state = STATE_INITIALIZING;
|
||||
char filename[MAX_FILENAME + 1];
|
||||
FsFile file;
|
||||
size_t receivedBytes = 0;
|
||||
size_t totalBytes = 0;
|
||||
char errorMessage[256];
|
||||
uint32_t txnId;
|
||||
|
||||
void intoState(State newState);
|
||||
|
||||
public:
|
||||
explicit BluetoothActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onCancel)
|
||||
: Activity("Bluetooth", renderer, mappedInput), onCancel(onCancel),
|
||||
serverCallbacks(this), requestCallbacks(this) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
};
|
||||
@ -23,7 +23,7 @@ void HomeActivity::taskTrampoline(void* param) {
|
||||
}
|
||||
|
||||
int HomeActivity::getMenuItemCount() const {
|
||||
int count = 3; // My Library, File transfer, Settings
|
||||
int count = 4; // My Library, File transfer, Bluetooth, Settings
|
||||
if (hasContinueReading) count++;
|
||||
if (hasOpdsUrl) count++;
|
||||
return count;
|
||||
@ -172,6 +172,7 @@ void HomeActivity::loop() {
|
||||
const int myLibraryIdx = idx++;
|
||||
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
|
||||
const int fileTransferIdx = idx++;
|
||||
const int bluetoothIdx = idx++;
|
||||
const int settingsIdx = idx;
|
||||
|
||||
if (selectorIndex == continueIdx) {
|
||||
@ -182,6 +183,8 @@ void HomeActivity::loop() {
|
||||
onOpdsBrowserOpen();
|
||||
} else if (selectorIndex == fileTransferIdx) {
|
||||
onFileTransferOpen();
|
||||
} else if (selectorIndex == bluetoothIdx) {
|
||||
onBluetoothOpen();
|
||||
} else if (selectorIndex == settingsIdx) {
|
||||
onSettingsOpen();
|
||||
}
|
||||
@ -500,7 +503,7 @@ void HomeActivity::render() {
|
||||
|
||||
// --- Bottom menu tiles ---
|
||||
// Build menu items dynamically
|
||||
std::vector<const char*> menuItems = {"My Library", "File Transfer", "Settings"};
|
||||
std::vector<const char*> menuItems = {"My Library", "File Transfer", "Bluetooth", "Settings"};
|
||||
if (hasOpdsUrl) {
|
||||
// Insert Calibre Library after My Library
|
||||
menuItems.insert(menuItems.begin() + 1, "Calibre Library");
|
||||
|
||||
@ -25,6 +25,7 @@ class HomeActivity final : public Activity {
|
||||
const std::function<void()> onMyLibraryOpen;
|
||||
const std::function<void()> onSettingsOpen;
|
||||
const std::function<void()> onFileTransferOpen;
|
||||
const std::function<void()> onBluetoothOpen;
|
||||
const std::function<void()> onOpdsBrowserOpen;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
@ -39,12 +40,13 @@ class HomeActivity final : public Activity {
|
||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onContinueReading, const std::function<void()>& onMyLibraryOpen,
|
||||
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
|
||||
const std::function<void()>& onOpdsBrowserOpen)
|
||||
const std::function<void()>& onBluetoothOpen, const std::function<void()>& onOpdsBrowserOpen)
|
||||
: Activity("Home", renderer, mappedInput),
|
||||
onContinueReading(onContinueReading),
|
||||
onMyLibraryOpen(onMyLibraryOpen),
|
||||
onSettingsOpen(onSettingsOpen),
|
||||
onFileTransferOpen(onFileTransferOpen),
|
||||
onBluetoothOpen(onBluetoothOpen),
|
||||
onOpdsBrowserOpen(onOpdsBrowserOpen) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "activities/bluetooth/BluetoothActivity.h"
|
||||
#include "activities/boot_sleep/BootActivity.h"
|
||||
#include "activities/boot_sleep/SleepActivity.h"
|
||||
#include "activities/browser/OpdsBookBrowserActivity.h"
|
||||
@ -226,6 +227,11 @@ void onGoToFileTransfer() {
|
||||
enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
|
||||
}
|
||||
|
||||
void onGoToBluetooth() {
|
||||
exitActivity();
|
||||
enterNewActivity(new BluetoothActivity(renderer, mappedInputManager, onGoHome));
|
||||
}
|
||||
|
||||
void onGoToSettings() {
|
||||
exitActivity();
|
||||
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
|
||||
@ -249,7 +255,7 @@ void onGoToBrowser() {
|
||||
void onGoHome() {
|
||||
exitActivity();
|
||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings,
|
||||
onGoToFileTransfer, onGoToBrowser));
|
||||
onGoToFileTransfer, onGoToBluetooth, onGoToBrowser));
|
||||
}
|
||||
|
||||
void setupDisplayAndFonts() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user