• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 "&nbsp;".
396                     for (int i = 1, n = end - start; i < n; ++i) {
397                         out.append("&nbsp;");
398                     }
399                     out.append(' ');
400                 } else if (c == '\r' || c == '\n') {
401                     out.append("<br>");
402                 } else if (c == '<') {
403                     out.append("&lt;");
404                 } else if (c == '>') {
405                     out.append("&gt;");
406                 } else if (c == '&') {
407                     out.append("&amp;");
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