content: /*
   * Copyright (C) 2016 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 com.android.internal.telephony;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.AppOpsManager;
  import android.app.PendingIntent;
  import android.app.role.IRoleManager;
  import android.content.Context;
  import android.content.Intent;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.os.ServiceManager.ServiceNotFoundException;
  import android.provider.Telephony.Sms.Intents;
  import android.telephony.IFinancialSmsCallback;
  import android.telephony.SmsManager;
  import android.telephony.SmsMessage;
  import android.telephony.SubscriptionManager;
  import android.text.TextUtils;
  import android.util.ArrayMap;
  import android.util.Base64;
  import android.util.Log;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.util.Preconditions;
  
  import java.security.SecureRandom;
  import java.util.Map;
  import java.util.Set;
  import java.util.concurrent.TimeUnit;
  
  
  /**
   *  Manager for app specific SMS requests. This can be used to implement SMS based
   *  communication channels (e.g. for SMS based phone number verification) without needing the
   *  {@link Manifest.permission#RECEIVE_SMS} permission.
   *
   *  {@link #createAppSpecificSmsRequest} allows an application to provide a {@link PendingIntent}
   *  that is triggered when an incoming SMS is received that contains the provided token.
   */
  public class AppSmsManager {
      private static final String LOG_TAG = "AppSmsManager";
  
      private static final long TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
      private final SecureRandom mRandom;
      private final Context mContext;
      private final Object mLock = new Object();
  
      @GuardedBy("mLock")
      private final Map<String, AppRequestInfo> mTokenMap;
      @GuardedBy("mLock")
      private final Map<String, AppRequestInfo> mPackageMap;
  
      public AppSmsManager(Context context) {
          mRandom = new SecureRandom();
          mTokenMap = new ArrayMap<>();
          mPackageMap = new ArrayMap<>();
          mContext = context;
      }
  
      /**
       * Create an app specific incoming SMS request for the the calling package.
       *
       * This method returns a token that if included in a subsequent incoming SMS message the
       * {@link Intents.SMS_RECEIVED_ACTION} intent will be delivered only to the calling package and
       * will not require the application have the {@link Manifest.permission#RECEIVE_SMS} permission.
       *
       * An app can only have one request at a time, if the app already has a request it will be
       * dropped and the new one will be added.
       *
       * @return Token to include in an SMS to have it delivered directly to the app.
       */
      public String createAppSpecificSmsToken(String callingPkg, PendingIntent intent) {
          // Check calling uid matches callingpkg.
          AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
          appOps.checkPackage(Binder.getCallingUid(), callingPkg);
  
          // Generate a nonce to store the request under.
          String token = generateNonce();
          synchronized (mLock) {
              // Only allow one request in flight from a package.
              if (mPackageMap.containsKey(callingPkg)) {
                  removeRequestLocked(mPackageMap.get(callingPkg));
              }
              // Store state.
              AppRequestInfo info = new AppRequestInfo(callingPkg, intent, token);
              addRequestLocked(info);
          }
          return token;
      }
  
      /**
       * Create an app specific incoming SMS request for the the calling package.
       *
       * This method returns a token that if included in a subsequent incoming SMS message the
       * {@link Intents.SMS_RECEIVED_ACTION} intent will be delivered only to the calling package and
       * will not require the application have the {@link Manifest.permission#RECEIVE_SMS} permission.
       *
       * An app can only have one request at a time, if the app already has a request it will be
       * dropped and the new one will be added.
       *
       * @return Token to include in an SMS to have it delivered directly to the app.
       */
      public String createAppSpecificSmsTokenWithPackageInfo(int subId,
              @NonNull String callingPackageName,
              @Nullable String prefixes,
              @NonNull PendingIntent intent) {
          Preconditions.checkStringNotEmpty(callingPackageName,
                  "callingPackageName cannot be null or empty.");
          Preconditions.checkNotNull(intent, "intent cannot be null");
          // Check calling uid matches callingpkg.
          AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
          appOps.checkPackage(Binder.getCallingUid(), callingPackageName);
  
          // Generate a token to store the request under.
          String token = PackageBasedTokenUtil.generateToken(mContext, callingPackageName);
          if (token != null) {
              synchronized (mLock) {
                  // Only allow one request in flight from a package.
                  if (mPackageMap.containsKey(callingPackageName)) {
                      removeRequestLocked(mPackageMap.get(callingPackageName));
                  }
                  // Store state.
                  AppRequestInfo info = new AppRequestInfo(
                          callingPackageName, intent, token, prefixes, subId, true);
                  addRequestLocked(info);
              }
          }
          return token;
      }
  
      /**
       * Get filtered SMS messages for financial app.
       */
      public void getSmsMessagesForFinancialApp(
              String callingPkg, Bundle params, final IFinancialSmsCallback callback) {
          try {
              IRoleManager roleManager = IRoleManager.Stub.asInterface(
                      ServiceManager.getServiceOrThrow(Context.ROLE_SERVICE));
              roleManager.getSmsMessagesForFinancialApp(callingPkg, params, callback);
          } catch (RemoteException e) {
              Log.e(LOG_TAG, "Receive RemoteException.");
              // do nothing
          } catch (ServiceNotFoundException e) {
              Log.e(LOG_TAG, "Service not found.");
              // do nothing
          }
      }
  
      /**
       * Handle an incoming SMS_DELIVER_ACTION intent if it is an app-only SMS.
       */
      public boolean handleSmsReceivedIntent(Intent intent) {
          // Sanity check the action.
          if (intent.getAction() != Intents.SMS_DELIVER_ACTION) {
              Log.wtf(LOG_TAG, "Got intent with incorrect action: " + intent.getAction());
              return false;
          }
  
          synchronized (mLock) {
              removeExpiredTokenLocked();
  
              String message = extractMessage(intent);
              if (TextUtils.isEmpty(message)) {
                  return false;
              }
  
              AppRequestInfo info = findAppRequestInfoSmsIntentLocked(message);
              if (info == null) {
                  // The message didn't contain a token -- nothing to do.
                  return false;
              }
  
              try {
                  Intent fillIn = new Intent()
                          .putExtras(intent.getExtras())
                          .putExtra(SmsManager.EXTRA_STATUS, SmsManager.RESULT_STATUS_SUCCESS)
                          .putExtra(SmsManager.EXTRA_SMS_MESSAGE, message)
                          .putExtra(SmsManager.EXTRA_SIM_SUBSCRIPTION_ID, info.subId)
                          .addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
  
                  info.pendingIntent.send(mContext, 0, fillIn);
              } catch (PendingIntent.CanceledException e) {
                  // The pending intent is canceled, send this SMS as normal.
                  removeRequestLocked(info);
                  return false;
              }
  
              removeRequestLocked(info);
              return true;
          }
      }
  
      private void removeExpiredTokenLocked() {
          final long currentTimeMillis = System.currentTimeMillis();
  
          final Set<String> keySet = mTokenMap.keySet();
          for (String token : keySet) {
              AppRequestInfo request = mTokenMap.get(token);
              if (request.packageBasedToken
                      && (currentTimeMillis - TIMEOUT_MILLIS > request.timestamp)) {
                  // Send the provided intent with SMS retriever status
                  try {
                      Intent fillIn = new Intent()
                              .putExtra(SmsManager.EXTRA_STATUS,
                                      SmsManager.RESULT_STATUS_TIMEOUT)
                              .addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
                      request.pendingIntent.send(mContext, 0, fillIn);
                  } catch (PendingIntent.CanceledException e) {
                      // do nothing
                  }
  
                  removeRequestLocked(request);
              }
          }
      }
  
      private String extractMessage(Intent intent) {
          SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
          if (messages == null) {
              return null;
          }
          StringBuilder fullMessageBuilder = new StringBuilder();
          for (SmsMessage message : messages) {
              if (message == null || message.getMessageBody() == null) {
                  continue;
              }
              fullMessageBuilder.append(message.getMessageBody());
          }
  
          return fullMessageBuilder.toString();
      }
  
      private AppRequestInfo findAppRequestInfoSmsIntentLocked(String fullMessage) {
          // Look for any tokens in the full message.
          for (String token : mTokenMap.keySet()) {
              if (fullMessage.trim().contains(token) && hasPrefix(token, fullMessage)) {
                  return mTokenMap.get(token);
              }
          }
          return null;
      }
  
      private String generateNonce() {
          byte[] bytes = new byte[8];
          mRandom.nextBytes(bytes);
          return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
      }
  
      private boolean hasPrefix(String token, String message) {
          AppRequestInfo request = mTokenMap.get(token);
          if (TextUtils.isEmpty(request.prefixes)) {
              return true;
          }
  
          String[] prefixes = request.prefixes.split(SmsManager.REGEX_PREFIX_DELIMITER);
          for (String prefix : prefixes) {
              if (message.startsWith(prefix)) {
                  return true;
              }
          }
          return false;
      }
  
      private void removeRequestLocked(AppRequestInfo info) {
          mTokenMap.remove(info.token);
          mPackageMap.remove(info.packageName);
      }
  
      private void addRequestLocked(AppRequestInfo info) {
          mTokenMap.put(info.token, info);
          mPackageMap.put(info.packageName, info);
      }
  
      private final class AppRequestInfo {
          public final String packageName;
          public final PendingIntent pendingIntent;
          public final String token;
          public final long timestamp;
          public final String prefixes;
          public final int subId;
          public final boolean packageBasedToken;
  
          AppRequestInfo(String packageName, PendingIntent pendingIntent, String token) {
            this(packageName, pendingIntent, token, null,
                    SubscriptionManager.INVALID_SUBSCRIPTION_ID, false);
          }
  
          AppRequestInfo(String packageName, PendingIntent pendingIntent, String token,
                  String prefixes, int subId, boolean packageBasedToken) {
              this.packageName = packageName;
              this.pendingIntent = pendingIntent;
              this.token = token;
              this.timestamp = System.currentTimeMillis();
              this.prefixes = prefixes;
              this.subId = subId;
              this.packageBasedToken = packageBasedToken;
          }
      }
  
  }
  