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