I am trying to send plain text via Bluetooth, but it is converted to HTML somewhere.
The code I'm using is basically this:
String content = "This is just a test";
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, content);
sendIntent.setType("text/plain");
String title = "Share with…";
startActivity(Intent.createChooser(sendIntent, title));
When I run this code and choose the Bluetooth option, the file is pushed to the remote system with the name "bluetooth_content_share.html" and with these contents:
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/></head><body>This is just a test</body></html>
I've tried calling setType
before adding the EXTRA_TEXT
with no change in symptoms. Other share actions ("Add to Dropbox", for example) get the data as plain text. And I have been able to use other applications ("ES File Explorer", for example) to successfully send plain text files via Bluetooth.
How can I get the data to be sent as just plain text like I asked?
I couldn't sleep so I decided to take a look at stackoverflow to see if there was anything interesting in the android tag. This question seemed simple enough, yet it proved very interesting since as you noted in the question it just creates a damn html file with the string as the content.
I assumed that bluetooth communication wanted to work with files and that Android was inferring that our text was html even though you clearly stated plain text.
The solution I came up with is basically forcing the app to share a text file as opossed to sharing the test String
. I've been able to test this and your code too, and I was able to replicate the magical creation of the html file. This should help you.
Update
Due to the op concerns about leaving a file in the storage, and not being able to use a temporary file, I've updated the code to add a FileObserver
to the file which allows us to monitor when a file is being modified and what type of action it's experiencing. In this case, all we need to monitor is the FileObserver.CLOSE_NOWRITE
action which will be triggered only when the file is being accessed to send it, and after it has finished working on it. Eliminating the file after it.
try {
//Create a file and write the String to it
BufferedWriter out;
final String filePath = Environment.getExternalStorageDirectory().getPath() + "/wadus.txt";
FileWriter fileWriter = new FileWriter(filePath);
out = new BufferedWriter(fileWriter);
out.write("I know you'll love me for finding the solution");
out.close();
//Access the file and share it through the original intent
File file = new File(filePath);
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
sendIntent.setType("text/plain");
String title = "Share with…";
//Create a file observer to monitor the access to the file
FileObserver fobsv = new FileObserver(filePath) {
@Override
public void onEvent(int event, String path) {
if (event == FileObserver.CLOSE_NOWRITE) {
//The file was previously written to, now it's been sent and closed
//we can safely delete it.
File file = new File(filePath);
file.delete();
}
}
};
fobsv.startWatching();
//Launch sharing intent
startActivity(Intent.createChooser(sendIntent, title));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
If anybody is wondering why we are setting the FileObserver
that far down the code, is to avoid it being triggered upon the creation and edition of the file itself. Since we added after the file has been written to, we will only be triggering the events that are required for sending it by bluetooth (in this case).
TL;DR: You either have to create a file or implement a ContentProvider and send it as EXTRA_STREAM
, but this breaks other apps that want to receive the data as text via EXTRA_TEXT
. It's possible to implement an exception for the "Bluetooth Share" app by using EXTRA_REPLACEMENT_EXTRAS
.
I was able to find this code in the source for the Android Bluetooth app (com/android/bluetooth/opp/BluetoothOppLauncherActivity.java):
if (action.equals(Intent.ACTION_SEND)) {
final String type = intent.getType();
final Uri stream = (Uri)intent.getParcelableExtra(Intent.EXTRA_STREAM);
CharSequence extra_text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
if (stream != null && type != null) {
// clipped
} else if (extra_text != null && type != null) {
if (V) Log.v(TAG, "Get ACTION_SEND intent with Extra_text = "
+ extra_text.toString() + "; mimetype = " + type);
final Uri fileUri = creatFileForSharedContent(this, extra_text);
// clipped
} else {
Log.e(TAG, "type is null; or sending file URI is null");
finish();
return;
}
So if there's EXTRA_TEXT
and no EXTRA_STREAM
, then send it to creatFileForSharedContent()
, which, in turn contains this:
String fileName = getString(R.string.bluetooth_share_file_name) + ".html";
context.deleteFile(fileName);
/*
* Convert the plain text to HTML
*/
StringBuffer sb = new StringBuffer("<html><head><meta http-equiv=\"Content-Type\""
+ " content=\"text/html; charset=UTF-8\"/></head><body>");
// Escape any inadvertent HTML in the text message
String text = escapeCharacterToDisplay(shareContent.toString());
// Regex that matches Web URL protocol part as case insensitive.
Pattern webUrlProtocol = Pattern.compile("(?i)(http|https)://");
Pattern pattern = Pattern.compile("("
+ Patterns.WEB_URL.pattern() + ")|("
+ Patterns.EMAIL_ADDRESS.pattern() + ")|("
+ Patterns.PHONE.pattern() + ")");
// Find any embedded URL's and linkify
Matcher m = pattern.matcher(text);
while (m.find()) {
//clipped
}
m.appendTail(sb);
sb.append("</body></html>");
In other words, the Bluetooth app explicitly converts anything sent as text to HTML. Thanks, Android!
The only two things that the Bluetooth app will accept as an EXTRA_STREAM
are content: and file: URIs (com/android/bluetooth/opp/BluetoothOppSendFileInfo.java):
if ("content".equals(scheme)) {
//clipped
} else if ("file".equals(scheme)) {
//clipped
} else {
// currently don't accept other scheme
return SEND_FILE_INFO_ERROR;
}
So trying to send a data: URI doesn't work.
This means you either have to create a file or implement a ContentProvider. Just to send some damned plain text!
But this has the potential to break sharing with other apps that want to receive the data via the EXTRA_TEXT
method. Fortunately, it's possible to create an EXTRA_STREAM
that is only provided to the Bluetooth Share app by using EXTRA_REPLACEMENT_EXTRAS
:
String content = "This is just a test";
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, content);
sendIntent.setType("text/plain");
String title = "Share with…";
Intent shareChooser = Intent.createChooser(sendIntent, title);
// Add Bluetooth Share-specific data
try {
// Create file with text to share
final File contentFile = new File(getActivity().getExternalCacheDir(), "plain.txt");
FileWriter contentFileWriter = new FileWriter(contentFile, false);
BufferedWriter contentWriter = new BufferedWriter(contentFileWriter);
contentWriter.write(content);
contentWriter.close();
Uri contentUri = Uri.fromFile(contentFile);
Bundle replacements = new Bundle();
shareChooser.putExtra(Intent.EXTRA_REPLACEMENT_EXTRAS, replacements);
// Create Extras Bundle just for Bluetooth Share
Bundle bluetoothExtra = new Bundle(sendIntent.getExtras());
replacements.putBundle("com.android.bluetooth", bluetoothExtra);
// Add file to Bluetooth Share's Extras
bluetoothExtra.putParcelable(Intent.EXTRA_STREAM, contentUri);
} catch (IOException e) {
// Handle file creation error
}
startActivity(shareChooser);
You still have to handle deleting the file, though, which becomes more complicated when the user chooses something other than Bluetooth to share with, since the file will never be opened, making the FileObserver
solution in Juan's answer become incomplete.