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