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