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