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