1 /* 2 * Copyright (c) 2008-2009, Motorola, Inc. 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * - Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * - Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * - Neither the name of the Motorola, Inc. nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 * POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.android.bluetooth.opp; 34 35 import com.android.bluetooth.R; 36 37 import java.io.File; 38 import java.io.FileNotFoundException; 39 import java.io.FileOutputStream; 40 import java.io.IOException; 41 import java.util.ArrayList; 42 import java.util.regex.Matcher; 43 import java.util.regex.Pattern; 44 import java.util.Locale; 45 46 import android.app.Activity; 47 import android.bluetooth.BluetoothDevicePicker; 48 import android.content.Intent; 49 import android.content.ContentResolver; 50 import android.content.Context; 51 import android.net.Uri; 52 import android.os.Bundle; 53 import android.provider.Settings; 54 import android.util.Log; 55 import android.util.Patterns; 56 import android.widget.Toast; 57 58 /** 59 * This class is designed to act as the entry point of handling the share intent 60 * via BT from other APPs. and also make "Bluetooth" available in sharing method 61 * selection dialog. 62 */ 63 public class BluetoothOppLauncherActivity extends Activity { 64 private static final String TAG = "BluetoothLauncherActivity"; 65 private static final boolean D = Constants.DEBUG; 66 private static final boolean V = Constants.VERBOSE; 67 68 // Regex that matches characters that have special meaning in HTML. '<', '>', '&' and 69 // multiple continuous spaces. 70 private static final Pattern PLAIN_TEXT_TO_ESCAPE = Pattern.compile("[<>&]| {2,}|\r?\n"); 71 72 @Override 73 public void onCreate(Bundle savedInstanceState) { 74 super.onCreate(savedInstanceState); 75 76 Intent intent = getIntent(); 77 String action = intent.getAction(); 78 79 if (action.equals(Intent.ACTION_SEND) || action.equals(Intent.ACTION_SEND_MULTIPLE)) { 80 //Check if Bluetooth is available in the beginning instead of at the end 81 if (!isBluetoothAllowed()) { 82 Intent in = new Intent(this, BluetoothOppBtErrorActivity.class); 83 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 84 in.putExtra("title", this.getString(R.string.airplane_error_title)); 85 in.putExtra("content", this.getString(R.string.airplane_error_msg)); 86 startActivity(in); 87 finish(); 88 return; 89 } 90 91 /* 92 * Other application is trying to share a file via Bluetooth, 93 * probably Pictures, videos, or vCards. The Intent should contain 94 * an EXTRA_STREAM with the data to attach. 95 */ 96 if (action.equals(Intent.ACTION_SEND)) { 97 // TODO: handle type == null case 98 final String type = intent.getType(); 99 final Uri stream = (Uri)intent.getParcelableExtra(Intent.EXTRA_STREAM); 100 CharSequence extra_text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT); 101 // If we get ACTION_SEND intent with EXTRA_STREAM, we'll use the 102 // uri data; 103 // If we get ACTION_SEND intent without EXTRA_STREAM, but with 104 // EXTRA_TEXT, we will try send this TEXT out; Currently in 105 // Browser, share one link goes to this case; 106 if (stream != null && type != null) { 107 if (V) Log.v(TAG, "Get ACTION_SEND intent: Uri = " + stream + "; mimetype = " 108 + type); 109 // Save type/stream, will be used when adding transfer 110 // session to DB. 111 Thread t = new Thread(new Runnable() { 112 public void run() { 113 sendFileInfo(type, stream.toString(), false /* isHandover */, 114 true /* fromExternal */); 115 } 116 }); 117 t.start(); 118 return; 119 } else if (extra_text != null && type != null) { 120 if (V) Log.v(TAG, "Get ACTION_SEND intent with Extra_text = " 121 + extra_text.toString() + "; mimetype = " + type); 122 final Uri fileUri = creatFileForSharedContent(this.createCredentialProtectedStorageContext(), extra_text); 123 if (fileUri != null) { 124 Thread t = new Thread(new Runnable() { 125 public void run() { 126 sendFileInfo(type, fileUri.toString(), false /* isHandover */, 127 false /* fromExternal */); 128 } 129 }); 130 t.start(); 131 return; 132 } else { 133 Log.w(TAG,"Error trying to do set text...File not created!"); 134 finish(); 135 return; 136 } 137 } else { 138 Log.e(TAG, "type is null; or sending file URI is null"); 139 finish(); 140 return; 141 } 142 } else if (action.equals(Intent.ACTION_SEND_MULTIPLE)) { 143 final String mimeType = intent.getType(); 144 final ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); 145 if (mimeType != null && uris != null) { 146 if (V) Log.v(TAG, "Get ACTION_SHARE_MULTIPLE intent: uris " + uris + "\n Type= " 147 + mimeType); 148 Thread t = new Thread(new Runnable() { 149 public void run() { 150 try { 151 BluetoothOppManager.getInstance(BluetoothOppLauncherActivity.this) 152 .saveSendingFileInfo(mimeType, uris, false /* isHandover */, 153 true /* fromExternal */); 154 //Done getting file info..Launch device picker 155 //and finish this activity 156 launchDevicePicker(); 157 finish(); 158 } catch (IllegalArgumentException exception) { 159 showToast(exception.getMessage()); 160 finish(); 161 } 162 } 163 }); 164 t.start(); 165 return; 166 } else { 167 Log.e(TAG, "type is null; or sending files URIs are null"); 168 finish(); 169 return; 170 } 171 } 172 } else if (action.equals(Constants.ACTION_OPEN)) { 173 Uri uri = getIntent().getData(); 174 if (V) Log.v(TAG, "Get ACTION_OPEN intent: Uri = " + uri); 175 176 Intent intent1 = new Intent(); 177 intent1.setAction(action); 178 intent1.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 179 intent1.setDataAndNormalize(uri); 180 this.sendBroadcast(intent1); 181 finish(); 182 } else { 183 Log.w(TAG, "Unsupported action: " + action); 184 finish(); 185 } 186 } 187 188 /** 189 * Turns on Bluetooth if not already on, or launches device picker if Bluetooth is on 190 * @return 191 */ 192 private final void launchDevicePicker() { 193 // TODO: In the future, we may send intent to DevicePickerActivity 194 // directly, 195 // and let DevicePickerActivity to handle Bluetooth Enable. 196 if (!BluetoothOppManager.getInstance(this).isEnabled()) { 197 if (V) Log.v(TAG, "Prepare Enable BT!! "); 198 Intent in = new Intent(this, BluetoothOppBtEnableActivity.class); 199 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 200 startActivity(in); 201 } else { 202 if (V) Log.v(TAG, "BT already enabled!! "); 203 Intent in1 = new Intent(BluetoothDevicePicker.ACTION_LAUNCH); 204 in1.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 205 in1.putExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false); 206 in1.putExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE, 207 BluetoothDevicePicker.FILTER_TYPE_TRANSFER); 208 in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE, 209 Constants.THIS_PACKAGE_NAME); 210 in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS, 211 BluetoothOppReceiver.class.getName()); 212 if (V) {Log.d(TAG,"Launching " +BluetoothDevicePicker.ACTION_LAUNCH );} 213 startActivity(in1); 214 } 215 } 216 /* Returns true if Bluetooth is allowed given current airplane mode settings. */ 217 private final boolean isBluetoothAllowed() { 218 final ContentResolver resolver = this.getContentResolver(); 219 220 // Check if airplane mode is on 221 final boolean isAirplaneModeOn = 222 Settings.System.getInt(resolver, Settings.Global.AIRPLANE_MODE_ON, 0) == 1; 223 if (!isAirplaneModeOn) { 224 return true; 225 } 226 227 // Check if airplane mode matters 228 final String airplaneModeRadios = 229 Settings.System.getString(resolver, Settings.Global.AIRPLANE_MODE_RADIOS); 230 final boolean isAirplaneSensitive = airplaneModeRadios == null ? true : 231 airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH); 232 if (!isAirplaneSensitive) { 233 return true; 234 } 235 236 // Check if Bluetooth may be enabled in airplane mode 237 final String airplaneModeToggleableRadios = Settings.System.getString( 238 resolver, Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS); 239 final boolean isAirplaneToggleable = airplaneModeToggleableRadios == null 240 ? false 241 : airplaneModeToggleableRadios.contains(Settings.Global.RADIO_BLUETOOTH); 242 if (isAirplaneToggleable) { 243 return true; 244 } 245 246 // If we get here we're not allowed to use Bluetooth right now 247 return false; 248 } 249 250 private Uri creatFileForSharedContent(Context context, CharSequence shareContent) { 251 if (shareContent == null) { 252 return null; 253 } 254 255 Uri fileUri = null; 256 FileOutputStream outStream = null; 257 try { 258 String fileName = getString(R.string.bluetooth_share_file_name) + ".html"; 259 context.deleteFile(fileName); 260 261 /* 262 * Convert the plain text to HTML 263 */ 264 StringBuffer sb = new StringBuffer("<html><head><meta http-equiv=\"Content-Type\"" 265 + " content=\"text/html; charset=UTF-8\"/></head><body>"); 266 // Escape any inadvertent HTML in the text message 267 String text = escapeCharacterToDisplay(shareContent.toString()); 268 269 // Regex that matches Web URL protocol part as case insensitive. 270 Pattern webUrlProtocol = Pattern.compile("(?i)(http|https)://"); 271 272 Pattern pattern = Pattern.compile("(" 273 + Patterns.WEB_URL.pattern() + ")|(" 274 + Patterns.EMAIL_ADDRESS.pattern() + ")|(" 275 + Patterns.PHONE.pattern() + ")"); 276 // Find any embedded URL's and linkify 277 Matcher m = pattern.matcher(text); 278 while (m.find()) { 279 String matchStr = m.group(); 280 String link = null; 281 282 // Find any embedded URL's and linkify 283 if (Patterns.WEB_URL.matcher(matchStr).matches()) { 284 Matcher proto = webUrlProtocol.matcher(matchStr); 285 if (proto.find()) { 286 // This is work around to force URL protocol part be lower case, 287 // because WebView could follow only lower case protocol link. 288 link = proto.group().toLowerCase(Locale.US) + 289 matchStr.substring(proto.end()); 290 } else { 291 // Patterns.WEB_URL matches URL without protocol part, 292 // so added default protocol to link. 293 link = "http://" + matchStr; 294 } 295 296 // Find any embedded email address 297 } else if (Patterns.EMAIL_ADDRESS.matcher(matchStr).matches()) { 298 link = "mailto:" + matchStr; 299 300 // Find any embedded phone numbers and linkify 301 } else if (Patterns.PHONE.matcher(matchStr).matches()) { 302 link = "tel:" + matchStr; 303 } 304 if (link != null) { 305 String href = String.format("<a href=\"%s\">%s</a>", link, matchStr); 306 m.appendReplacement(sb, href); 307 } 308 } 309 m.appendTail(sb); 310 sb.append("</body></html>"); 311 312 byte[] byteBuff = sb.toString().getBytes(); 313 314 outStream = context.openFileOutput(fileName, Context.MODE_PRIVATE); 315 if (outStream != null) { 316 outStream.write(byteBuff, 0, byteBuff.length); 317 fileUri = Uri.fromFile(new File(context.getFilesDir(), fileName)); 318 if (fileUri != null) { 319 if (D) Log.d(TAG, "Created one file for shared content: " 320 + fileUri.toString()); 321 } 322 } 323 } catch (FileNotFoundException e) { 324 Log.e(TAG, "FileNotFoundException: " + e.toString()); 325 e.printStackTrace(); 326 } catch (IOException e) { 327 Log.e(TAG, "IOException: " + e.toString()); 328 } catch (Exception e) { 329 Log.e(TAG, "Exception: " + e.toString()); 330 } finally { 331 try { 332 if (outStream != null) { 333 outStream.close(); 334 } 335 } catch (IOException e) { 336 e.printStackTrace(); 337 } 338 } 339 return fileUri; 340 } 341 342 /** 343 * Escape some special character as HTML escape sequence. 344 * 345 * @param text Text to be displayed using WebView. 346 * @return Text correctly escaped. 347 */ 348 private static String escapeCharacterToDisplay(String text) { 349 Pattern pattern = PLAIN_TEXT_TO_ESCAPE; 350 Matcher match = pattern.matcher(text); 351 352 if (match.find()) { 353 StringBuilder out = new StringBuilder(); 354 int end = 0; 355 do { 356 int start = match.start(); 357 out.append(text.substring(end, start)); 358 end = match.end(); 359 int c = text.codePointAt(start); 360 if (c == ' ') { 361 // Escape successive spaces into series of " ". 362 for (int i = 1, n = end - start; i < n; ++i) { 363 out.append(" "); 364 } 365 out.append(' '); 366 } else if (c == '\r' || c == '\n') { 367 out.append("<br>"); 368 } else if (c == '<') { 369 out.append("<"); 370 } else if (c == '>') { 371 out.append(">"); 372 } else if (c == '&') { 373 out.append("&"); 374 } 375 } while (match.find()); 376 out.append(text.substring(end)); 377 text = out.toString(); 378 } 379 return text; 380 } 381 382 private void sendFileInfo( 383 String mimeType, String uriString, boolean isHandover, boolean fromExternal) { 384 BluetoothOppManager manager = BluetoothOppManager.getInstance(getApplicationContext()); 385 try { 386 manager.saveSendingFileInfo(mimeType, uriString, isHandover, fromExternal); 387 launchDevicePicker(); 388 finish(); 389 } catch (IllegalArgumentException exception) { 390 showToast(exception.getMessage()); 391 finish(); 392 } 393 } 394 395 private void showToast(final String msg) { 396 BluetoothOppLauncherActivity.this.runOnUiThread(new Runnable() { 397 @Override 398 public void run() { 399 Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show(); 400 } 401 }); 402 } 403 404 } 405