BluetoothOppReceiveFileInfo.java revision 09e9cba205af60b3f42e7a4d891a7d1392e1f2a5
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 53 * file. It will only be used for inbounds share, e.g. receive a file to 54 * determine a correct save file name 55 */ 56public class BluetoothOppReceiveFileInfo { 57 58 /** absolute store file name */ 59 public String mFileName; 60 61 public long mLength; 62 63 public FileOutputStream mOutputStream; 64 65 public int mStatus; 66 67 public String mData; 68 69 public BluetoothOppReceiveFileInfo(String data, long length, int status) { 70 mData = data; 71 mStatus = status; 72 mLength = length; 73 } 74 75 public BluetoothOppReceiveFileInfo(String filename, long length, FileOutputStream outputStream, 76 int status) { 77 mFileName = filename; 78 mOutputStream = outputStream; 79 mStatus = status; 80 mLength = length; 81 } 82 83 public BluetoothOppReceiveFileInfo(int status) { 84 this(null, 0, null, status); 85 } 86 87 // public static final int BATCH_STATUS_CANCELED = 4; 88 public static BluetoothOppReceiveFileInfo generateFileInfo(Context context, int id) { 89 90 ContentResolver contentResolver = context.getContentResolver(); 91 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id); 92 String filename = null, hint = null; 93 long length = 0; 94 Cursor metadataCursor = contentResolver.query(contentUri, new String[] { 95 BluetoothShare.FILENAME_HINT, BluetoothShare.TOTAL_BYTES, BluetoothShare.MIMETYPE 96 }, null, null, null); 97 if (metadataCursor != null) { 98 try { 99 if (metadataCursor.moveToFirst()) { 100 hint = metadataCursor.getString(0); 101 length = metadataCursor.getInt(1); 102 } 103 } finally { 104 metadataCursor.close(); 105 } 106 } 107 108 File base = null; 109 StatFs stat = null; 110 111 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 112 String root = Environment.getExternalStorageDirectory().getPath(); 113 base = new File(root + Constants.DEFAULT_STORE_SUBDIR); 114 if (!base.isDirectory() && !base.mkdir()) { 115 if (Constants.LOGV) { 116 Log.v(Constants.TAG, "Receive File aborted - can't create base directory " 117 + base.getPath()); 118 } 119 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); 120 } 121 stat = new StatFs(base.getPath()); 122 } else { 123 if (Constants.LOGV) { 124 Log.v(Constants.TAG, "Receive File aborted - no external storage"); 125 } 126 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_ERROR_NO_SDCARD); 127 } 128 129 /* 130 * Check whether there's enough space on the target filesystem to save 131 * the file. Put a bit of margin (in case creating the file grows the 132 * system by a few blocks). 133 */ 134 if (stat.getBlockSize() * ((long)stat.getAvailableBlocks() - 4) < length) { 135 if (Constants.LOGV) { 136 Log.v(Constants.TAG, "Receive File aborted - not enough free space"); 137 } 138 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_ERROR_SDCARD_FULL); 139 } 140 141 filename = choosefilename(hint); 142 String extension = null; 143 int dotIndex = filename.indexOf('.'); 144 if (dotIndex < 0) { 145 // should not happen. It must be pre-rejected 146 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); 147 } else { 148 filename = filename.substring(0, dotIndex); 149 } 150 extension = filename.substring(dotIndex); 151 filename = base.getPath() + File.separator + filename; 152 153 // Generate a unique filename, create the file, return it. 154 String fullfilename = chooseUniquefilename(filename, extension); 155 if (Constants.LOGVV) { 156 Log.v(Constants.TAG, "Generated received filename " + fullfilename); 157 } 158 159 if (fullfilename != null) { 160 try { 161 new FileOutputStream(fullfilename).close(); 162 int index = fullfilename.lastIndexOf('/') + 1; 163 // update display name 164 if (index > 0) { 165 String displayName = fullfilename.substring(index); 166 if (Constants.LOGVV) { 167 Log.v(Constants.TAG, "New display name " + displayName); 168 } 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 (Constants.LOGV) { 178 Log.e(Constants.TAG, "Error when creating file " + fullfilename); 179 } 180 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); 181 } 182 } else { 183 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); 184 } 185 186 } 187 188 private static String chooseUniquefilename(String filename, String extension) { 189 String fullfilename = filename + extension; 190 if (!new File(fullfilename).exists()) { 191 return fullfilename; 192 } 193 filename = filename + Constants.filename_SEQUENCE_SEPARATOR; 194 /* 195 * This number is used to generate partially randomized filenames to 196 * avoid collisions. It starts at 1. The next 9 iterations increment it 197 * by 1 at a time (up to 10). The next 9 iterations increment it by 1 to 198 * 10 (random) at a time. The next 9 iterations increment it by 1 to 100 199 * (random) at a time. ... Up to the point where it increases by 200 * 100000000 at a time. (the maximum value that can be reached is 201 * 1000000000) As soon as a number is reached that generates a filename 202 * that doesn't exist, that filename is used. If the filename coming in 203 * is [base].[ext], the generated filenames are [base]-[sequence].[ext]. 204 */ 205 Random rnd = new Random(SystemClock.uptimeMillis()); 206 int sequence = 1; 207 for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) { 208 for (int iteration = 0; iteration < 9; ++iteration) { 209 fullfilename = filename + sequence + extension; 210 if (!new File(fullfilename).exists()) { 211 return fullfilename; 212 } 213 if (Constants.LOGVV) { 214 Log.v(Constants.TAG, "file with sequence number " + sequence + " exists"); 215 } 216 sequence += rnd.nextInt(magnitude) + 1; 217 } 218 } 219 return null; 220 } 221 222 private static String choosefilename(String hint) { 223 String filename = null; 224 225 // First, try to use the hint from the application, if there's one 226 if (filename == null && hint != null && !hint.endsWith("/")) { 227 if (Constants.LOGVV) { 228 Log.v(Constants.TAG, "getting filename from hint"); 229 } 230 int index = hint.lastIndexOf('/') + 1; 231 if (index > 0) { 232 filename = hint.substring(index); 233 } else { 234 filename = hint; 235 } 236 } 237 return filename; 238 } 239} 240