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