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;
     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         if (filename == null) {
    139             // should not happen. It must be pre-rejected
    140             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    141         }
    142         String extension = null;
    143         int dotIndex = filename.lastIndexOf(".");
    144         if (dotIndex < 0) {
    145             // should not happen. It must be pre-rejected
    146             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    147         } else {
    148             extension = filename.substring(dotIndex);
    149             filename = filename.substring(0, dotIndex);
    150         }
    151         filename = base.getPath() + File.separator + filename;
    152         // Generate a unique filename, create the file, return it.
    153         String fullfilename = chooseUniquefilename(filename, extension);
    154 
    155         if (!safeCanonicalPath(fullfilename)) {
    156             // If this second check fails, then we better reject the transfer
    157             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    158         }
    159         if (V) Log.v(Constants.TAG, "Generated received filename " + fullfilename);
    160 
    161         if (fullfilename != null) {
    162             try {
    163                 new FileOutputStream(fullfilename).close();
    164                 int index = fullfilename.lastIndexOf('/') + 1;
    165                 // update display name
    166                 if (index > 0) {
    167                     String displayName = fullfilename.substring(index);
    168                     if (V) Log.v(Constants.TAG, "New display name " + displayName);
    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 (D) Log.e(Constants.TAG, "Error when creating file " + fullfilename);
    178                 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    179             }
    180         } else {
    181             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    182         }
    183 
    184     }
    185 
    186     private static boolean safeCanonicalPath(String uniqueFileName) {
    187         try {
    188             File receiveFile = new File(uniqueFileName);
    189             if (sDesiredStoragePath == null) {
    190                 sDesiredStoragePath = Environment.getExternalStorageDirectory().getPath() +
    191                     Constants.DEFAULT_STORE_SUBDIR;
    192             }
    193             String canonicalPath = receiveFile.getCanonicalPath();
    194 
    195             // Check if canonical path is complete - case sensitive-wise
    196             if (!canonicalPath.startsWith(sDesiredStoragePath)) {
    197                 return false;
    198             }
    199 
    200 	    	return true;
    201         } catch (IOException ioe) {
    202             // If an exception is thrown, there might be something wrong with the file.
    203             return false;
    204         }
    205     }
    206 
    207     private static String chooseUniquefilename(String filename, String extension) {
    208         String fullfilename = filename + extension;
    209         if (!new File(fullfilename).exists()) {
    210             return fullfilename;
    211         }
    212         filename = filename + Constants.filename_SEQUENCE_SEPARATOR;
    213         /*
    214          * This number is used to generate partially randomized filenames to
    215          * avoid collisions. It starts at 1. The next 9 iterations increment it
    216          * by 1 at a time (up to 10). The next 9 iterations increment it by 1 to
    217          * 10 (random) at a time. The next 9 iterations increment it by 1 to 100
    218          * (random) at a time. ... Up to the point where it increases by
    219          * 100000000 at a time. (the maximum value that can be reached is
    220          * 1000000000) As soon as a number is reached that generates a filename
    221          * that doesn't exist, that filename is used. If the filename coming in
    222          * is [base].[ext], the generated filenames are [base]-[sequence].[ext].
    223          */
    224         Random rnd = new Random(SystemClock.uptimeMillis());
    225         int sequence = 1;
    226         for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
    227             for (int iteration = 0; iteration < 9; ++iteration) {
    228                 fullfilename = filename + sequence + extension;
    229                 if (!new File(fullfilename).exists()) {
    230                     return fullfilename;
    231                 }
    232                 if (V) Log.v(Constants.TAG, "file with sequence number " + sequence + " exists");
    233                 sequence += rnd.nextInt(magnitude) + 1;
    234             }
    235         }
    236         return null;
    237     }
    238 
    239     private static String choosefilename(String hint) {
    240         String filename = null;
    241 
    242         // First, try to use the hint from the application, if there's one
    243         if (filename == null && !(hint == null) && !hint.endsWith("/") && !hint.endsWith("\\")) {
    244             // Prevent abuse of path backslashes by converting all backlashes '\\' chars
    245             // to UNIX-style forward-slashes '/'
    246             hint = hint.replace('\\', '/');
    247             if (V) Log.v(Constants.TAG, "getting filename from hint");
    248             int index = hint.lastIndexOf('/') + 1;
    249             if (index > 0) {
    250                 filename = hint.substring(index);
    251             } else {
    252                 filename = hint;
    253             }
    254         }
    255         return filename;
    256     }
    257 }
    258