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