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 android.app.NotificationManager;
36 import android.bluetooth.BluetoothAdapter;
37 import android.bluetooth.BluetoothDevice;
38 import android.content.ActivityNotFoundException;
39 import android.content.ContentResolver;
40 import android.content.ContentValues;
41 import android.content.Context;
42 import android.content.Intent;
43 import android.content.pm.PackageManager;
44 import android.content.pm.ResolveInfo;
45 import android.database.Cursor;
46 import android.net.Uri;
47 import android.os.Environment;
48 import android.os.ParcelFileDescriptor;
49 import android.os.SystemProperties;
50 import android.util.Log;
51 
52 import com.android.bluetooth.R;
53 
54 import java.io.File;
55 import java.io.IOException;
56 import java.math.RoundingMode;
57 import java.text.DecimalFormat;
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.List;
61 import java.util.concurrent.ConcurrentHashMap;
62 
63 /**
64  * This class has some utilities for Opp application;
65  */
66 public class BluetoothOppUtility {
67     private static final String TAG = "BluetoothOppUtility";
68     private static final boolean D = Constants.DEBUG;
69     private static final boolean V = Constants.VERBOSE;
70     /** Whether the device has the "nosdcard" characteristic, or null if not-yet-known. */
71     private static Boolean sNoSdCard = null;
72 
73     private static final ConcurrentHashMap<Uri, BluetoothOppSendFileInfo> sSendFileMap =
74             new ConcurrentHashMap<Uri, BluetoothOppSendFileInfo>();
75 
isBluetoothShareUri(Uri uri)76     public static boolean isBluetoothShareUri(Uri uri) {
77         return uri.toString().startsWith(BluetoothShare.CONTENT_URI.toString());
78     }
79 
queryRecord(Context context, Uri uri)80     public static BluetoothOppTransferInfo queryRecord(Context context, Uri uri) {
81         BluetoothOppTransferInfo info = new BluetoothOppTransferInfo();
82         Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
83         if (cursor != null) {
84             if (cursor.moveToFirst()) {
85                 fillRecord(context, cursor, info);
86             }
87             cursor.close();
88         } else {
89             info = null;
90             if (V) {
91                 Log.v(TAG, "BluetoothOppManager Error: not got data from db for uri:" + uri);
92             }
93         }
94         return info;
95     }
96 
fillRecord(Context context, Cursor cursor, BluetoothOppTransferInfo info)97     public static void fillRecord(Context context, Cursor cursor, BluetoothOppTransferInfo info) {
98         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
99         info.mID = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
100         info.mStatus = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.STATUS));
101         info.mDirection = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION));
102         info.mTotalBytes = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES));
103         info.mCurrentBytes =
104                 cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES));
105         info.mTimeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
106         info.mDestAddr = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION));
107 
108         info.mFileName = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA));
109         if (info.mFileName == null) {
110             info.mFileName =
111                     cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT));
112         }
113         if (info.mFileName == null) {
114             info.mFileName = context.getString(R.string.unknown_file);
115         }
116 
117         info.mFileUri = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI));
118 
119         if (info.mFileUri != null) {
120             Uri u = Uri.parse(info.mFileUri);
121             info.mFileType = context.getContentResolver().getType(u);
122         } else {
123             Uri u = Uri.parse(info.mFileName);
124             info.mFileType = context.getContentResolver().getType(u);
125         }
126         if (info.mFileType == null) {
127             info.mFileType =
128                     cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE));
129         }
130 
131         BluetoothDevice remoteDevice = adapter.getRemoteDevice(info.mDestAddr);
132         info.mDeviceName = BluetoothOppManager.getInstance(context).getDeviceName(remoteDevice);
133 
134         int confirmationType =
135                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
136         info.mHandoverInitiated =
137                 confirmationType == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
138 
139         if (V) {
140             Log.v(TAG, "Get data from db:" + info.mFileName + info.mFileType + info.mDestAddr);
141         }
142     }
143 
144     /**
145      * Organize Array list for transfers in one batch
146      */
147     // This function is used when UI show batch transfer. Currently only show single transfer.
queryTransfersInBatch(Context context, Long timeStamp)148     public static ArrayList<String> queryTransfersInBatch(Context context, Long timeStamp) {
149         ArrayList<String> uris = new ArrayList();
150         final String where = BluetoothShare.TIMESTAMP + " == " + timeStamp;
151         Cursor metadataCursor =
152                 context.getContentResolver().query(BluetoothShare.CONTENT_URI, new String[]{
153                         BluetoothShare._DATA
154                 }, where, null, BluetoothShare._ID);
155 
156         if (metadataCursor == null) {
157             return null;
158         }
159 
160         for (metadataCursor.moveToFirst(); !metadataCursor.isAfterLast();
161                 metadataCursor.moveToNext()) {
162             String fileName = metadataCursor.getString(0);
163             Uri path = Uri.parse(fileName);
164             // If there is no scheme, then it must be a file
165             if (path.getScheme() == null) {
166                 path = Uri.fromFile(new File(fileName));
167             }
168             uris.add(path.toString());
169             if (V) {
170                 Log.d(TAG, "Uri in this batch: " + path.toString());
171             }
172         }
173         metadataCursor.close();
174         return uris;
175     }
176 
177     /**
178      * Open the received file with appropriate application, if can not find
179      * application to handle, display error dialog.
180      */
openReceivedFile(Context context, String fileName, String mimetype, Long timeStamp, Uri uri)181     public static void openReceivedFile(Context context, String fileName, String mimetype,
182             Long timeStamp, Uri uri) {
183         if (fileName == null || mimetype == null) {
184             Log.e(TAG, "ERROR: Para fileName ==null, or mimetype == null");
185             return;
186         }
187 
188         if (!isBluetoothShareUri(uri)) {
189             Log.e(TAG, "Trying to open a file that wasn't transfered over Bluetooth");
190             return;
191         }
192 
193         Uri path = null;
194         Cursor metadataCursor = context.getContentResolver().query(uri, new String[]{
195                 BluetoothShare.URI}, null, null, null);
196         if (metadataCursor != null) {
197             try {
198                 if (metadataCursor.moveToFirst()) {
199                     path = Uri.parse(metadataCursor.getString(0));
200                 }
201             } finally {
202                 metadataCursor.close();
203             }
204         }
205 
206         if (path == null) {
207             Log.e(TAG, "file uri not exist");
208             return;
209         }
210 
211         if (!fileExists(context, path)) {
212             Intent in = new Intent(context, BluetoothOppBtErrorActivity.class);
213             in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
214             in.putExtra("title", context.getString(R.string.not_exist_file));
215             in.putExtra("content", context.getString(R.string.not_exist_file_desc));
216             context.startActivity(in);
217 
218             // Due to the file is not existing, delete related info in btopp db
219             // to prevent this file from appearing in live folder
220             if (V) {
221                 Log.d(TAG, "This uri will be deleted: " + uri);
222             }
223             context.getContentResolver().delete(uri, null, null);
224             return;
225         }
226 
227         if (isRecognizedFileType(context, path, mimetype)) {
228             Intent activityIntent = new Intent(Intent.ACTION_VIEW);
229             activityIntent.setDataAndTypeAndNormalize(path, mimetype);
230 
231             List<ResolveInfo> resInfoList = context.getPackageManager()
232                     .queryIntentActivities(activityIntent, PackageManager.MATCH_DEFAULT_ONLY);
233 
234             activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
235             activityIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
236 
237             try {
238                 if (V) {
239                     Log.d(TAG, "ACTION_VIEW intent sent out: " + path + " / " + mimetype);
240                 }
241                 context.startActivity(activityIntent);
242             } catch (ActivityNotFoundException ex) {
243                 if (V) {
244                     Log.d(TAG, "no activity for handling ACTION_VIEW intent:  " + mimetype, ex);
245                 }
246             }
247         } else {
248             Intent in = new Intent(context, BluetoothOppBtErrorActivity.class);
249             in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
250             in.putExtra("title", context.getString(R.string.unknown_file));
251             in.putExtra("content", context.getString(R.string.unknown_file_desc));
252             context.startActivity(in);
253         }
254     }
255 
fileExists(Context context, Uri uri)256     static boolean fileExists(Context context, Uri uri) {
257         // Open a specific media item using ParcelFileDescriptor.
258         ContentResolver resolver = context.getContentResolver();
259         String readOnlyMode = "r";
260         ParcelFileDescriptor pfd = null;
261         try {
262             pfd = resolver.openFileDescriptor(uri, readOnlyMode);
263             return true;
264         } catch (IOException e) {
265             e.printStackTrace();
266         }
267         return false;
268     }
269 
270     /**
271      * To judge if the file type supported (can be handled by some app) by phone
272      * system.
273      */
isRecognizedFileType(Context context, Uri fileUri, String mimetype)274     public static boolean isRecognizedFileType(Context context, Uri fileUri, String mimetype) {
275         boolean ret = true;
276 
277         if (D) {
278             Log.d(TAG, "RecognizedFileType() fileUri: " + fileUri + " mimetype: " + mimetype);
279         }
280 
281         Intent mimetypeIntent = new Intent(Intent.ACTION_VIEW);
282         mimetypeIntent.setDataAndTypeAndNormalize(fileUri, mimetype);
283         List<ResolveInfo> list = context.getPackageManager()
284                 .queryIntentActivities(mimetypeIntent, PackageManager.MATCH_DEFAULT_ONLY);
285 
286         if (list.size() == 0) {
287             if (D) {
288                 Log.d(TAG, "NO application to handle MIME type " + mimetype);
289             }
290             ret = false;
291         }
292         return ret;
293     }
294 
295     /**
296      * update visibility to Hidden
297      */
updateVisibilityToHidden(Context context, Uri uri)298     public static void updateVisibilityToHidden(Context context, Uri uri) {
299         ContentValues updateValues = new ContentValues();
300         updateValues.put(BluetoothShare.VISIBILITY, BluetoothShare.VISIBILITY_HIDDEN);
301         context.getContentResolver().update(uri, updateValues, null, null);
302     }
303 
304     /**
305      * Helper function to build the progress text.
306      */
formatProgressText(long totalBytes, long currentBytes)307     public static String formatProgressText(long totalBytes, long currentBytes) {
308         DecimalFormat df = new DecimalFormat("0%");
309         df.setRoundingMode(RoundingMode.DOWN);
310         double percent = 0.0;
311         if (totalBytes > 0) {
312             percent = currentBytes / (double) totalBytes;
313         }
314         return df.format(percent);
315     }
316 
317     /**
318      * Whether the device has the "nosdcard" characteristic or not.
319      */
deviceHasNoSdCard()320     public static boolean deviceHasNoSdCard() {
321         if (sNoSdCard == null) {
322             String characteristics = SystemProperties.get("ro.build.characteristics", "");
323             sNoSdCard = Arrays.asList(characteristics).contains("nosdcard");
324         }
325         return sNoSdCard;
326     }
327 
328     /**
329      * Get status description according to status code.
330      */
getStatusDescription(Context context, int statusCode, String deviceName)331     public static String getStatusDescription(Context context, int statusCode, String deviceName) {
332         String ret;
333         if (statusCode == BluetoothShare.STATUS_PENDING) {
334             ret = context.getString(R.string.status_pending);
335         } else if (statusCode == BluetoothShare.STATUS_RUNNING) {
336             ret = context.getString(R.string.status_running);
337         } else if (statusCode == BluetoothShare.STATUS_SUCCESS) {
338             ret = context.getString(R.string.status_success);
339         } else if (statusCode == BluetoothShare.STATUS_NOT_ACCEPTABLE) {
340             ret = context.getString(R.string.status_not_accept);
341         } else if (statusCode == BluetoothShare.STATUS_FORBIDDEN) {
342             ret = context.getString(R.string.status_forbidden);
343         } else if (statusCode == BluetoothShare.STATUS_CANCELED) {
344             ret = context.getString(R.string.status_canceled);
345         } else if (statusCode == BluetoothShare.STATUS_FILE_ERROR) {
346             ret = context.getString(R.string.status_file_error);
347         } else if (statusCode == BluetoothShare.STATUS_ERROR_NO_SDCARD) {
348             int id = deviceHasNoSdCard()
349                     ? R.string.status_no_sd_card_nosdcard
350                     : R.string.status_no_sd_card_default;
351             ret = context.getString(id);
352         } else if (statusCode == BluetoothShare.STATUS_CONNECTION_ERROR) {
353             ret = context.getString(R.string.status_connection_error);
354         } else if (statusCode == BluetoothShare.STATUS_ERROR_SDCARD_FULL) {
355             int id = deviceHasNoSdCard() ? R.string.bt_sm_2_1_nosdcard : R.string.bt_sm_2_1_default;
356             ret = context.getString(id);
357         } else if ((statusCode == BluetoothShare.STATUS_BAD_REQUEST) || (statusCode
358                 == BluetoothShare.STATUS_LENGTH_REQUIRED) || (statusCode
359                 == BluetoothShare.STATUS_PRECONDITION_FAILED) || (statusCode
360                 == BluetoothShare.STATUS_UNHANDLED_OBEX_CODE) || (statusCode
361                 == BluetoothShare.STATUS_OBEX_DATA_ERROR)) {
362             ret = context.getString(R.string.status_protocol_error);
363         } else {
364             ret = context.getString(R.string.status_unknown_error);
365         }
366         return ret;
367     }
368 
369     /**
370      * Retry the failed transfer: Will insert a new transfer session to db
371      */
retryTransfer(Context context, BluetoothOppTransferInfo transInfo)372     public static void retryTransfer(Context context, BluetoothOppTransferInfo transInfo) {
373         ContentValues values = new ContentValues();
374         values.put(BluetoothShare.URI, transInfo.mFileUri);
375         values.put(BluetoothShare.MIMETYPE, transInfo.mFileType);
376         values.put(BluetoothShare.DESTINATION, transInfo.mDestAddr);
377 
378         final Uri contentUri =
379                 context.getContentResolver().insert(BluetoothShare.CONTENT_URI, values);
380         if (V) {
381             Log.v(TAG,
382                     "Insert contentUri: " + contentUri + "  to device: " + transInfo.mDeviceName);
383         }
384     }
385 
originalUri(Uri uri)386     static Uri originalUri(Uri uri) {
387         String mUri = uri.toString();
388         int atIndex = mUri.lastIndexOf("@");
389         if (atIndex != -1) {
390             mUri = mUri.substring(0, atIndex);
391             uri = Uri.parse(mUri);
392         }
393         if (V) Log.v(TAG, "originalUri: " + uri);
394         return uri;
395     }
396 
generateUri(Uri uri, BluetoothOppSendFileInfo sendFileInfo)397     static Uri generateUri(Uri uri, BluetoothOppSendFileInfo sendFileInfo) {
398         String fileInfo = sendFileInfo.toString();
399         int atIndex = fileInfo.lastIndexOf("@");
400         fileInfo = fileInfo.substring(atIndex);
401         uri = Uri.parse(uri + fileInfo);
402         if (V) Log.v(TAG, "generateUri: " + uri);
403         return uri;
404     }
405 
putSendFileInfo(Uri uri, BluetoothOppSendFileInfo sendFileInfo)406     static void putSendFileInfo(Uri uri, BluetoothOppSendFileInfo sendFileInfo) {
407         if (D) {
408             Log.d(TAG, "putSendFileInfo: uri=" + uri + " sendFileInfo=" + sendFileInfo);
409         }
410         if (sendFileInfo == BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR) {
411             Log.e(TAG, "putSendFileInfo: bad sendFileInfo, URI: " + uri);
412         }
413         sSendFileMap.put(uri, sendFileInfo);
414     }
415 
getSendFileInfo(Uri uri)416     static BluetoothOppSendFileInfo getSendFileInfo(Uri uri) {
417         if (D) {
418             Log.d(TAG, "getSendFileInfo: uri=" + uri);
419         }
420         BluetoothOppSendFileInfo info = sSendFileMap.get(uri);
421         return (info != null) ? info : BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR;
422     }
423 
closeSendFileInfo(Uri uri)424     static void closeSendFileInfo(Uri uri) {
425         if (D) {
426             Log.d(TAG, "closeSendFileInfo: uri=" + uri);
427         }
428         BluetoothOppSendFileInfo info = sSendFileMap.remove(uri);
429         if (info != null && info.mInputStream != null) {
430             try {
431                 info.mInputStream.close();
432             } catch (IOException ignored) {
433             }
434         }
435     }
436 
437     /**
438      * Checks if the URI is in Environment.getExternalStorageDirectory() as it
439      * is the only directory that is possibly readable by both the sender and
440      * the Bluetooth process.
441      */
isInExternalStorageDir(Uri uri)442     static boolean isInExternalStorageDir(Uri uri) {
443         if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
444             Log.e(TAG, "Not a file URI: " + uri);
445             return false;
446         }
447         final File file = new File(uri.getCanonicalUri().getPath());
448         return isSameOrSubDirectory(Environment.getExternalStorageDirectory(), file);
449     }
450 
451     /**
452      * Checks, whether the child directory is the same as, or a sub-directory of the base
453      * directory. Neither base nor child should be null.
454      */
isSameOrSubDirectory(File base, File child)455     static boolean isSameOrSubDirectory(File base, File child) {
456         try {
457             base = base.getCanonicalFile();
458             child = child.getCanonicalFile();
459             File parentFile = child;
460             while (parentFile != null) {
461                 if (base.equals(parentFile)) {
462                     return true;
463                 }
464                 parentFile = parentFile.getParentFile();
465             }
466             return false;
467         } catch (IOException ex) {
468             Log.e(TAG, "Error while accessing file", ex);
469             return false;
470         }
471     }
472 
cancelNotification(Context ctx)473     protected static void cancelNotification(Context ctx) {
474         NotificationManager nm = (NotificationManager) ctx
475                 .getSystemService(Context.NOTIFICATION_SERVICE);
476         nm.cancel(BluetoothOppNotification.NOTIFICATION_ID_PROGRESS);
477     }
478 
479 }
480