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;
42
43import android.app.Activity;
44import android.bluetooth.BluetoothDevice;
45import android.bluetooth.BluetoothDevicePicker;
46import android.content.Intent;
47import android.content.ContentResolver;
48import android.content.Context;
49import android.net.Uri;
50import android.os.Bundle;
51import android.util.Log;
52import android.provider.Settings;
53
54import android.util.Patterns;
55import java.util.regex.Matcher;
56import java.util.regex.Pattern;
57import 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 */
64public 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
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     */
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. */
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
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     */
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 "&nbsp;".
363                    for (int i = 1, n = end - start; i < n; ++i) {
364                        out.append("&nbsp;");
365                    }
366                    out.append(' ');
367                } else if (c == '\r' || c == '\n') {
368                    out.append("<br>");
369                } else if (c == '<') {
370                    out.append("&lt;");
371                } else if (c == '>') {
372                    out.append("&gt;");
373                } else if (c == '&') {
374                    out.append("&amp;");
375                }
376            } while (match.find());
377            out.append(text.substring(end));
378            text = out.toString();
379        }
380        return text;
381    }
382}
383