BluetoothOppReceiveFileInfo.java revision 9c11dad35ee454d303b4f56a87042fc094bb61d8
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
33package com.android.bluetooth.opp;
34
35import java.io.File;
36import java.io.FileOutputStream;
37import java.io.IOException;
38import java.util.Random;
39import java.io.UnsupportedEncodingException;
40import java.nio.charset.Charset;
41
42import android.content.ContentResolver;
43import android.content.ContentValues;
44import android.content.Context;
45import android.database.Cursor;
46import android.net.Uri;
47import android.os.Environment;
48import android.os.StatFs;
49import android.os.SystemClock;
50import 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 */
57public 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