content: /*
   * Portions Copyright (C) 2016 MonkeyMoto Productions, Inc.
   *
   * Modification at line 150 is based on the following comment:
   *
   *      https://code.google.com/p/android/issues/detail?id=78505#c19
   *
   * Changes are submitted to the public domain without reservation.
   */
  
  /*
   * Copyright (C) 2010 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.google.android.vending.licensing;
  
  import com.google.android.vending.licensing.util.Base64;
  import com.google.android.vending.licensing.util.Base64DecoderException;
  
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.os.Handler;
  import android.os.HandlerThread;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.provider.Settings.Secure;
  import android.util.Log;
  
  import java.security.KeyFactory;
  import java.security.NoSuchAlgorithmException;
  import java.security.PublicKey;
  import java.security.SecureRandom;
  import java.security.spec.InvalidKeySpecException;
  import java.security.spec.X509EncodedKeySpec;
  import java.util.Date;
  import java.util.HashSet;
  import java.util.LinkedList;
  import java.util.Queue;
  import java.util.Set;
  
  /**
   * Client library for Android Market license verifications.
   * <p>
   * The LicenseChecker is configured via a {@link Policy} which contains the
   * logic to determine whether a user should have access to the application. For
   * example, the Policy can define a threshold for allowable number of server or
   * client failures before the library reports the user as not having access.
   * <p>
   * Must also provide the Base64-encoded RSA public key associated with your
   * developer account. The public key is obtainable from the publisher site.
   */
  public class LicenseChecker implements ServiceConnection {
      private static final String TAG = "LicenseChecker";
  
      private static final String KEY_FACTORY_ALGORITHM = "RSA";
  
      // Timeout value (in milliseconds) for calls to service.
      private static final int TIMEOUT_MS = 10 * 1000;
  
      private static final SecureRandom RANDOM = new SecureRandom();
      private static final boolean DEBUG_LICENSE_ERROR = false;
  
      private ILicensingService mService;
  
      private PublicKey mPublicKey;
      private final Context mContext;
      private final Policy mPolicy;
      /**
       * A handler for running tasks on a background thread. We don't want license
       * processing to block the UI thread.
       */
      private Handler mHandler;
      private final String mPackageName;
      private final String mVersionCode;
      private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
      private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
  
      /**
       * @param context a Context
       * @param policy implementation of Policy
       * @param encodedPublicKey Base64-encoded RSA public key
       * @throws IllegalArgumentException if encodedPublicKey is invalid
       */
      public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
          mContext = context;
          mPolicy = policy;
          mPublicKey = generatePublicKey(encodedPublicKey);
          mPackageName = mContext.getPackageName();
          mVersionCode = getVersionCode(context, mPackageName);
          HandlerThread handlerThread = new HandlerThread("background thread");
          handlerThread.start();
          mHandler = new Handler(handlerThread.getLooper());
      }
  
      /**
       * Generates a PublicKey instance from a string containing the
       * Base64-encoded public key.
       * 
       * @param encodedPublicKey Base64-encoded public key
       * @throws IllegalArgumentException if encodedPublicKey is invalid
       */
      private static PublicKey generatePublicKey(String encodedPublicKey) {
          try {
              byte[] decodedKey = Base64.decode(encodedPublicKey);
              KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
  
              return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
          } catch (NoSuchAlgorithmException e) {
              // This won't happen in an Android-compatible environment.
              throw new RuntimeException(e);
          } catch (Base64DecoderException e) {
              Log.e(TAG, "Could not decode from Base64.");
              throw new IllegalArgumentException(e);
          } catch (InvalidKeySpecException e) {
              Log.e(TAG, "Invalid key specification.");
              throw new IllegalArgumentException(e);
          }
      }
  
      /**
       * Checks if the user should have access to the app.  Binds the service if necessary.
       * <p>
       * NOTE: This call uses a trivially obfuscated string (base64-encoded).  For best security,
       * we recommend obfuscating the string that is passed into bindService using another method
       * of your own devising.
       * <p>
       * source string: "com.android.vending.licensing.ILicensingService"
       * <p>
       * @param callback
       */
      public synchronized void checkAccess(LicenseCheckerCallback callback) {
          // If we have a valid recent LICENSED response, we can skip asking
          // Market.
          if (mPolicy.allowAccess()) {
              Log.i(TAG, "Using cached license response");
              callback.allow(Policy.LICENSED);
          } else {
              LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
                      callback, generateNonce(), mPackageName, mVersionCode);
  
              if (mService == null) {
                  Log.i(TAG, "Binding to licensing service.");
                  try {
                      boolean bindResult = mContext
                              .bindService(
                                      new Intent(
                                              new String(
                                                      Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))
                                              .setPackage("com.android.vending"),
                                      this, // ServiceConnection.
                                      Context.BIND_AUTO_CREATE);
  
                      if (bindResult) {
                          mPendingChecks.offer(validator);
                      } else {
                          Log.e(TAG, "Could not bind to service.");
                          handleServiceConnectionError(validator);
                      }
                  } catch (SecurityException e) {
                      callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
                  } catch (Base64DecoderException e) {
                      e.printStackTrace();
                  }
              } else {
                  mPendingChecks.offer(validator);
                  runChecks();
              }
          }
      }
  
      private void runChecks() {
          LicenseValidator validator;
          while ((validator = mPendingChecks.poll()) != null) {
              try {
                  Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName());
                  mService.checkLicense(
                          validator.getNonce(), validator.getPackageName(),
                          new ResultListener(validator));
                  mChecksInProgress.add(validator);
              } catch (RemoteException e) {
                  Log.w(TAG, "RemoteException in checkLicense call.", e);
                  handleServiceConnectionError(validator);
              }
          }
      }
  
      private synchronized void finishCheck(LicenseValidator validator) {
          mChecksInProgress.remove(validator);
          if (mChecksInProgress.isEmpty()) {
              cleanupService();
          }
      }
  
      private class ResultListener extends ILicenseResultListener.Stub {
          private final LicenseValidator mValidator;
          private Runnable mOnTimeout;
  
          public ResultListener(LicenseValidator validator) {
              mValidator = validator;
              mOnTimeout = new Runnable() {
                  public void run() {
                      Log.i(TAG, "Check timed out.");
                      handleServiceConnectionError(mValidator);
                      finishCheck(mValidator);
                  }
              };
              startTimeout();
          }
  
          private static final int ERROR_CONTACTING_SERVER = 0x101;
          private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
          private static final int ERROR_NON_MATCHING_UID = 0x103;
  
          // Runs in IPC thread pool. Post it to the Handler, so we can guarantee
          // either this or the timeout runs.
          public void verifyLicense(final int responseCode, final String signedData,
                  final String signature) {
              mHandler.post(new Runnable() {
                  public void run() {
                      Log.i(TAG, "Received response.");
                      // Make sure it hasn't already timed out.
                      if (mChecksInProgress.contains(mValidator)) {
                          clearTimeout();
                          mValidator.verify(mPublicKey, responseCode, signedData, signature);
                          finishCheck(mValidator);
                      }
                      if (DEBUG_LICENSE_ERROR) {
                          boolean logResponse;
                          String stringError = null;
                          switch (responseCode) {
                              case ERROR_CONTACTING_SERVER:
                                  logResponse = true;
                                  stringError = "ERROR_CONTACTING_SERVER";
                                  break;
                              case ERROR_INVALID_PACKAGE_NAME:
                                  logResponse = true;
                                  stringError = "ERROR_INVALID_PACKAGE_NAME";
                                  break;
                              case ERROR_NON_MATCHING_UID:
                                  logResponse = true;
                                  stringError = "ERROR_NON_MATCHING_UID";
                                  break;
                              default:
                                  logResponse = false;
                          }
  
                          if (logResponse) {
                              String android_id = Secure.getString(mContext.getContentResolver(),
                                      Secure.ANDROID_ID);
                              Date date = new Date();
                              Log.d(TAG, "Server Failure: " + stringError);
                              Log.d(TAG, "Android ID: " + android_id);
                              Log.d(TAG, "Time: " + date.toGMTString());
                          }
                      }
  
                  }
              });
          }
  
          private void startTimeout() {
              Log.i(TAG, "Start monitoring timeout.");
              mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
          }
  
          private void clearTimeout() {
              Log.i(TAG, "Clearing timeout.");
              mHandler.removeCallbacks(mOnTimeout);
          }
      }
  
      public synchronized void onServiceConnected(ComponentName name, IBinder service) {
          mService = ILicensingService.Stub.asInterface(service);
          runChecks();
      }
  
      public synchronized void onServiceDisconnected(ComponentName name) {
          // Called when the connection with the service has been
          // unexpectedly disconnected. That is, Market crashed.
          // If there are any checks in progress, the timeouts will handle them.
          Log.w(TAG, "Service unexpectedly disconnected.");
          mService = null;
      }
  
      /**
       * Generates policy response for service connection errors, as a result of
       * disconnections or timeouts.
       */
      private synchronized void handleServiceConnectionError(LicenseValidator validator) {
          mPolicy.processServerResponse(Policy.RETRY, null);
  
          if (mPolicy.allowAccess()) {
              validator.getCallback().allow(Policy.RETRY);
          } else {
              validator.getCallback().dontAllow(Policy.RETRY);
          }
      }
  
      /** Unbinds service if necessary and removes reference to it. */
      private void cleanupService() {
          if (mService != null) {
              try {
                  mContext.unbindService(this);
              } catch (IllegalArgumentException e) {
                  // Somehow we've already been unbound. This is a non-fatal
                  // error.
                  Log.e(TAG, "Unable to unbind from licensing service (already unbound)");
              }
              mService = null;
          }
      }
  
      /**
       * Inform the library that the context is about to be destroyed, so that any
       * open connections can be cleaned up.
       * <p>
       * Failure to call this method can result in a crash under certain
       * circumstances, such as during screen rotation if an Activity requests the
       * license check or when the user exits the application.
       */
      public synchronized void onDestroy() {
          cleanupService();
          mHandler.getLooper().quit();
      }
  
      /** Generates a nonce (number used once). */
      private int generateNonce() {
          return RANDOM.nextInt();
      }
  
      /**
       * Get version code for the application package name.
       * 
       * @param context
       * @param packageName application package name
       * @return the version code or empty string if package not found
       */
      private static String getVersionCode(Context context, String packageName) {
          try {
              return String.valueOf(context.getPackageManager().getPackageInfo(packageName, 0).
                      versionCode);
          } catch (NameNotFoundException e) {
              Log.e(TAG, "Package not found. could not get version code.");
              return "";
          }
      }
  }
  