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