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.bluetooth.BluetoothAdapter; 36 import android.bluetooth.BluetoothDevice; 37 import android.content.ContentResolver; 38 import android.content.ContentValues; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.content.SharedPreferences; 42 import android.net.Uri; 43 import android.os.Process; 44 import android.os.SystemClock; 45 import android.text.TextUtils; 46 import android.util.Log; 47 import android.util.Pair; 48 49 import com.android.bluetooth.BluetoothMethodProxy; 50 import com.android.bluetooth.R; 51 import com.android.internal.annotations.VisibleForTesting; 52 53 import java.util.ArrayList; 54 import java.util.Iterator; 55 import java.util.List; 56 57 /** 58 * This class provides a simplified interface on top of other Bluetooth service 59 * layer components; Also it handles some Opp application level variables. It's 60 * a singleton got from BluetoothOppManager.getInstance(context); 61 */ 62 public class BluetoothOppManager { 63 private static final String TAG = "BluetoothOppManager"; 64 private static final boolean V = Constants.VERBOSE; 65 66 @VisibleForTesting 67 static BluetoothOppManager sInstance; 68 69 /** Used when obtaining a reference to the singleton instance. */ 70 private static final Object INSTANCE_LOCK = new Object(); 71 72 private boolean mInitialized; 73 74 private Context mContext; 75 76 private BluetoothAdapter mAdapter; 77 78 @VisibleForTesting 79 String mMimeTypeOfSendingFile; 80 81 @VisibleForTesting 82 String mUriOfSendingFile; 83 84 @VisibleForTesting 85 String mMimeTypeOfSendingFiles; 86 87 @VisibleForTesting 88 ArrayList<Uri> mUrisOfSendingFiles; 89 90 private boolean mIsHandoverInitiated; 91 92 @VisibleForTesting 93 static final String OPP_PREFERENCE_FILE = "OPPMGR"; 94 95 private static final String SENDING_FLAG = "SENDINGFLAG"; 96 97 private static final String MIME_TYPE = "MIMETYPE"; 98 99 private static final String FILE_URI = "FILE_URI"; 100 101 private static final String MIME_TYPE_MULTIPLE = "MIMETYPE_MULTIPLE"; 102 103 private static final String FILE_URIS = "FILE_URIS"; 104 105 private static final String MULTIPLE_FLAG = "MULTIPLE_FLAG"; 106 107 private static final String ARRAYLIST_ITEM_SEPERATOR = ";"; 108 109 @VisibleForTesting 110 static final int ALLOWED_INSERT_SHARE_THREAD_NUMBER = 3; 111 112 // used to judge if need continue sending process after received a 113 // ENABLED_ACTION 114 public boolean mSendingFlag; 115 116 public boolean mMultipleFlag; 117 118 private int mFileNumInBatch; 119 120 private int mInsertShareThreadNum = 0; 121 122 // A list of devices that may send files over OPP to this device 123 // without user confirmation. Used for connection handover from forex NFC. 124 private List<Pair<String, Long>> mAcceptlist = new ArrayList<Pair<String, Long>>(); 125 126 // The time for which the acceptlist entries remain valid. 127 private static final int ACCEPTLIST_DURATION_MS = 15000; 128 129 /** 130 * Get singleton instance. 131 */ getInstance(Context context)132 public static BluetoothOppManager getInstance(Context context) { 133 synchronized (INSTANCE_LOCK) { 134 if (sInstance == null) { 135 sInstance = new BluetoothOppManager(); 136 } 137 sInstance.init(context); 138 139 return sInstance; 140 } 141 } 142 143 /** 144 * Set Singleton instance. Intended for testing purpose 145 */ 146 @VisibleForTesting setInstance(BluetoothOppManager instance)147 static void setInstance(BluetoothOppManager instance) { 148 sInstance = instance; 149 } 150 151 /** 152 * init 153 */ init(Context context)154 private boolean init(Context context) { 155 if (mInitialized) { 156 return true; 157 } 158 mInitialized = true; 159 160 mContext = context; 161 162 mAdapter = BluetoothAdapter.getDefaultAdapter(); 163 if (mAdapter == null) { 164 if (V) { 165 Log.v(TAG, "BLUETOOTH_SERVICE is not started! "); 166 } 167 } 168 169 // Restore data from preference 170 restoreApplicationData(); 171 172 return true; 173 } 174 175 cleanupAcceptlist()176 private void cleanupAcceptlist() { 177 // Removes expired entries 178 long curTime = SystemClock.elapsedRealtime(); 179 for (Iterator<Pair<String, Long>> iter = mAcceptlist.iterator(); iter.hasNext(); ) { 180 Pair<String, Long> entry = iter.next(); 181 if (curTime - entry.second > ACCEPTLIST_DURATION_MS) { 182 if (V) { 183 Log.v(TAG, "Cleaning out acceptlist entry " + entry.first); 184 } 185 iter.remove(); 186 } 187 } 188 } 189 addToAcceptlist(String address)190 public synchronized void addToAcceptlist(String address) { 191 if (address == null) { 192 return; 193 } 194 // Remove any existing entries 195 for (Iterator<Pair<String, Long>> iter = mAcceptlist.iterator(); iter.hasNext(); ) { 196 Pair<String, Long> entry = iter.next(); 197 if (entry.first.equals(address)) { 198 iter.remove(); 199 } 200 } 201 mAcceptlist.add(new Pair<String, Long>(address, SystemClock.elapsedRealtime())); 202 } 203 isAcceptlisted(String address)204 public synchronized boolean isAcceptlisted(String address) { 205 cleanupAcceptlist(); 206 for (Pair<String, Long> entry : mAcceptlist) { 207 if (entry.first.equals(address)) { 208 return true; 209 } 210 } 211 return false; 212 } 213 214 /** 215 * Restore data from preference 216 */ restoreApplicationData()217 private void restoreApplicationData() { 218 SharedPreferences settings = mContext.getSharedPreferences(OPP_PREFERENCE_FILE, 0); 219 220 // All member vars are not initialized till now 221 mSendingFlag = settings.getBoolean(SENDING_FLAG, false); 222 mMimeTypeOfSendingFile = settings.getString(MIME_TYPE, null); 223 mUriOfSendingFile = settings.getString(FILE_URI, null); 224 mMimeTypeOfSendingFiles = settings.getString(MIME_TYPE_MULTIPLE, null); 225 mMultipleFlag = settings.getBoolean(MULTIPLE_FLAG, false); 226 227 if (V) { 228 Log.v(TAG, "restoreApplicationData! " + mSendingFlag + mMultipleFlag 229 + mMimeTypeOfSendingFile + mUriOfSendingFile); 230 } 231 232 String strUris = settings.getString(FILE_URIS, null); 233 mUrisOfSendingFiles = new ArrayList<Uri>(); 234 if (strUris != null) { 235 String[] splitUri = strUris.split(ARRAYLIST_ITEM_SEPERATOR); 236 for (int i = 0; i < splitUri.length; i++) { 237 mUrisOfSendingFiles.add(Uri.parse(splitUri[i])); 238 if (V) { 239 Log.v(TAG, "Uri in batch: " + Uri.parse(splitUri[i])); 240 } 241 } 242 } 243 244 mContext.getSharedPreferences(OPP_PREFERENCE_FILE, 0).edit().clear().apply(); 245 } 246 247 /** 248 * Save application data to preference, need restore these data when service restart 249 */ storeApplicationData()250 private void storeApplicationData() { 251 SharedPreferences.Editor editor = 252 mContext.getSharedPreferences(OPP_PREFERENCE_FILE, 0).edit(); 253 editor.putBoolean(SENDING_FLAG, mSendingFlag); 254 editor.putBoolean(MULTIPLE_FLAG, mMultipleFlag); 255 if (mMultipleFlag) { 256 editor.putString(MIME_TYPE_MULTIPLE, mMimeTypeOfSendingFiles); 257 StringBuilder sb = new StringBuilder(); 258 for (int i = 0, count = mUrisOfSendingFiles.size(); i < count; i++) { 259 Uri uriContent = mUrisOfSendingFiles.get(i); 260 sb.append(uriContent); 261 sb.append(ARRAYLIST_ITEM_SEPERATOR); 262 } 263 String strUris = sb.toString(); 264 editor.putString(FILE_URIS, strUris); 265 266 editor.remove(MIME_TYPE); 267 editor.remove(FILE_URI); 268 } else { 269 editor.putString(MIME_TYPE, mMimeTypeOfSendingFile); 270 editor.putString(FILE_URI, mUriOfSendingFile); 271 272 editor.remove(MIME_TYPE_MULTIPLE); 273 editor.remove(FILE_URIS); 274 } 275 editor.apply(); 276 if (V) { 277 Log.v(TAG, "Application data stored to SharedPreference! "); 278 } 279 } 280 saveSendingFileInfo(String mimeType, String uriString, boolean isHandover, boolean fromExternal)281 public void saveSendingFileInfo(String mimeType, String uriString, boolean isHandover, 282 boolean fromExternal) throws IllegalArgumentException { 283 synchronized (BluetoothOppManager.this) { 284 mMultipleFlag = false; 285 mMimeTypeOfSendingFile = mimeType; 286 mIsHandoverInitiated = isHandover; 287 Uri uri = Uri.parse(uriString); 288 BluetoothOppSendFileInfo sendFileInfo = 289 BluetoothOppSendFileInfo.generateFileInfo(mContext, uri, mimeType, 290 fromExternal); 291 uri = BluetoothOppUtility.generateUri(uri, sendFileInfo); 292 BluetoothOppUtility.putSendFileInfo(uri, sendFileInfo); 293 mUriOfSendingFile = uri.toString(); 294 storeApplicationData(); 295 } 296 } 297 saveSendingFileInfo(String mimeType, ArrayList<Uri> uris, boolean isHandover, boolean fromExternal)298 public void saveSendingFileInfo(String mimeType, ArrayList<Uri> uris, boolean isHandover, 299 boolean fromExternal) throws IllegalArgumentException { 300 synchronized (BluetoothOppManager.this) { 301 mMultipleFlag = true; 302 mMimeTypeOfSendingFiles = mimeType; 303 mUrisOfSendingFiles = new ArrayList<Uri>(); 304 mIsHandoverInitiated = isHandover; 305 for (Uri uri : uris) { 306 BluetoothOppSendFileInfo sendFileInfo = 307 BluetoothOppSendFileInfo.generateFileInfo(mContext, uri, mimeType, 308 fromExternal); 309 uri = BluetoothOppUtility.generateUri(uri, sendFileInfo); 310 mUrisOfSendingFiles.add(uri); 311 BluetoothOppUtility.putSendFileInfo(uri, sendFileInfo); 312 } 313 storeApplicationData(); 314 } 315 } 316 317 /** 318 * Get the current status of Bluetooth hardware. 319 * @return true if Bluetooth enabled, false otherwise. 320 */ isEnabled()321 public boolean isEnabled() { 322 if (mAdapter != null) { 323 return BluetoothMethodProxy.getInstance().bluetoothAdapterIsEnabled(mAdapter); 324 } else { 325 if (V) { 326 Log.v(TAG, "BLUETOOTH_SERVICE is not available! "); 327 } 328 return false; 329 } 330 } 331 332 /** 333 * Enable Bluetooth hardware. 334 */ enableBluetooth()335 public void enableBluetooth() { 336 if (mAdapter != null) { 337 mAdapter.enable(); 338 } 339 } 340 341 /** 342 * Disable Bluetooth hardware. 343 */ disableBluetooth()344 public void disableBluetooth() { 345 if (mAdapter != null) { 346 mAdapter.disable(); 347 } 348 } 349 350 /** 351 * Get device name per bluetooth address. 352 */ getDeviceName(BluetoothDevice device)353 public String getDeviceName(BluetoothDevice device) { 354 String deviceName = null; 355 356 if (device != null) { 357 deviceName = device.getAlias(); 358 if (deviceName == null) { 359 deviceName = BluetoothOppPreference.getInstance(mContext).getName(device); 360 } 361 } 362 363 if (deviceName == null) { 364 deviceName = mContext.getString(R.string.unknown_device); 365 } 366 367 return deviceName; 368 } 369 getBatchSize()370 public int getBatchSize() { 371 synchronized (BluetoothOppManager.this) { 372 return mFileNumInBatch; 373 } 374 } 375 376 /** 377 * Fork a thread to insert share info to db. 378 */ startTransfer(BluetoothDevice device)379 public void startTransfer(BluetoothDevice device) { 380 if (V) { 381 Log.v(TAG, "Active InsertShareThread number is : " + mInsertShareThreadNum); 382 } 383 InsertShareInfoThread insertThread; 384 synchronized (BluetoothOppManager.this) { 385 if (mInsertShareThreadNum > ALLOWED_INSERT_SHARE_THREAD_NUMBER) { 386 Log.e(TAG, "Too many shares user triggered concurrently!"); 387 388 // Notice user 389 Intent in = new Intent(mContext, BluetoothOppBtErrorActivity.class); 390 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 391 in.putExtra("title", mContext.getString(R.string.enabling_progress_title)); 392 in.putExtra("content", mContext.getString(R.string.ErrorTooManyRequests)); 393 mContext.startActivity(in); 394 395 return; 396 } 397 insertThread = new InsertShareInfoThread(device, mMultipleFlag, mMimeTypeOfSendingFile, 398 mUriOfSendingFile, mMimeTypeOfSendingFiles, mUrisOfSendingFiles, 399 mIsHandoverInitiated); 400 if (mMultipleFlag) { 401 mFileNumInBatch = mUrisOfSendingFiles.size(); 402 } 403 } 404 405 insertThread.start(); 406 } 407 408 /** 409 * Thread to insert share info to db. In multiple files (say 100 files) 410 * share case, the inserting share info to db operation would be a time 411 * consuming operation, so need a thread to handle it. This thread allows 412 * multiple instances to support below case: User select multiple files to 413 * share to one device (say device 1), and then right away share to second 414 * device (device 2), we need insert all these share info to db. 415 */ 416 private class InsertShareInfoThread extends Thread { 417 private final BluetoothDevice mRemoteDevice; 418 419 private final String mTypeOfSingleFile; 420 421 private final String mUri; 422 423 private final String mTypeOfMultipleFiles; 424 425 private final ArrayList<Uri> mUris; 426 427 private final boolean mIsMultiple; 428 429 private final boolean mIsHandoverInitiated; 430 InsertShareInfoThread(BluetoothDevice device, boolean multiple, String typeOfSingleFile, String uri, String typeOfMultipleFiles, ArrayList<Uri> uris, boolean handoverInitiated)431 InsertShareInfoThread(BluetoothDevice device, boolean multiple, String typeOfSingleFile, 432 String uri, String typeOfMultipleFiles, ArrayList<Uri> uris, 433 boolean handoverInitiated) { 434 super("Insert ShareInfo Thread"); 435 this.mRemoteDevice = device; 436 this.mIsMultiple = multiple; 437 this.mTypeOfSingleFile = typeOfSingleFile; 438 this.mUri = uri; 439 this.mTypeOfMultipleFiles = typeOfMultipleFiles; 440 this.mUris = uris; 441 this.mIsHandoverInitiated = handoverInitiated; 442 443 synchronized (BluetoothOppManager.this) { 444 mInsertShareThreadNum++; 445 } 446 447 if (V) { 448 Log.v(TAG, "Thread id is: " + this.getId()); 449 } 450 } 451 452 @Override run()453 public void run() { 454 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 455 if (mRemoteDevice == null) { 456 Log.e(TAG, "Target bt device is null!"); 457 return; 458 } 459 if (mIsMultiple) { 460 insertMultipleShare(); 461 } else { 462 insertSingleShare(); 463 } 464 synchronized (BluetoothOppManager.this) { 465 mInsertShareThreadNum--; 466 } 467 } 468 469 /** 470 * Insert multiple sending sessions to db, only used by Opp application. 471 */ insertMultipleShare()472 private void insertMultipleShare() { 473 int count = mUris.size(); 474 Long ts = System.currentTimeMillis(); 475 for (int i = 0; i < count; i++) { 476 Uri fileUri = mUris.get(i); 477 ContentValues values = new ContentValues(); 478 values.put(BluetoothShare.URI, fileUri.toString()); 479 ContentResolver contentResolver = mContext.getContentResolver(); 480 fileUri = BluetoothOppUtility.originalUri(fileUri); 481 String contentType = contentResolver.getType(fileUri); 482 if (V) { 483 Log.v(TAG, "Got mimetype: " + contentType + " Got uri: " + fileUri); 484 } 485 if (TextUtils.isEmpty(contentType)) { 486 contentType = mTypeOfMultipleFiles; 487 } 488 489 values.put(BluetoothShare.MIMETYPE, contentType); 490 values.put(BluetoothShare.DESTINATION, mRemoteDevice.getIdentityAddress()); 491 values.put(BluetoothShare.TIMESTAMP, ts); 492 if (mIsHandoverInitiated) { 493 values.put(BluetoothShare.USER_CONFIRMATION, 494 BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED); 495 } 496 final Uri contentUri = BluetoothMethodProxy.getInstance().contentResolverInsert( 497 mContext.getContentResolver(), BluetoothShare.CONTENT_URI, values); 498 if (V) { 499 Log.v(TAG, "Insert contentUri: " + contentUri + " to device: " + getDeviceName( 500 mRemoteDevice)); 501 } 502 } 503 } 504 505 /** 506 * Insert single sending session to db, only used by Opp application. 507 */ insertSingleShare()508 private void insertSingleShare() { 509 ContentValues values = new ContentValues(); 510 values.put(BluetoothShare.URI, mUri); 511 values.put(BluetoothShare.MIMETYPE, mTypeOfSingleFile); 512 values.put(BluetoothShare.DESTINATION, mRemoteDevice.getIdentityAddress()); 513 if (mIsHandoverInitiated) { 514 values.put(BluetoothShare.USER_CONFIRMATION, 515 BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED); 516 } 517 final Uri contentUri = BluetoothMethodProxy.getInstance().contentResolverInsert( 518 mContext.getContentResolver(), BluetoothShare.CONTENT_URI, values); 519 if (V) { 520 Log.v(TAG, "Insert contentUri: " + contentUri + " to device: " + getDeviceName( 521 mRemoteDevice)); 522 } 523 } 524 } 525 cleanUpSendingFileInfo()526 void cleanUpSendingFileInfo() { 527 synchronized (BluetoothOppManager.this) { 528 if (V) { 529 Log.v(TAG, "cleanUpSendingFileInfo: mMultipleFlag = " + mMultipleFlag); 530 } 531 if (!mMultipleFlag && (mUriOfSendingFile != null)) { 532 Uri uri = Uri.parse(mUriOfSendingFile); 533 BluetoothOppUtility.closeSendFileInfo(uri); 534 } else if (mUrisOfSendingFiles != null) { 535 for (Uri uri : mUrisOfSendingFiles) { 536 BluetoothOppUtility.closeSendFileInfo(uri); 537 } 538 } 539 } 540 } 541 } 542