Home | History | Annotate | Download | only in opp
      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, 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