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