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