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 76 public BluetoothOppReceiveFileInfo(String data, long length, int status) { 77 mData = data; 78 mStatus = status; 79 mLength = length; 80 } 81 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 90 public BluetoothOppReceiveFileInfo(int status) { 91 this(null, 0, null, status); 92 } 93 94 // public static final int BATCH_STATUS_CANCELED = 4; 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 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 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 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