Merge "Download provider change for DRM Forward Lock plugin: to convert .dm files...
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / Helpers.java
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.providers.downloads;
18
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.pm.PackageManager;
22 import android.content.pm.ResolveInfo;
23 import android.net.Uri;
24 import android.os.Environment;
25 import android.os.SystemClock;
26 import android.provider.Downloads;
27 import android.util.Log;
28 import android.webkit.MimeTypeMap;
29
30 import java.io.File;
31 import java.util.Random;
32 import java.util.Set;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35
36 /**
37  * Some helper functions for the download manager
38  */
39 public class Helpers {
40     public static Random sRandom = new Random(SystemClock.uptimeMillis());
41
42     /** Regex used to parse content-disposition headers */
43     private static final Pattern CONTENT_DISPOSITION_PATTERN =
44             Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
45
46     private Helpers() {
47     }
48
49     /*
50      * Parse the Content-Disposition HTTP Header. The format of the header
51      * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
52      * This header provides a filename for content that is going to be
53      * downloaded to the file system. We only support the attachment type.
54      */
55     private static String parseContentDisposition(String contentDisposition) {
56         try {
57             Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
58             if (m.find()) {
59                 return m.group(1);
60             }
61         } catch (IllegalStateException ex) {
62              // This function is defined as returning null when it can't parse the header
63         }
64         return null;
65     }
66
67     /**
68      * Creates a filename (where the file should be saved) from info about a download.
69      */
70     static String generateSaveFile(
71             Context context,
72             String url,
73             String hint,
74             String contentDisposition,
75             String contentLocation,
76             String mimeType,
77             int destination,
78             long contentLength,
79             boolean isPublicApi, StorageManager storageManager) throws StopRequestException {
80         checkCanHandleDownload(context, mimeType, destination, isPublicApi);
81         String path;
82         File base = null;
83         if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
84             path = Uri.parse(hint).getPath();
85         } else {
86             base = storageManager.locateDestinationDirectory(mimeType, destination,
87                     contentLength);
88             path = chooseFilename(url, hint, contentDisposition, contentLocation,
89                                              destination);
90         }
91         storageManager.verifySpace(destination, path, contentLength);
92         path = getFullPath(path, mimeType, destination, base);
93         if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
94             path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
95         }
96         return path;
97     }
98
99     static String getFullPath(String filename, String mimeType, int destination,
100         File base) throws StopRequestException {
101         // Split filename between base and extension
102         // Add an extension if filename does not have one
103         String extension = null;
104         int dotIndex = filename.lastIndexOf('.');
105         boolean missingExtension = dotIndex < 0 || dotIndex < filename.lastIndexOf('/');
106         if (missingExtension) {
107             extension = chooseExtensionFromMimeType(mimeType, true);
108         } else {
109             extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
110             filename = filename.substring(0, dotIndex);
111         }
112
113         boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
114
115         if (base != null) {
116             filename = base.getPath() + File.separator + filename;
117         }
118
119         if (Constants.LOGVV) {
120             Log.v(Constants.TAG, "target file: " + filename + extension);
121         }
122         return chooseUniqueFilename(destination, filename, extension, recoveryDir);
123     }
124
125     private static void checkCanHandleDownload(Context context, String mimeType, int destination,
126             boolean isPublicApi) throws StopRequestException {
127         if (isPublicApi) {
128             return;
129         }
130
131         if (destination == Downloads.Impl.DESTINATION_EXTERNAL
132                 || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
133             if (mimeType == null) {
134                 throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
135                         "external download with no mime type not allowed");
136             }
137             if (!DownloadDrmHelper.isDrmMimeType(context, mimeType)) {
138                 // Check to see if we are allowed to download this file. Only files
139                 // that can be handled by the platform can be downloaded.
140                 // special case DRM files, which we should always allow downloading.
141                 Intent intent = new Intent(Intent.ACTION_VIEW);
142
143                 // We can provide data as either content: or file: URIs,
144                 // so allow both.  (I think it would be nice if we just did
145                 // everything as content: URIs)
146                 // Actually, right now the download manager's UId restrictions
147                 // prevent use from using content: so it's got to be file: or
148                 // nothing
149
150                 PackageManager pm = context.getPackageManager();
151                 intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
152                 ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
153                 //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list);
154
155                 if (ri == null) {
156                     if (Constants.LOGV) {
157                         Log.v(Constants.TAG, "no handler found for type " + mimeType);
158                     }
159                     throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
160                             "no handler found for this download type");
161                 }
162             }
163         }
164     }
165
166     private static String chooseFilename(String url, String hint, String contentDisposition,
167             String contentLocation, int destination) {
168         String filename = null;
169
170         // First, try to use the hint from the application, if there's one
171         if (filename == null && hint != null && !hint.endsWith("/")) {
172             if (Constants.LOGVV) {
173                 Log.v(Constants.TAG, "getting filename from hint");
174             }
175             int index = hint.lastIndexOf('/') + 1;
176             if (index > 0) {
177                 filename = hint.substring(index);
178             } else {
179                 filename = hint;
180             }
181         }
182
183         // If we couldn't do anything with the hint, move toward the content disposition
184         if (filename == null && contentDisposition != null) {
185             filename = parseContentDisposition(contentDisposition);
186             if (filename != null) {
187                 if (Constants.LOGVV) {
188                     Log.v(Constants.TAG, "getting filename from content-disposition");
189                 }
190                 int index = filename.lastIndexOf('/') + 1;
191                 if (index > 0) {
192                     filename = filename.substring(index);
193                 }
194             }
195         }
196
197         // If we still have nothing at this point, try the content location
198         if (filename == null && contentLocation != null) {
199             String decodedContentLocation = Uri.decode(contentLocation);
200             if (decodedContentLocation != null
201                     && !decodedContentLocation.endsWith("/")
202                     && decodedContentLocation.indexOf('?') < 0) {
203                 if (Constants.LOGVV) {
204                     Log.v(Constants.TAG, "getting filename from content-location");
205                 }
206                 int index = decodedContentLocation.lastIndexOf('/') + 1;
207                 if (index > 0) {
208                     filename = decodedContentLocation.substring(index);
209                 } else {
210                     filename = decodedContentLocation;
211                 }
212             }
213         }
214
215         // If all the other http-related approaches failed, use the plain uri
216         if (filename == null) {
217             String decodedUrl = Uri.decode(url);
218             if (decodedUrl != null
219                     && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) {
220                 int index = decodedUrl.lastIndexOf('/') + 1;
221                 if (index > 0) {
222                     if (Constants.LOGVV) {
223                         Log.v(Constants.TAG, "getting filename from uri");
224                     }
225                     filename = decodedUrl.substring(index);
226                 }
227             }
228         }
229
230         // Finally, if couldn't get filename from URI, get a generic filename
231         if (filename == null) {
232             if (Constants.LOGVV) {
233                 Log.v(Constants.TAG, "using default filename");
234             }
235             filename = Constants.DEFAULT_DL_FILENAME;
236         }
237
238         // The VFAT file system is assumed as target for downloads.
239         // Replace invalid characters according to the specifications of VFAT.
240         filename = replaceInvalidVfatCharacters(filename);
241
242         return filename;
243     }
244
245     private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) {
246         String extension = null;
247         if (mimeType != null) {
248             extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
249             if (extension != null) {
250                 if (Constants.LOGVV) {
251                     Log.v(Constants.TAG, "adding extension from type");
252                 }
253                 extension = "." + extension;
254             } else {
255                 if (Constants.LOGVV) {
256                     Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
257                 }
258             }
259         }
260         if (extension == null) {
261             if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
262                 if (mimeType.equalsIgnoreCase("text/html")) {
263                     if (Constants.LOGVV) {
264                         Log.v(Constants.TAG, "adding default html extension");
265                     }
266                     extension = Constants.DEFAULT_DL_HTML_EXTENSION;
267                 } else if (useDefaults) {
268                     if (Constants.LOGVV) {
269                         Log.v(Constants.TAG, "adding default text extension");
270                     }
271                     extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
272                 }
273             } else if (useDefaults) {
274                 if (Constants.LOGVV) {
275                     Log.v(Constants.TAG, "adding default binary extension");
276                 }
277                 extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
278             }
279         }
280         return extension;
281     }
282
283     private static String chooseExtensionFromFilename(String mimeType, int destination,
284             String filename, int lastDotIndex) {
285         String extension = null;
286         if (mimeType != null) {
287             // Compare the last segment of the extension against the mime type.
288             // If there's a mismatch, discard the entire extension.
289             String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
290                     filename.substring(lastDotIndex + 1));
291             if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) {
292                 extension = chooseExtensionFromMimeType(mimeType, false);
293                 if (extension != null) {
294                     if (Constants.LOGVV) {
295                         Log.v(Constants.TAG, "substituting extension from type");
296                     }
297                 } else {
298                     if (Constants.LOGVV) {
299                         Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
300                     }
301                 }
302             }
303         }
304         if (extension == null) {
305             if (Constants.LOGVV) {
306                 Log.v(Constants.TAG, "keeping extension");
307             }
308             extension = filename.substring(lastDotIndex);
309         }
310         return extension;
311     }
312
313     private static String chooseUniqueFilename(int destination, String filename,
314             String extension, boolean recoveryDir) throws StopRequestException {
315         String fullFilename = filename + extension;
316         if (!new File(fullFilename).exists()
317                 && (!recoveryDir ||
318                 (destination != Downloads.Impl.DESTINATION_CACHE_PARTITION &&
319                         destination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION &&
320                         destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE &&
321                         destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING))) {
322             return fullFilename;
323         }
324         filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
325         /*
326         * This number is used to generate partially randomized filenames to avoid
327         * collisions.
328         * It starts at 1.
329         * The next 9 iterations increment it by 1 at a time (up to 10).
330         * The next 9 iterations increment it by 1 to 10 (random) at a time.
331         * The next 9 iterations increment it by 1 to 100 (random) at a time.
332         * ... Up to the point where it increases by 100000000 at a time.
333         * (the maximum value that can be reached is 1000000000)
334         * As soon as a number is reached that generates a filename that doesn't exist,
335         *     that filename is used.
336         * If the filename coming in is [base].[ext], the generated filenames are
337         *     [base]-[sequence].[ext].
338         */
339         int sequence = 1;
340         for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
341             for (int iteration = 0; iteration < 9; ++iteration) {
342                 fullFilename = filename + sequence + extension;
343                 if (!new File(fullFilename).exists()) {
344                     return fullFilename;
345                 }
346                 if (Constants.LOGVV) {
347                     Log.v(Constants.TAG, "file with sequence number " + sequence + " exists");
348                 }
349                 sequence += sRandom.nextInt(magnitude) + 1;
350             }
351         }
352         throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
353                 "failed to generate an unused filename on internal download storage");
354     }
355
356     /**
357      * Returns whether the network is available
358      */
359     public static boolean isNetworkAvailable(SystemFacade system) {
360         return system.getActiveNetworkType() != null;
361     }
362
363     /**
364      * Checks whether the filename looks legitimate
365      */
366     static boolean isFilenameValid(String filename, File downloadsDataDir) {
367         filename = filename.replaceFirst("/+", "/"); // normalize leading slashes
368         return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
369                 || filename.startsWith(downloadsDataDir.toString())
370                 || filename.startsWith(Environment.getExternalStorageDirectory().toString());
371     }
372
373     /**
374      * Checks whether this looks like a legitimate selection parameter
375      */
376     public static void validateSelection(String selection, Set<String> allowedColumns) {
377         try {
378             if (selection == null || selection.isEmpty()) {
379                 return;
380             }
381             Lexer lexer = new Lexer(selection, allowedColumns);
382             parseExpression(lexer);
383             if (lexer.currentToken() != Lexer.TOKEN_END) {
384                 throw new IllegalArgumentException("syntax error");
385             }
386         } catch (RuntimeException ex) {
387             if (Constants.LOGV) {
388                 Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
389             } else if (false) {
390                 Log.d(Constants.TAG, "invalid selection triggered " + ex);
391             }
392             throw ex;
393         }
394
395     }
396
397     // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] *
398     //             | statement [AND_OR expression]*
399     private static void parseExpression(Lexer lexer) {
400         for (;;) {
401             // ( expression )
402             if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) {
403                 lexer.advance();
404                 parseExpression(lexer);
405                 if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) {
406                     throw new IllegalArgumentException("syntax error, unmatched parenthese");
407                 }
408                 lexer.advance();
409             } else {
410                 // statement
411                 parseStatement(lexer);
412             }
413             if (lexer.currentToken() != Lexer.TOKEN_AND_OR) {
414                 break;
415             }
416             lexer.advance();
417         }
418     }
419
420     // statement <- COLUMN COMPARE VALUE
421     //            | COLUMN IS NULL
422     private static void parseStatement(Lexer lexer) {
423         // both possibilities start with COLUMN
424         if (lexer.currentToken() != Lexer.TOKEN_COLUMN) {
425             throw new IllegalArgumentException("syntax error, expected column name");
426         }
427         lexer.advance();
428
429         // statement <- COLUMN COMPARE VALUE
430         if (lexer.currentToken() == Lexer.TOKEN_COMPARE) {
431             lexer.advance();
432             if (lexer.currentToken() != Lexer.TOKEN_VALUE) {
433                 throw new IllegalArgumentException("syntax error, expected quoted string");
434             }
435             lexer.advance();
436             return;
437         }
438
439         // statement <- COLUMN IS NULL
440         if (lexer.currentToken() == Lexer.TOKEN_IS) {
441             lexer.advance();
442             if (lexer.currentToken() != Lexer.TOKEN_NULL) {
443                 throw new IllegalArgumentException("syntax error, expected NULL");
444             }
445             lexer.advance();
446             return;
447         }
448
449         // didn't get anything good after COLUMN
450         throw new IllegalArgumentException("syntax error after column name");
451     }
452
453     /**
454      * A simple lexer that recognizes the words of our restricted subset of SQL where clauses
455      */
456     private static class Lexer {
457         public static final int TOKEN_START = 0;
458         public static final int TOKEN_OPEN_PAREN = 1;
459         public static final int TOKEN_CLOSE_PAREN = 2;
460         public static final int TOKEN_AND_OR = 3;
461         public static final int TOKEN_COLUMN = 4;
462         public static final int TOKEN_COMPARE = 5;
463         public static final int TOKEN_VALUE = 6;
464         public static final int TOKEN_IS = 7;
465         public static final int TOKEN_NULL = 8;
466         public static final int TOKEN_END = 9;
467
468         private final String mSelection;
469         private final Set<String> mAllowedColumns;
470         private int mOffset = 0;
471         private int mCurrentToken = TOKEN_START;
472         private final char[] mChars;
473
474         public Lexer(String selection, Set<String> allowedColumns) {
475             mSelection = selection;
476             mAllowedColumns = allowedColumns;
477             mChars = new char[mSelection.length()];
478             mSelection.getChars(0, mChars.length, mChars, 0);
479             advance();
480         }
481
482         public int currentToken() {
483             return mCurrentToken;
484         }
485
486         public void advance() {
487             char[] chars = mChars;
488
489             // consume whitespace
490             while (mOffset < chars.length && chars[mOffset] == ' ') {
491                 ++mOffset;
492             }
493
494             // end of input
495             if (mOffset == chars.length) {
496                 mCurrentToken = TOKEN_END;
497                 return;
498             }
499
500             // "("
501             if (chars[mOffset] == '(') {
502                 ++mOffset;
503                 mCurrentToken = TOKEN_OPEN_PAREN;
504                 return;
505             }
506
507             // ")"
508             if (chars[mOffset] == ')') {
509                 ++mOffset;
510                 mCurrentToken = TOKEN_CLOSE_PAREN;
511                 return;
512             }
513
514             // "?"
515             if (chars[mOffset] == '?') {
516                 ++mOffset;
517                 mCurrentToken = TOKEN_VALUE;
518                 return;
519             }
520
521             // "=" and "=="
522             if (chars[mOffset] == '=') {
523                 ++mOffset;
524                 mCurrentToken = TOKEN_COMPARE;
525                 if (mOffset < chars.length && chars[mOffset] == '=') {
526                     ++mOffset;
527                 }
528                 return;
529             }
530
531             // ">" and ">="
532             if (chars[mOffset] == '>') {
533                 ++mOffset;
534                 mCurrentToken = TOKEN_COMPARE;
535                 if (mOffset < chars.length && chars[mOffset] == '=') {
536                     ++mOffset;
537                 }
538                 return;
539             }
540
541             // "<", "<=" and "<>"
542             if (chars[mOffset] == '<') {
543                 ++mOffset;
544                 mCurrentToken = TOKEN_COMPARE;
545                 if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) {
546                     ++mOffset;
547                 }
548                 return;
549             }
550
551             // "!="
552             if (chars[mOffset] == '!') {
553                 ++mOffset;
554                 mCurrentToken = TOKEN_COMPARE;
555                 if (mOffset < chars.length && chars[mOffset] == '=') {
556                     ++mOffset;
557                     return;
558                 }
559                 throw new IllegalArgumentException("Unexpected character after !");
560             }
561
562             // columns and keywords
563             // first look for anything that looks like an identifier or a keyword
564             //     and then recognize the individual words.
565             // no attempt is made at discarding sequences of underscores with no alphanumeric
566             //     characters, even though it's not clear that they'd be legal column names.
567             if (isIdentifierStart(chars[mOffset])) {
568                 int startOffset = mOffset;
569                 ++mOffset;
570                 while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) {
571                     ++mOffset;
572                 }
573                 String word = mSelection.substring(startOffset, mOffset);
574                 if (mOffset - startOffset <= 4) {
575                     if (word.equals("IS")) {
576                         mCurrentToken = TOKEN_IS;
577                         return;
578                     }
579                     if (word.equals("OR") || word.equals("AND")) {
580                         mCurrentToken = TOKEN_AND_OR;
581                         return;
582                     }
583                     if (word.equals("NULL")) {
584                         mCurrentToken = TOKEN_NULL;
585                         return;
586                     }
587                 }
588                 if (mAllowedColumns.contains(word)) {
589                     mCurrentToken = TOKEN_COLUMN;
590                     return;
591                 }
592                 throw new IllegalArgumentException("unrecognized column or keyword");
593             }
594
595             // quoted strings
596             if (chars[mOffset] == '\'') {
597                 ++mOffset;
598                 while (mOffset < chars.length) {
599                     if (chars[mOffset] == '\'') {
600                         if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') {
601                             ++mOffset;
602                         } else {
603                             break;
604                         }
605                     }
606                     ++mOffset;
607                 }
608                 if (mOffset == chars.length) {
609                     throw new IllegalArgumentException("unterminated string");
610                 }
611                 ++mOffset;
612                 mCurrentToken = TOKEN_VALUE;
613                 return;
614             }
615
616             // anything we don't recognize
617             throw new IllegalArgumentException("illegal character: " + chars[mOffset]);
618         }
619
620         private static final boolean isIdentifierStart(char c) {
621             return c == '_' ||
622                     (c >= 'A' && c <= 'Z') ||
623                     (c >= 'a' && c <= 'z');
624         }
625
626         private static final boolean isIdentifierChar(char c) {
627             return c == '_' ||
628                     (c >= 'A' && c <= 'Z') ||
629                     (c >= 'a' && c <= 'z') ||
630                     (c >= '0' && c <= '9');
631         }
632     }
633
634     /**
635      * Replace invalid filename characters according to
636      * specifications of the VFAT.
637      * @note Package-private due to testing.
638      */
639     private static String replaceInvalidVfatCharacters(String filename) {
640         final char START_CTRLCODE = 0x00;
641         final char END_CTRLCODE = 0x1f;
642         final char QUOTEDBL = 0x22;
643         final char ASTERISK = 0x2A;
644         final char SLASH = 0x2F;
645         final char COLON = 0x3A;
646         final char LESS = 0x3C;
647         final char GREATER = 0x3E;
648         final char QUESTION = 0x3F;
649         final char BACKSLASH = 0x5C;
650         final char BAR = 0x7C;
651         final char DEL = 0x7F;
652         final char UNDERSCORE = 0x5F;
653
654         StringBuffer sb = new StringBuffer();
655         char ch;
656         boolean isRepetition = false;
657         for (int i = 0; i < filename.length(); i++) {
658             ch = filename.charAt(i);
659             if ((START_CTRLCODE <= ch &&
660                 ch <= END_CTRLCODE) ||
661                 ch == QUOTEDBL ||
662                 ch == ASTERISK ||
663                 ch == SLASH ||
664                 ch == COLON ||
665                 ch == LESS ||
666                 ch == GREATER ||
667                 ch == QUESTION ||
668                 ch == BACKSLASH ||
669                 ch == BAR ||
670                 ch == DEL){
671                 if (!isRepetition) {
672                     sb.append(UNDERSCORE);
673                     isRepetition = true;
674                 }
675             } else {
676                 sb.append(ch);
677                 isRepetition = false;
678             }
679         }
680         return sb.toString();
681     }
682 }