Recently Written · git

subread.koplugin

KOReader plugin: the book follows the narration of an audiobook, from an .srt made by subread.space

git clone https://github.com/equwal/subread.koplugin

Log | Files | Refs


subread/android_player.lua (4767 bytes)

1 --[[--
2 Bridge to the audio player on Android, through SubRead Overlay.
3 
4 KOReader has no notification access, so it cannot see the media session of
5 the player. SubRead Overlay (space.subread.overlay) has that access and
6 gives the state of the player to other apps with a content provider. This
7 module calls that provider over JNI, with the helpers of the KOReader
8 launcher (android.lua). It must only be loaded on Android.
9 
10 Each function returns nil when the provider cannot be reached: SubRead
11 Overlay is not installed, or the call threw a Java exception.
12 --]]--
13 
14 local android = require("android")
15 local ffi = require("ffi")
16 local logger = require("logger")
17 
18 local AndroidPlayer = {}
19 
20 -- The content provider of SubRead Overlay. See its PlayerProvider.
21 AndroidPlayer.URI = "content://space.subread.overlay.player/state"
22 -- Name of the app that the user must install.
23 AndroidPlayer.PACKAGE = "space.subread.overlay"
24 
25 local NULL = ffi.cast("void *", nil)
26 
27 --- Clears a pending Java exception. Returns true when there was one.
28 -- A pending exception makes the next JNI call abort the process, so each
29 -- call that can throw is followed by this check.
30 local function clearException(jni, what)
31     local env = jni.env
32     if env[0].ExceptionCheck(env) == ffi.C.JNI_TRUE then
33         env[0].ExceptionClear(env)
34         logger.warn("SubRead: Java exception in", what)
35         return true
36     end
37     return false
38 end
39 
40 --- Runs fn(jni, resolver, uri) with the ContentResolver of the activity
41 --- and the Uri of the provider. Frees the local references after.
42 local function withResolver(fn)
43     return android.jni:context(android.app.activity.vm, function(jni)
44         local env = jni.env
45         local resolver = jni:callObjectMethod(android.app.activity.clazz,
46             "getContentResolver", "()Landroid/content/ContentResolver;")
47         local uri_text = env[0].NewStringUTF(env, AndroidPlayer.URI)
48         local uri = jni:callStaticObjectMethod("android/net/Uri", "parse",
49             "(Ljava/lang/String;)Landroid/net/Uri;", uri_text)
50         local result = fn(jni, resolver, uri)
51         env[0].DeleteLocalRef(env, uri)
52         env[0].DeleteLocalRef(env, uri_text)
53         env[0].DeleteLocalRef(env, resolver)
54         return result
55     end)
56 end
57 
58 --- Reads the state line of the player. Returns nil when there is no provider.
59 function AndroidPlayer.query()
60     local ok, line = pcall(withResolver, function(jni, resolver, uri)
61         local env = jni.env
62         -- ContentResolver.query(Uri, String[], String, String[], String).
63         -- It returns null when no app has the provider.
64         local cursor = jni:callObjectMethod(resolver, "query",
65             "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;",
66             uri, NULL, NULL, NULL, NULL)
67         if clearException(jni, "query") or cursor == nil then return nil end
68         local line
69         if jni:callBooleanMethod(cursor, "moveToFirst", "()Z") then
70             local text = jni:callObjectMethod(cursor, "getString",
71                 "(I)Ljava/lang/String;", ffi.new("int32_t", 0))
72             if text ~= nil then
73                 line = jni:to_string(text)
74                 env[0].DeleteLocalRef(env, text)
75             end
76         end
77         jni:callVoidMethod(cursor, "close", "()V")
78         env[0].DeleteLocalRef(env, cursor)
79         return line
80     end)
81     if not ok then
82         logger.warn("SubRead: player query failed:", line)
83         return nil
84     end
85     return line
86 end
87 
88 --- Sends a command to the player: "play", "pause", or "seek" with the
89 --- position in milliseconds as a string. Returns true when the provider
90 --- took the call.
91 function AndroidPlayer.call(method, argument)
92     local ok, done = pcall(withResolver, function(jni, resolver, uri)
93         local env = jni.env
94         local method_text = env[0].NewStringUTF(env, method)
95         local argument_text = argument and env[0].NewStringUTF(env, tostring(argument)) or NULL
96         -- ContentResolver.call(Uri, String, String, Bundle). It throws when
97         -- no app has the provider.
98         local bundle = jni:callObjectMethod(resolver, "call",
99             "(Landroid/net/Uri;Ljava/lang/String;Ljava/lang/String;Landroid/os/Bundle;)Landroid/os/Bundle;",
100             uri, method_text, argument_text, NULL)
101         local failed = clearException(jni, "call " .. method)
102         if bundle ~= nil then env[0].DeleteLocalRef(env, bundle) end
103         if argument_text ~= NULL then env[0].DeleteLocalRef(env, argument_text) end
104         env[0].DeleteLocalRef(env, method_text)
105         return not failed
106     end)
107     if not ok then
108         logger.warn("SubRead: player call failed:", done)
109         return false
110     end
111     return done
112 end
113 
114 return AndroidPlayer