• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.ContentResolver;
36 import android.content.ContentValues;
37 import android.content.Context;
38 import android.database.Cursor;
39 import android.net.Uri;
40 import android.os.Environment;
41 import android.os.StatFs;
42 import android.os.SystemClock;
43 import android.util.Log;
44 
45 import java.io.File;
46 import java.io.FileOutputStream;
47 import java.io.IOException;
48 import java.io.UnsupportedEncodingException;
49 import java.util.Random;
50 
51 /**
52  * This class stores information about a single receiving file. It will only be
53  * used for inbounds share, e.g. receive a file to determine a correct save file
54  * name
55  */
56 public class BluetoothOppReceiveFileInfo {
57     private static final boolean D = Constants.DEBUG;
58     private static final boolean V = Constants.VERBOSE;
59     private static String sDesiredStoragePath = null;
60 
61     /* To truncate the name of the received file if the length exceeds 245 */
62     private static final int OPP_LENGTH_OF_FILE_NAME = 244;
63 
64 
65     /** absolute store file name */
66     public String mFileName;
67 
68     public long mLength;
69 
70     public FileOutputStream mOutputStream;
71 
72     public int mStatus;
73 
74     public String mData;
75 
BluetoothOppReceiveFileInfo(String data, long length, int status)76     public BluetoothOppReceiveFileInfo(String data, long length, int status) {
77         mData = data;
78         mStatus = status;
79         mLength = length;
80     }
81 
BluetoothOppReceiveFileInfo(String filename, long length, FileOutputStream outputStream, int status)82     public BluetoothOppReceiveFileInfo(String filename, long length, FileOutputStream outputStream,
83             int status) {
84         mFileName = filename;
85         mOutputStream = outputStream;
86         mStatus = status;
87         mLength = length;
88     }
89 
BluetoothOppReceiveFileInfo(int status)90     public BluetoothOppReceiveFileInfo(int status) {
91         this(null, 0, null, status);
92     }
93 
94     // public static final int BATCH_STATUS_CANCELED = 4;
generateFileInfo(Context context, int id)95     public static BluetoothOppReceiveFileInfo generateFileInfo(Context context, int id) {
96 
97         ContentResolver contentResolver = context.getContentResolver();
98         Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id);
99         String filename = null, hint = null, mimeType = null;
100         long length = 0;
101         Cursor metadataCursor = contentResolver.query(contentUri, new String[]{
102                 BluetoothShare.FILENAME_HINT, BluetoothShare.TOTAL_BYTES, BluetoothShare.MIMETYPE
103         }, null, null, null);
104         if (metadataCursor != null) {
105             try {
106                 if (metadataCursor.moveToFirst()) {
107                     hint = metadataCursor.getString(0);
108                     length = metadataCursor.getLong(1);
109                     mimeType = metadataCursor.getString(2);
110                 }
111             } finally {
112                 metadataCursor.close();
113             }
114         }
115 
116         File base = null;
117         StatFs stat = null;
118 
119         if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
120             String root = Environment.getExternalStorageDirectory().getPath();
121             base = new File(root + Constants.DEFAULT_STORE_SUBDIR);
122             if (!base.isDirectory() && !base.mkdir()) {
123                 if (D) {
124                     Log.d(Constants.TAG,
125                             "Receive File aborted - can't create base directory " + base.getPath());
126                 }
127                 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
128             }
129             stat = new StatFs(base.getPath());
130         } else {
131             if (D) {
132                 Log.d(Constants.TAG, "Receive File aborted - no external storage");
133             }
134             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_ERROR_NO_SDCARD);
135         }
136 
137         /*
138          * Check whether there's enough space on the target filesystem to save
139          * the file. Put a bit of margin (in case creating the file grows the
140          * system by a few blocks).
141          */
142         if (stat.getBlockSizeLong() * (stat.getAvailableBlocksLong() - 4) < length) {
143             if (D) {
144                 Log.d(Constants.TAG, "Receive File aborted - not enough free space");
145             }
146             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_ERROR_SDCARD_FULL);
147         }
148 
149         filename = choosefilename(hint);
150         if (filename == null) {
151             // should not happen. It must be pre-rejected
152             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
153         }
154         String extension = null;
155         int dotIndex = filename.lastIndexOf(".");
156         if (dotIndex < 0) {
157             if (mimeType == null) {
158                 // should not happen. It must be pre-rejected
159                 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
160             } else {
161                 extension = "";
162             }
163         } else {
164             extension = filename.substring(dotIndex);
165             filename = filename.substring(0, dotIndex);
166         }
167         if (D) {
168             Log.d(Constants.TAG, " File Name " + filename);
169         }
170 
171         if (filename.getBytes().length > OPP_LENGTH_OF_FILE_NAME) {
172           /* Including extn of the file, Linux supports 255 character as a maximum length of the
173            * file name to be created. Hence, Instead of sending OBEX_HTTP_INTERNAL_ERROR,
174            * as a response, truncate the length of the file name and save it. This check majorly
175            * helps in the case of vcard, where Phone book app supports contact name to be saved
176            * more than 255 characters, But the server rejects the card just because the length of
177            * vcf file name received exceeds 255 Characters.
178            */
179             Log.i(Constants.TAG, " File Name Length :" + filename.length());
180             Log.i(Constants.TAG, " File Name Length in Bytes:" + filename.getBytes().length);
181 
182             try {
183                 byte[] oldfilename = filename.getBytes("UTF-8");
184                 byte[] newfilename = new byte[OPP_LENGTH_OF_FILE_NAME];
185                 System.arraycopy(oldfilename, 0, newfilename, 0, OPP_LENGTH_OF_FILE_NAME);
186                 filename = new String(newfilename, "UTF-8");
187             } catch (UnsupportedEncodingException e) {
188                 Log.e(Constants.TAG, "Exception: " + e);
189             }
190             if (D) {
191                 Log.d(Constants.TAG, "File name is too long. Name is truncated as: " + filename);
192             }
193         }
194 
195         filename = base.getPath() + File.separator + filename;
196         // Generate a unique filename, create the file, return it.
197         String fullfilename = chooseUniquefilename(filename, extension);
198 
199         if (!safeCanonicalPath(fullfilename)) {
200             // If this second check fails, then we better reject the transfer
201             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
202         }
203         if (V) {
204             Log.v(Constants.TAG, "Generated received filename " + fullfilename);
205         }
206 
207         if (fullfilename != null) {
208             try {
209                 new FileOutputStream(fullfilename).close();
210                 int index = fullfilename.lastIndexOf('/') + 1;
211                 // update display name
212                 if (index > 0) {
213                     String displayName = fullfilename.substring(index);
214                     if (V) {
215                         Log.v(Constants.TAG, "New display name " + displayName);
216                     }
217                     ContentValues updateValues = new ContentValues();
218                     updateValues.put(BluetoothShare.FILENAME_HINT, displayName);
219                     context.getContentResolver().update(contentUri, updateValues, null, null);
220 
221                 }
222                 return new BluetoothOppReceiveFileInfo(fullfilename, length,
223                         new FileOutputStream(fullfilename), 0);
224             } catch (IOException e) {
225                 if (D) {
226                     Log.e(Constants.TAG, "Error when creating file " + fullfilename);
227                 }
228                 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
229             }
230         } else {
231             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
232         }
233 
234     }
235 
safeCanonicalPath(String uniqueFileName)236     private static boolean safeCanonicalPath(String uniqueFileName) {
237         try {
238             File receiveFile = new File(uniqueFileName);
239             if (sDesiredStoragePath == null) {
240                 sDesiredStoragePath = Environment.getExternalStorageDirectory().getPath()
241                         + Constants.DEFAULT_STORE_SUBDIR;
242             }
243             String canonicalPath = receiveFile.getCanonicalPath();
244 
245             // Check if canonical path is complete - case sensitive-wise
246             if (!canonicalPath.startsWith(sDesiredStoragePath)) {
247                 return false;
248             }
249 
250             return true;
251         } catch (IOException ioe) {
252             // If an exception is thrown, there might be something wrong with the file.
253             return false;
254         }
255     }
256 
chooseUniquefilename(String filename, String extension)257     private static String chooseUniquefilename(String filename, String extension) {
258         String fullfilename = filename + extension;
259         if (!new File(fullfilename).exists()) {
260             return fullfilename;
261         }
262         filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
263         /*
264          * This number is used to generate partially randomized filenames to
265          * avoid collisions. It starts at 1. The next 9 iterations increment it
266          * by 1 at a time (up to 10). The next 9 iterations increment it by 1 to
267          * 10 (random) at a time. The next 9 iterations increment it by 1 to 100
268          * (random) at a time. ... Up to the point where it increases by
269          * 100000000 at a time. (the maximum value that can be reached is
270          * 1000000000) As soon as a number is reached that generates a filename
271          * that doesn't exist, that filename is used. If the filename coming in
272          * is [base].[ext], the generated filenames are [base]-[sequence].[ext].
273          */
274         Random rnd = new Random(SystemClock.uptimeMillis());
275         int sequence = 1;
276         for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
277             for (int iteration = 0; iteration < 9; ++iteration) {
278                 fullfilename = filename + sequence + extension;
279                 if (!new File(fullfilename).exists()) {
280                     return fullfilename;
281                 }
282                 if (V) {
283                     Log.v(Constants.TAG, "file with sequence number " + sequence + " exists");
284                 }
285                 sequence += rnd.nextInt(magnitude) + 1;
286             }
287         }
288         return null;
289     }
290 
choosefilename(String hint)291     private static String choosefilename(String hint) {
292         String filename = null;
293 
294         // First, try to use the hint from the application, if there's one
295         if (filename == null && !(hint == null) && !hint.endsWith("/") && !hint.endsWith("\\")) {
296             // Prevent abuse of path backslashes by converting all backlashes '\\' chars
297             // to UNIX-style forward-slashes '/'
298             hint = hint.replace('\\', '/');
299             // Convert all whitespace characters to spaces.
300             hint = hint.replaceAll("\\s", " ");
301             // Replace illegal fat filesystem characters from the
302             // filename hint i.e. :"<>*?| with something safe.
303             hint = hint.replaceAll("[:\"<>*?|]", "_");
304             if (V) {
305                 Log.v(Constants.TAG, "getting filename from hint");
306             }
307             int index = hint.lastIndexOf('/') + 1;
308             if (index > 0) {
309                 filename = hint.substring(index);
310             } else {
311                 filename = hint;
312             }
313         }
314         return filename;
315     }
316 }
317