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