Plugin.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. package com.getcapacitor;
  2. import android.app.Activity;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.content.pm.PackageInfo;
  6. import android.content.pm.PackageManager;
  7. import android.net.Uri;
  8. import android.os.Bundle;
  9. import androidx.appcompat.app.AppCompatActivity;
  10. import androidx.core.app.ActivityCompat;
  11. import org.json.JSONException;
  12. import org.json.JSONObject;
  13. import java.util.ArrayList;
  14. import java.util.Arrays;
  15. import java.util.HashMap;
  16. import java.util.List;
  17. import java.util.Map;
  18. /**
  19. * Plugin is the base class for all plugins, containing a number of
  20. * convenient features for interacting with the {@link Bridge}, managing
  21. * plugin permissions, tracking lifecycle events, and more.
  22. *
  23. * You should inherit from this class when creating new plugins, along with
  24. * adding the {@link NativePlugin} annotation to add additional required
  25. * metadata about the Plugin
  26. */
  27. public class Plugin {
  28. // The key we will use inside of a persisted Bundle for the JSON blob
  29. // for a plugin call options.
  30. private static final String BUNDLE_PERSISTED_OPTIONS_JSON_KEY = "_json";
  31. // Reference to the Bridge
  32. protected Bridge bridge;
  33. // Reference to the PluginHandle wrapper for this Plugin
  34. protected PluginHandle handle;
  35. // A way for plugins to quickly save a call that they will
  36. // need to reference between activity/permissions starts/requests
  37. protected PluginCall savedLastCall;
  38. // Stored event listeners
  39. private final Map<String, List<PluginCall>> eventListeners;
  40. // Stored results of an event if an event was fired and
  41. // no listeners were attached yet. Only stores the last value.
  42. private final Map<String, JSObject> retainedEventArguments;
  43. public Plugin() {
  44. eventListeners = new HashMap<>();
  45. retainedEventArguments = new HashMap<>();
  46. }
  47. /**
  48. * Called when the plugin has been connected to the bridge
  49. * and is ready to start initializing.
  50. */
  51. public void load() {}
  52. /**
  53. * Get the main {@link Context} for the current Activity (your app)
  54. * @return the Context for the current activity
  55. */
  56. public Context getContext() { return this.bridge.getContext(); }
  57. /**
  58. * Get the main {@link Activity} for the app
  59. * @return the Activity for the current app
  60. */
  61. public AppCompatActivity getActivity() { return (AppCompatActivity) this.bridge.getActivity(); }
  62. /**
  63. * Set the Bridge instance for this plugin
  64. * @param bridge
  65. */
  66. public void setBridge(Bridge bridge) {
  67. this.bridge = bridge;
  68. }
  69. /**
  70. * Get the Bridge instance for this plugin
  71. */
  72. public Bridge getBridge() { return this.bridge; }
  73. /**
  74. * Set the wrapper {@link PluginHandle} instance for this plugin that
  75. * contains additional metadata about the Plugin instance (such
  76. * as indexed methods for reflection, and {@link NativePlugin} annotation data).
  77. * @param pluginHandle
  78. */
  79. public void setPluginHandle(PluginHandle pluginHandle) {
  80. this.handle = pluginHandle;
  81. }
  82. /**
  83. * Return the wrapper {@link PluginHandle} for this plugin.
  84. *
  85. * This wrapper contains additional metadata about the plugin instance,
  86. * such as indexed methods for reflection, and {@link NativePlugin} annotation data).
  87. * @return
  88. */
  89. public PluginHandle getPluginHandle() { return this.handle; }
  90. /**
  91. * Get the root App ID
  92. * @return
  93. */
  94. public String getAppId() {
  95. return getContext().getPackageName();
  96. }
  97. /**
  98. * Called to save a {@link PluginCall} in order to reference it
  99. * later, such as in an activity or permissions result handler
  100. * @param lastCall
  101. */
  102. public void saveCall(PluginCall lastCall) {
  103. this.savedLastCall = lastCall;
  104. }
  105. /**
  106. * Set the last saved call to null to free memory
  107. */
  108. public void freeSavedCall() {
  109. if (!this.savedLastCall.isReleased()) {
  110. this.savedLastCall.release(bridge);
  111. }
  112. this.savedLastCall = null;
  113. }
  114. /**
  115. * Get the last saved call, if any
  116. * @return
  117. */
  118. public PluginCall getSavedCall() {
  119. return this.savedLastCall;
  120. }
  121. public Object getConfigValue(String key) {
  122. try {
  123. JSONObject plugins = bridge.getConfig().getObject("plugins");
  124. if (plugins == null) {
  125. return null;
  126. }
  127. JSONObject pluginConfig = plugins.getJSONObject(getPluginHandle().getId());
  128. return pluginConfig.get(key);
  129. } catch (JSONException ex) {
  130. return null;
  131. }
  132. }
  133. /**
  134. * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
  135. * @param neededPermissions
  136. * @return
  137. */
  138. public String[] getUndefinedPermissions(String[] neededPermissions) {
  139. ArrayList<String> undefinedPermissions = new ArrayList<String>();
  140. String[] requestedPermissions = getManifestPermissions();
  141. if (requestedPermissions != null && requestedPermissions.length > 0)
  142. {
  143. List<String> requestedPermissionsList = Arrays.asList(requestedPermissions);
  144. ArrayList<String> requestedPermissionsArrayList = new ArrayList<String>();
  145. requestedPermissionsArrayList.addAll(requestedPermissionsList);
  146. for (String permission: neededPermissions) {
  147. if (!requestedPermissionsArrayList.contains(permission)) {
  148. undefinedPermissions.add(permission);
  149. }
  150. }
  151. String[] undefinedPermissionArray = new String[undefinedPermissions.size()];
  152. undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray);
  153. return undefinedPermissionArray;
  154. }
  155. return neededPermissions;
  156. }
  157. /**
  158. * Check whether the given permission has been defined in the AndroidManifest.xml
  159. * @param permission
  160. * @return
  161. */
  162. public boolean hasDefinedPermission(String permission) {
  163. boolean hasPermission = false;
  164. String[] requestedPermissions = getManifestPermissions();
  165. if (requestedPermissions != null && requestedPermissions.length > 0)
  166. {
  167. List<String> requestedPermissionsList = Arrays.asList(requestedPermissions);
  168. ArrayList<String> requestedPermissionsArrayList = new ArrayList<String>();
  169. requestedPermissionsArrayList.addAll(requestedPermissionsList);
  170. if (requestedPermissionsArrayList.contains(permission)) {
  171. hasPermission = true;
  172. }
  173. }
  174. return hasPermission;
  175. }
  176. /**
  177. * Get the permissions defined in AndroidManifest.xml
  178. * @return
  179. */
  180. private String[] getManifestPermissions(){
  181. String[] requestedPermissions = null;
  182. try {
  183. PackageManager pm = getContext().getPackageManager();
  184. PackageInfo packageInfo = pm.getPackageInfo(getAppId(), PackageManager.GET_PERMISSIONS);
  185. if (packageInfo != null) {
  186. requestedPermissions = packageInfo.requestedPermissions;
  187. }
  188. } catch (Exception ex) {
  189. }
  190. return requestedPermissions;
  191. }
  192. /**
  193. * Check whether any of the given permissions has been defined in the AndroidManifest.xml
  194. * @param permissions
  195. * @return
  196. */
  197. public boolean hasDefinedPermissions(String[] permissions) {
  198. for (String permission: permissions) {
  199. if (!hasDefinedPermission(permission)){
  200. return false;
  201. }
  202. }
  203. return true;
  204. }
  205. /**
  206. * Check whether any of annotation permissions has been defined in the AndroidManifest.xml
  207. * @return
  208. */
  209. public boolean hasDefinedRequiredPermissions() {
  210. NativePlugin annotation = handle.getPluginAnnotation();
  211. return hasDefinedPermissions(annotation.permissions());
  212. }
  213. /**
  214. * Check whether the given permission has been granted by the user
  215. * @param permission
  216. * @return
  217. */
  218. public boolean hasPermission(String permission) {
  219. return ActivityCompat.checkSelfPermission(this.getContext(), permission) == PackageManager.PERMISSION_GRANTED;
  220. }
  221. /**
  222. * If the {@link NativePlugin} annotation specified a set of permissions,
  223. * this method checks if each is granted. Note: if you are okay
  224. * with a limited subset of the permissions being granted, check
  225. * each one individually instead with hasPermission
  226. * @return
  227. */
  228. public boolean hasRequiredPermissions() {
  229. NativePlugin annotation = handle.getPluginAnnotation();
  230. for (String perm : annotation.permissions()) {
  231. if (!hasPermission(perm)) {
  232. return false;
  233. }
  234. }
  235. return true;
  236. }
  237. /**
  238. * Helper to make requesting permissions easy
  239. * @param permissions the set of permissions to request
  240. * @param requestCode the requestCode to use to associate the result with the plugin
  241. */
  242. public void pluginRequestPermissions(String[] permissions, int requestCode) {
  243. ActivityCompat.requestPermissions(getActivity(), permissions, requestCode);
  244. }
  245. /**
  246. * Request all of the specified permissions in the NativePlugin annotation (if any)
  247. */
  248. public void pluginRequestAllPermissions() {
  249. NativePlugin annotation = handle.getPluginAnnotation();
  250. ActivityCompat.requestPermissions(getActivity(), annotation.permissions(), annotation.permissionRequestCode());
  251. }
  252. /**
  253. * Helper to make requesting individual permissions easy
  254. * @param permission the permission to request
  255. * @param requestCode the requestCode to use to associate the result with the plugin
  256. */
  257. public void pluginRequestPermission(String permission, int requestCode) {
  258. ActivityCompat.requestPermissions(getActivity(), new String[] { permission }, requestCode);
  259. }
  260. /**
  261. * Add a listener for the given event
  262. * @param eventName
  263. * @param call
  264. */
  265. private void addEventListener(String eventName, PluginCall call) {
  266. List<PluginCall> listeners = eventListeners.get(eventName);
  267. if (listeners == null || listeners.isEmpty()) {
  268. listeners = new ArrayList<PluginCall>();
  269. eventListeners.put(eventName, listeners);
  270. // Must add the call before sending retained arguments
  271. listeners.add(call);
  272. sendRetainedArgumentsForEvent(eventName);
  273. } else {
  274. listeners.add(call);
  275. }
  276. }
  277. /**
  278. * Remove a listener from the given event
  279. * @param eventName
  280. * @param call
  281. */
  282. private void removeEventListener(String eventName, PluginCall call) {
  283. List<PluginCall> listeners = eventListeners.get(eventName);
  284. if (listeners == null) {
  285. return;
  286. }
  287. listeners.remove(call);
  288. }
  289. /**
  290. * Notify all listeners that an event occurred
  291. * @param eventName
  292. * @param data
  293. */
  294. protected void notifyListeners(String eventName, JSObject data, boolean retainUntilConsumed) {
  295. Logger.verbose(getLogTag(), "Notifying listeners for event " + eventName);
  296. List<PluginCall> listeners = eventListeners.get(eventName);
  297. if (listeners == null || listeners.isEmpty()) {
  298. Logger.debug(getLogTag(), "No listeners found for event " + eventName);
  299. if (retainUntilConsumed) {
  300. retainedEventArguments.put(eventName, data);
  301. }
  302. return;
  303. }
  304. for(PluginCall call : listeners) {
  305. call.success(data);
  306. }
  307. }
  308. /**
  309. * Notify all listeners that an event occurred
  310. * This calls {@link Plugin#notifyListeners(String, JSObject, boolean)}
  311. * with retainUntilConsumed set to false
  312. * @param eventName
  313. * @param data
  314. */
  315. protected void notifyListeners(String eventName, JSObject data) {
  316. notifyListeners(eventName, data, false);
  317. }
  318. /**
  319. * Check if there are any listeners for the given event
  320. */
  321. protected boolean hasListeners(String eventName) {
  322. List<PluginCall> listeners = eventListeners.get(eventName);
  323. if (listeners == null) {
  324. return false;
  325. }
  326. return listeners.size() > 0;
  327. }
  328. /**
  329. * Send retained arguments (if any) for this event. This
  330. * is called only when the first listener for an event is added
  331. * @param eventName
  332. */
  333. private void sendRetainedArgumentsForEvent(String eventName) {
  334. JSObject retained = retainedEventArguments.get(eventName);
  335. if (retained == null) {
  336. return;
  337. }
  338. notifyListeners(eventName, retained);
  339. retainedEventArguments.remove(eventName);
  340. }
  341. /**
  342. * Exported plugin call for adding a listener to this plugin
  343. * @param call
  344. */
  345. @SuppressWarnings("unused")
  346. @PluginMethod(returnType=PluginMethod.RETURN_NONE)
  347. public void addListener(PluginCall call) {
  348. String eventName = call.getString("eventName");
  349. call.save();
  350. addEventListener(eventName, call);
  351. }
  352. /**
  353. * Exported plugin call to remove a listener from this plugin
  354. * @param call
  355. */
  356. @SuppressWarnings("unused")
  357. @PluginMethod(returnType=PluginMethod.RETURN_NONE)
  358. public void removeListener(PluginCall call) {
  359. String eventName = call.getString("eventName");
  360. String callbackId = call.getString("callbackId");
  361. PluginCall savedCall = bridge.getSavedCall(callbackId);
  362. if (savedCall != null) {
  363. removeEventListener(eventName, savedCall);
  364. bridge.releaseCall(savedCall);
  365. }
  366. }
  367. /**
  368. * Exported plugin call to remove all listeners from this plugin
  369. * @param call
  370. */
  371. @SuppressWarnings("unused")
  372. @PluginMethod(returnType=PluginMethod.RETURN_NONE)
  373. public void removeAllListeners(PluginCall call) {
  374. eventListeners.clear();
  375. }
  376. /**
  377. * Exported plugin call to request all permissions for this plugin
  378. * @param call
  379. */
  380. @SuppressWarnings("unused")
  381. @PluginMethod()
  382. public void requestPermissions(PluginCall call) {
  383. // Should be overridden, does nothing by default
  384. NativePlugin annotation = this.handle.getPluginAnnotation();
  385. String[] perms = annotation.permissions();
  386. if (perms.length > 0) {
  387. // Save the call so we can return data back once the permission request has completed
  388. saveCall(call);
  389. pluginRequestPermissions(perms, annotation.permissionRequestCode());
  390. } else {
  391. call.success();
  392. }
  393. }
  394. /**
  395. * Handle request permissions result. A plugin can override this to handle the result
  396. * themselves, or this method will handle the result for our convenient requestPermissions
  397. * call.
  398. * @param requestCode
  399. * @param permissions
  400. * @param grantResults
  401. */
  402. protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
  403. if (!hasDefinedPermissions(permissions)) {
  404. StringBuilder builder = new StringBuilder();
  405. builder.append("Missing the following permissions in AndroidManifest.xml:\n");
  406. String[] missing = getUndefinedPermissions(permissions);
  407. for (String perm: missing) {
  408. builder.append(perm + "\n");
  409. }
  410. savedLastCall.error(builder.toString());
  411. savedLastCall = null;
  412. }
  413. }
  414. /**
  415. * Called before the app is destroyed to give a plugin the chance to
  416. * save the last call options for a saved plugin. By default, this
  417. * method saves the full JSON blob of the options call. Since Bundle sizes
  418. * may be limited, plugins that expect to be called with large data
  419. * objects (such as a file), should override this method and selectively
  420. * store option values in a {@link Bundle} to avoid exceeding limits.
  421. * @return a new {@link Bundle} with fields set from the options of the last saved {@link PluginCall}
  422. */
  423. protected Bundle saveInstanceState() {
  424. PluginCall savedCall = getSavedCall();
  425. if (savedCall == null) {
  426. return null;
  427. }
  428. Bundle ret = new Bundle();
  429. JSObject callData = savedCall.getData();
  430. if (callData != null) {
  431. ret.putString(BUNDLE_PERSISTED_OPTIONS_JSON_KEY, callData.toString());
  432. }
  433. return ret;
  434. }
  435. /**
  436. * Called when the app is opened with a previously un-handled
  437. * activity response. If the plugin that started the activity
  438. * stored data in {@link Plugin#saveInstanceState()} then this
  439. * method will be called to allow the plugin to restore from that.
  440. * @param state
  441. */
  442. protected void restoreState(Bundle state) {
  443. }
  444. /**
  445. * Handle activity result, should be overridden by each plugin
  446. * @param requestCode
  447. * @param resultCode
  448. * @param data
  449. */
  450. protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {}
  451. /**
  452. * Handle onNewIntent
  453. * @param intent
  454. */
  455. protected void handleOnNewIntent(Intent intent) {}
  456. /**
  457. * Handle onStart
  458. */
  459. protected void handleOnStart() {}
  460. /**
  461. * Handle onRestart
  462. */
  463. protected void handleOnRestart() {}
  464. /**
  465. * Handle onResume
  466. */
  467. protected void handleOnResume() {}
  468. /**
  469. * Handle onPause
  470. */
  471. protected void handleOnPause() {}
  472. /**
  473. * Handle onStop
  474. */
  475. protected void handleOnStop() {}
  476. /**
  477. * Handle onDestroy
  478. */
  479. protected void handleOnDestroy() {}
  480. /**
  481. * Give the plugins a chance to take control when a URL is about to be loaded in the WebView.
  482. * Returning true causes the WebView to abort loading the URL.
  483. * Returning false causes the WebView to continue loading the URL.
  484. * Returning null will defer to the default Capacitor policy
  485. */
  486. @SuppressWarnings("unused")
  487. public Boolean shouldOverrideLoad(Uri url) { return null; }
  488. /**
  489. * Start a new Activity.
  490. *
  491. * Note: This method must be used by all plugins instead of calling
  492. * {@link Activity#startActivityForResult} as it associates the plugin with
  493. * any resulting data from the new Activity even if this app
  494. * is destroyed by the OS (to free up memory, for example).
  495. * @param intent
  496. * @param resultCode
  497. */
  498. protected void startActivityForResult(PluginCall call, Intent intent, int resultCode) {
  499. bridge.startActivityForPluginWithResult(call, intent, resultCode);
  500. }
  501. /**
  502. * Execute the given runnable on the Bridge's task handler
  503. * @param runnable
  504. */
  505. public void execute(Runnable runnable) {
  506. bridge.execute(runnable);
  507. }
  508. /**
  509. * Shortcut for getting the plugin log tag
  510. * @param subTags
  511. */
  512. protected String getLogTag(String... subTags) {
  513. return Logger.tags(subTags);
  514. }
  515. /**
  516. * Gets a plugin log tag with the child's class name as subTag.
  517. */
  518. protected String getLogTag() {
  519. return Logger.tags(this.getClass().getSimpleName());
  520. }
  521. }