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