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 import java.io.UnsupportedEncodingException;
     40 import java.nio.charset.Charset;
     41 
     42 import android.content.ContentResolver;
     43 import android.content.ContentValues;
     44 import android.content.Context;
     45 import android.database.Cursor;
     46 import android.net.Uri;
     47 import android.os.Environment;
     48 import android.os.StatFs;
     49 import android.os.SystemClock;
     50 import android.util.Log;
     51 
     52 /**
     53  * This class stores information about a single receiving file. It will only be
     54  * used for inbounds share, e.g. receive a file to determine a correct save file
     55  * name
     56  */
     57 public class BluetoothOppReceiveFileInfo {
     58     private static final boolean D = Constants.DEBUG;
     59     private static final boolean V = Constants.VERBOSE;
     60     private static String sDesiredStoragePath = null;
     61 
     62     /* To truncate the name of the received file if the length exceeds 245 */
     63     private static final int OPP_LENGTH_OF_FILE_NAME = 244;
     64 
     65 
     66     /** absolute store file name */
     67     public String mFileName;
     68 
     69     public long mLength;
     70 
     71     public FileOutputStream mOutputStream;
     72 
     73     public int mStatus;
     74 
     75     public String mData;
     76 
     77     public BluetoothOppReceiveFileInfo(String data, long length, int status) {
     78         mData = data;
     79         mStatus = status;
     80         mLength = length;
     81     }
     82 
     83     public BluetoothOppReceiveFileInfo(String filename, long length, FileOutputStream outputStream,
     84             int status) {
     85         mFileName = filename;
     86         mOutputStream = outputStream;
     87         mStatus = status;
     88         mLength = length;
     89     }
     90 
     91     public BluetoothOppReceiveFileInfo(int status) {
     92         this(null, 0, null, status);
     93     }
     94 
     95     // public static final int BATCH_STATUS_CANCELED = 4;
     96     public static BluetoothOppReceiveFileInfo generateFileInfo(Context context, int id) {
     97 
     98         ContentResolver contentResolver = context.getContentResolver();
     99         Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id);
    100         String filename = null, hint = null, mimeType = null;
    101         long length = 0;
    102         Cursor metadataCursor = contentResolver.query(contentUri, new String[] {
    103                 BluetoothShare.FILENAME_HINT, BluetoothShare.TOTAL_BYTES, BluetoothShare.MIMETYPE
    104         }, null, null, null);
    105         if (metadataCursor != null) {
    106             try {
    107                 if (metadataCursor.moveToFirst()) {
    108                     hint = metadataCursor.getString(0);
    109                     length = metadataCursor.getLong(1);
    110                     mimeType = metadataCursor.getString(2);
    111                 }
    112             } finally {
    113                 metadataCursor.close();
    114             }
    115         }
    116 
    117         File base = null;
    118         StatFs stat = null;
    119 
    120         if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    121             String root = Environment.getExternalStorageDirectory().getPath();
    122             base = new File(root + Constants.DEFAULT_STORE_SUBDIR);
    123             if (!base.isDirectory() && !base.mkdir()) {
    124                 if (D) Log.d(Constants.TAG, "Receive File aborted - can't create base directory "
    125                             + base.getPath());
    126                 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    127             }
    128             stat = new StatFs(base.getPath());
    129         } else {
    130             if (D) Log.d(Constants.TAG, "Receive File aborted - no external storage");
    131             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_ERROR_NO_SDCARD);
    132         }
    133 
    134         /*
    135          * Check whether there's enough space on the target filesystem to save
    136          * the file. Put a bit of margin (in case creating the file grows the
    137          * system by a few blocks).
    138          */
    139         if (stat.getBlockSizeLong() * (stat.getAvailableBlocksLong() - 4) < length) {
    140             if (D) Log.d(Constants.TAG, "Receive File aborted - not enough free space");
    141             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_ERROR_SDCARD_FULL);
    142         }
    143 
    144         filename = choosefilename(hint);
    145         if (filename == null) {
    146             // should not happen. It must be pre-rejected
    147             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    148         }
    149         String extension = null;
    150         int dotIndex = filename.lastIndexOf(".");
    151         if (dotIndex < 0) {
    152             if (mimeType == null) {
    153                 // should not happen. It must be pre-rejected
    154                 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    155             } else {
    156                 extension = "";
    157             }
    158         } else {
    159             extension = filename.substring(dotIndex);
    160             filename = filename.substring(0, dotIndex);
    161         }
    162         if (D) Log.d(Constants.TAG, " File Name " + filename);
    163 
    164         if (filename.getBytes().length > OPP_LENGTH_OF_FILE_NAME) {
    165           /* Including extn of the file, Linux supports 255 character as a maximum length of the
    166            * file name to be created. Hence, Instead of sending OBEX_HTTP_INTERNAL_ERROR,
    167            * as a response, truncate the length of the file name and save it. This check majorly
    168            * helps in the case of vcard, where Phone book app supports contact name to be saved
    169            * more than 255 characters, But the server rejects the card just because the length of
    170            * vcf file name received exceeds 255 Characters.
    171            */
    172               Log.i(Constants.TAG, " File Name Length :" + filename.length());
    173               Log.i(Constants.TAG, " File Name Length in Bytes:" + filename.getBytes().length);
    174 
    175           try {
    176               byte[] oldfilename = filename.getBytes("UTF-8");
    177               byte[] newfilename = new byte[OPP_LENGTH_OF_FILE_NAME];
    178               System.arraycopy(oldfilename, 0, newfilename, 0, OPP_LENGTH_OF_FILE_NAME);
    179               filename = new String(newfilename, "UTF-8");
    180           } catch (UnsupportedEncodingException e) {
    181               Log.e(Constants.TAG, "Exception: " + e);
    182           }
    183           if (D) Log.d(Constants.TAG, "File name is too long. Name is truncated as: " + filename);
    184         }
    185 
    186         filename = base.getPath() + File.separator + filename;
    187         // Generate a unique filename, create the file, return it.
    188         String fullfilename = chooseUniquefilename(filename, extension);
    189 
    190         if (!safeCanonicalPath(fullfilename)) {
    191             // If this second check fails, then we better reject the transfer
    192             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    193         }
    194         if (V) Log.v(Constants.TAG, "Generated received filename " + fullfilename);
    195 
    196         if (fullfilename != null) {
    197             try {
    198                 new FileOutputStream(fullfilename).close();
    199                 int index = fullfilename.lastIndexOf('/') + 1;
    200                 // update display name
    201                 if (index > 0) {
    202                     String displayName = fullfilename.substring(index);
    203                     if (V) Log.v(Constants.TAG, "New display name " + displayName);
    204                     ContentValues updateValues = new ContentValues();
    205                     updateValues.put(BluetoothShare.FILENAME_HINT, displayName);
    206                     context.getContentResolver().update(contentUri, updateValues, null, null);
    207 
    208                 }
    209                 return new BluetoothOppReceiveFileInfo(fullfilename, length, new FileOutputStream(
    210                         fullfilename), 0);
    211             } catch (IOException e) {
    212                 if (D) Log.e(Constants.TAG, "Error when creating file " + fullfilename);
    213                 return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    214             }
    215         } else {
    216             return new BluetoothOppReceiveFileInfo(BluetoothShare.STATUS_FILE_ERROR);
    217         }
    218 
    219     }
    220 
    221     private static boolean safeCanonicalPath(String uniqueFileName) {
    222         try {
    223             File receiveFile = new File(uniqueFileName);
    224             if (sDesiredStoragePath == null) {
    225                 sDesiredStoragePath = Environment.getExternalStorageDirectory().getPath() +
    226                     Constants.DEFAULT_STORE_SUBDIR;
    227             }
    228             String canonicalPath = receiveFile.getCanonicalPath();
    229 
    230             // Check if canonical path is complete - case sensitive-wise
    231             if (!canonicalPath.startsWith(sDesiredStoragePath)) {
    232                 return false;
    233             }
    234 
    235             return true;
    236         } catch (IOException ioe) {
    237             // If an exception is thrown, there might be something wrong with the file.
    238             return false;
    239         }
    240     }
    241 
    242     private static String chooseUniquefilename(String filename, String extension) {
    243         String fullfilename = filename + extension;
    244         if (!new File(fullfilename).exists()) {
    245             return fullfilename;
    246         }
    247         filename = filename + Constants.filename_SEQUENCE_SEPARATOR;
    248         /*
    249          * This number is used to generate partially randomized filenames to
    250          * avoid collisions. It starts at 1. The next 9 iterations increment it
    251          * by 1 at a time (up to 10). The next 9 iterations increment it by 1 to
    252          * 10 (random) at a time. The next 9 iterations increment it by 1 to 100
    253          * (random) at a time. ... Up to the point where it increases by
    254          * 100000000 at a time. (the maximum value that can be reached is
    255          * 1000000000) As soon as a number is reached that generates a filename
    256          * that doesn't exist, that filename is used. If the filename coming in
    257          * is [base].[ext], the generated filenames are [base]-[sequence].[ext].
    258          */
    259         Random rnd = new Random(SystemClock.uptimeMillis());
    260         int sequence = 1;
    261         for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
    262             for (int iteration = 0; iteration < 9; ++iteration) {
    263                 fullfilename = filename + sequence + extension;
    264                 if (!new File(fullfilename).exists()) {
    265                     return fullfilename;
    266                 }
    267                 if (V) Log.v(Constants.TAG, "file with sequence number " + sequence + " exists");
    268                 sequence += rnd.nextInt(magnitude) + 1;
    269             }
    270         }
    271         return null;
    272     }
    273 
    274     private static String choosefilename(String hint) {
    275         String filename = null;
    276 
    277         // First, try to use the hint from the application, if there's one
    278         if (filename == null && !(hint == null) && !hint.endsWith("/") && !hint.endsWith("\\")) {
    279             // Prevent abuse of path backslashes by converting all backlashes '\\' chars
    280             // to UNIX-style forward-slashes '/'
    281             hint = hint.replace('\\', '/');
    282             // Convert all whitespace characters to spaces.
    283             hint = hint.replaceAll("\\s", " ");
    284             // Replace illegal fat filesystem characters from the
    285             // filename hint i.e. :"<>*?| with something safe.
    286             hint = hint.replaceAll("[:\"<>*?|]", "_");
    287             if (V) Log.v(Constants.TAG, "getting filename from hint");
    288             int index = hint.lastIndexOf('/') + 1;
    289             if (index > 0) {
    290                 filename = hint.substring(index);
    291             } else {
    292                 filename = hint;
    293             }
    294         }
    295         return filename;
    296     }
    297 }
    298