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