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 android.app.Activity; 36 import android.bluetooth.BluetoothDevicePicker; 37 import android.content.ContentResolver; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.net.Uri; 41 import android.os.Bundle; 42 import android.provider.Settings; 43 import android.util.Log; 44 import android.util.Patterns; 45 import android.widget.Toast; 46 47 import com.android.bluetooth.R; 48 49 import java.io.File; 50 import java.io.FileNotFoundException; 51 import java.io.FileOutputStream; 52 import java.io.IOException; 53 import java.util.ArrayList; 54 import java.util.Locale; 55 import java.util.regex.Matcher; 56 import java.util.regex.Pattern; 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 = "BluetoothOppLauncherActivity"; 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 onCreate(Bundle savedInstanceState)73 public void onCreate(Bundle savedInstanceState) { 74 super.onCreate(savedInstanceState); 75 76 Intent intent = getIntent(); 77 String action = intent.getAction(); 78 if (action == null) { 79 Log.w(TAG, " Received " + intent + " with null action"); 80 finish(); 81 return; 82 } 83 84 if (action.equals(Intent.ACTION_SEND) || action.equals(Intent.ACTION_SEND_MULTIPLE)) { 85 //Check if Bluetooth is available in the beginning instead of at the end 86 if (!isBluetoothAllowed()) { 87 Intent in = new Intent(this, BluetoothOppBtErrorActivity.class); 88 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 89 in.putExtra("title", this.getString(R.string.airplane_error_title)); 90 in.putExtra("content", this.getString(R.string.airplane_error_msg)); 91 startActivity(in); 92 finish(); 93 return; 94 } 95 96 /* 97 * Other application is trying to share a file via Bluetooth, 98 * probably Pictures, videos, or vCards. The Intent should contain 99 * an EXTRA_STREAM with the data to attach. 100 */ 101 if (action.equals(Intent.ACTION_SEND)) { 102 // TODO: handle type == null case 103 final String type = intent.getType(); 104 final Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); 105 CharSequence extraText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT); 106 // If we get ACTION_SEND intent with EXTRA_STREAM, we'll use the 107 // uri data; 108 // If we get ACTION_SEND intent without EXTRA_STREAM, but with 109 // EXTRA_TEXT, we will try send this TEXT out; Currently in 110 // Browser, share one link goes to this case; 111 if (stream != null && type != null) { 112 if (V) { 113 Log.v(TAG, 114 "Get ACTION_SEND intent: Uri = " + stream + "; mimetype = " + type); 115 } 116 // Save type/stream, will be used when adding transfer 117 // session to DB. 118 Thread t = new Thread(new Runnable() { 119 @Override 120 public void run() { 121 sendFileInfo(type, stream.toString(), false /* isHandover */, true /* 122 fromExternal */); 123 } 124 }); 125 t.start(); 126 return; 127 } else if (extraText != null && type != null) { 128 if (V) { 129 Log.v(TAG, 130 "Get ACTION_SEND intent with Extra_text = " + extraText.toString() 131 + "; mimetype = " + type); 132 } 133 final Uri fileUri = creatFileForSharedContent( 134 this.createCredentialProtectedStorageContext(), extraText); 135 if (fileUri != null) { 136 Thread t = new Thread(new Runnable() { 137 @Override 138 public void run() { 139 sendFileInfo(type, fileUri.toString(), false /* isHandover */, 140 false /* fromExternal */); 141 } 142 }); 143 t.start(); 144 return; 145 } else { 146 Log.w(TAG, "Error trying to do set text...File not created!"); 147 finish(); 148 return; 149 } 150 } else { 151 Log.e(TAG, "type is null; or sending file URI is null"); 152 finish(); 153 return; 154 } 155 } else if (action.equals(Intent.ACTION_SEND_MULTIPLE)) { 156 final String mimeType = intent.getType(); 157 final ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); 158 if (mimeType != null && uris != null) { 159 if (V) { 160 Log.v(TAG, "Get ACTION_SHARE_MULTIPLE intent: uris " + uris + "\n Type= " 161 + mimeType); 162 } 163 Thread t = new Thread(new Runnable() { 164 @Override 165 public void run() { 166 try { 167 BluetoothOppManager.getInstance(BluetoothOppLauncherActivity.this) 168 .saveSendingFileInfo(mimeType, uris, false /* isHandover */, 169 true /* fromExternal */); 170 //Done getting file info..Launch device picker 171 //and finish this activity 172 launchDevicePicker(); 173 finish(); 174 } catch (IllegalArgumentException exception) { 175 showToast(exception.getMessage()); 176 finish(); 177 } 178 } 179 }); 180 t.start(); 181 return; 182 } else { 183 Log.e(TAG, "type is null; or sending files URIs are null"); 184 finish(); 185 return; 186 } 187 } 188 } else if (action.equals(Constants.ACTION_OPEN)) { 189 Uri uri = getIntent().getData(); 190 if (V) { 191 Log.v(TAG, "Get ACTION_OPEN intent: Uri = " + uri); 192 } 193 194 Intent intent1 = new Intent(); 195 intent1.setAction(action); 196 intent1.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 197 intent1.setDataAndNormalize(uri); 198 this.sendBroadcast(intent1); 199 finish(); 200 } else { 201 Log.w(TAG, "Unsupported action: " + action); 202 finish(); 203 } 204 } 205 206 /** 207 * Turns on Bluetooth if not already on, or launches device picker if Bluetooth is on 208 * @return 209 */ launchDevicePicker()210 private void launchDevicePicker() { 211 // TODO: In the future, we may send intent to DevicePickerActivity 212 // directly, 213 // and let DevicePickerActivity to handle Bluetooth Enable. 214 if (!BluetoothOppManager.getInstance(this).isEnabled()) { 215 if (V) { 216 Log.v(TAG, "Prepare Enable BT!! "); 217 } 218 Intent in = new Intent(this, BluetoothOppBtEnableActivity.class); 219 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 220 startActivity(in); 221 } else { 222 if (V) { 223 Log.v(TAG, "BT already enabled!! "); 224 } 225 Intent in1 = new Intent(BluetoothDevicePicker.ACTION_LAUNCH); 226 in1.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 227 in1.putExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false); 228 in1.putExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE, 229 BluetoothDevicePicker.FILTER_TYPE_TRANSFER); 230 in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE, Constants.THIS_PACKAGE_NAME); 231 in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS, 232 BluetoothOppReceiver.class.getName()); 233 if (V) { 234 Log.d(TAG, "Launching " + BluetoothDevicePicker.ACTION_LAUNCH); 235 } 236 startActivity(in1); 237 } 238 } 239 240 /* Returns true if Bluetooth is allowed given current airplane mode settings. */ isBluetoothAllowed()241 private boolean isBluetoothAllowed() { 242 final ContentResolver resolver = this.getContentResolver(); 243 244 // Check if airplane mode is on 245 final boolean isAirplaneModeOn = 246 Settings.System.getInt(resolver, Settings.Global.AIRPLANE_MODE_ON, 0) == 1; 247 if (!isAirplaneModeOn) { 248 return true; 249 } 250 251 // Check if airplane mode matters 252 final String airplaneModeRadios = 253 Settings.System.getString(resolver, Settings.Global.AIRPLANE_MODE_RADIOS); 254 final boolean isAirplaneSensitive = 255 airplaneModeRadios == null || airplaneModeRadios.contains( 256 Settings.Global.RADIO_BLUETOOTH); 257 if (!isAirplaneSensitive) { 258 return true; 259 } 260 261 // Check if Bluetooth may be enabled in airplane mode 262 final String airplaneModeToggleableRadios = Settings.System.getString(resolver, 263 Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS); 264 final boolean isAirplaneToggleable = 265 airplaneModeToggleableRadios != null && airplaneModeToggleableRadios.contains( 266 Settings.Global.RADIO_BLUETOOTH); 267 if (isAirplaneToggleable) { 268 return true; 269 } 270 271 // If we get here we're not allowed to use Bluetooth right now 272 return false; 273 } 274 creatFileForSharedContent(Context context, CharSequence shareContent)275 private Uri creatFileForSharedContent(Context context, CharSequence shareContent) { 276 if (shareContent == null) { 277 return null; 278 } 279 280 Uri fileUri = null; 281 FileOutputStream outStream = null; 282 try { 283 String fileName = getString(R.string.bluetooth_share_file_name) + ".html"; 284 context.deleteFile(fileName); 285 286 /* 287 * Convert the plain text to HTML 288 */ 289 StringBuffer sb = new StringBuffer("<html><head><meta http-equiv=\"Content-Type\"" 290 + " content=\"text/html; charset=UTF-8\"/></head><body>"); 291 // Escape any inadvertent HTML in the text message 292 String text = escapeCharacterToDisplay(shareContent.toString()); 293 294 // Regex that matches Web URL protocol part as case insensitive. 295 Pattern webUrlProtocol = Pattern.compile("(?i)(http|https)://"); 296 297 Pattern pattern = Pattern.compile( 298 "(" + Patterns.WEB_URL.pattern() + ")|(" + Patterns.EMAIL_ADDRESS.pattern() 299 + ")|(" + Patterns.PHONE.pattern() + ")"); 300 // Find any embedded URL's and linkify 301 Matcher m = pattern.matcher(text); 302 while (m.find()) { 303 String matchStr = m.group(); 304 String link = null; 305 306 // Find any embedded URL's and linkify 307 if (Patterns.WEB_URL.matcher(matchStr).matches()) { 308 Matcher proto = webUrlProtocol.matcher(matchStr); 309 if (proto.find()) { 310 // This is work around to force URL protocol part be lower case, 311 // because WebView could follow only lower case protocol link. 312 link = proto.group().toLowerCase(Locale.US) + matchStr.substring( 313 proto.end()); 314 } else { 315 // Patterns.WEB_URL matches URL without protocol part, 316 // so added default protocol to link. 317 link = "http://" + matchStr; 318 } 319 320 // Find any embedded email address 321 } else if (Patterns.EMAIL_ADDRESS.matcher(matchStr).matches()) { 322 link = "mailto:" + matchStr; 323 324 // Find any embedded phone numbers and linkify 325 } else if (Patterns.PHONE.matcher(matchStr).matches()) { 326 link = "tel:" + matchStr; 327 } 328 if (link != null) { 329 String href = String.format("<a href=\"%s\">%s</a>", link, matchStr); 330 m.appendReplacement(sb, href); 331 } 332 } 333 m.appendTail(sb); 334 sb.append("</body></html>"); 335 336 byte[] byteBuff = sb.toString().getBytes(); 337 338 outStream = context.openFileOutput(fileName, Context.MODE_PRIVATE); 339 if (outStream != null) { 340 outStream.write(byteBuff, 0, byteBuff.length); 341 fileUri = Uri.fromFile(new File(context.getFilesDir(), fileName)); 342 if (fileUri != null) { 343 if (D) { 344 Log.d(TAG, "Created one file for shared content: " + fileUri.toString()); 345 } 346 } 347 } 348 } catch (FileNotFoundException e) { 349 Log.e(TAG, "FileNotFoundException: " + e.toString()); 350 e.printStackTrace(); 351 } catch (IOException e) { 352 Log.e(TAG, "IOException: " + e.toString()); 353 } catch (Exception e) { 354 Log.e(TAG, "Exception: " + e.toString()); 355 } finally { 356 try { 357 if (outStream != null) { 358 outStream.close(); 359 } 360 } catch (IOException e) { 361 e.printStackTrace(); 362 } 363 } 364 return fileUri; 365 } 366 367 /** 368 * Escape some special character as HTML escape sequence. 369 * 370 * @param text Text to be displayed using WebView. 371 * @return Text correctly escaped. 372 */ escapeCharacterToDisplay(String text)373 private static String escapeCharacterToDisplay(String text) { 374 Pattern pattern = PLAIN_TEXT_TO_ESCAPE; 375 Matcher match = pattern.matcher(text); 376 377 if (match.find()) { 378 StringBuilder out = new StringBuilder(); 379 int end = 0; 380 do { 381 int start = match.start(); 382 out.append(text.substring(end, start)); 383 end = match.end(); 384 int c = text.codePointAt(start); 385 if (c == ' ') { 386 // Escape successive spaces into series of " ". 387 for (int i = 1, n = end - start; i < n; ++i) { 388 out.append(" "); 389 } 390 out.append(' '); 391 } else if (c == '\r' || c == '\n') { 392 out.append("<br>"); 393 } else if (c == '<') { 394 out.append("<"); 395 } else if (c == '>') { 396 out.append(">"); 397 } else if (c == '&') { 398 out.append("&"); 399 } 400 } while (match.find()); 401 out.append(text.substring(end)); 402 text = out.toString(); 403 } 404 return text; 405 } 406 sendFileInfo(String mimeType, String uriString, boolean isHandover, boolean fromExternal)407 private void sendFileInfo(String mimeType, String uriString, boolean isHandover, 408 boolean fromExternal) { 409 BluetoothOppManager manager = BluetoothOppManager.getInstance(getApplicationContext()); 410 try { 411 manager.saveSendingFileInfo(mimeType, uriString, isHandover, fromExternal); 412 launchDevicePicker(); 413 finish(); 414 } catch (IllegalArgumentException exception) { 415 showToast(exception.getMessage()); 416 finish(); 417 } 418 } 419 showToast(final String msg)420 private void showToast(final String msg) { 421 BluetoothOppLauncherActivity.this.runOnUiThread(new Runnable() { 422 @Override 423 public void run() { 424 Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show(); 425 } 426 }); 427 } 428 429 } 430