content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License
   */
  
  package android.telephony.mbms;
  
  import android.annotation.SystemApi;
  import android.content.BroadcastReceiver;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.ActivityInfo;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.ResolveInfo;
  import android.net.Uri;
  import android.os.Bundle;
  import android.telephony.MbmsDownloadSession;
  import android.telephony.mbms.vendor.VendorUtils;
  import android.util.Log;
  
  import com.android.internal.annotations.VisibleForTesting;
  
  import java.io.File;
  import java.io.FileFilter;
  import java.io.IOException;
  import java.nio.file.FileSystems;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.nio.file.StandardCopyOption;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Objects;
  import java.util.UUID;
  
  /**
   * The {@link BroadcastReceiver} responsible for handling intents sent from the middleware. Apps
   * that wish to download using MBMS APIs should declare this class in their AndroidManifest.xml as
   * follows:
  <pre>{@code
  <receiver
      android:name="android.telephony.mbms.MbmsDownloadReceiver"
      android:permission="android.permission.SEND_EMBMS_INTENTS"
      android:enabled="true"
      android:exported="true">
  </receiver>}</pre>
   */
  public class MbmsDownloadReceiver extends BroadcastReceiver {
      /** @hide */
      public static final String DOWNLOAD_TOKEN_SUFFIX = ".download_token";
      /** @hide */
      public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority";
  
      private static final String EMBMS_INTENT_PERMISSION = "android.permission.SEND_EMBMS_INTENTS";
  
      /**
       * Indicates that the requested operation completed without error.
       * @hide
       */
      @SystemApi
      public static final int RESULT_OK = 0;
  
      /**
       * Indicates that the intent sent had an invalid action. This will be the result if
       * {@link Intent#getAction()} returns anything other than
       * {@link VendorUtils#ACTION_DOWNLOAD_RESULT_INTERNAL},
       * {@link VendorUtils#ACTION_FILE_DESCRIPTOR_REQUEST}, or
       * {@link VendorUtils#ACTION_CLEANUP}.
       * This is a fatal result code and no result extras should be expected.
       * @hide
       */
      @SystemApi
      public static final int RESULT_INVALID_ACTION = 1;
  
      /**
       * Indicates that the intent was missing some required extras.
       * This is a fatal result code and no result extras should be expected.
       * @hide
       */
      @SystemApi
      public static final int RESULT_MALFORMED_INTENT = 2;
  
      /**
       * Indicates that the supplied value for {@link VendorUtils#EXTRA_TEMP_FILE_ROOT}
       * does not match what the app has stored.
       * This is a fatal result code and no result extras should be expected.
       * @hide
       */
      @SystemApi
      public static final int RESULT_BAD_TEMP_FILE_ROOT = 3;
  
      /**
       * Indicates that the manager was unable to move the completed download to its final location.
       * This is a fatal result code and no result extras should be expected.
       * @hide
       */
      @SystemApi
      public static final int RESULT_DOWNLOAD_FINALIZATION_ERROR = 4;
  
      /**
       * Indicates that the manager was unable to generate one or more of the requested file
       * descriptors.
       * This is a non-fatal result code -- some file descriptors may still be generated, but there
       * is no guarantee that they will be the same number as requested.
       * @hide
       */
      @SystemApi
      public static final int RESULT_TEMP_FILE_GENERATION_ERROR = 5;
  
      /**
       * Indicates that the manager was unable to notify the app of the completed download.
       * This is a fatal result code and no result extras should be expected.
       * @hide
       */
      @SystemApi
      public static final int RESULT_APP_NOTIFICATION_ERROR = 6;
  
  
      private static final String LOG_TAG = "MbmsDownloadReceiver";
      private static final String TEMP_FILE_SUFFIX = ".embms.temp";
      private static final String TEMP_FILE_STAGING_LOCATION = "staged_completed_files";
  
      private static final int MAX_TEMP_FILE_RETRIES = 5;
  
      private String mFileProviderAuthorityCache = null;
      private String mMiddlewarePackageNameCache = null;
  
      /** @hide */
      @Override
      public void onReceive(Context context, Intent intent) {
          verifyPermissionIntegrity(context);
  
          if (!verifyIntentContents(context, intent)) {
              setResultCode(RESULT_MALFORMED_INTENT);
              return;
          }
          if (!Objects.equals(intent.getStringExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT),
                  MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath())) {
              setResultCode(RESULT_BAD_TEMP_FILE_ROOT);
              return;
          }
  
          if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) {
              moveDownloadedFile(context, intent);
              cleanupPostMove(context, intent);
          } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) {
              generateTempFiles(context, intent);
          } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) {
              cleanupTempFiles(context, intent);
          } else {
              setResultCode(RESULT_INVALID_ACTION);
          }
      }
  
      private boolean verifyIntentContents(Context context, Intent intent) {
          if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) {
              if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT)) {
                  Log.w(LOG_TAG, "Download result did not include a result code. Ignoring.");
                  return false;
              }
              if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST)) {
                  Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring.");
                  return false;
              }
              // We do not need to verify below extras if the result is not success.
              if (MbmsDownloadSession.RESULT_SUCCESSFUL !=
                      intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT,
                      MbmsDownloadSession.RESULT_CANCELLED)) {
                  return true;
              }
              if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) {
                  Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring.");
                  return false;
              }
              if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO)) {
                  Log.w(LOG_TAG, "Download result did not include the associated file info. " +
                          "Ignoring.");
                  return false;
              }
              if (!intent.hasExtra(VendorUtils.EXTRA_FINAL_URI)) {
                  Log.w(LOG_TAG, "Download result did not include the path to the final " +
                          "temp file. Ignoring.");
                  return false;
              }
              DownloadRequest request = intent.getParcelableExtra(
                      MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST);
              String expectedTokenFileName = request.getHash() + DOWNLOAD_TOKEN_SUFFIX;
              File expectedTokenFile = new File(
                      MbmsUtils.getEmbmsTempFileDirForService(context, request.getFileServiceId()),
                      expectedTokenFileName);
              if (!expectedTokenFile.exists()) {
                  Log.w(LOG_TAG, "Supplied download request does not match a token that we have. " +
                          "Expected " + expectedTokenFile);
                  return false;
              }
          } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) {
              if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) {
                  Log.w(LOG_TAG, "Temp file request did not include the associated service id." +
                          " Ignoring.");
                  return false;
              }
              if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) {
                  Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring.");
                  return false;
              }
          } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) {
              if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) {
                  Log.w(LOG_TAG, "Cleanup request did not include the associated service id." +
                          " Ignoring.");
                  return false;
              }
              if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) {
                  Log.w(LOG_TAG, "Cleanup request did not include the temp file root. Ignoring.");
                  return false;
              }
              if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE)) {
                  Log.w(LOG_TAG, "Cleanup request did not include the list of temp files in use. " +
                          "Ignoring.");
                  return false;
              }
          }
          return true;
      }
  
      private void moveDownloadedFile(Context context, Intent intent) {
          DownloadRequest request = intent.getParcelableExtra(
                  MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST);
          Intent intentForApp = request.getIntentForApp();
          if (intentForApp == null) {
              Log.i(LOG_TAG, "Malformed app notification intent");
              setResultCode(RESULT_APP_NOTIFICATION_ERROR);
              return;
          }
  
          int result = intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT,
                  MbmsDownloadSession.RESULT_CANCELLED);
          intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result);
          intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request);
  
          if (result != MbmsDownloadSession.RESULT_SUCCESSFUL) {
              Log.i(LOG_TAG, "Download request indicated a failed download. Aborting.");
              context.sendBroadcast(intentForApp);
              setResultCode(RESULT_OK);
              return;
          }
  
          Uri finalTempFile = intent.getParcelableExtra(VendorUtils.EXTRA_FINAL_URI);
          if (!verifyTempFilePath(context, request.getFileServiceId(), finalTempFile)) {
              Log.w(LOG_TAG, "Download result specified an invalid temp file " + finalTempFile);
              setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR);
              return;
          }
  
          FileInfo completedFileInfo =
                  (FileInfo) intent.getParcelableExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO);
          Path appSpecifiedDestination = FileSystems.getDefault().getPath(
                  request.getDestinationUri().getPath());
  
          Uri finalLocation;
          try {
              String relativeLocation = getFileRelativePath(request.getSourceUri().getPath(),
                      completedFileInfo.getUri().getPath());
              finalLocation = moveToFinalLocation(finalTempFile, appSpecifiedDestination,
                      relativeLocation);
          } catch (IOException e) {
              Log.w(LOG_TAG, "Failed to move temp file to final destination");
              setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR);
              return;
          }
          intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_COMPLETED_FILE_URI, finalLocation);
          intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, completedFileInfo);
  
          context.sendBroadcast(intentForApp);
          setResultCode(RESULT_OK);
      }
  
      private void cleanupPostMove(Context context, Intent intent) {
          DownloadRequest request = intent.getParcelableExtra(
                  MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST);
          if (request == null) {
              Log.w(LOG_TAG, "Intent does not include a DownloadRequest. Ignoring.");
              return;
          }
  
          List<Uri> tempFiles = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_LIST);
          if (tempFiles == null) {
              return;
          }
  
          for (Uri tempFileUri : tempFiles) {
              if (verifyTempFilePath(context, request.getFileServiceId(), tempFileUri)) {
                  File tempFile = new File(tempFileUri.getSchemeSpecificPart());
                  if (!tempFile.delete()) {
                      Log.w(LOG_TAG, "Failed to delete temp file at " + tempFile.getPath());
                  }
              }
          }
      }
  
      private void generateTempFiles(Context context, Intent intent) {
          String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID);
          if (serviceId == null) {
              Log.w(LOG_TAG, "Temp file request did not include the associated service id. " +
                      "Ignoring.");
              setResultCode(RESULT_MALFORMED_INTENT);
              return;
          }
          int fdCount = intent.getIntExtra(VendorUtils.EXTRA_FD_COUNT, 0);
          List<Uri> pausedList = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_PAUSED_LIST);
  
          if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) {
              Log.i(LOG_TAG, "No temp files actually requested. Ending.");
              setResultCode(RESULT_OK);
              setResultExtras(Bundle.EMPTY);
              return;
          }
  
          ArrayList<UriPathPair> freshTempFiles =
                  generateFreshTempFiles(context, serviceId, fdCount);
          ArrayList<UriPathPair> pausedFiles =
                  generateUrisForPausedFiles(context, serviceId, pausedList);
  
          Bundle result = new Bundle();
          result.putParcelableArrayList(VendorUtils.EXTRA_FREE_URI_LIST, freshTempFiles);
          result.putParcelableArrayList(VendorUtils.EXTRA_PAUSED_URI_LIST, pausedFiles);
          setResultCode(RESULT_OK);
          setResultExtras(result);
      }
  
      private ArrayList<UriPathPair> generateFreshTempFiles(Context context, String serviceId,
              int freshFdCount) {
          File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId);
          if (!tempFileDir.exists()) {
              tempFileDir.mkdirs();
          }
  
          // Name the files with the template "N-UUID", where N is the request ID and UUID is a
          // random uuid.
          ArrayList<UriPathPair> result = new ArrayList<>(freshFdCount);
          for (int i = 0; i < freshFdCount; i++) {
              File tempFile = generateSingleTempFile(tempFileDir);
              if (tempFile == null) {
                  setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR);
                  Log.w(LOG_TAG, "Failed to generate a temp file. Moving on.");
                  continue;
              }
              Uri fileUri = Uri.fromFile(tempFile);
              Uri contentUri = MbmsTempFileProvider.getUriForFile(
                      context, getFileProviderAuthorityCached(context), tempFile);
              context.grantUriPermission(getMiddlewarePackageCached(context), contentUri,
                      Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
              result.add(new UriPathPair(fileUri, contentUri));
          }
  
          return result;
      }
  
      private static File generateSingleTempFile(File tempFileDir) {
          int numTries = 0;
          while (numTries < MAX_TEMP_FILE_RETRIES) {
              numTries++;
              String fileName =  UUID.randomUUID() + TEMP_FILE_SUFFIX;
              File tempFile = new File(tempFileDir, fileName);
              try {
                  if (tempFile.createNewFile()) {
                      return tempFile.getCanonicalFile();
                  }
              } catch (IOException e) {
                  continue;
              }
          }
          return null;
      }
  
      private ArrayList<UriPathPair> generateUrisForPausedFiles(Context context,
              String serviceId, List<Uri> pausedFiles) {
          if (pausedFiles == null) {
              return new ArrayList<>(0);
          }
          ArrayList<UriPathPair> result = new ArrayList<>(pausedFiles.size());
  
          for (Uri fileUri : pausedFiles) {
              if (!verifyTempFilePath(context, serviceId, fileUri)) {
                  Log.w(LOG_TAG, "Supplied file " + fileUri + " is not a valid temp file to resume");
                  setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR);
                  continue;
              }
              File tempFile = new File(fileUri.getSchemeSpecificPart());
              if (!tempFile.exists()) {
                  Log.w(LOG_TAG, "Supplied file " + fileUri + " does not exist.");
                  setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR);
                  continue;
              }
              Uri contentUri = MbmsTempFileProvider.getUriForFile(
                      context, getFileProviderAuthorityCached(context), tempFile);
              context.grantUriPermission(getMiddlewarePackageCached(context), contentUri,
                      Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  
              result.add(new UriPathPair(fileUri, contentUri));
          }
          return result;
      }
  
      private void cleanupTempFiles(Context context, Intent intent) {
          String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID);
          File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId);
          final List<Uri> filesInUse =
                  intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE);
          File[] filesToDelete = tempFileDir.listFiles(new FileFilter() {
              @Override
              public boolean accept(File file) {
                  File canonicalFile;
                  try {
                      canonicalFile = file.getCanonicalFile();
                  } catch (IOException e) {
                      Log.w(LOG_TAG, "Got IOException canonicalizing " + file + ", not deleting.");
                      return false;
                  }
                  // Reject all files that don't match what we think a temp file should look like
                  // e.g. download tokens
                  if (!canonicalFile.getName().endsWith(TEMP_FILE_SUFFIX)) {
                      return false;
                  }
                  // If any of the files in use match the uri, return false to reject it from the
                  // list to delete.
                  Uri fileInUseUri = Uri.fromFile(canonicalFile);
                  return !filesInUse.contains(fileInUseUri);
              }
          });
          for (File fileToDelete : filesToDelete) {
              fileToDelete.delete();
          }
      }
  
      /*
       * Moves a tempfile located at fromPath to its final home where the app wants it
       */
      private static Uri moveToFinalLocation(Uri fromPath, Path appSpecifiedPath,
              String relativeLocation) throws IOException {
          if (!ContentResolver.SCHEME_FILE.equals(fromPath.getScheme())) {
              Log.w(LOG_TAG, "Downloaded file location uri " + fromPath +
                      " does not have a file scheme");
              return null;
          }
  
          Path fromFile = FileSystems.getDefault().getPath(fromPath.getPath());
          Path toFile = appSpecifiedPath.resolve(relativeLocation);
  
          if (!Files.isDirectory(toFile.getParent())) {
              Files.createDirectories(toFile.getParent());
          }
          Path result = Files.move(fromFile, toFile,
                  StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
  
          return Uri.fromFile(result.toFile());
      }
  
      /**
       * @hide
       */
      @VisibleForTesting
      public static String getFileRelativePath(String sourceUriPath, String fileInfoPath) {
          if (sourceUriPath.endsWith("*")) {
              // This is a wildcard path. Strip the last path component and use that as the root of
              // the relative path.
              int lastSlash = sourceUriPath.lastIndexOf('/');
              sourceUriPath = sourceUriPath.substring(0, lastSlash);
          }
          if (!fileInfoPath.startsWith(sourceUriPath)) {
              Log.e(LOG_TAG, "File location specified in FileInfo does not match the source URI."
                      + " source: " + sourceUriPath + " fileinfo path: " + fileInfoPath);
              return null;
          }
          if (fileInfoPath.length() == sourceUriPath.length()) {
              // This is the single-file download case. Return the name of the file so that the
              // receiver puts the file directly into the dest directory.
              return sourceUriPath.substring(sourceUriPath.lastIndexOf('/') + 1);
          }
  
          String prefixOmittedPath = fileInfoPath.substring(sourceUriPath.length());
          if (prefixOmittedPath.startsWith("/")) {
              prefixOmittedPath = prefixOmittedPath.substring(1);
          }
          return prefixOmittedPath;
      }
  
      private static boolean verifyTempFilePath(Context context, String serviceId,
              Uri filePath) {
          if (!ContentResolver.SCHEME_FILE.equals(filePath.getScheme())) {
              Log.w(LOG_TAG, "Uri " + filePath + " does not have a file scheme");
              return false;
          }
  
          String path = filePath.getSchemeSpecificPart();
          File tempFile = new File(path);
          if (!tempFile.exists()) {
              Log.w(LOG_TAG, "File at " + path + " does not exist.");
              return false;
          }
  
          if (!MbmsUtils.isContainedIn(
                  MbmsUtils.getEmbmsTempFileDirForService(context, serviceId), tempFile)) {
              Log.w(LOG_TAG, "File at " + path + " is not contained in the temp file root," +
                      " which is " + MbmsUtils.getEmbmsTempFileDirForService(context, serviceId));
              return false;
          }
  
          return true;
      }
  
      private String getFileProviderAuthorityCached(Context context) {
          if (mFileProviderAuthorityCache != null) {
              return mFileProviderAuthorityCache;
          }
  
          mFileProviderAuthorityCache = getFileProviderAuthority(context);
          return mFileProviderAuthorityCache;
      }
  
      private static String getFileProviderAuthority(Context context) {
          ApplicationInfo appInfo;
          try {
              appInfo = context.getPackageManager()
                      .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
          } catch (PackageManager.NameNotFoundException e) {
              throw new RuntimeException("Package manager couldn't find " + context.getPackageName());
          }
          if (appInfo.metaData == null) {
              throw new RuntimeException("App must declare the file provider authority as metadata " +
                      "in the manifest.");
          }
          String authority = appInfo.metaData.getString(MBMS_FILE_PROVIDER_META_DATA_KEY);
          if (authority == null) {
              throw new RuntimeException("App must declare the file provider authority as metadata " +
                      "in the manifest.");
          }
          return authority;
      }
  
      private String getMiddlewarePackageCached(Context context) {
          if (mMiddlewarePackageNameCache == null) {
              mMiddlewarePackageNameCache = MbmsUtils.getMiddlewareServiceInfo(context,
                      MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION).packageName;
          }
          return mMiddlewarePackageNameCache;
      }
  
      private void verifyPermissionIntegrity(Context context) {
          PackageManager pm = context.getPackageManager();
          Intent queryIntent = new Intent(context, MbmsDownloadReceiver.class);
          List<ResolveInfo> infos = pm.queryBroadcastReceivers(queryIntent, 0);
          if (infos.size() != 1) {
              throw new IllegalStateException("Non-unique download receiver in your app");
          }
          ActivityInfo selfInfo = infos.get(0).activityInfo;
          if (selfInfo == null) {
              throw new IllegalStateException("Queried ResolveInfo does not contain a receiver");
          }
          if (MbmsUtils.getOverrideServiceName(context,
                  MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION) != null) {
              // If an override was specified, just make sure that the permission isn't null.
              if (selfInfo.permission == null) {
                  throw new IllegalStateException(
                          "MbmsDownloadReceiver must require some permission");
              }
              return;
          }
          if (!Objects.equals(EMBMS_INTENT_PERMISSION, selfInfo.permission)) {
              throw new IllegalStateException("MbmsDownloadReceiver must require the " +
                      "SEND_EMBMS_INTENTS permission.");
          }
      }
  }
  