MainActivity.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. #define WV_DEBUG
  2. using Android.App;
  3. using Android.OS;
  4. using Android.Util;
  5. using Android.Views;
  6. using Android.Webkit;
  7. using Java.Interop;
  8. using Java.Lang;
  9. using System;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Threading.Tasks;
  13. using Android;
  14. using Android.Content;
  15. using Android.Content.PM;
  16. using Android.Widget;
  17. namespace LnoidWv
  18. {
  19. using JsLab.JsApi;
  20. // ReSharper disable once ClassNeverInstantiated.Global
  21. [Activity(Label = "!轻离阅", MainLauncher = true, Icon = "@drawable/icon")]
  22. public partial class MainActivity : Activity { }
  23. public class NativeFunctions : JsApiBase
  24. {
  25. [JsApi]
  26. // ReSharper disable once UnusedMember.Global
  27. public bool PathExist(string path)
  28. {
  29. return Directory.Exists(path);
  30. }
  31. [JsApi]
  32. // ReSharper disable once UnusedMember.Global
  33. public object GetDirs(string path, bool full = false)
  34. {
  35. var directories = Directory.GetDirectories(path);
  36. var array = full
  37. ? directories
  38. : directories.Select(Path.GetFileName).ToArray();
  39. Array.Sort(array);
  40. return array;
  41. }
  42. [JsApi]
  43. // ReSharper disable once UnusedMember.Global
  44. public object GetPrefixes(string dataPath, Action<object> cbProcess = null)
  45. {
  46. var directories = Directory.GetDirectories(dataPath);
  47. Array.Sort(directories);
  48. var progress = cbProcess == null
  49. ? new Action<int>(delegate { })
  50. : p => cbProcess(new { Total = directories.Length, Current = p });
  51. return directories.Select(delegate (string prefixPath, int index)
  52. {
  53. var foo = new
  54. {
  55. Path = prefixPath,
  56. Prefix = Path.GetFileName(prefixPath)?.ToUpper(),
  57. SeriesCount = Directory.GetDirectories(prefixPath).Length,
  58. };
  59. progress(index + 1);
  60. return foo;
  61. }).ToArray();
  62. }
  63. [JsApi]
  64. // ReSharper disable once UnusedMember.Global
  65. public object GetSeries(string prefixPath, Action<object> cbProcess = null)
  66. {
  67. var directories = Directory.GetDirectories(prefixPath);
  68. Array.Sort(directories);
  69. var progress = cbProcess == null
  70. ? new Action<int>(delegate { })
  71. : p => cbProcess(new { Total = directories.Length, Current = p });
  72. return directories
  73. .OrderBy(p => p)
  74. .Select((seriesPath, index) =>
  75. {
  76. var subDirs = Directory.GetDirectories(seriesPath).OrderBy(p => p).ToArray();
  77. var obj = new
  78. {
  79. Path = seriesPath,
  80. SeriesName = File.ReadAllText(Path.Combine(seriesPath, "sname.txt")),
  81. VolumeCount = subDirs.Length,
  82. CoverData = subDirs
  83. .Select(s => Path.Combine(s, "cover.jpg")).Where(File.Exists)
  84. .Select(p => "data:image/jpeg;base64," + Convert.ToBase64String(File.ReadAllBytes(p)))
  85. .FirstOrDefault()
  86. };
  87. progress(index + 1);
  88. return obj;
  89. }).ToArray();
  90. }
  91. [JsApi]
  92. // ReSharper disable once UnusedMember.Global
  93. public object GetVolumes(string seriesPath, Action<object> cbProcess = null)
  94. {
  95. var directories = Directory.GetDirectories(seriesPath);
  96. Array.Sort(directories);
  97. var progress = cbProcess == null
  98. ? new Action<int>(delegate { })
  99. : p => cbProcess(new { Total = directories.Length, Current = p });
  100. return directories.OrderBy(p => p)
  101. .Select((volumePath, index) =>
  102. {
  103. var b64 = Convert.ToBase64String(File.ReadAllBytes(Path.Combine(volumePath, "cover.jpg")), Base64FormattingOptions.None);
  104. var obj = new
  105. {
  106. Path = volumePath,
  107. VolumeName = File.ReadAllText(Path.Combine(volumePath, "vname.txt")),
  108. ChapterCount = Directory.GetDirectories(volumePath).Length,
  109. CoverData = $"data:image/jpeg;base64,{b64}",
  110. };
  111. progress(index + 1);
  112. return obj;
  113. }).ToArray();
  114. }
  115. [JsApi]
  116. // ReSharper disable once UnusedMember.Global
  117. public object GetChapters(string volumePath, Action<object> cbProcess = null)
  118. {
  119. var directories = Directory.GetDirectories(volumePath);
  120. Array.Sort(directories);
  121. var progress = cbProcess == null
  122. ? new Action<int>(delegate { })
  123. : p => cbProcess(new { Total = directories.Length, Current = p });
  124. return directories
  125. .OrderBy(p => p)
  126. .Select((chapterPath, index) =>
  127. {
  128. var pictures1 = Directory.GetFiles(chapterPath, "*.jpg");
  129. var pictures2 = Directory.GetFiles(chapterPath, "*.JPG");
  130. var firstBase64 = pictures1.Concat(pictures2)
  131. .OrderBy(filename => filename)
  132. .Select(filename => "data:image/jpeg;base64," + Convert.ToBase64String(File.ReadAllBytes(filename), Base64FormattingOptions.None))
  133. .FirstOrDefault();
  134. var obj = new
  135. {
  136. Path = chapterPath,
  137. ChapterName = File.ReadAllText(Path.Combine(chapterPath, "cname.txt")),
  138. FirstPicture = firstBase64
  139. };
  140. progress(index + 1);
  141. return obj;
  142. }).ToArray();
  143. }
  144. [JsApi]
  145. // ReSharper disable once UnusedMember.Global
  146. public object GetChapterContent(string chapterPath, Action<object> cbProcess = null)
  147. {
  148. var lines = File.ReadAllLines(Path.Combine(chapterPath, "ctext.txt"));
  149. var progress = cbProcess == null
  150. ? new Action<int>(delegate { })
  151. : p => cbProcess(new { Total = lines.Length, Current = p });
  152. for (var i = 0; i < lines.Length; i++)
  153. {
  154. var line = lines[i];
  155. if (!line.StartsWith("<img")) continue;
  156. var picnam = System.Web.HttpUtility.HtmlDecode(line)
  157. .Replace("<img src=\"hybrid://pic?", "")
  158. .Replace("\"/>", "")
  159. ;
  160. var picabs = Path.Combine(chapterPath, picnam);
  161. var b64 = Convert.ToBase64String(File.ReadAllBytes(picabs), Base64FormattingOptions.None);
  162. lines[i] = $"<img src='data:image/jpeg;base64,{b64}' />";
  163. progress(i + 1);
  164. }
  165. return lines;
  166. }
  167. [JsApi]
  168. // ReSharper disable once UnusedMember.Global
  169. public string ReadSetting()
  170. {
  171. #if !Android22
  172. if (Android.OS.Environment.IsExternalStorageManager == false)
  173. {
  174. var uri = Android.Net.Uri.Parse("package:" + Application.Context.ApplicationInfo.PackageName);
  175. var intent = new Intent(Android.Provider.Settings.ActionManageAppAllFilesAccessPermission, uri);
  176. _mainActivity.StartActivityForResult(intent, 9527);
  177. }
  178. #endif
  179. var localStorage = _mainActivity.Application.FilesDir.Path;
  180. var configFilePath = Path.Combine(localStorage, "config.json");
  181. return File.Exists(configFilePath)
  182. ? File.ReadAllText(configFilePath)
  183. : null;
  184. }
  185. [JsApi]
  186. // ReSharper disable once UnusedMember.Global
  187. public void WriteSetting(string json)
  188. {
  189. var localStorage = _mainActivity.Application.FilesDir.Path;
  190. var configFilePath = Path.Combine(localStorage, "config.json");
  191. File.WriteAllText(configFilePath, json);
  192. }
  193. [JsApi]
  194. // ReSharper disable once UnusedMember.Global
  195. public void ExitApp()
  196. {
  197. JavaSystem.Exit(0);
  198. }
  199. [JsApi]
  200. // ReSharper disable once UnusedMember.Global
  201. public void WriteLog(string content)
  202. {
  203. Log.Debug($"{_mainActivity.Application.PackageName}::JsApi::{nameof(WriteLog)}", content);
  204. }
  205. [JsApi]
  206. // ReSharper disable once UnusedMember.Global
  207. public void CopyText(string text)
  208. {
  209. var myClipboard = (ClipboardManager)_mainActivity.GetSystemService(Context.ClipboardService);
  210. var myClip = ClipData.NewPlainText("text", text);
  211. myClipboard.PrimaryClip = myClip;
  212. Toast.MakeText(_mainActivity.Application, "文本已复制", ToastLength.Short).Show();
  213. }
  214. private readonly MainActivity _mainActivity;
  215. public NativeFunctions(MainActivity mainActivity, string jsBindName) : base(jsBindName)
  216. {
  217. _mainActivity = mainActivity;
  218. }
  219. protected override void CallJs(string code)
  220. {
  221. _mainActivity.RunOnUiThread(() => _mainActivity.CallJs(code));
  222. }
  223. #if DEBUG || WV_DEBUG
  224. [JsApi]
  225. // ReSharper disable once UnusedMember.Global
  226. public void ReLoad()
  227. {
  228. _mainActivity.RunOnUiThread(() => _mainActivity.ReLoad());
  229. }
  230. #endif
  231. // TODO: UI Config - GridSize(0.5x-10x) - TextSize(5-127) - UiZoom(25-500%) - TextZoom(25-500%) - PictureMode(Auto,FitW/H)
  232. // TODO: Fav/Continue (Chapter Entry) -> (Content Scroll Percent)
  233. // TODO: State S/L -- Restore on back
  234. // TODO: Read Zip file directly
  235. // TODO: Indexing for Improve load speed
  236. // BUG: DON'T Reload after screen rotated
  237. // TODO: Implement more functions to improve this app.
  238. }
  239. // ReSharper disable once ClassNeverInstantiated.Global
  240. public partial class MainActivity
  241. {
  242. private const string MainUrl =
  243. //#if DEBUG
  244. // "http://192.168.23.97/main.html"
  245. // // "http://192.168.1.233/main.html"
  246. // // "http://lnoidebugserver/main.html"
  247. //#else
  248. "file:///android_asset/main.html"
  249. //#endif
  250. ;
  251. private const string JsBindName = "LnoidNativeFunctions";
  252. private WebView _view;
  253. protected override void OnCreate(Bundle bundle)
  254. {
  255. base.OnCreate(bundle);
  256. #if !Android22
  257. if (CheckSelfPermission(Manifest.Permission.ReadExternalStorage) != Permission.Granted)
  258. RequestPermissions(new[] { Manifest.Permission.ReadExternalStorage }, 123321123); // is an app-defined int constant that should be quite unique
  259. //var uri = Android.Net.Uri.Parse("package:" + Application.Context.ApplicationInfo.PackageName);
  260. //var intent = new Intent(Android.Provider.Settings.ActionManageAppAllFilesAccessPermission, uri, ApplicationContext, GetType());
  261. //StartActivity(intent);
  262. #endif
  263. RequestWindowFeature(WindowFeatures.NoTitle);
  264. #if DEBUG || WV_DEBUG
  265. #if !Android22
  266. if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat)
  267. WebView.SetWebContentsDebuggingEnabled(true);
  268. #else
  269. WebView.EnablePlatformNotifications();
  270. #endif
  271. #else
  272. #if !Android22
  273. if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat)
  274. WebView.SetWebContentsDebuggingEnabled(false);
  275. #endif
  276. #endif
  277. _view = new WebView(this);
  278. _view.SetWebChromeClient(new WebChromeClient());
  279. #pragma warning disable 618
  280. _view.LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.FillParent);
  281. #pragma warning restore 618
  282. _view.SetPadding(0, 0, 0, 0);
  283. SetContentView(_view);
  284. _view.SetInitialScale(100);
  285. _view.Settings.JavaScriptEnabled = true;
  286. var nativeFunctions = new NativeFunctions(this, JsBindName);
  287. _view.AddJavascriptInterface(nativeFunctions, JsBindName);
  288. _view.LoadUrl(MainUrl);
  289. }
  290. internal void CallJs(string code)
  291. {
  292. _view.LoadUrl($"javascript:{code}");
  293. }
  294. public override void OnBackPressed()
  295. {
  296. CallJs("__backButton_Pressed();");
  297. }
  298. public override bool OnKeyUp(Keycode keyCode, KeyEvent e)
  299. {
  300. if (keyCode == Keycode.Menu)
  301. {
  302. CallJs("__menuButton_Pressed();");
  303. return true;
  304. }
  305. return base.OnKeyUp(keyCode, e);
  306. }
  307. #if WV_DEBUG
  308. public void ReLoad()
  309. {
  310. _view.LoadUrl(MainUrl);
  311. }
  312. #endif
  313. }
  314. }
  315. // ReSharper disable once CheckNamespace
  316. namespace JsLab.JsApi
  317. {
  318. using System;
  319. using System.Collections.Generic;
  320. using System.Linq;
  321. using System.Reflection;
  322. public abstract class JsApiBase : Java.Lang.Object
  323. {
  324. private readonly string _jsBindName;
  325. private readonly IReadOnlyDictionary<string, JsApiMethod> _jsApiMethodDictionary;
  326. [AttributeUsage(AttributeTargets.Method)]
  327. protected sealed class JsApiAttribute : Attribute
  328. {
  329. }
  330. // ReSharper disable UnusedAutoPropertyAccessor.Local
  331. // ReSharper disable ClassNeverInstantiated.Local
  332. private sealed class InvokeRequestModel
  333. {
  334. public string MethodName { get; set; }
  335. public object[] Arguments { get; set; }
  336. }
  337. private sealed class InvokeRequestCallback
  338. {
  339. public string CallBack { get; set; }
  340. }
  341. // ReSharper restore ClassNeverInstantiated.Local
  342. // ReSharper restore UnusedAutoPropertyAccessor.Local
  343. private class JsApiMethod
  344. {
  345. public MethodInfo MethodInfo { get; }
  346. public IReadOnlyList<JsApiParmeter> ParameterInfos { get; }
  347. public JsApiMethod(MethodInfo methodInfo, IReadOnlyList<JsApiParmeter> parameterInfos)
  348. {
  349. MethodInfo = methodInfo;
  350. ParameterInfos = parameterInfos;
  351. }
  352. }
  353. private class JsApiParmeter
  354. {
  355. public string Name { get; }
  356. public bool IsCallback { get; }
  357. public JsApiParmeter(string name, bool isCallback)
  358. {
  359. Name = name;
  360. IsCallback = isCallback;
  361. }
  362. }
  363. protected JsApiBase(string jsBindName)
  364. {
  365. var myType = GetType();
  366. var tObjectAction = typeof(Action<object>);
  367. _jsApiMethodDictionary = myType.GetMethods()
  368. .Where(p => p.IsDefined(typeof(JsApiAttribute), true))
  369. .Select(p => new JsApiMethod(
  370. p,
  371. p.GetParameters()
  372. .Select(q => new JsApiParmeter(q.Name, q.ParameterType == tObjectAction))
  373. .ToArray()
  374. ))
  375. .ToDictionary(p => p.MethodInfo.Name);
  376. _jsBindName = jsBindName;
  377. }
  378. [Export]
  379. #if !Android22
  380. [JavascriptInterface]
  381. #endif
  382. // ReSharper disable once MemberCanBePrivate.Global
  383. public string GenerateFunctionJsonStub()
  384. {
  385. var methods = _jsApiMethodDictionary.Values.ToArray();
  386. // ReSharper disable once UseObjectOrCollectionInitializer
  387. var list = new List<string>(methods.Length);
  388. list.Add("$mkCallback:function(c,r){" +
  389. "if(typeof c!=='function') "
  390. + "return undefined;" +
  391. "if(!this.$cbIdx)"
  392. + "this.$cbIdx=1;" +
  393. "else "
  394. + "this.$cbIdx++;" +
  395. "var cbName='__cb_'+this.$cbIdx;" +
  396. "window[cbName]=function(d){"
  397. + "if(r)delete window[cbName];"
  398. + "c(d);" +
  399. "};" +
  400. "return cbName;" +
  401. "}");
  402. foreach (var jsApiMethod in methods)
  403. {
  404. var method = jsApiMethod.MethodInfo;
  405. var parameterInfos = jsApiMethod.ParameterInfos;
  406. var lstArgs = parameterInfos.Select((p, i) => "p" + i + "_" + p.Name).ToList();
  407. var argList = string.Join(",", lstArgs);
  408. var fnSync =
  409. $"{method.Name}:function({argList})" + "{" +
  410. "var arg={" + $"MethodName:'{method.Name}',Arguments:[{argList}]" + "};" +
  411. "var argson=JSON.stringify(arg);" +
  412. $"return JSON.parse({_jsBindName}.InvokeInterface(argson));" + "}";
  413. lstArgs.Insert(0, "p_callback");
  414. var argListAsync = string.Join(",", lstArgs);
  415. //Async version with callback
  416. var fnAsync =
  417. $"{method.Name}Async:function({argListAsync})" + "{" +
  418. $"if(!p_callback) throw 'callback are required.[{method.Name}Async]';" +
  419. "var args=[];";
  420. var lstCbToDel = new List<string>(parameterInfos.Count);
  421. for (var i = 0; i < parameterInfos.Count; i++)
  422. {
  423. var item = parameterInfos[i];
  424. var pName = lstArgs[i + 1];// skip p_callback
  425. if (item.IsCallback)
  426. {
  427. fnAsync +=
  428. $"var cbName_{i}='undefined';" +
  429. $"if(typeof {pName}!=='function')" + "{"
  430. + "args.push(null);" +
  431. "}else{"
  432. + $"cbName_{i}=this.$mkCallback({pName},false);"
  433. + $"args.push(cbName_{i});" +
  434. "}";
  435. lstCbToDel.Add($"cbName_{i}");
  436. }
  437. else
  438. {
  439. fnAsync += $"args.push({pName});";
  440. }
  441. }
  442. fnAsync +=
  443. "var cbName=this.$mkCallback(function(retr){"
  444. + string.Join("", lstCbToDel.Select(p => $"delete window[{p}];"))
  445. + "p_callback(retr);" +
  446. "},true);" +
  447. "var arg={" + $"MethodName:'{method.Name}'" + ",Arguments:args" + ",CallBack:cbName" + "};" +
  448. "var argson=JSON.stringify(arg);" +
  449. $"{_jsBindName}.InvokeInterfaceAsync(argson)" +
  450. // cancel token handle: too lazy to implement!
  451. "}";
  452. list.Add(fnSync);
  453. list.Add(fnAsync);
  454. }
  455. return "{" + string.Join(",", list) + "}";
  456. }
  457. [Export]
  458. #if !Android22
  459. [JavascriptInterface]
  460. #endif
  461. // ReSharper disable once UnusedMember.Global
  462. public string GenerateFunctionStubForEval()
  463. {
  464. var ret = $"({GenerateFunctionJsonStub()})";
  465. return ret;
  466. }
  467. [Export]
  468. #if !Android22
  469. [JavascriptInterface]
  470. #endif
  471. // ReSharper disable once UnusedMember.Global
  472. public void InvokeInterfaceAsync(string json)
  473. {
  474. // cancel token handle: too lazy to implement!
  475. #if Android22
  476. var im = fastJSON.JSON.ToObject<InvokeRequestCallback>(json);
  477. #else
  478. var im = Newtonsoft.Json.JsonConvert.DeserializeObject<InvokeRequestCallback>(json);
  479. #endif
  480. Task.Factory.StartNew(() =>
  481. {
  482. var ret = InvokeInterface(json);
  483. var code = $"{im.CallBack}({ret})";
  484. CallJs(code);
  485. });
  486. }
  487. [Export]
  488. #if !Android22
  489. [JavascriptInterface]
  490. #endif
  491. // ReSharper disable once UnusedMember.Global
  492. // ReSharper disable once MemberCanBePrivate.Global
  493. public string InvokeInterface(string json)
  494. {
  495. object r;
  496. try
  497. {
  498. #if Android22
  499. var im = fastJSON.JSON.ToObject<InvokeRequestModel>(json);
  500. #else
  501. var im = Newtonsoft.Json.JsonConvert.DeserializeObject<InvokeRequestModel>(json);
  502. #endif
  503. var method = _jsApiMethodDictionary[im.MethodName];
  504. for (var i = 0; i < im.Arguments.Length; i++)
  505. {
  506. if (im.Arguments[i] == null)
  507. {
  508. im.Arguments[i] = Type.Missing;
  509. }
  510. else if (method.ParameterInfos[i].IsCallback)
  511. {
  512. var jsFunction = (string)im.Arguments[i];
  513. im.Arguments[i] = new Action<object>(
  514. o => CallJs($"{jsFunction}({Newtonsoft.Json.JsonConvert.SerializeObject(o)})"));
  515. }
  516. }
  517. r = new
  518. {
  519. Success = true,
  520. Result = method.MethodInfo.Invoke(this, im.Arguments)
  521. };
  522. }
  523. catch (Exception ex)
  524. {
  525. r = new
  526. {
  527. Success = false,
  528. Exception = ex
  529. };
  530. }
  531. #if Android22
  532. return fastJSON.JSON.ToJSON(r);
  533. #else
  534. return Newtonsoft.Json.JsonConvert.SerializeObject(r);
  535. #endif
  536. }
  537. protected abstract void CallJs(string code);
  538. }
  539. }
  540. #if Android22
  541. namespace System.Net
  542. {
  543. internal class WebUtility
  544. {
  545. public static readonly Func<string, string> HtmlEncode = Web.HttpUtility.HtmlEncode;
  546. }
  547. }
  548. #endif