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