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 33package com.android.bluetooth.opp; 34 35import java.io.File; 36import java.io.FileOutputStream; 37import java.io.IOException; 38import java.util.Random; 39 40import android.content.ContentResolver; 41import android.content.ContentValues; 42import android.content.Context; 43import android.database.Cursor; 44import android.net.Uri; 45import android.os.Environment; 46import android.os.StatFs; 47import android.os.SystemClock; 48import 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 */ 55public 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, 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 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 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 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