2020-09-15 18:39:32 +00:00
local FFIUtil = require ( " ffi/util " )
2014-10-30 18:42:18 +00:00
local Generic = require ( " device/generic/device " )
2019-02-06 14:51:50 +00:00
local A , android = pcall ( require , " android " ) -- luacheck: ignore
2019-07-05 20:57:58 +00:00
local Geom = require ( " ui/geometry " )
2014-10-30 18:42:18 +00:00
local ffi = require ( " ffi " )
2018-06-02 16:10:55 +00:00
local C = ffi.C
2019-01-06 17:14:06 +00:00
local lfs = require ( " libs/libkoreader-lfs " )
2016-12-29 08:10:38 +00:00
local logger = require ( " logger " )
2020-06-25 19:33:51 +00:00
local util = require ( " util " )
2019-02-06 14:51:50 +00:00
local _ = require ( " gettext " )
2020-09-15 18:39:32 +00:00
local T = FFIUtil.template
2014-10-30 18:42:18 +00:00
local function yes ( ) return true end
2015-09-27 14:29:59 +00:00
local function no ( ) return false end
2014-10-30 18:42:18 +00:00
2019-03-03 00:07:56 +00:00
local function canUpdateApk ( )
-- disable updates on fdroid builds, since they manage their own repo.
2019-04-17 18:04:07 +00:00
return ( android.prop . flavor ~= " fdroid " )
2019-03-03 00:07:56 +00:00
end
2019-06-18 23:57:52 +00:00
local function getCodename ( )
local api = android.app . activity.sdkVersion
local codename = " "
2019-12-10 21:26:45 +00:00
if api > 29 then
codename = " R "
elseif api == 29 then
codename = " Q "
elseif api == 28 then
2019-06-18 23:57:52 +00: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
2020-08-27 19:41:16 +00:00
-- thirdparty app support
local external = require ( " device/thirdparty " ) : new {
dicts = {
{ " Aard2 " , " Aard2 " , false , " itkach.aard2 " , " aard2 " } ,
{ " Alpus " , " Alpus " , false , " com.ngcomputing.fora.android " , " search " } ,
{ " ColorDict " , " ColorDict " , false , " com.socialnmobile.colordict " , " colordict " } ,
{ " Eudic " , " Eudic " , false , " com.eusoft.eudic " , " send " } ,
2020-09-19 18:25:51 +00:00
{ " EudicPlay " , " Eudic (Google Play) " , false , " com.qianyan.eudic " , " send " } ,
2020-08-27 19:41:16 +00:00
{ " Fora " , " Fora Dict " , false , " com.ngc.fora " , " search " } ,
2020-09-19 18:25:51 +00:00
{ " ForaPro " , " Fora Dict Pro " , false , " com.ngc.fora.android " , " search " } ,
2020-08-27 19:41:16 +00:00
{ " GoldenFree " , " GoldenDict Free " , false , " mobi.goldendict.android.free " , " send " } ,
{ " GoldenPro " , " GoldenDict Pro " , false , " mobi.goldendict.android " , " send " } ,
{ " Kiwix " , " Kiwix " , false , " org.kiwix.kiwixmobile " , " text " } ,
{ " Mdict " , " Mdict " , false , " cn.mdict " , " send " } ,
{ " QuickDic " , " QuickDic " , false , " de.reimardoeffinger.quickdic " , " quickdic " } ,
} ,
check = function ( self , app )
return android.isPackageEnabled ( app )
end ,
}
2019-07-08 12:19:36 +00:00
2014-10-30 18:42:18 +00:00
local Device = Generic : new {
2019-04-17 18:04:07 +00:00
isAndroid = yes ,
model = android.prop . product ,
2015-03-29 00:59:51 +00:00
hasKeys = yes ,
2015-09-27 14:29:59 +00:00
hasDPad = no ,
2020-06-19 07:41:50 +00:00
hasExitOptions = no ,
2019-02-08 09:45:09 +00:00
hasEinkScreen = function ( ) return android.isEink ( ) end ,
2019-03-03 14:48:23 +00:00
hasColorScreen = function ( ) return not android.isEink ( ) end ,
2016-06-23 17:50:24 +00:00
hasFrontlight = yes ,
2020-08-02 20:27:49 +00:00
hasNaturalLight = android.isWarmthDevice ,
2019-08-15 12:49:15 +00:00
canRestart = no ,
2020-06-19 07:41:50 +00:00
canSuspend = no ,
2019-01-17 20:44:15 +00:00
firmware_rev = android.app . activity.sdkVersion ,
2020-06-19 07:41:50 +00:00
home_dir = android.getExternalStoragePath ( ) ,
2017-09-23 21:58:34 +00:00
display_dpi = android.lib . AConfiguration_getDensity ( android.app . config ) ,
2019-10-08 16:15:43 +00:00
isHapticFeedbackEnabled = yes ,
2018-02-16 20:44:10 +00:00
hasClipboard = yes ,
2019-03-03 00:07:56 +00:00
hasOTAUpdates = canUpdateApk ,
2020-09-28 23:06:41 +00:00
hasFastWifiStatusQuery = yes ,
2019-03-31 17:19:07 +00:00
canOpenLink = yes ,
2019-03-20 16:28:19 +00:00
openLink = function ( self , link )
if not link or type ( link ) ~= " string " then return end
return android.openLink ( link ) == 0
end ,
2019-12-21 14:59:23 +00:00
canImportFiles = function ( ) return android.app . activity.sdkVersion >= 19 end ,
2020-11-27 19:51:40 +00:00
hasExternalSD = function ( ) return android.getExternalSdPath ( ) end ,
2019-12-21 14:59:23 +00:00
importFile = function ( path ) android.importFile ( path ) end ,
2020-01-05 11:56:01 +00:00
canShareText = yes ,
doShareText = function ( text ) android.sendText ( text ) end ,
2019-12-21 14:59:23 +00:00
2019-07-08 12:19:36 +00:00
canExternalDictLookup = yes ,
2020-08-27 19:41:16 +00:00
getExternalDictLookupList = function ( ) return external.dicts end ,
2019-07-08 12:19:36 +00:00
doExternalDictLookup = function ( self , text , method , callback )
2020-08-27 19:41:16 +00:00
external.when_back_callback = callback
local _ , app , action = external : checkMethod ( " dict " , method )
2020-11-18 15:43:20 +00:00
if action then
2020-08-27 19:41:16 +00:00
android.dictLookup ( text , app , action )
2019-07-08 12:19:36 +00:00
end
end ,
2019-02-18 16:01:00 +00: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 18:42:18 +00:00
}
function Device : init ( )
2016-12-29 08:10:38 +00:00
self.screen = require ( " ffi/framebuffer_android " ) : new { device = self , debug = logger.dbg }
2014-10-30 18:42:18 +00: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-03 04:52:30 +00:00
handleMiscEv = function ( this , ev )
2020-07-08 12:42:19 +00:00
local UIManager = require ( " ui/uimanager " )
2016-12-29 08:10:38 +00:00
logger.dbg ( " Android application event " , ev.code )
2018-06-02 16:10:55 +00:00
if ev.code == C.APP_CMD_SAVE_STATE then
2014-10-30 18:42:18 +00:00
return " SaveState "
2020-10-22 18:18:36 +00:00
elseif ev.code == C.APP_CMD_DESTROY then
UIManager : quit ( )
2019-01-10 02:11:06 +00: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 11:25:04 +00:00
this.device . screen : _updateWindow ( )
2020-07-08 12:42:19 +00: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 " )
2020-10-13 00:03:20 +00:00
local FileManager = require ( " apps/filemanager/filemanager " )
2020-07-08 12:42:19 +00:00
UIManager : broadcastEvent ( Event : new ( " SetDimensions " , new_size ) )
UIManager : broadcastEvent ( Event : new ( " ScreenResize " , new_size ) )
UIManager : broadcastEvent ( Event : new ( " RedrawCurrentPage " ) )
2020-10-13 00:03:20 +00:00
if FileManager.instance then
FileManager.instance : reinit ( FileManager.instance . path ,
FileManager.instance . focused_file )
UIManager : setDirty ( FileManager.instance . banner , function ( )
return " ui " , FileManager.instance . banner.dimen
end )
end
2020-07-08 12:42:19 +00:00
end
-- to-do: keyboard connected, disconnected
2019-01-06 17:14:06 +00:00
elseif ev.code == C.APP_CMD_RESUME then
2020-08-27 19:41:16 +00:00
if external.when_back_callback then
external.when_back_callback ( )
external.when_back_callback = nil
2019-07-08 12:19:36 +00:00
end
2019-01-06 17:14:06 +00:00
local new_file = android.getIntent ( )
if new_file ~= nil and lfs.attributes ( new_file , " mode " ) == " file " then
2019-03-05 18:22:45 +00:00
-- we cannot blit to a window here since we have no focus yet.
local InfoMessage = require ( " ui/widget/infomessage " )
2020-01-04 00:18:51 +00:00
local BD = require ( " ui/bidi " )
2019-03-05 18:22:45 +00:00
UIManager : scheduleIn ( 0.1 , function ( )
UIManager : show ( InfoMessage : new {
2020-01-04 00:18:51 +00:00
text = T ( _ ( " Opening file '%1'. " ) , BD.filepath ( new_file ) ) ,
2019-03-05 18:22:45 +00:00
timeout = 0.0 ,
} )
end )
UIManager : scheduleIn ( 0.2 , function ( )
require ( " apps/reader/readerui " ) : doShowReader ( new_file )
end )
2019-12-21 14:59:23 +00: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 17:14:06 +00:00
end
2014-10-30 18:42:18 +00:00
end
end ,
2018-02-16 20:44:10 +00:00
hasClipboardText = function ( )
return android.hasClipboardText ( )
end ,
getClipboardText = function ( )
return android.getClipboardText ( )
end ,
setClipboardText = function ( text )
return android.setClipboardText ( text )
end ,
2014-10-30 18:42:18 +00:00
}
-- check if we have a keyboard
2017-09-23 21:58:34 +00:00
if android.lib . AConfiguration_getKeyboard ( android.app . config )
2018-06-02 16:10:55 +00:00
== C.ACONFIGURATION_KEYBOARD_QWERTY
2014-10-30 18:42:18 +00:00
then
self.hasKeyboard = yes
end
-- check if we have a touchscreen
2017-09-23 21:58:34 +00:00
if android.lib . AConfiguration_getTouchscreen ( android.app . config )
2018-06-02 16:10:55 +00:00
~= C.ACONFIGURATION_TOUCHSCREEN_NOTOUCH
2014-10-30 18:42:18 +00:00
then
self.isTouchDevice = yes
end
2019-10-08 16:15:43 +00:00
-- check if we use custom timeouts
2019-07-26 21:04:20 +00:00
if android.needsWakelocks ( ) then
2019-10-08 16:15:43 +00:00
android.timeout . set ( C.AKEEP_SCREEN_ON_ENABLED )
2019-07-26 21:04:20 +00:00
else
local timeout = G_reader_settings : readSetting ( " android_screen_timeout " )
2019-10-08 16:15:43 +00:00
if timeout then
if timeout == C.AKEEP_SCREEN_ON_ENABLED
2020-11-04 18:15:07 +00:00
or timeout > C.AKEEP_SCREEN_ON_DISABLED
and android.settings . hasPermission ( " settings " )
then
2019-10-08 16:15:43 +00:00
android.timeout . set ( timeout )
2019-08-14 11:40:42 +00:00
end
2019-07-26 21:04:20 +00:00
end
2019-01-10 02:11:06 +00:00
end
2019-07-05 20:57:58 +00:00
-- check if we disable fullscreen support
if G_reader_settings : isTrue ( " disable_android_fullscreen " ) then
self : toggleFullscreen ( )
end
2019-08-01 16:27:24 +00:00
2020-05-05 09:18:18 +00: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 16:27:24 +00: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 09:18:18 +00:00
android.setVolumeKeysIgnored ( true )
2019-08-01 16:27:24 +00:00
end
2019-07-05 20:57:58 +00:00
2020-06-15 05:43:37 +00:00
-- check if we ignore the back button completely
if G_reader_settings : isTrue ( " android_ignore_back_button " ) then
android.setBackButtonIgnored ( true )
end
2014-11-24 08:52:01 +00:00
Generic.init ( self )
2014-10-30 18:42:18 +00:00
end
2017-10-21 20:27:09 +00:00
function Device : initNetworkManager ( NetworkMgr )
2019-02-17 15:22:41 +00:00
function NetworkMgr : turnOnWifi ( complete_callback )
2020-02-07 15:36:17 +00:00
android.openWifiSettings ( )
2017-10-21 20:27:09 +00:00
end
2019-02-17 15:22:41 +00:00
function NetworkMgr : turnOffWifi ( complete_callback )
2020-02-07 15:36:17 +00:00
android.openWifiSettings ( )
2017-10-21 20:27:09 +00:00
end
2020-02-07 15:36:17 +00: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 15:51:34 +00:00
end
2017-10-21 20:27:09 +00:00
end
2019-10-08 16:15:43 +00:00
function Device : performHapticFeedback ( type )
android.hapticFeedback ( C [ " AHAPTIC_ " .. type ] )
end
2020-06-13 10:57:08 +00:00
function Device : setIgnoreInput ( enable )
android.setIgnoreInput ( enable )
end
2019-02-06 14:51:50 +00:00
function Device : retrieveNetworkInfo ( )
2020-02-07 15:36:17 +00: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 14:51:50 +00:00
return _ ( " Not connected " )
else
2020-02-07 15:36:17 +00: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 14:51:50 +00:00
end
2019-07-05 20:57:58 +00: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
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 14:51:50 +00:00
end
2019-06-18 23:57:52 +00:00
function Device : info ( )
local is_eink , eink_platform = android.isEink ( )
2020-07-08 12:42:19 +00:00
local product_type = android.getPlatformName ( )
2019-01-17 20:44:15 +00:00
2019-06-18 23:57:52 +00: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 20:44:15 +00:00
2020-07-08 12:42:19 +00:00
local platform_text = " "
if product_type ~= " android " then
2020-07-08 15:55:17 +00:00
platform_text = " \n " .. T ( _ ( " Device type: %1 " ) , product_type ) .. " \n "
2020-07-08 12:42:19 +00:00
end
2019-06-18 23:57:52 +00:00
local eink_text = " "
if is_eink then
2020-07-08 15:55:17 +00:00
eink_text = " \n " .. T ( _ ( " E-ink display supported. \n Platform: %1 " ) , eink_platform ) .. " \n "
2019-06-18 23:57:52 +00:00
end
local wakelocks_text = " "
if android.needsWakelocks ( ) then
2020-07-08 15:55:17 +00: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 20:44:15 +00:00
end
2020-07-08 12:42:19 +00:00
return common_text .. platform_text .. eink_text .. wakelocks_text
2019-06-18 23:57:52 +00:00
end
2020-01-04 19:53:49 +00:00
function Device : epdTest ( )
android.einkTest ( )
end
2019-06-18 23:57:52 +00: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 20:44:15 +00:00
end
2020-06-25 19:33:51 +00: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
2020-11-10 14:00:56 +00:00
function Device : isValidPath ( path )
return android.isPathInsideSandbox ( path )
end
2020-08-28 11:40:35 +00:00
--swallow all events
local function processEvents ( )
local events = ffi.new ( " int[1] " )
local source = ffi.new ( " struct android_poll_source*[1] " )
local poll_state = android.lib . ALooper_pollAll ( - 1 , nil , events , ffi.cast ( " void** " , source ) )
if poll_state >= 0 then
if source [ 0 ] ~= nil then
if source [ 0 ] . id == C.LOOPER_ID_MAIN then
local cmd = C.android_app_read_cmd ( android.app )
C.android_app_pre_exec_cmd ( android.app , cmd )
C.android_app_post_exec_cmd ( android.app , cmd )
elseif source [ 0 ] . id == C.LOOPER_ID_INPUT then
local event = ffi.new ( " AInputEvent*[1] " )
while android.lib . AInputQueue_getEvent ( android.app . inputQueue , event ) >= 0 do
if android.lib . AInputQueue_preDispatchEvent ( android.app . inputQueue , event [ 0 ] ) == 0 then
android.lib . AInputQueue_finishEvent ( android.app . inputQueue , event [ 0 ] , 1 )
end
end
end
end
end
end
2020-08-02 20:27:49 +00:00
function Device : showLightDialog ( )
local title = android.isEink ( ) and _ ( " Frontlight settings " ) or _ ( " Light settings " )
android.lights . showDialog ( title , _ ( " Brightness " ) , _ ( " Warmth " ) , _ ( " OK " ) , _ ( " Cancel " ) )
repeat
2020-08-28 11:40:35 +00:00
processEvents ( ) -- swallow all events, including the last one
2020-09-15 18:39:32 +00:00
FFIUtil.usleep ( 25000 ) -- sleep 25ms before next check if dialog was quit
2020-08-02 20:27:49 +00:00
until ( android.lights . dialogState ( ) ~= C.ALIGHTS_DIALOG_OPENED )
2020-08-28 11:40:35 +00:00
local GestureDetector = require ( " device/gesturedetector " )
GestureDetector : clearStates ( )
2020-08-02 20:27:49 +00:00
local action = android.lights . dialogState ( )
if action == C.ALIGHTS_DIALOG_OK then
self.powerd . fl_intensity = self.powerd : frontlightIntensityHW ( )
logger.dbg ( " Dialog OK, brightness: " .. self.powerd . fl_intensity )
if android.isWarmthDevice ( ) then
self.powerd . fl_warmth = self.powerd : getWarmth ( )
logger.dbg ( " Dialog OK, warmth: " .. self.powerd . fl_warmth )
end
2020-08-29 14:46:59 +00:00
local Event = require ( " ui/event " )
local UIManager = require ( " ui/uimanager " )
UIManager : broadcastEvent ( Event : new ( " FrontlightStateChanged " ) )
2020-08-02 20:27:49 +00:00
elseif action == C.ALIGHTS_DIALOG_CANCEL then
logger.dbg ( " Dialog Cancel, brightness: " .. self.powerd . fl_intensity )
self.powerd : setIntensityHW ( self.powerd . fl_intensity )
if android.isWarmthDevice ( ) then
logger.dbg ( " Dialog Cancel, warmth: " .. self.powerd . fl_warmth )
self.powerd : setWarmth ( self.powerd . fl_warmth )
end
end
end
2020-12-10 15:59:14 +00:00
function Device : untar ( archive , extract_to )
return android.untar ( archive , extract_to )
end
2019-03-03 00:07:56 +00:00
android.LOGI ( string.format ( " Android %s - %s (API %d) - flavor: %s " ,
2019-04-17 18:04:07 +00:00
android.prop . version , getCodename ( ) , Device.firmware_rev , android.prop . flavor ) )
2019-01-17 20:44:15 +00:00
2014-10-30 18:42:18 +00:00
return Device