/*
* 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.appsimobile.appsii.promo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.util.Log;
import com.appsimobile.appsii.BuildConfig;
import com.appsimobile.appsii.unlock.IAppsiPlugin;
import java.security.SecureRandom;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
/**
* A helper class that can be used to connect to a Appsi Plugin.
* use {@link #connectToService()} for this. The returned Object
* can query the plugin's licensing status. However, before that
* can be done it must first be connected to the Plugin.
* <p/>
* You can use {@link AppsiPluginChecker#waitForConnection()}
* to wait for this. Querying the plugin can be done with
* {@link AppsiPluginChecker#checkLicense()} this call will block
* while the plugin performs the verification.
*/
public class PluginConnectionHelper {
static final String TAG = "RpcConnectionHelper";
private static final SecureRandom RANDOM = new SecureRandom();
final String mSalt;
final int mNonce;
final ServiceConnectionImpl mServiceConnection;
private final Context mContext;
private final String mPackageName;
boolean mConnected;
IAppsiPlugin mService;
/**
* @param context a Context
*
* @throws IllegalArgumentException if encodedPublicKey is invalid
*/
public PluginConnectionHelper(Context context, String packageName) {
mContext = context;
mPackageName = packageName;
mNonce = generateNonce();
mSalt = UUID.randomUUID().toString();
mServiceConnection = new ServiceConnectionImpl();
}
/**
* Generates a nonce (number used once).
*/
private int generateNonce() {
return RANDOM.nextInt();
}
public boolean isInstalledAndSignedCorrectly() {
// Besides checking that is is installed, also check that
// the signatures match
return mContext.getPackageManager().checkSignatures(
BuildConfig.APPLICATION_ID, mPackageName) == PackageManager.SIGNATURE_MATCH;
}
/**
* Start the process of connecting to the plugin. This just tries to bind to the service.
* It returns null if the package is not installed.
* <p/>
* It will throw an IllegalStateException if the service is already bound.
*/
@Nullable
public synchronized AppsiPluginChecker connectToService() {
if (mConnected) throw new IllegalStateException("Connect can only be used once");
mConnected = true;
if (!isInstalled()) return null;
Intent intent = new Intent();
intent.setClassName(mPackageName, mPackageName + ".Appsii");
try {
boolean bindResult = mContext
.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
if (!bindResult) {
Log.e(TAG, "Could not bind to service.");
return null;
}
} catch (SecurityException e) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "Could not bind to service (2).", e);
}
Log.e(TAG, "Could not bind to service (2).");
return null;
}
return mServiceConnection;
}
public boolean isInstalled() {
try {
mContext.getPackageManager().getApplicationInfo(mPackageName, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
/**
* 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();
}
/**
* Unbinds service if necessary and removes reference to it.
*/
void cleanupService() {
if (mService != null) {
try {
mContext.unbindService(mServiceConnection);
} 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;
}
}
public interface AppsiPluginChecker {
boolean waitForConnection() throws InterruptedException;
Bundle checkLicense() throws RemoteException;
}
private final class ServiceConnectionImpl implements ServiceConnection, AppsiPluginChecker {
final CountDownLatch mCountDownLatch = new CountDownLatch(1);
volatile boolean mConnected;
ServiceConnectionImpl() {
}
public boolean waitForConnection() throws InterruptedException {
mCountDownLatch.await();
return mConnected;
}
public Bundle checkLicense() throws RemoteException {
try {
Bundle bundle = new Bundle();
bundle.putInt("nonce", mNonce);
boolean valid = isInstalledAndSignedCorrectly();
// even if the package is invalid, always perform the validation
// this is confusion to say the least.
Bundle result = mService.verifyLicense(bundle, mSalt);
if (valid) return result;
return null;
} finally {
cleanupService();
}
}
public synchronized void onServiceConnected(ComponentName name, IBinder service) {
mService = IAppsiPlugin.Stub.asInterface(service);
mConnected = true;
mCountDownLatch.countDown();
}
public synchronized void onServiceDisconnected(ComponentName name) {
mCountDownLatch.countDown();
mConnected = false;
// 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;
}
}
}