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