BluetoothOppReceiveFileInfo.java revision 1f556d99c4bb7cae5ed55c171ce7f65b5010fa57
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.FileNotFoundException; 37import java.io.FileOutputStream; 38import java.io.IOException; 39import java.util.Random; 40 41import android.content.ContentResolver; 42import android.content.ContentValues; 43import android.content.Context; 44import android.database.Cursor; 45import android.net.Uri; 46import android.os.Environment; 47import android.os.StatFs; 48import android.os.SystemClock; 49import android.util.Log; 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 */ 56public class BluetoothOppReceiveFileInfo { 57 private static final boolean D = Constants.DEBUG; 58 private static final boolean V = Constants.VERBOSE; 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 String extension = null; 139 int dotIndex = filename.indexOf('.'); 140 if (dotIndex < 0) { 141 // should not happen. It must be pre-rejected 142 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); 143 } else { 144 extension = filename.substring(dotIndex); 145 filename = filename.substring(0, dotIndex); 146 } 147 filename = base.getPath() + File.separator + filename; 148 // Generate a unique filename, create the file, return it. 149 String fullfilename = chooseUniquefilename(filename, extension); 150 if (V) Log.v(Constants.TAG, "Generated received filename " + fullfilename); 151 152 if (fullfilename != null) { 153 try { 154 new FileOutputStream(fullfilename).close(); 155 int index = fullfilename.lastIndexOf('/') + 1; 156 // update display name 157 if (index > 0) { 158 String displayName = fullfilename.substring(index); 159 if (V) Log.v(Constants.TAG, "New display name " + displayName); 160 ContentValues updateValues = new ContentValues(); 161 updateValues.put(BluetoothShare.FILENAME_HINT, displayName); 162 context.getContentResolver().update(contentUri, updateValues, null, null); 163 164 } 165 return new BluetoothOppReceiveFileInfo(fullfilename, length, new FileOutputStream( 166 fullfilename), 0); 167 } catch (IOException e) { 168 if (D) Log.e(Constants.TAG, "Error when creating file " + fullfilename); 169 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); 170 } 171 } else { 172 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); 173 } 174 175 } 176 177 private static String chooseUniquefilename(String filename, String extension) { 178 String fullfilename = filename + extension; 179 if (!new File(fullfilename).exists()) { 180 return fullfilename; 181 } 182 filename = filename + Constants.filename_SEQUENCE_SEPARATOR; 183 /* 184 * This number is used to generate partially randomized filenames to 185 * avoid collisions. It starts at 1. The next 9 iterations increment it 186 * by 1 at a time (up to 10). The next 9 iterations increment it by 1 to 187 * 10 (random) at a time. The next 9 iterations increment it by 1 to 100 188 * (random) at a time. ... Up to the point where it increases by 189 * 100000000 at a time. (the maximum value that can be reached is 190 * 1000000000) As soon as a number is reached that generates a filename 191 * that doesn't exist, that filename is used. If the filename coming in 192 * is [base].[ext], the generated filenames are [base]-[sequence].[ext]. 193 */ 194 Random rnd = new Random(SystemClock.uptimeMillis()); 195 int sequence = 1; 196 for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) { 197 for (int iteration = 0; iteration < 9; ++iteration) { 198 fullfilename = filename + sequence + extension; 199 if (!new File(fullfilename).exists()) { 200 return fullfilename; 201 } 202 if (V) Log.v(Constants.TAG, "file with sequence number " + sequence + " exists"); 203 sequence += rnd.nextInt(magnitude) + 1; 204 } 205 } 206 return null; 207 } 208 209 private static String choosefilename(String hint) { 210 String filename = null; 211 212 // First, try to use the hint from the application, if there's one 213 if (filename == null && !(hint == null) && !hint.endsWith("/") && !hint.endsWith("\\")) { 214 // Prevent abuse of path backslashes by converting all backlashes '\\' chars 215 // to UNIX-style forward-slashes '/' 216 hint = hint.replace('\\', '/'); 217 if (V) Log.v(Constants.TAG, "getting filename from hint"); 218 int index = hint.lastIndexOf('/') + 1; 219 if (index > 0) { 220 filename = hint.substring(index); 221 } else { 222 filename = hint; 223 } 224 } 225 return filename; 226 } 227} 228