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