| /* |
| * Copyright (c) 2008-2009, Motorola, Inc. |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright notice, |
| * this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * |
| * - Neither the name of the Motorola, Inc. nor the names of its contributors |
| * may be used to endorse or promote products derived from this software |
| * without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
| * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| * POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package com.android.bluetooth.opp; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.Random; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Environment; |
| import android.os.StatFs; |
| import android.os.SystemClock; |
| import android.util.Log; |
| |
| /** |
| * This class stores information about a single receiving file. It will only be |
| * used for inbounds share, e.g. receive a file to determine a correct save file |
| * name |
| */ |
| public class BluetoothOppReceiveFileInfo { |
| private static final boolean D = Constants.DEBUG; |
| private static final boolean V = Constants.VERBOSE; |
| private static String sDesiredStoragePath = null; |
| |
| /** absolute store file name */ |
| public String mFileName; |
| |
| public long mLength; |
| |
| public FileOutputStream mOutputStream; |
| |
| public int mStatus; |
| |
| public String mData; |
| |
| public BluetoothOppReceiveFileInfo(String data, long length, int status) { |
| mData = data; |
| mStatus = status; |
| mLength = length; |
| } |
| |
| public BluetoothOppReceiveFileInfo(String filename, long length, FileOutputStream outputStream, |
| int status) { |
| mFileName = filename; |
| mOutputStream = outputStream; |
| mStatus = status; |
| mLength = length; |
| } |
| |
| public BluetoothOppReceiveFileInfo(int status) { |
| this(null, 0, null, status); |
| } |
| |
| // public static final int BATCH_STATUS_CANCELED = 4; |
| public static BluetoothOppReceiveFileInfo generateFileInfo(Context context, int id) { |
| |
| ContentResolver contentResolver = context.getContentResolver(); |
| Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id); |
| String filename = null, hint = null, mimeType = null; |
| long length = 0; |
| Cursor metadataCursor = contentResolver.query(contentUri, new String[] { |
| BluetoothShare.FILENAME_HINT, BluetoothShare.TOTAL_BYTES, BluetoothShare.MIMETYPE |
| }, null, null, null); |
| if (metadataCursor != null) { |
| try { |
| if (metadataCursor.moveToFirst()) { |
| hint = metadataCursor.getString(0); |
| length = metadataCursor.getInt(1); |
| mimeType = metadataCursor.getString(2); |
| } |
| } finally { |
| metadataCursor.close(); |
| } |
| } |
| |
| File base = null; |
| StatFs stat = null; |
| |
| if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { |
| String root = Environment.getExternalStorageDirectory().getPath(); |
| base = new File(root + Constants.DEFAULT_STORE_SUBDIR); |
| if (!base.isDirectory() && !base.mkdir()) { |
| if (D) Log.d(Constants.TAG, "Receive File aborted - can't create base directory " |
| + base.getPath()); |
| return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); |
| } |
| stat = new StatFs(base.getPath()); |
| } else { |
| if (D) Log.d(Constants.TAG, "Receive File aborted - no external storage"); |
| return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_ERROR_NO_SDCARD); |
| } |
| |
| /* |
| * Check whether there's enough space on the target filesystem to save |
| * the file. Put a bit of margin (in case creating the file grows the |
| * system by a few blocks). |
| */ |
| if (stat.getBlockSize() * ((long)stat.getAvailableBlocks() - 4) < length) { |
| if (D) Log.d(Constants.TAG, "Receive File aborted - not enough free space"); |
| return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_ERROR_SDCARD_FULL); |
| } |
| |
| filename = choosefilename(hint); |
| if (filename == null) { |
| // should not happen. It must be pre-rejected |
| return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); |
| } |
| String extension = null; |
| int dotIndex = filename.lastIndexOf("."); |
| if (dotIndex < 0) { |
| if (mimeType == null) { |
| // should not happen. It must be pre-rejected |
| return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); |
| } else { |
| extension = ""; |
| } |
| } else { |
| extension = filename.substring(dotIndex); |
| filename = filename.substring(0, dotIndex); |
| } |
| filename = base.getPath() + File.separator + filename; |
| // Generate a unique filename, create the file, return it. |
| String fullfilename = chooseUniquefilename(filename, extension); |
| |
| if (!safeCanonicalPath(fullfilename)) { |
| // If this second check fails, then we better reject the transfer |
| return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); |
| } |
| if (V) Log.v(Constants.TAG, "Generated received filename " + fullfilename); |
| |
| if (fullfilename != null) { |
| try { |
| new FileOutputStream(fullfilename).close(); |
| int index = fullfilename.lastIndexOf('/') + 1; |
| // update display name |
| if (index > 0) { |
| String displayName = fullfilename.substring(index); |
| if (V) Log.v(Constants.TAG, "New display name " + displayName); |
| ContentValues updateValues = new ContentValues(); |
| updateValues.put(BluetoothShare.FILENAME_HINT, displayName); |
| context.getContentResolver().update(contentUri, updateValues, null, null); |
| |
| } |
| return new BluetoothOppReceiveFileInfo(fullfilename, length, new FileOutputStream( |
| fullfilename), 0); |
| } catch (IOException e) { |
| if (D) Log.e(Constants.TAG, "Error when creating file " + fullfilename); |
| return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); |
| } |
| } else { |
| return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR); |
| } |
| |
| } |
| |
| private static boolean safeCanonicalPath(String uniqueFileName) { |
| try { |
| File receiveFile = new File(uniqueFileName); |
| if (sDesiredStoragePath == null) { |
| sDesiredStoragePath = Environment.getExternalStorageDirectory().getPath() + |
| Constants.DEFAULT_STORE_SUBDIR; |
| } |
| String canonicalPath = receiveFile.getCanonicalPath(); |
| |
| // Check if canonical path is complete - case sensitive-wise |
| if (!canonicalPath.startsWith(sDesiredStoragePath)) { |
| return false; |
| } |
| |
| return true; |
| } catch (IOException ioe) { |
| // If an exception is thrown, there might be something wrong with the file. |
| return false; |
| } |
| } |
| |
| private static String chooseUniquefilename(String filename, String extension) { |
| String fullfilename = filename + extension; |
| if (!new File(fullfilename).exists()) { |
| return fullfilename; |
| } |
| filename = filename + Constants.filename_SEQUENCE_SEPARATOR; |
| /* |
| * This number is used to generate partially randomized filenames to |
| * avoid collisions. It starts at 1. The next 9 iterations increment it |
| * by 1 at a time (up to 10). The next 9 iterations increment it by 1 to |
| * 10 (random) at a time. The next 9 iterations increment it by 1 to 100 |
| * (random) at a time. ... Up to the point where it increases by |
| * 100000000 at a time. (the maximum value that can be reached is |
| * 1000000000) As soon as a number is reached that generates a filename |
| * that doesn't exist, that filename is used. If the filename coming in |
| * is [base].[ext], the generated filenames are [base]-[sequence].[ext]. |
| */ |
| Random rnd = new Random(SystemClock.uptimeMillis()); |
| int sequence = 1; |
| for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) { |
| for (int iteration = 0; iteration < 9; ++iteration) { |
| fullfilename = filename + sequence + extension; |
| if (!new File(fullfilename).exists()) { |
| return fullfilename; |
| } |
| if (V) Log.v(Constants.TAG, "file with sequence number " + sequence + " exists"); |
| sequence += rnd.nextInt(magnitude) + 1; |
| } |
| } |
| return null; |
| } |
| |
| private static String choosefilename(String hint) { |
| String filename = null; |
| |
| // First, try to use the hint from the application, if there's one |
| if (filename == null && !(hint == null) && !hint.endsWith("/") && !hint.endsWith("\\")) { |
| // Prevent abuse of path backslashes by converting all backlashes '\\' chars |
| // to UNIX-style forward-slashes '/' |
| hint = hint.replace('\\', '/'); |
| // Convert all whitespace characters to spaces. |
| hint = hint.replaceAll("\\s", " "); |
| // Replace illegal fat filesystem characters from the |
| // filename hint i.e. :"<>*?| with something safe. |
| hint = hint.replaceAll("[:\"<>*?|]", "_"); |
| if (V) Log.v(Constants.TAG, "getting filename from hint"); |
| int index = hint.lastIndexOf('/') + 1; |
| if (index > 0) { |
| filename = hint.substring(index); |
| } else { |
| filename = hint; |
| } |
| } |
| return filename; |
| } |
| } |