Keine Beschreibung

EmailComposerImpl.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. /*
  2. * Copyright (c) 2014-2015 by appPlant UG. All rights reserved.
  3. *
  4. * @APPPLANT_LICENSE_HEADER_START@
  5. *
  6. * This file contains Original Code and/or Modifications of Original Code
  7. * as defined in and that are subject to the Apache License
  8. * Version 2.0 (the 'License'). You may not use this file except in
  9. * compliance with the License. Please obtain a copy of the License at
  10. * http://opensource.org/licenses/Apache-2.0/ and read it before using this
  11. * file.
  12. *
  13. * The Original Code and all software distributed under the License are
  14. * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
  15. * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
  16. * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
  17. * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  18. * Please see the License for the specific language governing rights and
  19. * limitations under the License.
  20. *
  21. * @APPPLANT_LICENSE_HEADER_END@
  22. */
  23. package de.martinreinhardt.cordova.plugins.email;
  24. import android.content.Context;
  25. import android.content.Intent;
  26. import android.content.pm.PackageManager;
  27. import android.content.pm.ResolveInfo;
  28. import android.content.res.AssetManager;
  29. import android.content.res.Resources;
  30. import android.media.MediaScannerConnection;
  31. import android.net.Uri;
  32. import android.text.Html;
  33. import android.util.Base64;
  34. import android.util.Log;
  35. import org.json.JSONArray;
  36. import org.json.JSONException;
  37. import org.json.JSONObject;
  38. import java.io.File;
  39. import java.io.FileOutputStream;
  40. import java.io.IOException;
  41. import java.io.InputStream;
  42. import java.io.OutputStream;
  43. import java.util.ArrayList;
  44. import java.util.List;
  45. import java.util.concurrent.atomic.AtomicReference;
  46. import static de.martinreinhardt.cordova.plugins.email.EmailComposer.LOG_TAG;
  47. /**
  48. * Implements the interface methods of the plugin.
  49. */
  50. public class EmailComposerImpl {
  51. /**
  52. * The default mailto: scheme.
  53. */
  54. static private final String MAILTO_SCHEME = "mailto:";
  55. /**
  56. * Path where to put tmp the attachments.
  57. */
  58. static private final String ATTACHMENT_FOLDER = "/email_composer";
  59. /**
  60. * Cleans the attachment folder.
  61. *
  62. * @param ctx
  63. * The application context.
  64. */
  65. @SuppressWarnings("ResultOfMethodCallIgnored")
  66. public void cleanupAttachmentFolder (Context ctx) {
  67. try {
  68. File dir = new File(ctx.getExternalCacheDir() + ATTACHMENT_FOLDER);
  69. if (!dir.isDirectory())
  70. return;
  71. File[] files = dir.listFiles();
  72. for (File file : files) { file.delete(); }
  73. } catch (Exception npe){
  74. Log.w(LOG_TAG, "Missing external cache dir");
  75. }
  76. }
  77. /**
  78. * Tells if the device has the capability to send emails.
  79. *
  80. * @param id
  81. * The app id.
  82. * @param ctx
  83. * The application context.
  84. */
  85. public boolean[] canSendMail (String id, Context ctx) {
  86. // is possible with specified app
  87. boolean withScheme = isAppInstalled(id, ctx);
  88. // is possible in general
  89. boolean isPossible = isEmailClientExist(ctx);
  90. return new boolean[] { isPossible, withScheme };
  91. }
  92. /**
  93. * The intent with the containing email properties.
  94. *
  95. * @param params
  96. * The email properties like subject or body
  97. * @param ctx
  98. * The context of the application.
  99. * @return
  100. * The resulting intent.
  101. * @throws JSONException
  102. */
  103. public Intent getDraftWithProperties (JSONObject params, Context ctx)
  104. throws JSONException {
  105. Intent mail = getEmailIntent();
  106. String app = params.optString("app", MAILTO_SCHEME);
  107. if (params.has("subject"))
  108. setSubject(params.getString("subject"), mail);
  109. if (params.has("body"))
  110. setBody(params.getString("body"), params.optBoolean("isHtml"), mail);
  111. if (params.has("to"))
  112. setRecipients(params.getJSONArray("to"), mail);
  113. if (params.has("cc"))
  114. setCcRecipients(params.getJSONArray("cc"), mail);
  115. if (params.has("bcc"))
  116. setBccRecipients(params.getJSONArray("bcc"), mail);
  117. if (params.has("attachments"))
  118. setAttachments(params.getJSONArray("attachments"), mail, ctx);
  119. if (!app.equals(MAILTO_SCHEME) && isAppInstalled(app, ctx)) {
  120. mail.setPackage(app);
  121. }
  122. return mail;
  123. }
  124. /**
  125. * Setter for the subject.
  126. *
  127. * @param subject
  128. * The subject of the email.
  129. * @param draft
  130. * The intent to send.
  131. */
  132. private void setSubject (String subject, Intent draft) {
  133. draft.putExtra(Intent.EXTRA_SUBJECT, subject);
  134. }
  135. /**
  136. * Setter for the body.
  137. *
  138. * @param body
  139. * The body of the email.
  140. * @param isHTML
  141. * Indicates the encoding (HTML or plain text).
  142. * @param draft
  143. * The intent to send.
  144. */
  145. private void setBody (String body, Boolean isHTML, Intent draft) {
  146. CharSequence text = isHTML ? Html.fromHtml(body) : body;
  147. draft.putExtra(Intent.EXTRA_TEXT, text);
  148. }
  149. /**
  150. * Setter for the recipients.
  151. *
  152. * @param recipients
  153. * List of email addresses.
  154. * @param draft
  155. * The intent to send.
  156. * @throws JSONException
  157. */
  158. private void setRecipients (JSONArray recipients, Intent draft) throws JSONException {
  159. String[] receivers = new String[recipients.length()];
  160. for (int i = 0; i < recipients.length(); i++) {
  161. receivers[i] = recipients.getString(i);
  162. }
  163. draft.putExtra(Intent.EXTRA_EMAIL, receivers);
  164. }
  165. /**
  166. * Setter for the cc recipients.
  167. *
  168. * @param recipients
  169. * List of email addresses.
  170. * @param draft
  171. * The intent to send.
  172. * @throws JSONException
  173. */
  174. private void setCcRecipients (JSONArray recipients, Intent draft) throws JSONException {
  175. String[] receivers = new String[recipients.length()];
  176. for (int i = 0; i < recipients.length(); i++) {
  177. receivers[i] = recipients.getString(i);
  178. }
  179. draft.putExtra(Intent.EXTRA_CC, receivers);
  180. }
  181. /**
  182. * Setter for the bcc recipients.
  183. *
  184. * @param recipients
  185. * List of email addresses.
  186. * @param draft
  187. * The intent to send.
  188. * @throws JSONException
  189. */
  190. private void setBccRecipients (JSONArray recipients, Intent draft) throws JSONException {
  191. String[] receivers = new String[recipients.length()];
  192. for (int i = 0; i < recipients.length(); i++) {
  193. receivers[i] = recipients.getString(i);
  194. }
  195. draft.putExtra(Intent.EXTRA_BCC, receivers);
  196. }
  197. /**
  198. * Setter for the attachments.
  199. *
  200. * @param attachments
  201. * List of URIs to attach.
  202. * @param draft
  203. * The intent to send.
  204. * @param ctx
  205. * The application context.
  206. * @throws JSONException
  207. */
  208. private void setAttachments (JSONArray attachments, Intent draft,
  209. Context ctx) throws JSONException {
  210. ArrayList<Uri> uris = new ArrayList<Uri>();
  211. for (int i = 0; i < attachments.length(); i++) {
  212. Uri uri = getUriForPath(attachments.getString(i), ctx);
  213. uris.add(uri);
  214. }
  215. if (uris.isEmpty())
  216. return;
  217. if (uris.size() == 1) {
  218. draft.setAction(Intent.ACTION_SEND)
  219. .setType("message/rfc822")
  220. .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
  221. .putExtra(Intent.EXTRA_STREAM, uris.get(0));
  222. } else {
  223. draft.setAction(Intent.ACTION_SEND_MULTIPLE)
  224. .setType("message/rfc822")
  225. .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
  226. .putExtra(Intent.EXTRA_STREAM, uris);
  227. }
  228. }
  229. /**
  230. * The URI for an attachment path.
  231. *
  232. * @param path
  233. * The given path to the attachment.
  234. * @param ctx
  235. * The application context.
  236. * @return
  237. * The URI pointing to the given path.
  238. */
  239. private Uri getUriForPath (String path, Context ctx) {
  240. Uri result = null;
  241. if (path.startsWith("res:")) {
  242. result = getUriForResourcePath(path, ctx);
  243. } else if (path.startsWith("file:///")) {
  244. result = getUriForAbsolutePath(path);
  245. } else if (path.startsWith("file://")) {
  246. result = getUriForAssetPath(path, ctx);
  247. } else if (path.startsWith("base64:")) {
  248. result = getUriForBase64Content(path, ctx);
  249. }
  250. if (result == null) {
  251. result = Uri.parse(path);
  252. }
  253. return getCorrespondingMediaFileUriIfPossible(result, ctx);
  254. }
  255. /**
  256. * The URI for a file.
  257. *
  258. * @param path
  259. * The given absolute path.
  260. * @return
  261. * The URI pointing to the given path.
  262. */
  263. private Uri getUriForAbsolutePath (String path) {
  264. String absPath = path.replaceFirst("file://", "");
  265. File file = new File(absPath);
  266. if (!file.exists()) {
  267. Log.e(LOG_TAG, "File not found: " + file.getAbsolutePath());
  268. }
  269. return Uri.fromFile(file);
  270. }
  271. /**
  272. * The URI for an asset.
  273. *
  274. * @param path
  275. * The given asset path.
  276. * @param ctx
  277. * The application context.
  278. * @return
  279. * The URI pointing to the given path.
  280. */
  281. @SuppressWarnings("ResultOfMethodCallIgnored")
  282. private Uri getUriForAssetPath (String path, Context ctx) {
  283. String resPath = path.replaceFirst("file:/", "www");
  284. String fileName = resPath.substring(resPath.lastIndexOf('/') + 1);
  285. File dir = ctx.getExternalCacheDir();
  286. if (dir == null) {
  287. Log.e(LOG_TAG, "Missing external cache dir");
  288. return Uri.EMPTY;
  289. }
  290. String storage = dir.toString() + ATTACHMENT_FOLDER;
  291. File file = new File(storage, fileName);
  292. new File(storage).mkdir();
  293. FileOutputStream outStream = null;
  294. try {
  295. AssetManager assets = ctx.getAssets();
  296. outStream = new FileOutputStream(file);
  297. InputStream inputStream = assets.open(resPath);
  298. copyFile(inputStream, outStream);
  299. outStream.flush();
  300. outStream.close();
  301. } catch (Exception e) {
  302. Log.e(LOG_TAG, "File not found: assets/" + resPath);
  303. e.printStackTrace();
  304. } finally {
  305. if (outStream != null) {
  306. safeClose(outStream);
  307. }
  308. }
  309. return Uri.fromFile(file);
  310. }
  311. /**
  312. * The URI for a resource.
  313. *
  314. * @param path
  315. * The given relative path.
  316. * @param ctx
  317. * The application context.
  318. * @return
  319. * The URI pointing to the given path
  320. */
  321. @SuppressWarnings("ResultOfMethodCallIgnored")
  322. private Uri getUriForResourcePath (String path, Context ctx) {
  323. String resPath = path.replaceFirst("res://", "");
  324. String fileName = resPath.substring(resPath.lastIndexOf('/') + 1);
  325. String resName = fileName.substring(0, fileName.lastIndexOf('.'));
  326. String extension = resPath.substring(resPath.lastIndexOf('.'));
  327. File dir = ctx.getExternalCacheDir();
  328. if (dir == null) {
  329. Log.e(LOG_TAG, "Missing external cache dir");
  330. return Uri.EMPTY;
  331. }
  332. String storage = dir.toString() + ATTACHMENT_FOLDER;
  333. int resId = getResId(resPath, ctx);
  334. File file = new File(storage, resName + extension);
  335. if (resId == 0) {
  336. Log.e(LOG_TAG, "File not found: " + resPath);
  337. }
  338. new File(storage).mkdir();
  339. FileOutputStream outStream = null;
  340. try {
  341. Resources res = ctx.getResources();
  342. outStream = new FileOutputStream(file);
  343. InputStream inputStream = res.openRawResource(resId);
  344. copyFile(inputStream, outStream);
  345. outStream.flush();
  346. outStream.close();
  347. } catch (Exception e) {
  348. e.printStackTrace();
  349. } finally {
  350. if (outStream != null) {
  351. safeClose(outStream);
  352. }
  353. }
  354. return Uri.fromFile(file);
  355. }
  356. /**
  357. * The URI for a base64 encoded content.
  358. *
  359. * @param content
  360. * The given base64 encoded content.
  361. * @param ctx
  362. * The application context.
  363. * @return
  364. * The URI including the given content.
  365. */
  366. @SuppressWarnings("ResultOfMethodCallIgnored")
  367. private Uri getUriForBase64Content (String content, Context ctx) {
  368. String resName = content.substring(content.indexOf(":") + 1, content.indexOf("//"));
  369. String resData = content.substring(content.indexOf("//") + 2);
  370. File dir = ctx.getExternalCacheDir();
  371. byte[] bytes;
  372. try {
  373. bytes = Base64.decode(resData, 0);
  374. } catch (Exception ignored) {
  375. Log.e(LOG_TAG, "Invalid Base64 string");
  376. return Uri.EMPTY;
  377. }
  378. if (dir == null) {
  379. Log.e(LOG_TAG, "Missing external cache dir");
  380. return Uri.EMPTY;
  381. }
  382. String storage = dir.toString() + ATTACHMENT_FOLDER;
  383. File file = new File(storage, resName);
  384. new File(storage).mkdir();
  385. FileOutputStream outStream = null;
  386. try {
  387. outStream = new FileOutputStream(file);
  388. outStream.write(bytes);
  389. outStream.flush();
  390. outStream.close();
  391. } catch (Exception e) {
  392. e.printStackTrace();
  393. } finally {
  394. if (outStream != null) {
  395. safeClose(outStream);
  396. }
  397. }
  398. return Uri.fromFile(file);
  399. }
  400. /**
  401. * Get corresponding Media File URI for a givin URI
  402. * if available, otherwise it returns the same input URI.
  403. *
  404. * NOTE: Becuase MediaScannerConnection works in callback
  405. * fashion, we instead wait for its result using a timing-out
  406. * while loop, and we might further think for a better solution.
  407. *
  408. * @param uri
  409. * The given uri.
  410. * @param ctx
  411. * The application context.
  412. * @return
  413. * The URI pointing to the corresponding Media File if available,
  414. * otherwise it returns the same given uri.
  415. */
  416. private Uri getCorrespondingMediaFileUriIfPossible(Uri uri, Context ctx) {
  417. return getCorrespondingMediaFileUriIfPossible(uri.toString(), ctx);
  418. }
  419. /**
  420. * Get corresponding Media File URI for a givin path String
  421. * if available, otherwise it returns the same input URI.
  422. *
  423. * NOTE: Becuase MediaScannerConnection works in callback
  424. * fashion, we instead wait for its result using a timing-out
  425. * while loop, and we might further think for a better solution.
  426. *
  427. * @param path
  428. * The given path.
  429. * @param ctx
  430. * The application context.
  431. * @return
  432. * The URI pointing to the corresponding Media File if available,
  433. * otherwise it returns the same given path.
  434. */
  435. private Uri getCorrespondingMediaFileUriIfPossible(String path, Context ctx) {
  436. final AtomicReference<Uri> result = new AtomicReference<Uri>();
  437. MediaScannerConnection.scanFile(ctx, new String[]{path}, null, new MediaScannerConnection.OnScanCompletedListener() {
  438. @Override
  439. public void onScanCompleted(String path, Uri uri) {
  440. if (uri != null) {
  441. result.set(uri);
  442. } else {
  443. result.set(Uri.parse(path));
  444. }
  445. }
  446. });
  447. // Wait until media scanner scans path and gets
  448. // its corresponding content:// uri
  449. long startTime = System.currentTimeMillis();
  450. long maxWait = 5 * 1000; // 5 secs
  451. while (result.get() == null && System.currentTimeMillis() - startTime < maxWait) {
  452. try {
  453. Thread.sleep(100);
  454. } catch (Exception e) {
  455. // ignore
  456. }
  457. }
  458. return result.get() != null? result.get() : Uri.parse(path);
  459. }
  460. /**
  461. * Writes an InputStream to an OutputStream
  462. *
  463. * @param in
  464. * The input stream.
  465. * @param out
  466. * The output stream.
  467. */
  468. private void copyFile (InputStream in, OutputStream out) throws IOException {
  469. byte[] buffer = new byte[1024];
  470. int read;
  471. while ((read = in.read(buffer)) != -1) {
  472. out.write(buffer, 0, read);
  473. }
  474. }
  475. /**
  476. * Returns the resource ID for the given resource path.
  477. *
  478. * @param ctx
  479. * The application context.
  480. * @return
  481. * The resource ID for the given resource.
  482. */
  483. private int getResId (String resPath, Context ctx) {
  484. Resources res = ctx.getResources();
  485. int resId;
  486. String pkgName = ctx.getPackageName();
  487. String dirName = "drawable";
  488. String fileName = resPath;
  489. if (resPath.contains("/")) {
  490. dirName = resPath.substring(0, resPath.lastIndexOf('/'));
  491. fileName = resPath.substring(resPath.lastIndexOf('/') + 1);
  492. }
  493. String resName = fileName.substring(0, fileName.lastIndexOf('.'));
  494. resId = res.getIdentifier(resName, dirName, pkgName);
  495. if (resId == 0) {
  496. resId = res.getIdentifier(resName, "drawable", pkgName);
  497. }
  498. return resId;
  499. }
  500. /**
  501. * If email apps are available.
  502. *
  503. * @param ctx
  504. * The application context.
  505. * @return
  506. * true if available, otherwise false
  507. */
  508. private boolean isEmailClientExist (Context ctx) {
  509. Log.i(LOG_TAG, "isEmailClientExist()");
  510. return getAppsCountHandlesIntent(ctx, getEmailIntent()) > 0;
  511. }
  512. /**
  513. * Get apps count which are able to handle specific Intent.
  514. *
  515. * @param ctx
  516. * The application context.
  517. * @param intent
  518. * The intent to test against.
  519. * @return
  520. * The apps count
  521. */
  522. private int getAppsCountHandlesIntent(Context ctx, Intent intent) {
  523. if (ctx == null || intent == null) return 0;
  524. PackageManager manager = ctx.getPackageManager();
  525. List<ResolveInfo> infos = manager.queryIntentActivities(intent, 0);
  526. return infos.size();
  527. }
  528. /**
  529. * Ask the package manager if the app is installed on the device.
  530. *
  531. * @param id
  532. * The app id.
  533. * @param ctx
  534. * The application context.
  535. * @return
  536. * true if yes otherwise false.
  537. */
  538. private boolean isAppInstalled (String id, Context ctx) {
  539. if (id == null || id.equalsIgnoreCase(MAILTO_SCHEME)) {
  540. Intent intent = getEmailIntent();
  541. PackageManager pm = ctx.getPackageManager();
  542. int apps = pm.queryIntentActivities(intent, 0).size();
  543. return (apps > 0);
  544. }
  545. try {
  546. ctx.getPackageManager().getPackageInfo(id, 0);
  547. return true;
  548. } catch (PackageManager.NameNotFoundException e) {
  549. return false;
  550. }
  551. }
  552. /**
  553. * Setup an intent to send to email apps only.
  554. *
  555. * @return intent
  556. */
  557. private static Intent getEmailIntent() {
  558. Intent intent = new Intent(Intent.ACTION_SENDTO,
  559. Uri.parse(MAILTO_SCHEME));
  560. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  561. return intent;
  562. }
  563. /**
  564. * Attempt to safely close the given stream.
  565. *
  566. * @param outStream
  567. * The stream to close.
  568. * @return
  569. * true if successful, false otherwise
  570. */
  571. private static boolean safeClose (final FileOutputStream outStream) {
  572. if (outStream != null) {
  573. try {
  574. outStream.close();
  575. return true;
  576. } catch (IOException e) {
  577. Log.e(LOG_TAG, "Error attempting to safely close resource: " + e.getMessage());
  578. }
  579. }
  580. return false;
  581. }
  582. }