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