OSDN Git Service

EMBMS API tweaks
[android-x86/frameworks-base.git] / telephony / java / android / telephony / mbms / MbmsDownloadReceiver.java
1 /*
2  * Copyright (C) 2017 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 android.telephony.mbms;
18
19 import android.annotation.SystemApi;
20 import android.content.BroadcastReceiver;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.ActivityInfo;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ResolveInfo;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.telephony.MbmsDownloadSession;
31 import android.telephony.mbms.vendor.VendorUtils;
32 import android.util.Log;
33
34 import java.io.File;
35 import java.io.FileFilter;
36 import java.io.IOException;
37 import java.nio.file.FileSystems;
38 import java.nio.file.Files;
39 import java.nio.file.Path;
40 import java.nio.file.StandardCopyOption;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.Objects;
44 import java.util.UUID;
45
46 /**
47  * The {@link BroadcastReceiver} responsible for handling intents sent from the middleware. Apps
48  * that wish to download using MBMS APIs should declare this class in their AndroidManifest.xml as
49  * follows:
50 <pre>{@code
51 <receiver
52     android:name="android.telephony.mbms.MbmsDownloadReceiver"
53     android:permission="android.permission.SEND_EMBMS_INTENTS"
54     android:enabled="true"
55     android:exported="true">
56 </receiver>}</pre>
57  */
58 public class MbmsDownloadReceiver extends BroadcastReceiver {
59     /** @hide */
60     public static final String DOWNLOAD_TOKEN_SUFFIX = ".download_token";
61     /** @hide */
62     public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority";
63
64     private static final String EMBMS_INTENT_PERMISSION = "android.permission.SEND_EMBMS_INTENTS";
65
66     /**
67      * Indicates that the requested operation completed without error.
68      * @hide
69      */
70     @SystemApi
71     public static final int RESULT_OK = 0;
72
73     /**
74      * Indicates that the intent sent had an invalid action. This will be the result if
75      * {@link Intent#getAction()} returns anything other than
76      * {@link VendorUtils#ACTION_DOWNLOAD_RESULT_INTERNAL},
77      * {@link VendorUtils#ACTION_FILE_DESCRIPTOR_REQUEST}, or
78      * {@link VendorUtils#ACTION_CLEANUP}.
79      * This is a fatal result code and no result extras should be expected.
80      * @hide
81      */
82     @SystemApi
83     public static final int RESULT_INVALID_ACTION = 1;
84
85     /**
86      * Indicates that the intent was missing some required extras.
87      * This is a fatal result code and no result extras should be expected.
88      * @hide
89      */
90     @SystemApi
91     public static final int RESULT_MALFORMED_INTENT = 2;
92
93     /**
94      * Indicates that the supplied value for {@link VendorUtils#EXTRA_TEMP_FILE_ROOT}
95      * does not match what the app has stored.
96      * This is a fatal result code and no result extras should be expected.
97      * @hide
98      */
99     @SystemApi
100     public static final int RESULT_BAD_TEMP_FILE_ROOT = 3;
101
102     /**
103      * Indicates that the manager was unable to move the completed download to its final location.
104      * This is a fatal result code and no result extras should be expected.
105      * @hide
106      */
107     @SystemApi
108     public static final int RESULT_DOWNLOAD_FINALIZATION_ERROR = 4;
109
110     /**
111      * Indicates that the manager was unable to generate one or more of the requested file
112      * descriptors.
113      * This is a non-fatal result code -- some file descriptors may still be generated, but there
114      * is no guarantee that they will be the same number as requested.
115      * @hide
116      */
117     @SystemApi
118     public static final int RESULT_TEMP_FILE_GENERATION_ERROR = 5;
119
120     /**
121      * Indicates that the manager was unable to notify the app of the completed download.
122      * This is a fatal result code and no result extras should be expected.
123      * @hide
124      */
125     @SystemApi
126     public static final int RESULT_APP_NOTIFICATION_ERROR = 6;
127
128
129     private static final String LOG_TAG = "MbmsDownloadReceiver";
130     private static final String TEMP_FILE_SUFFIX = ".embms.temp";
131     private static final String TEMP_FILE_STAGING_LOCATION = "staged_completed_files";
132
133     private static final int MAX_TEMP_FILE_RETRIES = 5;
134
135     private String mFileProviderAuthorityCache = null;
136     private String mMiddlewarePackageNameCache = null;
137
138     /** @hide */
139     @Override
140     public void onReceive(Context context, Intent intent) {
141         verifyPermissionIntegrity(context);
142
143         if (!verifyIntentContents(context, intent)) {
144             setResultCode(RESULT_MALFORMED_INTENT);
145             return;
146         }
147         if (!Objects.equals(intent.getStringExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT),
148                 MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath())) {
149             setResultCode(RESULT_BAD_TEMP_FILE_ROOT);
150             return;
151         }
152
153         if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) {
154             moveDownloadedFile(context, intent);
155             cleanupPostMove(context, intent);
156         } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) {
157             generateTempFiles(context, intent);
158         } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) {
159             cleanupTempFiles(context, intent);
160         } else {
161             setResultCode(RESULT_INVALID_ACTION);
162         }
163     }
164
165     private boolean verifyIntentContents(Context context, Intent intent) {
166         if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) {
167             if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT)) {
168                 Log.w(LOG_TAG, "Download result did not include a result code. Ignoring.");
169                 return false;
170             }
171             if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST)) {
172                 Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring.");
173                 return false;
174             }
175             // We do not need to verify below extras if the result is not success.
176             if (MbmsDownloadSession.RESULT_SUCCESSFUL !=
177                     intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT,
178                     MbmsDownloadSession.RESULT_CANCELLED)) {
179                 return true;
180             }
181             if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) {
182                 Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring.");
183                 return false;
184             }
185             if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO)) {
186                 Log.w(LOG_TAG, "Download result did not include the associated file info. " +
187                         "Ignoring.");
188                 return false;
189             }
190             if (!intent.hasExtra(VendorUtils.EXTRA_FINAL_URI)) {
191                 Log.w(LOG_TAG, "Download result did not include the path to the final " +
192                         "temp file. Ignoring.");
193                 return false;
194             }
195             DownloadRequest request = intent.getParcelableExtra(
196                     MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST);
197             String expectedTokenFileName = request.getHash() + DOWNLOAD_TOKEN_SUFFIX;
198             File expectedTokenFile = new File(
199                     MbmsUtils.getEmbmsTempFileDirForService(context, request.getFileServiceId()),
200                     expectedTokenFileName);
201             if (!expectedTokenFile.exists()) {
202                 Log.w(LOG_TAG, "Supplied download request does not match a token that we have. " +
203                         "Expected " + expectedTokenFile);
204                 return false;
205             }
206         } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) {
207             if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) {
208                 Log.w(LOG_TAG, "Temp file request did not include the associated service id." +
209                         " Ignoring.");
210                 return false;
211             }
212             if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) {
213                 Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring.");
214                 return false;
215             }
216         } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) {
217             if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) {
218                 Log.w(LOG_TAG, "Cleanup request did not include the associated service id." +
219                         " Ignoring.");
220                 return false;
221             }
222             if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) {
223                 Log.w(LOG_TAG, "Cleanup request did not include the temp file root. Ignoring.");
224                 return false;
225             }
226             if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE)) {
227                 Log.w(LOG_TAG, "Cleanup request did not include the list of temp files in use. " +
228                         "Ignoring.");
229                 return false;
230             }
231         }
232         return true;
233     }
234
235     private void moveDownloadedFile(Context context, Intent intent) {
236         DownloadRequest request = intent.getParcelableExtra(
237                 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST);
238         Intent intentForApp = request.getIntentForApp();
239         if (intentForApp == null) {
240             Log.i(LOG_TAG, "Malformed app notification intent");
241             setResultCode(RESULT_APP_NOTIFICATION_ERROR);
242             return;
243         }
244
245         int result = intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT,
246                 MbmsDownloadSession.RESULT_CANCELLED);
247         intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result);
248         intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request);
249
250         if (result != MbmsDownloadSession.RESULT_SUCCESSFUL) {
251             Log.i(LOG_TAG, "Download request indicated a failed download. Aborting.");
252             context.sendBroadcast(intentForApp);
253             setResultCode(RESULT_OK);
254             return;
255         }
256
257         Uri finalTempFile = intent.getParcelableExtra(VendorUtils.EXTRA_FINAL_URI);
258         if (!verifyTempFilePath(context, request.getFileServiceId(), finalTempFile)) {
259             Log.w(LOG_TAG, "Download result specified an invalid temp file " + finalTempFile);
260             setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR);
261             return;
262         }
263
264         FileInfo completedFileInfo =
265                 (FileInfo) intent.getParcelableExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO);
266         Path appSpecifiedDestination = FileSystems.getDefault().getPath(
267                 request.getDestinationUri().getPath());
268
269         Uri finalLocation;
270         try {
271             finalLocation = moveToFinalLocation(finalTempFile, appSpecifiedDestination);
272         } catch (IOException e) {
273             Log.w(LOG_TAG, "Failed to move temp file to final destination");
274             setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR);
275             return;
276         }
277         intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_COMPLETED_FILE_URI, finalLocation);
278         intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, completedFileInfo);
279
280         context.sendBroadcast(intentForApp);
281         setResultCode(RESULT_OK);
282     }
283
284     private void cleanupPostMove(Context context, Intent intent) {
285         DownloadRequest request = intent.getParcelableExtra(
286                 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST);
287         if (request == null) {
288             Log.w(LOG_TAG, "Intent does not include a DownloadRequest. Ignoring.");
289             return;
290         }
291
292         List<Uri> tempFiles = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_LIST);
293         if (tempFiles == null) {
294             return;
295         }
296
297         for (Uri tempFileUri : tempFiles) {
298             if (verifyTempFilePath(context, request.getFileServiceId(), tempFileUri)) {
299                 File tempFile = new File(tempFileUri.getSchemeSpecificPart());
300                 tempFile.delete();
301             }
302         }
303     }
304
305     private void generateTempFiles(Context context, Intent intent) {
306         String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID);
307         if (serviceId == null) {
308             Log.w(LOG_TAG, "Temp file request did not include the associated service id. " +
309                     "Ignoring.");
310             setResultCode(RESULT_MALFORMED_INTENT);
311             return;
312         }
313         int fdCount = intent.getIntExtra(VendorUtils.EXTRA_FD_COUNT, 0);
314         List<Uri> pausedList = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_PAUSED_LIST);
315
316         if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) {
317             Log.i(LOG_TAG, "No temp files actually requested. Ending.");
318             setResultCode(RESULT_OK);
319             setResultExtras(Bundle.EMPTY);
320             return;
321         }
322
323         ArrayList<UriPathPair> freshTempFiles =
324                 generateFreshTempFiles(context, serviceId, fdCount);
325         ArrayList<UriPathPair> pausedFiles =
326                 generateUrisForPausedFiles(context, serviceId, pausedList);
327
328         Bundle result = new Bundle();
329         result.putParcelableArrayList(VendorUtils.EXTRA_FREE_URI_LIST, freshTempFiles);
330         result.putParcelableArrayList(VendorUtils.EXTRA_PAUSED_URI_LIST, pausedFiles);
331         setResultCode(RESULT_OK);
332         setResultExtras(result);
333     }
334
335     private ArrayList<UriPathPair> generateFreshTempFiles(Context context, String serviceId,
336             int freshFdCount) {
337         File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId);
338         if (!tempFileDir.exists()) {
339             tempFileDir.mkdirs();
340         }
341
342         // Name the files with the template "N-UUID", where N is the request ID and UUID is a
343         // random uuid.
344         ArrayList<UriPathPair> result = new ArrayList<>(freshFdCount);
345         for (int i = 0; i < freshFdCount; i++) {
346             File tempFile = generateSingleTempFile(tempFileDir);
347             if (tempFile == null) {
348                 setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR);
349                 Log.w(LOG_TAG, "Failed to generate a temp file. Moving on.");
350                 continue;
351             }
352             Uri fileUri = Uri.fromFile(tempFile);
353             Uri contentUri = MbmsTempFileProvider.getUriForFile(
354                     context, getFileProviderAuthorityCached(context), tempFile);
355             context.grantUriPermission(getMiddlewarePackageCached(context), contentUri,
356                     Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
357             result.add(new UriPathPair(fileUri, contentUri));
358         }
359
360         return result;
361     }
362
363     private static File generateSingleTempFile(File tempFileDir) {
364         int numTries = 0;
365         while (numTries < MAX_TEMP_FILE_RETRIES) {
366             numTries++;
367             String fileName =  UUID.randomUUID() + TEMP_FILE_SUFFIX;
368             File tempFile = new File(tempFileDir, fileName);
369             try {
370                 if (tempFile.createNewFile()) {
371                     return tempFile.getCanonicalFile();
372                 }
373             } catch (IOException e) {
374                 continue;
375             }
376         }
377         return null;
378     }
379
380     private ArrayList<UriPathPair> generateUrisForPausedFiles(Context context,
381             String serviceId, List<Uri> pausedFiles) {
382         if (pausedFiles == null) {
383             return new ArrayList<>(0);
384         }
385         ArrayList<UriPathPair> result = new ArrayList<>(pausedFiles.size());
386
387         for (Uri fileUri : pausedFiles) {
388             if (!verifyTempFilePath(context, serviceId, fileUri)) {
389                 Log.w(LOG_TAG, "Supplied file " + fileUri + " is not a valid temp file to resume");
390                 setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR);
391                 continue;
392             }
393             File tempFile = new File(fileUri.getSchemeSpecificPart());
394             if (!tempFile.exists()) {
395                 Log.w(LOG_TAG, "Supplied file " + fileUri + " does not exist.");
396                 setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR);
397                 continue;
398             }
399             Uri contentUri = MbmsTempFileProvider.getUriForFile(
400                     context, getFileProviderAuthorityCached(context), tempFile);
401             context.grantUriPermission(getMiddlewarePackageCached(context), contentUri,
402                     Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
403
404             result.add(new UriPathPair(fileUri, contentUri));
405         }
406         return result;
407     }
408
409     private void cleanupTempFiles(Context context, Intent intent) {
410         String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID);
411         File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId);
412         final List<Uri> filesInUse =
413                 intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE);
414         File[] filesToDelete = tempFileDir.listFiles(new FileFilter() {
415             @Override
416             public boolean accept(File file) {
417                 File canonicalFile;
418                 try {
419                     canonicalFile = file.getCanonicalFile();
420                 } catch (IOException e) {
421                     Log.w(LOG_TAG, "Got IOException canonicalizing " + file + ", not deleting.");
422                     return false;
423                 }
424                 // Reject all files that don't match what we think a temp file should look like
425                 // e.g. download tokens
426                 if (!canonicalFile.getName().endsWith(TEMP_FILE_SUFFIX)) {
427                     return false;
428                 }
429                 // If any of the files in use match the uri, return false to reject it from the
430                 // list to delete.
431                 Uri fileInUseUri = Uri.fromFile(canonicalFile);
432                 return !filesInUse.contains(fileInUseUri);
433             }
434         });
435         for (File fileToDelete : filesToDelete) {
436             fileToDelete.delete();
437         }
438     }
439
440     /*
441      * Moves a tempfile located at fromPath to its final home where the app wants it
442      */
443     private static Uri moveToFinalLocation(Uri fromPath, Path appSpecifiedPath) throws IOException {
444         if (!ContentResolver.SCHEME_FILE.equals(fromPath.getScheme())) {
445             Log.w(LOG_TAG, "Downloaded file location uri " + fromPath +
446                     " does not have a file scheme");
447             return null;
448         }
449
450         Path fromFile = FileSystems.getDefault().getPath(fromPath.getPath());
451         if (!Files.isDirectory(appSpecifiedPath)) {
452             Files.createDirectory(appSpecifiedPath);
453         }
454         // TODO: do we want to support directory trees within the download directory?
455         Path result = Files.move(fromFile, appSpecifiedPath.resolve(fromFile.getFileName()),
456                 StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
457
458         return Uri.fromFile(result.toFile());
459     }
460
461     private static boolean verifyTempFilePath(Context context, String serviceId,
462             Uri filePath) {
463         if (!ContentResolver.SCHEME_FILE.equals(filePath.getScheme())) {
464             Log.w(LOG_TAG, "Uri " + filePath + " does not have a file scheme");
465             return false;
466         }
467
468         String path = filePath.getSchemeSpecificPart();
469         File tempFile = new File(path);
470         if (!tempFile.exists()) {
471             Log.w(LOG_TAG, "File at " + path + " does not exist.");
472             return false;
473         }
474
475         if (!MbmsUtils.isContainedIn(
476                 MbmsUtils.getEmbmsTempFileDirForService(context, serviceId), tempFile)) {
477             return false;
478         }
479
480         return true;
481     }
482
483     private String getFileProviderAuthorityCached(Context context) {
484         if (mFileProviderAuthorityCache != null) {
485             return mFileProviderAuthorityCache;
486         }
487
488         mFileProviderAuthorityCache = getFileProviderAuthority(context);
489         return mFileProviderAuthorityCache;
490     }
491
492     private static String getFileProviderAuthority(Context context) {
493         ApplicationInfo appInfo;
494         try {
495             appInfo = context.getPackageManager()
496                     .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
497         } catch (PackageManager.NameNotFoundException e) {
498             throw new RuntimeException("Package manager couldn't find " + context.getPackageName());
499         }
500         if (appInfo.metaData == null) {
501             throw new RuntimeException("App must declare the file provider authority as metadata " +
502                     "in the manifest.");
503         }
504         String authority = appInfo.metaData.getString(MBMS_FILE_PROVIDER_META_DATA_KEY);
505         if (authority == null) {
506             throw new RuntimeException("App must declare the file provider authority as metadata " +
507                     "in the manifest.");
508         }
509         return authority;
510     }
511
512     private String getMiddlewarePackageCached(Context context) {
513         if (mMiddlewarePackageNameCache == null) {
514             mMiddlewarePackageNameCache = MbmsUtils.getMiddlewareServiceInfo(context,
515                     MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION).packageName;
516         }
517         return mMiddlewarePackageNameCache;
518     }
519
520     private void verifyPermissionIntegrity(Context context) {
521         PackageManager pm = context.getPackageManager();
522         Intent queryIntent = new Intent(context, MbmsDownloadReceiver.class);
523         List<ResolveInfo> infos = pm.queryBroadcastReceivers(queryIntent, 0);
524         if (infos.size() != 1) {
525             throw new IllegalStateException("Non-unique download receiver in your app");
526         }
527         ActivityInfo selfInfo = infos.get(0).activityInfo;
528         if (selfInfo == null) {
529             throw new IllegalStateException("Queried ResolveInfo does not contain a receiver");
530         }
531         if (MbmsUtils.getOverrideServiceName(context,
532                 MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION) != null) {
533             // If an override was specified, just make sure that the permission isn't null.
534             if (selfInfo.permission == null) {
535                 throw new IllegalStateException(
536                         "MbmsDownloadReceiver must require some permission");
537             }
538             return;
539         }
540         if (!Objects.equals(EMBMS_INTENT_PERMISSION, selfInfo.permission)) {
541             throw new IllegalStateException("MbmsDownloadReceiver must require the " +
542                     "SEND_EMBMS_INTENTS permission.");
543         }
544     }
545 }