2014-10-30 19:42:18 +01:00
local Generic = require ( " device/generic/device " )
2019-02-06 15:51:50 +01:00
local A , android = pcall ( require , " android " ) -- luacheck: ignore
2019-07-05 22:57:58 +02:00
local Geom = require ( " ui/geometry " )
2014-10-30 19:42:18 +01:00
local ffi = require ( " ffi " )
2018-06-02 12:10:55 -04:00
local C = ffi.C
2019-01-06 18:14:06 +01:00
local lfs = require ( " libs/libkoreader-lfs " )
2016-12-29 00:10:38 -08:00
local logger = require ( " logger " )
2020-06-25 21:33:51 +02:00
local util = require ( " util " )
2019-02-06 15:51:50 +01:00
local _ = require ( " gettext " )
local T = require ( " ffi/util " ) . template
2014-10-30 19:42:18 +01:00
local function yes ( ) return true end
2015-09-27 22:29:59 +08:00
local function no ( ) return false end
2014-10-30 19:42:18 +01:00
2019-03-03 01:07:56 +01:00
local function canUpdateApk ( )
-- disable updates on fdroid builds, since they manage their own repo.
2019-04-17 20:04:07 +02:00
return ( android.prop . flavor ~= " fdroid " )
2019-03-03 01:07:56 +01:00
end
2019-06-19 01:57:52 +02:00
local function getCodename ( )
local api = android.app . activity.sdkVersion
local codename = " "
2019-12-10 22:26:45 +01:00
if api > 29 then
codename = " R "
elseif api == 29 then
codename = " Q "
elseif api == 28 then
2019-06-19 01:57:52 +02:00
codename = " Pie "
elseif api == 27 or api == 26 then
codename = " Oreo "
elseif api == 25 or api == 24 then
codename = " Nougat "
elseif api == 23 then
codename = " Marshmallow "
elseif api == 22 or api == 21 then
codename = " Lollipop "
elseif api == 19 then
codename = " KitKat "
elseif api < 19 and api >= 16 then
codename = " Jelly Bean "
elseif api < 16 and api >= 14 then
codename = " Ice Cream Sandwich "
end
return codename
end
2019-07-08 14:19:36 +02:00
local EXTERNAL_DICTS_AVAILABILITY_CHECKED = false
local EXTERNAL_DICTS = require ( " device/android/dictionaries " )
local external_dict_when_back_callback = nil
local function getExternalDicts ( )
if not EXTERNAL_DICTS_AVAILABILITY_CHECKED then
EXTERNAL_DICTS_AVAILABILITY_CHECKED = true
for i , v in ipairs ( EXTERNAL_DICTS ) do
local package = v [ 4 ]
if android.isPackageEnabled ( package ) then
v [ 3 ] = true
end
end
end
return EXTERNAL_DICTS
end
2014-10-30 19:42:18 +01:00
local Device = Generic : new {
2019-04-17 20:04:07 +02:00
isAndroid = yes ,
model = android.prop . product ,
2015-03-29 08:59:51 +08:00
hasKeys = yes ,
2015-09-27 22:29:59 +08:00
hasDPad = no ,
2020-06-19 09:41:50 +02:00
hasExitOptions = no ,
2019-02-08 10:45:09 +01:00
hasEinkScreen = function ( ) return android.isEink ( ) end ,
2019-03-03 15:48:23 +01:00
hasColorScreen = function ( ) return not android.isEink ( ) end ,
2016-06-24 01:50:24 +08:00
hasFrontlight = yes ,
2019-09-04 20:52:24 +02:00
hasLightLevelFallback = yes ,
2019-08-15 14:49:15 +02:00
canRestart = no ,
2020-06-19 09:41:50 +02:00
canSuspend = no ,
2019-01-17 21:44:15 +01:00
firmware_rev = android.app . activity.sdkVersion ,
2020-06-19 09:41:50 +02:00
home_dir = android.getExternalStoragePath ( ) ,
2017-09-24 05:58:34 +08:00
display_dpi = android.lib . AConfiguration_getDensity ( android.app . config ) ,
2019-10-08 18:15:43 +02:00
isHapticFeedbackEnabled = yes ,
2018-02-16 21:44:10 +01:00
hasClipboard = yes ,
2019-03-03 01:07:56 +01:00
hasOTAUpdates = canUpdateApk ,
2019-03-31 19:19:07 +02:00
canOpenLink = yes ,
2019-03-20 17:28:19 +01:00
openLink = function ( self , link )
if not link or type ( link ) ~= " string " then return end
return android.openLink ( link ) == 0
end ,
2019-12-21 15:59:23 +01:00
canImportFiles = function ( ) return android.app . activity.sdkVersion >= 19 end ,
importFile = function ( path ) android.importFile ( path ) end ,
isValidPath = function ( path ) return android.isPathInsideSandbox ( path ) end ,
2020-01-05 12:56:01 +01:00
canShareText = yes ,
doShareText = function ( text ) android.sendText ( text ) end ,
2019-12-21 15:59:23 +01:00
2019-07-08 14:19:36 +02:00
canExternalDictLookup = yes ,
getExternalDictLookupList = getExternalDicts ,
doExternalDictLookup = function ( self , text , method , callback )
external_dict_when_back_callback = callback
local package , action = nil
for i , v in ipairs ( getExternalDicts ( ) ) do
if v [ 1 ] == method then
package = v [ 4 ]
action = v [ 5 ]
break
end
end
android.dictLookup ( text , package , action )
end ,
2019-02-18 08:01:00 -08:00
--[[
Disable jit on some modules on android to make koreader on Android more stable .
The strategy here is that we only use precious mcode memory ( jitting )
on deep loops like the several blitting methods in blitbuffer.lua and
the pixel - copying methods in mupdf.lua . So that a small amount of mcode
memory ( 64 KB ) allocated when koreader is launched in the android.lua
is enough for the program and it won ' t need to jit other parts of lua
code and thus won ' t allocate mcode memory any more which by our
observation will be harder and harder as we run koreader .
] ] --
should_restrict_JIT = true ,
2014-10-30 19:42:18 +01:00
}
function Device : init ( )
2016-12-29 00:10:38 -08:00
self.screen = require ( " ffi/framebuffer_android " ) : new { device = self , debug = logger.dbg }
2014-10-30 19:42:18 +01:00
self.powerd = require ( " device/android/powerd " ) : new { device = self }
self.input = require ( " device/input " ) : new {
device = self ,
event_map = require ( " device/android/event_map " ) ,
2016-04-02 21:52:30 -07:00
handleMiscEv = function ( this , ev )
2020-07-08 14:42:19 +02:00
local UIManager = require ( " ui/uimanager " )
2016-12-29 00:10:38 -08:00
logger.dbg ( " Android application event " , ev.code )
2018-06-02 12:10:55 -04:00
if ev.code == C.APP_CMD_SAVE_STATE then
2014-10-30 19:42:18 +01:00
return " SaveState "
2019-01-10 03:11:06 +01:00
elseif ev.code == C.APP_CMD_GAINED_FOCUS
or ev.code == C.APP_CMD_INIT_WINDOW
or ev.code == C.APP_CMD_WINDOW_REDRAW_NEEDED then
2019-03-05 12:25:04 +01:00
this.device . screen : _updateWindow ( )
2020-07-08 14:42:19 +02:00
elseif ev.code == C.APP_CMD_CONFIG_CHANGED then
-- orientation and size changes
if android.screen . width ~= android.getScreenWidth ( )
or android.screen . height ~= android.getScreenHeight ( ) then
this.device . screen : resize ( )
local new_size = this.device . screen : getSize ( )
logger.info ( " Resizing screen to " , new_size )
local Event = require ( " ui/event " )
UIManager : broadcastEvent ( Event : new ( " SetDimensions " , new_size ) )
UIManager : broadcastEvent ( Event : new ( " ScreenResize " , new_size ) )
UIManager : broadcastEvent ( Event : new ( " RedrawCurrentPage " ) )
end
-- to-do: keyboard connected, disconnected
2019-01-06 18:14:06 +01:00
elseif ev.code == C.APP_CMD_RESUME then
2019-07-08 14:19:36 +02:00
EXTERNAL_DICTS_AVAILABILITY_CHECKED = false
if external_dict_when_back_callback then
external_dict_when_back_callback ( )
external_dict_when_back_callback = nil
end
2019-01-06 18:14:06 +01:00
local new_file = android.getIntent ( )
if new_file ~= nil and lfs.attributes ( new_file , " mode " ) == " file " then
2019-03-05 19:22:45 +01:00
-- we cannot blit to a window here since we have no focus yet.
local InfoMessage = require ( " ui/widget/infomessage " )
2020-01-04 01:18:51 +01:00
local BD = require ( " ui/bidi " )
2019-03-05 19:22:45 +01:00
UIManager : scheduleIn ( 0.1 , function ( )
UIManager : show ( InfoMessage : new {
2020-01-04 01:18:51 +01:00
text = T ( _ ( " Opening file '%1'. " ) , BD.filepath ( new_file ) ) ,
2019-03-05 19:22:45 +01:00
timeout = 0.0 ,
} )
end )
UIManager : scheduleIn ( 0.2 , function ( )
require ( " apps/reader/readerui " ) : doShowReader ( new_file )
end )
2019-12-21 15:59:23 +01:00
else
-- check if we're resuming from importing content.
local content_path = android.getLastImportedPath ( )
if content_path ~= nil then
local FileManager = require ( " apps/filemanager/filemanager " )
UIManager : scheduleIn ( 0.5 , function ( )
if FileManager.instance then
FileManager.instance : onRefresh ( )
else
FileManager : showFiles ( content_path )
end
end )
end
2019-01-06 18:14:06 +01:00
end
2014-10-30 19:42:18 +01:00
end
end ,
2018-02-16 21:44:10 +01:00
hasClipboardText = function ( )
return android.hasClipboardText ( )
end ,
getClipboardText = function ( )
return android.getClipboardText ( )
end ,
setClipboardText = function ( text )
return android.setClipboardText ( text )
end ,
2014-10-30 19:42:18 +01:00
}
-- check if we have a keyboard
2017-09-24 05:58:34 +08:00
if android.lib . AConfiguration_getKeyboard ( android.app . config )
2018-06-02 12:10:55 -04:00
== C.ACONFIGURATION_KEYBOARD_QWERTY
2014-10-30 19:42:18 +01:00
then
self.hasKeyboard = yes
end
-- check if we have a touchscreen
2017-09-24 05:58:34 +08:00
if android.lib . AConfiguration_getTouchscreen ( android.app . config )
2018-06-02 12:10:55 -04:00
~= C.ACONFIGURATION_TOUCHSCREEN_NOTOUCH
2014-10-30 19:42:18 +01:00
then
self.isTouchDevice = yes
end
2019-10-08 18:15:43 +02:00
-- check if we use custom timeouts
2019-07-26 23:04:20 +02:00
if android.needsWakelocks ( ) then
2019-10-08 18:15:43 +02:00
android.timeout . set ( C.AKEEP_SCREEN_ON_ENABLED )
2019-07-26 23:04:20 +02:00
else
local timeout = G_reader_settings : readSetting ( " android_screen_timeout " )
2019-10-08 18:15:43 +02:00
if timeout then
if timeout == C.AKEEP_SCREEN_ON_ENABLED
or ( timeout > C.AKEEP_SCREEN_ON_DISABLED
and android.settings . canWrite ( ) ) then
android.timeout . set ( timeout )
2019-08-14 13:40:42 +02:00
end
2019-07-26 23:04:20 +02:00
end
2019-01-10 03:11:06 +01:00
end
2019-07-05 22:57:58 +02:00
-- check if we disable fullscreen support
if G_reader_settings : isTrue ( " disable_android_fullscreen " ) then
self : toggleFullscreen ( )
end
2019-08-01 18:27:24 +02:00
2020-05-05 11:18:18 +02:00
-- check if we allow haptic feedback in spite of system settings
if G_reader_settings : isTrue ( " haptic_feedback_override " ) then
android.setHapticOverride ( true )
end
2019-08-01 18:27:24 +02:00
-- check if we ignore volume keys and then they're forwarded to system services.
if G_reader_settings : isTrue ( " android_ignore_volume_keys " ) then
2020-05-05 11:18:18 +02:00
android.setVolumeKeysIgnored ( true )
2019-08-01 18:27:24 +02:00
end
2019-07-05 22:57:58 +02:00
2020-06-15 07:43:37 +02:00
-- check if we ignore the back button completely
if G_reader_settings : isTrue ( " android_ignore_back_button " ) then
android.setBackButtonIgnored ( true )
end
2019-09-04 20:52:24 +02:00
-- check if we enable a custom light level for this activity
local last_value = G_reader_settings : readSetting ( " fl_last_level " )
if type ( last_value ) == " number " and last_value >= 0 then
Device : setScreenBrightness ( last_value )
end
2014-11-24 08:52:01 +00:00
Generic.init ( self )
2014-10-30 19:42:18 +01:00
end
2017-10-21 22:27:09 +02:00
function Device : initNetworkManager ( NetworkMgr )
2019-02-17 16:22:41 +01:00
function NetworkMgr : turnOnWifi ( complete_callback )
2020-02-07 16:36:17 +01:00
android.openWifiSettings ( )
2017-10-21 22:27:09 +02:00
end
2019-02-17 16:22:41 +01:00
function NetworkMgr : turnOffWifi ( complete_callback )
2020-02-07 16:36:17 +01:00
android.openWifiSettings ( )
2017-10-21 22:27:09 +02:00
end
2020-02-07 16:36:17 +01:00
function NetworkMgr : openSettings ( )
android.openWifiSettings ( )
end
function NetworkMgr : isWifiOn ( )
local ok = android.getNetworkInfo ( )
ok = tonumber ( ok )
if not ok then return false end
return ok == 1
2017-10-28 17:51:34 +02:00
end
2017-10-21 22:27:09 +02:00
end
2019-10-08 18:15:43 +02:00
function Device : performHapticFeedback ( type )
android.hapticFeedback ( C [ " AHAPTIC_ " .. type ] )
end
2020-06-13 12:57:08 +02:00
function Device : setIgnoreInput ( enable )
android.setIgnoreInput ( enable )
end
2019-02-06 15:51:50 +01:00
function Device : retrieveNetworkInfo ( )
2020-02-07 16:36:17 +01:00
local ok , type = android.getNetworkInfo ( )
ok , type = tonumber ( ok ) , tonumber ( type )
if not ok or not type or type == C.ANETWORK_NONE then
2019-02-06 15:51:50 +01:00
return _ ( " Not connected " )
else
2020-02-07 16:36:17 +01:00
if type == C.ANETWORK_WIFI then
return _ ( " Connected to Wi-Fi " )
elseif type == C.ANETWORK_MOBILE then
return _ ( " Connected to mobile data network " )
elseif type == C.ANETWORK_ETHERNET then
return _ ( " Connected to Ethernet " )
elseif type == C.ANETWORK_BLUETOOTH then
return _ ( " Connected to Bluetooth " )
elseif type == C.ANETWORK_VPN then
return _ ( " Connected to VPN " )
end
return _ ( " Unknown connection " )
2019-02-06 15:51:50 +01:00
end
2019-07-05 22:57:58 +02:00
end
function Device : setViewport ( x , y , w , h )
logger.info ( string.format ( " Switching viewport to new geometry [x=%d,y=%d,w=%d,h=%d] " , x , y , w , h ) )
local viewport = Geom : new { x = x , y = y , w = w , h = h }
self.screen : setViewport ( viewport )
end
2019-09-04 20:52:24 +02:00
function Device : setScreenBrightness ( level )
android.setScreenBrightness ( level )
end
2019-07-05 22:57:58 +02:00
function Device : toggleFullscreen ( )
local api = android.app . activity.sdkVersion
if api >= 19 then
logger.dbg ( " ignoring fullscreen toggle, reason: always in immersive mode " )
elseif api < 19 and api >= 17 then
local width = android.getScreenWidth ( )
local height = android.getScreenHeight ( )
local available_height = android.getScreenAvailableHeight ( )
local is_fullscreen = android.isFullscreen ( )
android.setFullscreen ( not is_fullscreen )
G_reader_settings : saveSetting ( " disable_android_fullscreen " , is_fullscreen )
is_fullscreen = android.isFullscreen ( )
if is_fullscreen then
self : setViewport ( 0 , 0 , width , height )
else
self : setViewport ( 0 , 0 , width , available_height )
end
else
logger.dbg ( " ignoring fullscreen toggle, reason: legacy api " .. api )
end
2019-02-06 15:51:50 +01:00
end
2019-06-19 01:57:52 +02:00
function Device : info ( )
local is_eink , eink_platform = android.isEink ( )
2020-07-08 14:42:19 +02:00
local product_type = android.getPlatformName ( )
2019-01-17 21:44:15 +01:00
2019-06-19 01:57:52 +02:00
local common_text = T ( _ ( " %1 \n \n OS: Android %2, api %3 \n Build flavor: %4 \n " ) ,
android.prop . product , getCodename ( ) , Device.firmware_rev , android.prop . flavor )
2019-01-17 21:44:15 +01:00
2020-07-08 14:42:19 +02:00
local platform_text = " "
if product_type ~= " android " then
2020-07-08 17:55:17 +02:00
platform_text = " \n " .. T ( _ ( " Device type: %1 " ) , product_type ) .. " \n "
2020-07-08 14:42:19 +02:00
end
2019-06-19 01:57:52 +02:00
local eink_text = " "
if is_eink then
2020-07-08 17:55:17 +02:00
eink_text = " \n " .. T ( _ ( " E-ink display supported. \n Platform: %1 " ) , eink_platform ) .. " \n "
2019-06-19 01:57:52 +02:00
end
local wakelocks_text = " "
if android.needsWakelocks ( ) then
2020-07-08 17:55:17 +02:00
wakelocks_text = " \n " .. _ ( " This device needs CPU, screen and touchscreen always on. \n Screen timeout will be ignored while the app is in the foreground! " ) .. " \n "
2019-01-17 21:44:15 +01:00
end
2020-07-08 14:42:19 +02:00
return common_text .. platform_text .. eink_text .. wakelocks_text
2019-06-19 01:57:52 +02:00
end
2020-01-04 20:53:49 +01:00
function Device : epdTest ( )
android.einkTest ( )
end
2019-06-19 01:57:52 +02:00
function Device : exit ( )
android.LOGI ( string.format ( " Stopping %s main activity " , android.prop . name ) ) ;
android.lib . ANativeActivity_finish ( android.app . activity )
2019-01-17 21:44:15 +01:00
end
2020-06-25 21:33:51 +02:00
function Device : canExecuteScript ( file )
local file_ext = string.lower ( util.getFileNameSuffix ( file ) )
if android.prop . flavor ~= " fdroid " and file_ext == " sh " then
return true
end
end
2019-03-03 01:07:56 +01:00
android.LOGI ( string.format ( " Android %s - %s (API %d) - flavor: %s " ,
2019-04-17 20:04:07 +02:00
android.prop . version , getCodename ( ) , Device.firmware_rev , android.prop . flavor ) )
2019-01-17 21:44:15 +01:00
2014-10-30 19:42:18 +01:00
return Device