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.BluetoothProfile;
36 import android.bluetooth.BluetoothProtoEnums;
37 import android.content.ContentResolver;
38 import android.content.Context;
39 import android.content.res.AssetFileDescriptor;
40 import android.database.Cursor;
41 import android.database.sqlite.SQLiteException;
42 import android.net.Uri;
43 import android.provider.OpenableColumns;
44 import android.util.EventLog;
45 import android.util.Log;
46 
47 import com.android.bluetooth.BluetoothMethodProxy;
48 import com.android.bluetooth.BluetoothStatsLog;
49 import com.android.bluetooth.R;
50 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
51 
52 import java.io.File;
53 import java.io.FileInputStream;
54 import java.io.FileNotFoundException;
55 import java.io.IOException;
56 
57 /**
58  * This class stores information about a single sending file It will only be used for outbound
59  * share.
60  */
61 // Next tag value for ContentProfileErrorReportUtils.report(): 15
62 public class BluetoothOppSendFileInfo {
63     private static final String TAG = "BluetoothOppSendFileInfo";
64 
65     /** Reusable SendFileInfo for error status. */
66     static final BluetoothOppSendFileInfo SEND_FILE_INFO_ERROR =
67             new BluetoothOppSendFileInfo(null, null, 0, null, BluetoothShare.STATUS_FILE_ERROR);
68 
69     /** readable media file name */
70     public final String mFileName;
71 
72     /** media file input stream */
73     public final FileInputStream mInputStream;
74 
75     /** vCard string data */
76     public final String mData;
77 
78     public final int mStatus;
79 
80     public final String mMimetype;
81 
82     public final long mLength;
83 
84     /** for media file */
BluetoothOppSendFileInfo( String fileName, String type, long length, FileInputStream inputStream, int status)85     public BluetoothOppSendFileInfo(
86             String fileName, String type, long length, FileInputStream inputStream, int status) {
87         mFileName = fileName;
88         mMimetype = type;
89         mLength = length;
90         mInputStream = inputStream;
91         mStatus = status;
92         mData = null;
93     }
94 
95     /** for vCard, or later for vCal, vNote. Not used currently */
BluetoothOppSendFileInfo(String data, String type, long length, int status)96     public BluetoothOppSendFileInfo(String data, String type, long length, int status) {
97         mFileName = null;
98         mInputStream = null;
99         mData = data;
100         mMimetype = type;
101         mLength = length;
102         mStatus = status;
103     }
104 
generateFileInfo( Context context, Uri uri, String type, boolean fromExternal)105     public static BluetoothOppSendFileInfo generateFileInfo(
106             Context context, Uri uri, String type, boolean fromExternal) {
107         ContentResolver contentResolver = context.getContentResolver();
108         String scheme = uri.getScheme();
109         String fileName = null;
110         String contentType;
111         long length = 0;
112         // Support all Uri with "content" scheme
113         // This will allow more 3rd party applications to share files via
114         // bluetooth
115         if ("content".equals(scheme)) {
116             if (fromExternal && BluetoothOppUtility.isForbiddenContent(uri)) {
117                 EventLog.writeEvent(0x534e4554, "179910660", -1, uri.toString());
118                 Log.e(TAG, "Content from forbidden URI is not allowed.");
119                 ContentProfileErrorReportUtils.report(
120                         BluetoothProfile.OPP,
121                         BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
122                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
123                         0);
124                 return SEND_FILE_INFO_ERROR;
125             }
126 
127             contentType = contentResolver.getType(uri);
128             Cursor metadataCursor;
129             try {
130                 metadataCursor =
131                         BluetoothMethodProxy.getInstance()
132                                 .contentResolverQuery(
133                                         contentResolver,
134                                         uri,
135                                         new String[] {
136                                             OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE
137                                         },
138                                         null,
139                                         null,
140                                         null);
141             } catch (SQLiteException e) {
142                 ContentProfileErrorReportUtils.report(
143                         BluetoothProfile.OPP,
144                         BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
145                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
146                         1);
147                 // some content providers don't support the DISPLAY_NAME or SIZE columns
148                 metadataCursor = null;
149             } catch (SecurityException e) {
150                 ContentProfileErrorReportUtils.report(
151                         BluetoothProfile.OPP,
152                         BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
153                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
154                         2);
155                 Log.e(TAG, "generateFileInfo: Permission error, could not access URI: " + uri);
156                 return SEND_FILE_INFO_ERROR;
157             }
158 
159             if (metadataCursor != null) {
160                 try {
161                     if (metadataCursor.moveToFirst()) {
162                         int indexName = metadataCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
163                         int indexSize = metadataCursor.getColumnIndex(OpenableColumns.SIZE);
164                         if (indexName != -1) {
165                             fileName = metadataCursor.getString(indexName);
166                         }
167                         if (indexSize != -1) {
168                             length = metadataCursor.getLong(indexSize);
169                         }
170                         Log.d(TAG, "fileName = " + fileName + " length = " + length);
171                     }
172                 } finally {
173                     metadataCursor.close();
174                 }
175             }
176             if (fileName == null) {
177                 // use last segment of URI if DISPLAY_NAME query fails
178                 fileName = uri.getLastPathSegment();
179                 Log.d(TAG, "fileName from URI :" + fileName);
180             }
181         } else if ("file".equals(scheme)) {
182             if (uri.getPath() == null) {
183                 Log.e(TAG, "Invalid URI path: " + uri);
184                 ContentProfileErrorReportUtils.report(
185                         BluetoothProfile.OPP,
186                         BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
187                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
188                         3);
189                 return SEND_FILE_INFO_ERROR;
190             }
191             if (fromExternal && !BluetoothOppUtility.isInExternalStorageDir(uri)) {
192                 EventLog.writeEvent(0x534e4554, "35310991", -1, uri.getPath());
193                 Log.e(
194                         TAG,
195                         "File based URI not in Environment.getExternalStorageDirectory() is not "
196                                 + "allowed.");
197                 ContentProfileErrorReportUtils.report(
198                         BluetoothProfile.OPP,
199                         BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
200                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
201                         4);
202                 return SEND_FILE_INFO_ERROR;
203             }
204             fileName = uri.getLastPathSegment();
205             contentType = type;
206             File f = new File(uri.getPath());
207             length = f.length();
208         } else {
209             // currently don't accept other scheme
210             return SEND_FILE_INFO_ERROR;
211         }
212         FileInputStream is = null;
213         if (scheme.equals("content")) {
214             try {
215                 // We've found that content providers don't always have the
216                 // right size in _OpenableColumns.SIZE
217                 // As a second source of getting the correct file length,
218                 // get a file descriptor and get the stat length
219                 AssetFileDescriptor fd =
220                         BluetoothMethodProxy.getInstance()
221                                 .contentResolverOpenAssetFileDescriptor(contentResolver, uri, "r");
222                 long statLength = fd.getLength();
223                 if (length != statLength && statLength > 0) {
224                     Log.e(
225                             TAG,
226                             "Content provider length is wrong ("
227                                     + Long.toString(length)
228                                     + "), using stat length ("
229                                     + Long.toString(statLength)
230                                     + ")");
231                     ContentProfileErrorReportUtils.report(
232                             BluetoothProfile.OPP,
233                             BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
234                             BluetoothStatsLog
235                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
236                             5);
237                     length = statLength;
238                 }
239 
240                 try {
241                     // This creates an auto-closing input-stream, so
242                     // the file descriptor will be closed whenever the InputStream
243                     // is closed.
244                     is = fd.createInputStream();
245 
246                     // If the database doesn't contain the file size, get the size
247                     // by reading through the entire stream
248                     if (length == 0) {
249                         length = getStreamSize(is);
250                         Log.w(TAG, "File length not provided. Length from stream = " + length);
251                         ContentProfileErrorReportUtils.report(
252                                 BluetoothProfile.OPP,
253                                 BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
254                                 BluetoothStatsLog
255                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
256                                 6);
257                         // Reset the stream
258                         fd =
259                                 BluetoothMethodProxy.getInstance()
260                                         .contentResolverOpenAssetFileDescriptor(
261                                                 contentResolver, uri, "r");
262                         is = fd.createInputStream();
263                     }
264                 } catch (IOException e) {
265                     ContentProfileErrorReportUtils.report(
266                             BluetoothProfile.OPP,
267                             BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
268                             BluetoothStatsLog
269                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
270                             7);
271                     try {
272                         fd.close();
273                     } catch (IOException e2) {
274                         ContentProfileErrorReportUtils.report(
275                                 BluetoothProfile.OPP,
276                                 BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
277                                 BluetoothStatsLog
278                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
279                                 8);
280                         // Ignore
281                     }
282                 }
283             } catch (FileNotFoundException e) {
284                 ContentProfileErrorReportUtils.report(
285                         BluetoothProfile.OPP,
286                         BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
287                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
288                         9);
289                 // Ignore
290             } catch (SecurityException e) {
291                 ContentProfileErrorReportUtils.report(
292                         BluetoothProfile.OPP,
293                         BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
294                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
295                         10);
296                 return SEND_FILE_INFO_ERROR;
297             }
298         }
299 
300         if (is == null) {
301             try {
302                 is =
303                         (FileInputStream)
304                                 BluetoothMethodProxy.getInstance()
305                                         .contentResolverOpenInputStream(contentResolver, uri);
306 
307                 // If the database doesn't contain the file size, get the size
308                 // by reading through the entire stream
309                 if (length == 0) {
310                     length = getStreamSize(is);
311                     // Reset the stream
312                     is =
313                             (FileInputStream)
314                                     BluetoothMethodProxy.getInstance()
315                                             .contentResolverOpenInputStream(contentResolver, uri);
316                 }
317             } catch (FileNotFoundException e) {
318                 ContentProfileErrorReportUtils.report(
319                         BluetoothProfile.OPP,
320                         BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
321                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
322                         11);
323                 return SEND_FILE_INFO_ERROR;
324             } catch (IOException e) {
325                 ContentProfileErrorReportUtils.report(
326                         BluetoothProfile.OPP,
327                         BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
328                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
329                         12);
330                 return SEND_FILE_INFO_ERROR;
331             }
332         }
333 
334         if (length == 0) {
335             Log.e(TAG, "Could not determine size of file");
336             ContentProfileErrorReportUtils.report(
337                     BluetoothProfile.OPP,
338                     BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
339                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
340                     13);
341             return SEND_FILE_INFO_ERROR;
342         } else if (length > 0xffffffffL) {
343             Log.e(TAG, "File of size: " + length + " bytes can't be transferred");
344             ContentProfileErrorReportUtils.report(
345                     BluetoothProfile.OPP,
346                     BluetoothProtoEnums.BLUETOOTH_OPP_SEND_FILE_INFO,
347                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
348                     14);
349             throw new IllegalArgumentException(
350                     context.getString(R.string.bluetooth_opp_file_limit_exceeded));
351         }
352 
353         return new BluetoothOppSendFileInfo(fileName, contentType, length, is, 0);
354     }
355 
getStreamSize(FileInputStream is)356     private static long getStreamSize(FileInputStream is) throws IOException {
357         long length = 0;
358         byte[] unused = new byte[4096];
359         int bytesRead = is.read(unused, 0, 4096);
360         while (bytesRead != -1) {
361             length += bytesRead;
362             bytesRead = is.read(unused, 0, 4096);
363         }
364         return length;
365     }
366 }
367