Recently I’ve had to write an Android Application that can send some of its data via email. It’s easy to send the data but I wanted to style the email using CSS. After trying every possible scenario here’s my results.
You can add < b >, < i > and other small styling tags but you can’t use things like < style > or < img >.
This list of tags is written up here and here:
https://commonsware.com/blog/Android/2010/05/26/html-tags-supported-by-textview.html
https://support.google.com/mail/bin/answer.py?hl=en&answer=8260
You can style a html file and use it as an attachment!
This is the work around I’m going to demonstrate to you now.
What we are going to do:
- Take some user input
- Create a HTML file
- Store the HTML file on the phone
- Start an Email Intent sending the HTML
As a heads up, we are going to avoid the use of the SD card. This keeps our life a little bit easier in that we don’t have to worry if the SD card is mounted or not. To do this we create a temporary file in our own local storage (this is the HTML file), we then create a content provider that will give the gmail application access to this file.
Note – The code is heavily commented rather than bore you with paragraphs and shock you with my illiteracy.
Ok so first we have the activity that takes some input from the user and they hit send. I’ve put some simple verification of the user input. The task to create the email is in an AsyncTask so we need a dialog to show up while this is loading.
MainActivity.class
package com.blundell.tutorial.ui.phone; import com.blundell.tutorial.R; import com.blundell.tutorial.domain.HtmlFile; import com.blundell.tutorial.service.task.CreateHtmlTask; import com.blundell.tutorial.service.task.CreateHtmlTask.OnTaskFinishedListener; import com.blundell.tutorial.ui.fragment.LoadingDialogFragment; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.text.Html; import android.view.View; import android.widget.EditText; import android.widget.Toast; public class MainActivity extends FragmentActivity implements OnTaskFinishedListener { private LoadingDialogFragment loadingDialog; private EditText nameEditText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); nameEditText = (EditText) findViewById(R.id.main_edit_text_enter_name); } // The XML onClick for out button public void onSendEmailClick(View button){ // Verify the input String input = nameEditText.getText().toString(); if(verifyNameInput(input)){ // We are about to do a task on another thread - so inform the user we are waiting showLoadingDialog(); // Start our ASync Task to do the long running process of creating the HTML // Passing in the internal storage folder for our app - this is where the file will be created new CreateHtmlTask(getCacheDir(), this).execute(input); } else { // If the verification failed - simple feedback Toast.makeText(this, "Please enter your name", Toast.LENGTH_SHORT).show(); } } /** * Do some simple verification on the input * @param input * @return */ private static boolean verifyNameInput(String input) { // Check we havent been passed null // Remove the whitespace from the beginning and end // Check it's not empty if(input != null && !"".equals(input = input.trim())){ // Check for smart people if(input.equalsIgnoreCase("no")){ return false; } // **Assumption** know ones name is less than 3 chars (Li?) if(input.length() < 3){ return false; } // If we've passed all verification return true return true; } return false; } @Override public void onHtmlCreated(HtmlFile htmlFile) { // Get rid of the dialog - we have finished dismissLoadingDialog(); // Check we were sent a valid file if(htmlFile.isValid()){ // Deal with the file we were sent startSendEmailIntent(htmlFile.getFilePath()); } else { // Error checking - in a real app you might want to be more informative Toast.makeText(this, "Something went wrong!", Toast.LENGTH_SHORT).show(); } } private void startSendEmailIntent(Uri attachmentUri) { // Create a new intent - we are 'sending' data Intent intent = new Intent(Intent.ACTION_SEND); // Mime type of html - so we can add some funky html tags in the email <b> etc </b> intent.setType("text/html"); // The subject of your email intent.putExtra(Intent.EXTRA_SUBJECT, "Subject"); // The uri to the attachment that is the real guts of our email intent.putExtra(Intent.EXTRA_STREAM, attachmentUri); // The email message intent.putExtra(Intent.EXTRA_TEXT, Html.fromHtml("Message body. <b>Funky!</b> <i>not</i>")); // Let the user select which application to send the email with, we have added a title // to give a hint that they should pick an email client Intent chooser = Intent.createChooser(intent, "Send Email"); startActivity(chooser); } private void showLoadingDialog() { // Use our loading fragment to show progress this.loadingDialog = LoadingDialogFragment.newInstance().show(getSupportFragmentManager()); } private void dismissLoadingDialog() { // Safety check if(this.loadingDialog != null){ this.loadingDialog.dismiss(); } } }
Our loading dialog is a fragment that is shown on the screen whilst we do some long running task.
LoadingDialogFragment.java
package com.blundell.tutorial.ui.fragment; import com.blundell.tutorial.R; import com.blundell.tutorial.util.Log; import android.app.Dialog; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentManager; /** * This isn't the subject of this Tutorial, if you want it explaining please ask * * @author paul.blundell */ public class LoadingDialogFragment extends DialogFragment { private static final String ID = "loadingDialog"; public static LoadingDialogFragment newInstance() { LoadingDialogFragment f = new LoadingDialogFragment(); return f; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog d = new Dialog(getActivity()); d.setTitle("Loading"); d.setContentView(R.layout.fragment_loading); return d; } public LoadingDialogFragment show(FragmentManager manager){ show(manager, ID); return this; } @Override public void dismiss() { try{ super.dismiss(); } catch (Exception e) { // Null because it's not attached or some bs, why can't it just die quietly Log.w("Dialog tried to dismiss and failed. Are you bothered?"); } } }
The task to create the HTML is an ASyncTask meaning it runs in it’s own thread. In this example is it pretty simple we just add some bold text and an image. You could create whatever complex layout you want here. It also has an interface to listen for when the task finished. That way your activity will know when we have finished.
CreateHtmlTask.java
package com.blundell.tutorial.service.task; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import com.blundell.tutorial.domain.HtmlFile; import com.blundell.tutorial.util.Log; import android.os.AsyncTask; /** * This class creates your HTML its input is an array of Strings that in this scenario are used to show the persons name * Once the HTML is created it is saved to the file system * We then wrap this file in our own domain object type and send it back to the calling class * * @author paul.blundell */ public class CreateHtmlTask extends AsyncTask<String, Integer, HtmlFile>{ // This is an interface so whoever started this task can be informed when it is finished public interface OnTaskFinishedListener { void onHtmlCreated(HtmlFile html); } // The finished listener private final OnTaskFinishedListener taskFinishedListener; private final File folder; // Let the listener be set in the constructor (making it obvious to anyone using this class they can be informed when it is finished) // Note they can still pass null to not listen public CreateHtmlTask(File storageFolder, OnTaskFinishedListener taskFinishedListener) { this.folder = storageFolder; this.taskFinishedListener = taskFinishedListener; } @Override protected HtmlFile doInBackground(String... params) { // We are wrapping the File in our own domain object so we can add some convenience methods to it HtmlFile htmlFile; try { // Create whatever HTML you want here - don't forget to escape strings String name = params[0]; StringBuilder builder = new StringBuilder(); builder.append("<html>"); builder.append("<head>"); builder.append("</head>"); builder.append("<body>"); builder.append("<p>"); builder.append("Hello "); builder.append(name); builder.append("<img src=\"https://developer.android.com/images/jb-android-4.1.png\"/>"); builder.append("</body>"); builder.append("</html>"); String content = builder.toString(); // Store the file File file = createTempFile(folder, "temp_file.html", content); // Create our domain object wrapping the file htmlFile = new HtmlFile(file); } catch (IOException e) { Log.e("IOException - creating safe HtmlFile", e); // Create a 'NullSafe' HtmlFile object if an error occurs htmlFile = new HtmlFile(null); } return htmlFile; } /** * Creates a file - doesn't do any clean up * @param folder - the folder to save the file in * @param filename - the file name * @param fileContent - the content to put in the file * @return the created File * @throws IOException - if anything goes wrong :-( */ private static File createTempFile(File folder, String filename, String fileContent) throws IOException { File f = new File(folder, filename); f.createNewFile(); BufferedWriter buf = new BufferedWriter(new FileWriter(f)); buf.append(fileContent); buf.close(); return f; } @Override protected void onPostExecute(HtmlFile result) { super.onPostExecute(result); // Inform the class listening we have finished - sending back the completed Html File if(this.taskFinishedListener != null) this.taskFinishedListener.onHtmlCreated(result); } }
The CreateHtmlTask creates us our file that is full of HTML, to allow the activity to check if this file has been created we wrap the java.io.File in our own HtmlFile object this gives us a convenient place to write helper methods on the File. For instance to check if it is null but wrap that in a doWeHaveTheFile() method. This is more human readable.
HtmlFile.java
package com.blundell.tutorial.domain; import java.io.File; import com.blundell.tutorial.service.provider.CacheFileProvider; import android.net.Uri; /** * This is a wrapper class for our 'File' * It lets us add methods and test the file without having to be locked to using java.io.File * @author paul.blundell */ public class HtmlFile { private final File file; public HtmlFile(File file) { this.file = file; } /** * A convenience method to check that we are in a happy state * @return */ public boolean isValid(){ return this.file != null; } private String getFileName(){ return file.getName(); } /** * @return a uri that is a pointer to the html file we have created - this can be used by content providers */ public Uri getFilePath(){ return Uri.parse("content://"+ CacheFileProvider.AUTHORITY +"/"+ getFileName()); } }
Finally because we store our file on internal storage we need to have a content provider that will allow other applications to view the file. This is done using a Provider and a declaration in the AndroidManifest.
CacheFileProvider.java:
package com.blundell.tutorial.service.provider; import java.io.File; import java.io.FileNotFoundException; import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; /** * Thanks to Stephen Nicholas for the information to allow Gmail to view internal app data * https://stephendnicholas.com/archives/974 * * @author paul.blundell */ public class CacheFileProvider extends ContentProvider { private static final int A_MATCH = 1; public static final String AUTHORITY = "com.blundell.tutorial.cacheFileProvider"; private UriMatcher uriMatcher; @Override public boolean onCreate() { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(AUTHORITY, "*", A_MATCH); return true; } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { if(uriMatcher.match(uri) == A_MATCH){ String fileLocation = getContext().getCacheDir() + File.separator + uri.getLastPathSegment(); File externallyVisibleFile = new File(fileLocation); ParcelFileDescriptor pfd = ParcelFileDescriptor.open(externallyVisibleFile, ParcelFileDescriptor.MODE_READ_ONLY); return pfd; } return super.openFile(uri, mode); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public String getType(Uri uri) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } }
AndroidManifest.xml
<manifest xmlns:android="https://schemas.android.com/apk/res/android" package="com.blundell.tutorial" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".ui.phone.MainActivity" android:label="@string/title_activity_main" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- This is declaring we provide some data to other apps (i.e. our html email attachment) --> <provider android:name=".service.provider.CacheFileProvider" android:authorities="com.blundell.tutorial.cacheFileProvider" android:exported="true" /> </application> </manifest>
Thats it, just click the button and the Intent does the rest! Email with attached Html File.
Here is the eclipse source:
Send Html Attachment Eclipse Source
Here is a GitHub mirror:
Coming soon
Some of the references I used in this research:
https://developer.android.com/reference/android/text/Html.html#toHtml(android.text.Spanned)
https://code.google.com/p/android/issues/detail?id=8640
https://stackoverflow.com/questions/1555171/why-gmail-blocked-the-css
hii.. i am designing an app in which i have to send email with css and i found your blog post helpful. Thanks for your efforts. I have one query though… in MainActivity.class i have used button click in given fashion:
public class Discount extends FragmentActivity implements CreateHtmlTask.OnTaskFinishedListener {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_discount);
public void onClick(View v) {
showLoadingDialog();
// no authentication needed
// Start our ASync Task to do the long running process of creating the HTML
// Passing in the internal storage folder for our app – this is where the file will be created
new CreateHtmlTask(getCacheDir(), v);
}
I am getting following error:
CreateHtmlTask (File, package.CreateHtmlTask.onTaskFinishedListener) in CreateHtmlTask cannot be applied to (File, android.view.View).
Can you explain this?
You need to pass a listener as the second parameter, not a raw view.
new CreateHtmlTask(getCacheDir(), this);
or newCreateHtmlTask(getCacheDir(), new CreateHtmlTask.onTaskFinishedListener() { });