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