The IPC is implemented using an Android Messenger and a service Binder. The connect method + * should be called whenever the client wants to bind to the service. It opens up a service connection + * that ends up calling the onServiceConnected client API that passes the service messenger + * in. If the client wants to be notified by the service, it is responsible for then passing its + * messenger to the service in a separate call. + * + *
Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}. + * + *
When your application first starts, you should first check whether your app's expansion files are + * already on the device. If not, you should then call {@link #startDownloadServiceIfRequired}, which + * starts your {@link impl.DownloaderService} to download the expansion files if necessary. The method + * returns a value indicating whether download is required or not. + * + *
If a download is required, {@link #startDownloadServiceIfRequired} begins the download through + * the specified service and you should then call {@link #CreateStub} to instantiate a member {@link + * IStub} object that you need in order to receive calls through your {@link IDownloaderClient} + * interface. + */ +public class DownloaderClientMarshaller { + public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10; + public static final int MSG_ONDOWNLOADPROGRESS = 11; + public static final int MSG_ONSERVICECONNECTED = 12; + + public static final String PARAM_NEW_STATE = "newState"; + public static final String PARAM_PROGRESS = "progress"; + public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; + + public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED; + public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED; + public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED; + + private static class Proxy implements IDownloaderClient { + private Messenger mServiceMessenger; + + @Override + public void onDownloadStateChanged(int newState) { + Bundle params = new Bundle(1); + params.putInt(PARAM_NEW_STATE, newState); + send(MSG_ONDOWNLOADSTATE_CHANGED, params); + } + + @Override + public void onDownloadProgress(DownloadProgressInfo progress) { + Bundle params = new Bundle(1); + params.putParcelable(PARAM_PROGRESS, progress); + send(MSG_ONDOWNLOADPROGRESS, params); + } + + private void send(int method, Bundle params) { + Message m = Message.obtain(null, method); + m.setData(params); + try { + mServiceMessenger.send(m); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + public Proxy(Messenger msg) { + mServiceMessenger = msg; + } + + @Override + public void onServiceConnected(Messenger m) { + /** + * This is never called through the proxy. + */ + } + } + + private static class Stub implements IStub { + private IDownloaderClient mItf = null; + private Class> mDownloaderServiceClass; + private boolean mBound; + private Messenger mServiceMessenger; + private Context mContext; + /** + * Target we publish for clients to send messages to IncomingHandler. + */ + final Messenger mMessenger = new Messenger(new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ONDOWNLOADPROGRESS: + Bundle bun = msg.getData(); + if ( null != mContext ) { + bun.setClassLoader(mContext.getClassLoader()); + DownloadProgressInfo dpi = (DownloadProgressInfo) msg.getData() + .getParcelable(PARAM_PROGRESS); + mItf.onDownloadProgress(dpi); + } + break; + case MSG_ONDOWNLOADSTATE_CHANGED: + mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE)); + break; + case MSG_ONSERVICECONNECTED: + mItf.onServiceConnected( + (Messenger) msg.getData().getParcelable(PARAM_MESSENGER)); + break; + } + } + }); + + public Stub(IDownloaderClient itf, Class> downloaderService) { + mItf = itf; + mDownloaderServiceClass = downloaderService; + } + + /** + * Class for interacting with the main interface of the service. + */ + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + // This is called when the connection with the service has been + // established, giving us the object we can use to + // interact with the service. We are communicating with the + // service using a Messenger, so here we get a client-side + // representation of that from the raw IBinder object. + mServiceMessenger = new Messenger(service); + mItf.onServiceConnected( + mServiceMessenger); + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + mServiceMessenger = null; + } + }; + + @Override + public void connect(Context c) { + mContext = c; + Intent bindIntent = new Intent(c, mDownloaderServiceClass); + bindIntent.putExtra(PARAM_MESSENGER, mMessenger); + if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) { + if ( Constants.LOGVV ) { + Log.d(Constants.TAG, "Service Unbound"); + } + } else { + mBound = true; + } + + } + + @Override + public void disconnect(Context c) { + if (mBound) { + c.unbindService(mConnection); + mBound = false; + } + mContext = null; + } + + @Override + public Messenger getMessenger() { + return mMessenger; + } + } + + /** + * Returns a proxy that will marshal calls to IDownloaderClient methods + * + * @param msg + * @return + */ + public static IDownloaderClient CreateProxy(Messenger msg) { + return new Proxy(msg); + } + + /** + * Returns a stub object that, when connected, will listen for marshaled + * {@link IDownloaderClient} methods and translate them into calls to the supplied + * interface. + * + * @param itf An implementation of IDownloaderClient that will be called + * when remote method calls are unmarshaled. + * @param downloaderService The class for your implementation of {@link + * impl.DownloaderService}. + * @return The {@link IStub} that allows you to connect to the service such that + * your {@link IDownloaderClient} receives status updates. + */ + public static IStub CreateStub(IDownloaderClient itf, Class> downloaderService) { + return new Stub(itf, downloaderService); + } + + /** + * Starts the download if necessary. This function starts a flow that does ` + * many things. 1) Checks to see if the APK version has been checked and + * the metadata database updated 2) If the APK version does not match, + * checks the new LVL status to see if a new download is required 3) If the + * APK version does match, then checks to see if the download(s) have been + * completed 4) If the downloads have been completed, returns + * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the + * startup of an application to quickly ascertain if the application needs + * to wait to hear about any updated APK expansion files. Note that this does + * mean that the application MUST be run for the first time with a network + * connection, even if Market delivers all of the files. + * + * @param context Your application Context. + * @param notificationClient A PendingIntent to start the Activity in your application + * that shows the download progress and which will also start the application when download + * completes. + * @param serviceClass the class of your {@link imp.DownloaderService} implementation + * @return whether the service was started and the reason for starting the service. + * Either {@link #NO_DOWNLOAD_REQUIRED}, {@link #LVL_CHECK_REQUIRED}, or {@link + * #DOWNLOAD_REQUIRED}. + * @throws NameNotFoundException + */ + public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient, + Class> serviceClass) + throws NameNotFoundException { + return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, + serviceClass); + } + + /** + * This version assumes that the intent contains the pending intent as a parameter. This + * is used for responding to alarms. + *
The pending intent must be in an extra with the key {@link + * impl.DownloaderService#EXTRA_PENDING_INTENT}. + * + * @param context + * @param notificationClient + * @param serviceClass the class of the service to start + * @return + * @throws NameNotFoundException + */ + public static int startDownloadServiceIfRequired(Context context, Intent notificationClient, + Class> serviceClass) + throws NameNotFoundException { + return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, + serviceClass); + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java new file mode 100644 index 000000000..054eaa989 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2012 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.expansion.downloader; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; + + + +/** + * This class is used by the client activity to proxy requests to the Downloader + * Service. + * + * Most importantly, you must call {@link #CreateProxy} during the {@link + * IDownloaderClient#onServiceConnected} callback in your activity in order to instantiate + * an {@link IDownloaderService} object that you can then use to issue commands to the {@link + * DownloaderService} (such as to pause and resume downloads). + */ +public class DownloaderServiceMarshaller { + + public static final int MSG_REQUEST_ABORT_DOWNLOAD = + 1; + public static final int MSG_REQUEST_PAUSE_DOWNLOAD = + 2; + public static final int MSG_SET_DOWNLOAD_FLAGS = + 3; + public static final int MSG_REQUEST_CONTINUE_DOWNLOAD = + 4; + public static final int MSG_REQUEST_DOWNLOAD_STATE = + 5; + public static final int MSG_REQUEST_CLIENT_UPDATE = + 6; + + public static final String PARAMS_FLAGS = "flags"; + public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; + + private static class Proxy implements IDownloaderService { + private Messenger mMsg; + + private void send(int method, Bundle params) { + Message m = Message.obtain(null, method); + m.setData(params); + try { + mMsg.send(m); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + public Proxy(Messenger msg) { + mMsg = msg; + } + + @Override + public void requestAbortDownload() { + send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle()); + } + + @Override + public void requestPauseDownload() { + send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle()); + } + + @Override + public void setDownloadFlags(int flags) { + Bundle params = new Bundle(); + params.putInt(PARAMS_FLAGS, flags); + send(MSG_SET_DOWNLOAD_FLAGS, params); + } + + @Override + public void requestContinueDownload() { + send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle()); + } + + @Override + public void requestDownloadStatus() { + send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle()); + } + + @Override + public void onClientUpdated(Messenger clientMessenger) { + Bundle bundle = new Bundle(1); + bundle.putParcelable(PARAM_MESSENGER, clientMessenger); + send(MSG_REQUEST_CLIENT_UPDATE, bundle); + } + } + + private static class Stub implements IStub { + private IDownloaderService mItf = null; + final Messenger mMessenger = new Messenger(new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REQUEST_ABORT_DOWNLOAD: + mItf.requestAbortDownload(); + break; + case MSG_REQUEST_CONTINUE_DOWNLOAD: + mItf.requestContinueDownload(); + break; + case MSG_REQUEST_PAUSE_DOWNLOAD: + mItf.requestPauseDownload(); + break; + case MSG_SET_DOWNLOAD_FLAGS: + mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS)); + break; + case MSG_REQUEST_DOWNLOAD_STATE: + mItf.requestDownloadStatus(); + break; + case MSG_REQUEST_CLIENT_UPDATE: + mItf.onClientUpdated((Messenger) msg.getData().getParcelable( + PARAM_MESSENGER)); + break; + } + } + }); + + public Stub(IDownloaderService itf) { + mItf = itf; + } + + @Override + public Messenger getMessenger() { + return mMessenger; + } + + @Override + public void connect(Context c) { + + } + + @Override + public void disconnect(Context c) { + + } + } + + /** + * Returns a proxy that will marshall calls to IDownloaderService methods + * + * @param ctx + * @return + */ + public static IDownloaderService CreateProxy(Messenger msg) { + return new Proxy(msg); + } + + /** + * Returns a stub object that, when connected, will listen for marshalled + * IDownloaderService methods and translate them into calls to the supplied + * interface. + * + * @param itf An implementation of IDownloaderService that will be called + * when remote method calls are unmarshalled. + * @return + */ + public static IStub CreateStub(IDownloaderService itf) { + return new Stub(itf); + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Helpers.java new file mode 100644 index 000000000..1e84e54a0 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/Helpers.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2012 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.expansion.downloader; + +import com.android.vending.expansion.downloader.R; + +import android.content.Context; +import android.os.Environment; +import android.os.StatFs; +import android.os.SystemClock; +import android.util.Log; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Some helper functions for the download manager + */ +public class Helpers { + + public static Random sRandom = new Random(SystemClock.uptimeMillis()); + + /** Regex used to parse content-disposition headers */ + private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern + .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); + + private Helpers() { + } + + /* + * Parse the Content-Disposition HTTP Header. The format of the header is + * defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This + * header provides a filename for content that is going to be downloaded to + * the file system. We only support the attachment type. + */ + static String parseContentDisposition(String contentDisposition) { + try { + Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); + if (m.find()) { + return m.group(1); + } + } catch (IllegalStateException ex) { + // This function is defined as returning null when it can't parse + // the header + } + return null; + } + + /** + * @return the root of the filesystem containing the given path + */ + public static File getFilesystemRoot(String path) { + File cache = Environment.getDownloadCacheDirectory(); + if (path.startsWith(cache.getPath())) { + return cache; + } + File external = Environment.getExternalStorageDirectory(); + if (path.startsWith(external.getPath())) { + return external; + } + throw new IllegalArgumentException( + "Cannot determine filesystem root for " + path); + } + + public static boolean isExternalMediaMounted() { + if (!Environment.getExternalStorageState().equals( + Environment.MEDIA_MOUNTED)) { + // No SD card found. + if ( Constants.LOGVV ) { + Log.d(Constants.TAG, "no external storage"); + } + return false; + } + return true; + } + + /** + * @return the number of bytes available on the filesystem rooted at the + * given File + */ + public static long getAvailableBytes(File root) { + StatFs stat = new StatFs(root.getPath()); + // put a bit of margin (in case creating the file grows the system by a + // few blocks) + long availableBlocks = (long) stat.getAvailableBlocks() - 4; + return stat.getBlockSize() * availableBlocks; + } + + /** + * Checks whether the filename looks legitimate + */ + public static boolean isFilenameValid(String filename) { + filename = filename.replaceFirst("/+", "/"); // normalize leading + // slashes + return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) + || filename.startsWith(Environment.getExternalStorageDirectory().toString()); + } + + /* + * Delete the given file from device + */ + /* package */static void deleteFile(String path) { + try { + File file = new File(path); + file.delete(); + } catch (Exception e) { + Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); + } + } + + /** + * Showing progress in MB here. It would be nice to choose the unit (KB, MB, + * GB) based on total file size, but given what we know about the expected + * ranges of file sizes for APK expansion files, it's probably not necessary. + * + * @param overallProgress + * @param overallTotal + * @return + */ + + static public String getDownloadProgressString(long overallProgress, long overallTotal) { + if (overallTotal == 0) { + if ( Constants.LOGVV ) { + Log.e(Constants.TAG, "Notification called when total is zero"); + } + return ""; + } + return String.format("%.2f", + (float) overallProgress / (1024.0f * 1024.0f)) + + "MB /" + + String.format("%.2f", (float) overallTotal / + (1024.0f * 1024.0f)) + "MB"; + } + + /** + * Adds a percentile to getDownloadProgressString. + * + * @param overallProgress + * @param overallTotal + * @return + */ + static public String getDownloadProgressStringNotification(long overallProgress, + long overallTotal) { + if (overallTotal == 0) { + if ( Constants.LOGVV ) { + Log.e(Constants.TAG, "Notification called when total is zero"); + } + return ""; + } + return getDownloadProgressString(overallProgress, overallTotal) + " (" + + getDownloadProgressPercent(overallProgress, overallTotal) + ")"; + } + + public static String getDownloadProgressPercent(long overallProgress, long overallTotal) { + if (overallTotal == 0) { + if ( Constants.LOGVV ) { + Log.e(Constants.TAG, "Notification called when total is zero"); + } + return ""; + } + return Long.toString(overallProgress * 100 / overallTotal) + "%"; + } + + public static String getSpeedString(float bytesPerMillisecond) { + return String.format("%.2f", bytesPerMillisecond * 1000 / 1024); + } + + public static String getTimeRemaining(long durationInMilliseconds) { + SimpleDateFormat sdf; + if (durationInMilliseconds > 1000 * 60 * 60) { + sdf = new SimpleDateFormat("HH:mm", Locale.getDefault()); + } else { + sdf = new SimpleDateFormat("mm:ss", Locale.getDefault()); + } + return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset())); + } + + /** + * Returns the file name (without full path) for an Expansion APK file from + * the given context. + * + * @param c the context + * @param mainFile true for main file, false for patch file + * @param versionCode the version of the file + * @return String the file name of the expansion file + */ + public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) { + return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb"; + } + + /** + * Returns the filename (where the file should be saved) from info about a + * download + */ + static public String generateSaveFileName(Context c, String fileName) { + String path = getSaveFilePath(c) + + File.separator + fileName; + return path; + } + + static public String getSaveFilePath(Context c) { + File root = Environment.getExternalStorageDirectory(); + String path = root.toString() + Constants.EXP_PATH + c.getPackageName(); + return path; + } + + /** + * Helper function to ascertain the existence of a file and return + * true/false appropriately + * + * @param c the app/activity/service context + * @param fileName the name (sans path) of the file to query + * @param fileSize the size that the file must match + * @param deleteFileOnMismatch if the file sizes do not match, delete the + * file + * @return true if it does exist, false otherwise + */ + static public boolean doesFileExist(Context c, String fileName, long fileSize, + boolean deleteFileOnMismatch) { + // the file may have been delivered by Market --- let's make sure + // it's the size we expect + File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); + if (fileForNewFile.exists()) { + if (fileForNewFile.length() == fileSize) { + return true; + } + if (deleteFileOnMismatch) { + // delete the file --- we won't be able to resume + // because we cannot confirm the integrity of the file + fileForNewFile.delete(); + } + } + return false; + } + + /** + * Converts download states that are returned by the {@link + * IDownloaderClient#onDownloadStateChanged} callback into usable strings. + * This is useful if using the state strings built into the library to display user messages. + * @param state One of the STATE_* constants from {@link IDownloaderClient}. + * @return string resource ID for the corresponding string. + */ + static public int getDownloaderStringResourceIDFromState(int state) { + switch (state) { + case IDownloaderClient.STATE_IDLE: + return R.string.state_idle; + case IDownloaderClient.STATE_FETCHING_URL: + return R.string.state_fetching_url; + case IDownloaderClient.STATE_CONNECTING: + return R.string.state_connecting; + case IDownloaderClient.STATE_DOWNLOADING: + return R.string.state_downloading; + case IDownloaderClient.STATE_COMPLETED: + return R.string.state_completed; + case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE: + return R.string.state_paused_network_unavailable; + case IDownloaderClient.STATE_PAUSED_BY_REQUEST: + return R.string.state_paused_by_request; + case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: + return R.string.state_paused_wifi_disabled; + case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: + return R.string.state_paused_wifi_unavailable; + case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED: + return R.string.state_paused_wifi_disabled; + case IDownloaderClient.STATE_PAUSED_NEED_WIFI: + return R.string.state_paused_wifi_unavailable; + case IDownloaderClient.STATE_PAUSED_ROAMING: + return R.string.state_paused_roaming; + case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE: + return R.string.state_paused_network_setup_failure; + case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: + return R.string.state_paused_sdcard_unavailable; + case IDownloaderClient.STATE_FAILED_UNLICENSED: + return R.string.state_failed_unlicensed; + case IDownloaderClient.STATE_FAILED_FETCHING_URL: + return R.string.state_failed_fetching_url; + case IDownloaderClient.STATE_FAILED_SDCARD_FULL: + return R.string.state_failed_sdcard_full; + case IDownloaderClient.STATE_FAILED_CANCELED: + return R.string.state_failed_cancelled; + default: + return R.string.state_unknown; + } + } + +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java new file mode 100644 index 000000000..b8511a62a --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2012 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.expansion.downloader; + +import android.os.Messenger; + +/** + * This interface should be implemented by the client activity for the + * downloader. It is used to pass status from the service to the client. + */ +public interface IDownloaderClient { + static final int STATE_IDLE = 1; + static final int STATE_FETCHING_URL = 2; + static final int STATE_CONNECTING = 3; + static final int STATE_DOWNLOADING = 4; + static final int STATE_COMPLETED = 5; + + static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6; + static final int STATE_PAUSED_BY_REQUEST = 7; + + /** + * Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and + * STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and + * cellular permission will restart the service. Wi-Fi disabled means that + * the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the + * other case Wi-Fi is enabled but not available. + */ + static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8; + static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9; + + /** + * Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that + * Wi-Fi is unavailable and cellular permission will NOT restart the + * service. Wi-Fi disabled means that the Wi-Fi manager is returning that + * Wi-Fi is not enabled, while in the other case Wi-Fi is enabled but not + * available. + *
+ * The service does not return these values. We recommend that app + * developers with very large payloads do not allow these payloads to be + * downloaded over cellular connections. + */ + static final int STATE_PAUSED_WIFI_DISABLED = 10; + static final int STATE_PAUSED_NEED_WIFI = 11; + + static final int STATE_PAUSED_ROAMING = 12; + + /** + * Scary case. We were on a network that redirected us to another website + * that delivered us the wrong file. + */ + static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13; + + static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14; + + static final int STATE_FAILED_UNLICENSED = 15; + static final int STATE_FAILED_FETCHING_URL = 16; + static final int STATE_FAILED_SDCARD_FULL = 17; + static final int STATE_FAILED_CANCELED = 18; + + static final int STATE_FAILED = 19; + + /** + * Called internally by the stub when the service is bound to the client. + *
+ * Critical implementation detail. In onServiceConnected we create the + * remote service and marshaler. This is how we pass the client information + * back to the service so the client can be properly notified of changes. We + * must do this every time we reconnect to the service. + *
+ * That is, when you receive this callback, you should call + * {@link DownloaderServiceMarshaller#CreateProxy} to instantiate a member + * instance of {@link IDownloaderService}, then call + * {@link IDownloaderService#onClientUpdated} with the Messenger retrieved + * from your {@link IStub} proxy object. + * + * @param m the service Messenger. This Messenger is used to call the + * service API from the client. + */ + void onServiceConnected(Messenger m); + + /** + * Called when the download state changes. Depending on the state, there may + * be user requests. The service is free to change the download state in the + * middle of a user request, so the client should be able to handle this. + *
+ * The Downloader Library includes a collection of string resources that + * correspond to each of the states, which you can use to provide users a + * useful message based on the state provided in this callback. To fetch the + * appropriate string for a state, call + * {@link Helpers#getDownloaderStringResourceIDFromState}. + *
+ * What this means to the developer: The application has gotten a message + * that the download has paused due to lack of WiFi. The developer should + * then show UI asking the user if they want to enable downloading over + * cellular connections with appropriate warnings. If the application + * suddenly starts downloading, the application should revert to showing the + * progress again, rather than leaving up the download over cellular UI up. + * + * @param newState one of the STATE_* values defined in IDownloaderClient + */ + void onDownloadStateChanged(int newState); + + /** + * Shows the download progress. This is intended to be used to fill out a + * client UI. This progress should only be shown in a few states such as + * STATE_DOWNLOADING. + * + * @param progress the DownloadProgressInfo object containing the current + * progress of all downloads. + */ + void onDownloadProgress(DownloadProgressInfo progress); +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderService.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderService.java new file mode 100644 index 000000000..4789afe19 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IDownloaderService.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 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.expansion.downloader; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; +import android.os.Messenger; + +/** + * This interface is implemented by the DownloaderService and by the + * DownloaderServiceMarshaller. It contains functions to control the service. + * When a client binds to the service, it must call the onClientUpdated + * function. + *
+ * You can acquire a proxy that implements this interface for your service by + * calling {@link DownloaderServiceMarshaller#CreateProxy} during the + * {@link IDownloaderClient#onServiceConnected} callback. At which point, you + * should immediately call {@link #onClientUpdated}. + */ +public interface IDownloaderService { + /** + * Set this flag in response to the + * IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then + * call RequestContinueDownload to resume a download + */ + public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1; + + /** + * Request that the service abort the current download. The service should + * respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}. + */ + void requestAbortDownload(); + + /** + * Request that the service pause the current download. The service should + * respond by changing the state to + * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. + */ + void requestPauseDownload(); + + /** + * Request that the service continue a paused download, when in any paused + * or failed state, including + * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. + */ + void requestContinueDownload(); + + /** + * Set the flags for this download (e.g. + * {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}). + * + * @param flags + */ + void setDownloadFlags(int flags); + + /** + * Requests that the download status be sent to the client. + */ + void requestDownloadStatus(); + + /** + * Call this when you get {@link + * IDownloaderClient.onServiceConnected(Messenger m)} from the + * DownloaderClient to register the client with the service. It will + * automatically send the current status to the client. + * + * @param clientMessenger + */ + void onClientUpdated(Messenger clientMessenger); +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IStub.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IStub.java new file mode 100644 index 000000000..d5bc3a843 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/IStub.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2012 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.expansion.downloader; + +import android.content.Context; +import android.os.Messenger; + +/** + * This is the interface that is used to connect/disconnect from the downloader + * service. + *
+ * You should get a proxy object that implements this interface by calling + * {@link DownloaderClientMarshaller#CreateStub} in your activity when the + * downloader service starts. Then, call {@link #connect} during your activity's + * onResume() and call {@link #disconnect} during onStop(). + *
+ * Then during the {@link IDownloaderClient#onServiceConnected} callback, you + * should call {@link #getMessenger} to pass the stub's Messenger object to + * {@link IDownloaderService#onClientUpdated}. + */ +public interface IStub { + Messenger getMessenger(); + + void connect(Context c); + + void disconnect(Context c); +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/SystemFacade.java new file mode 100644 index 000000000..12edd97ab --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/SystemFacade.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2012 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.expansion.downloader; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.telephony.TelephonyManager; +import android.util.Log; + +/** + * Contains useful helper functions, typically tied to the application context. + */ +class SystemFacade { + private Context mContext; + private NotificationManager mNotificationManager; + + public SystemFacade(Context context) { + mContext = context; + mNotificationManager = (NotificationManager) + mContext.getSystemService(Context.NOTIFICATION_SERVICE); + } + + public long currentTimeMillis() { + return System.currentTimeMillis(); + } + + public Integer getActiveNetworkType() { + ConnectivityManager connectivity = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivity == null) { + Log.w(Constants.TAG, "couldn't get connectivity manager"); + return null; + } + + NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); + if (activeInfo == null) { + if (Constants.LOGVV) { + Log.v(Constants.TAG, "network is not available"); + } + return null; + } + return activeInfo.getType(); + } + + public boolean isNetworkRoaming() { + ConnectivityManager connectivity = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivity == null) { + Log.w(Constants.TAG, "couldn't get connectivity manager"); + return false; + } + + NetworkInfo info = connectivity.getActiveNetworkInfo(); + boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE); + TelephonyManager tm = (TelephonyManager) mContext + .getSystemService(Context.TELEPHONY_SERVICE); + if (null == tm) { + Log.w(Constants.TAG, "couldn't get telephony manager"); + return false; + } + boolean isRoaming = isMobile && tm.isNetworkRoaming(); + if (Constants.LOGVV && isRoaming) { + Log.v(Constants.TAG, "network is roaming"); + } + return isRoaming; + } + + public Long getMaxBytesOverMobile() { + return (long) Integer.MAX_VALUE; + } + + public Long getRecommendedMaxBytesOverMobile() { + return 2097152L; + } + + public void sendBroadcast(Intent intent) { + mContext.sendBroadcast(intent); + } + + public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException { + return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid; + } + + public void postNotification(long id, Notification notification) { + /** + * TODO: The system notification manager takes ints, not longs, as IDs, + * but the download manager uses IDs take straight from the database, + * which are longs. This will have to be dealt with at some point. + */ + mNotificationManager.notify((int) id, notification); + } + + public void cancelNotification(long id) { + mNotificationManager.cancel((int) id); + } + + public void cancelAllNotifications() { + mNotificationManager.cancelAll(); + } + + public void startThread(Thread thread) { + thread.start(); + } +} diff --git a/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java new file mode 100644 index 000000000..4667acce6 --- /dev/null +++ b/platform/android/libs/apk_expansion/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java @@ -0,0 +1,536 @@ +/* + * Copyright (C) 2012 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. + */ + +/* + * This is a port of AndroidHttpClient to pre-Froyo devices, that takes advantage of + * the SSLSessionCache added Froyo devices using reflection. + */ + +package com.google.android.vending.expansion.downloader.impl; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.client.protocol.ClientContext; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.scheme.SocketFactory; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.RequestWrapper; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.BasicHttpProcessor; +import org.apache.http.protocol.HttpContext; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.SSLCertificateSocketFactory; +import android.os.Looper; +import android.util.Log; + +/** + * Subclass of the Apache {@link DefaultHttpClient} that is configured with + * reasonable default settings and registered schemes for Android, and + * also lets the user add {@link HttpRequestInterceptor} classes. + * Don't create this directly, use the {@link #newInstance} factory method. + * + *
This client processes cookies but does not retain them by default. + * To retain cookies, simply add a cookie store to the HttpContext:
+ * + *context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);+ */ +public final class AndroidHttpClient implements HttpClient { + + static Class> sSslSessionCacheClass; + static { + // if we are on Froyo+ devices, we can take advantage of the SSLSessionCache + try { + sSslSessionCacheClass = Class.forName("android.net.SSLSessionCache"); + } catch (Exception e) { + + } + } + + // Gzip of data shorter than this probably won't be worthwhile + public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256; + + // Default connection and socket timeout of 60 seconds. Tweak to taste. + private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000; + + private static final String TAG = "AndroidHttpClient"; + + + /** Interceptor throws an exception if the executing thread is blocked */ + private static final HttpRequestInterceptor sThreadCheckInterceptor = + new HttpRequestInterceptor() { + public void process(HttpRequest request, HttpContext context) { + // Prevent the HttpRequest from being sent on the main thread + if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) { + throw new RuntimeException("This thread forbids HTTP requests"); + } + } + }; + + /** + * Create a new HttpClient with reasonable defaults (which you can update). + * + * @param userAgent to report in your HTTP requests + * @param context to use for caching SSL sessions (may be null for no caching) + * @return AndroidHttpClient for you to use for all your requests. + */ + public static AndroidHttpClient newInstance(String userAgent, Context context) { + HttpParams params = new BasicHttpParams(); + + // Turn off stale checking. Our connections break all the time anyway, + // and it's not worth it to pay the penalty of checking every time. + HttpConnectionParams.setStaleCheckingEnabled(params, false); + + HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT); + HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT); + HttpConnectionParams.setSocketBufferSize(params, 8192); + + // Don't handle redirects -- return them to the caller. Our code + // often wants to re-POST after a redirect, which we must do ourselves. + HttpClientParams.setRedirecting(params, false); + + Object sessionCache = null; + // Use a session cache for SSL sockets -- Froyo only + if ( null != context && null != sSslSessionCacheClass ) { + Constructor> ct; + try { + ct = sSslSessionCacheClass.getConstructor(Context.class); + sessionCache = ct.newInstance(context); + } catch (SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchMethodException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (InstantiationException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalAccessException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (InvocationTargetException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + // Set the specified user agent and register standard protocols. + HttpProtocolParams.setUserAgent(params, userAgent); + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register(new Scheme("http", + PlainSocketFactory.getSocketFactory(), 80)); + SocketFactory sslCertificateSocketFactory = null; + if ( null != sessionCache ) { + Method getHttpSocketFactoryMethod; + try { + getHttpSocketFactoryMethod = SSLCertificateSocketFactory.class.getDeclaredMethod("getHttpSocketFactory",Integer.TYPE, sSslSessionCacheClass); + sslCertificateSocketFactory = (SocketFactory)getHttpSocketFactoryMethod.invoke(null, SOCKET_OPERATION_TIMEOUT, sessionCache); + } catch (SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchMethodException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalAccessException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (InvocationTargetException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + if ( null == sslCertificateSocketFactory ) { + sslCertificateSocketFactory = SSLSocketFactory.getSocketFactory(); + } + schemeRegistry.register(new Scheme("https", + sslCertificateSocketFactory, 443)); + + ClientConnectionManager manager = + new ThreadSafeClientConnManager(params, schemeRegistry); + + // We use a factory method to modify superclass initialization + // parameters without the funny call-a-static-method dance. + return new AndroidHttpClient(manager, params); + } + + /** + * Create a new HttpClient with reasonable defaults (which you can update). + * @param userAgent to report in your HTTP requests. + * @return AndroidHttpClient for you to use for all your requests. + */ + public static AndroidHttpClient newInstance(String userAgent) { + return newInstance(userAgent, null /* session cache */); + } + + private final HttpClient delegate; + + private RuntimeException mLeakedException = new IllegalStateException( + "AndroidHttpClient created and never closed"); + + private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) { + this.delegate = new DefaultHttpClient(ccm, params) { + @Override + protected BasicHttpProcessor createHttpProcessor() { + // Add interceptor to prevent making requests from main thread. + BasicHttpProcessor processor = super.createHttpProcessor(); + processor.addRequestInterceptor(sThreadCheckInterceptor); + processor.addRequestInterceptor(new CurlLogger()); + + return processor; + } + + @Override + protected HttpContext createHttpContext() { + // Same as DefaultHttpClient.createHttpContext() minus the + // cookie store. + HttpContext context = new BasicHttpContext(); + context.setAttribute( + ClientContext.AUTHSCHEME_REGISTRY, + getAuthSchemes()); + context.setAttribute( + ClientContext.COOKIESPEC_REGISTRY, + getCookieSpecs()); + context.setAttribute( + ClientContext.CREDS_PROVIDER, + getCredentialsProvider()); + return context; + } + }; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (mLeakedException != null) { + Log.e(TAG, "Leak found", mLeakedException); + mLeakedException = null; + } + } + + /** + * Modifies a request to indicate to the server that we would like a + * gzipped response. (Uses the "Accept-Encoding" HTTP header.) + * @param request the request to modify + * @see #getUngzippedContent + */ + public static void modifyRequestToAcceptGzipResponse(HttpRequest request) { + request.addHeader("Accept-Encoding", "gzip"); + } + + /** + * Gets the input stream from a response entity. If the entity is gzipped + * then this will get a stream over the uncompressed data. + * + * @param entity the entity whose content should be read + * @return the input stream to read from + * @throws IOException + */ + public static InputStream getUngzippedContent(HttpEntity entity) + throws IOException { + InputStream responseStream = entity.getContent(); + if (responseStream == null) return responseStream; + Header header = entity.getContentEncoding(); + if (header == null) return responseStream; + String contentEncoding = header.getValue(); + if (contentEncoding == null) return responseStream; + if (contentEncoding.contains("gzip")) responseStream + = new GZIPInputStream(responseStream); + return responseStream; + } + + /** + * Release resources associated with this client. You must call this, + * or significant resources (sockets and memory) may be leaked. + */ + public void close() { + if (mLeakedException != null) { + getConnectionManager().shutdown(); + mLeakedException = null; + } + } + + public HttpParams getParams() { + return delegate.getParams(); + } + + public ClientConnectionManager getConnectionManager() { + return delegate.getConnectionManager(); + } + + public HttpResponse execute(HttpUriRequest request) throws IOException { + return delegate.execute(request); + } + + public HttpResponse execute(HttpUriRequest request, HttpContext context) + throws IOException { + return delegate.execute(request, context); + } + + public HttpResponse execute(HttpHost target, HttpRequest request) + throws IOException { + return delegate.execute(target, request); + } + + public HttpResponse execute(HttpHost target, HttpRequest request, + HttpContext context) throws IOException { + return delegate.execute(target, request, context); + } + + public
+ * These values will vary based on the the way the application is configured in + * the Android Market publishing console, such as whether the application is + * marked as free or is within its refund period, as well as how often an + * application is checking with the licensing service. + *
+ * Developers who need more fine grained control over their application's
+ * licensing policy should implement a custom Policy.
+ */
+public class APKExpansionPolicy implements Policy {
+
+ private static final String TAG = "APKExpansionPolicy";
+ private static final String PREFS_FILE = "com.android.vending.licensing.APKExpansionPolicy";
+ private static final String PREF_LAST_RESPONSE = "lastResponse";
+ private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
+ private static final String PREF_RETRY_UNTIL = "retryUntil";
+ private static final String PREF_MAX_RETRIES = "maxRetries";
+ private static final String PREF_RETRY_COUNT = "retryCount";
+ private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
+ private static final String DEFAULT_RETRY_UNTIL = "0";
+ private static final String DEFAULT_MAX_RETRIES = "0";
+ private static final String DEFAULT_RETRY_COUNT = "0";
+
+ private static final long MILLIS_PER_MINUTE = 60 * 1000;
+
+ private long mValidityTimestamp;
+ private long mRetryUntil;
+ private long mMaxRetries;
+ private long mRetryCount;
+ private long mLastResponseTime = 0;
+ private int mLastResponse;
+ private PreferenceObfuscator mPreferences;
+ private Vector
+ * This data will be used for computing future policy decisions. The
+ * following parameters are processed:
+ *
+ * The LICENSED response from the server contains a user identifier unique to
+ * the <application, user> pair. The developer can send this identifier
+ * to their own server along with some device identifier (a random number
+ * generated and stored once per application installation,
+ * {@link android.telephony.TelephonyManager#getDeviceId getDeviceId},
+ * {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc).
+ * The more sources used to identify the device, the harder it will be for an
+ * attacker to spoof.
+ *
+ * The server can look at the <application, user, device id> tuple and
+ * restrict a user's application license to run on at most 10 different devices
+ * in a week (for example). We recommend not being too restrictive because a
+ * user might legitimately have multiple devices or be in the process of
+ * changing phones. This will catch egregious violations of multiple people
+ * sharing one license.
+ */
+public interface DeviceLimiter {
+
+ /**
+ * Checks if this device is allowed to use the given user's license.
+ *
+ * @param userId the user whose license the server responded with
+ * @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs
+ */
+ int isDeviceAllowed(String userId);
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicenseResultListener.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicenseResultListener.java
new file mode 100644
index 000000000..d90d6eac7
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicenseResultListener.java
@@ -0,0 +1,99 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ * Original file: aidl/ILicenseResultListener.aidl
+ */
+package com.google.android.vending.licensing;
+import java.lang.String;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Binder;
+import android.os.Parcel;
+public interface ILicenseResultListener extends android.os.IInterface
+{
+/** Local-side IPC implementation stub class. */
+public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener
+{
+private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener";
+/** Construct the stub at attach it to the interface. */
+public Stub()
+{
+this.attachInterface(this, DESCRIPTOR);
+}
+/**
+ * Cast an IBinder object into an ILicenseResultListener interface,
+ * generating a proxy if needed.
+ */
+public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj)
+{
+if ((obj==null)) {
+return null;
+}
+android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
+if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) {
+return ((com.google.android.vending.licensing.ILicenseResultListener)iin);
+}
+return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj);
+}
+public android.os.IBinder asBinder()
+{
+return this;
+}
+public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+{
+switch (code)
+{
+case INTERFACE_TRANSACTION:
+{
+reply.writeString(DESCRIPTOR);
+return true;
+}
+case TRANSACTION_verifyLicense:
+{
+data.enforceInterface(DESCRIPTOR);
+int _arg0;
+_arg0 = data.readInt();
+java.lang.String _arg1;
+_arg1 = data.readString();
+java.lang.String _arg2;
+_arg2 = data.readString();
+this.verifyLicense(_arg0, _arg1, _arg2);
+return true;
+}
+}
+return super.onTransact(code, data, reply, flags);
+}
+private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener
+{
+private android.os.IBinder mRemote;
+Proxy(android.os.IBinder remote)
+{
+mRemote = remote;
+}
+public android.os.IBinder asBinder()
+{
+return mRemote;
+}
+public java.lang.String getInterfaceDescriptor()
+{
+return DESCRIPTOR;
+}
+public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+_data.writeInt(responseCode);
+_data.writeString(signedData);
+_data.writeString(signature);
+mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY);
+}
+finally {
+_data.recycle();
+}
+}
+}
+static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
+}
+public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException;
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicensingService.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicensingService.java
new file mode 100644
index 000000000..95599544e
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ILicensingService.java
@@ -0,0 +1,99 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ * Original file: aidl/ILicensingService.aidl
+ */
+package com.google.android.vending.licensing;
+import java.lang.String;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Binder;
+import android.os.Parcel;
+public interface ILicensingService extends android.os.IInterface
+{
+/** Local-side IPC implementation stub class. */
+public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService
+{
+private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService";
+/** Construct the stub at attach it to the interface. */
+public Stub()
+{
+this.attachInterface(this, DESCRIPTOR);
+}
+/**
+ * Cast an IBinder object into an ILicensingService interface,
+ * generating a proxy if needed.
+ */
+public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj)
+{
+if ((obj==null)) {
+return null;
+}
+android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
+if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicensingService))) {
+return ((com.google.android.vending.licensing.ILicensingService)iin);
+}
+return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj);
+}
+public android.os.IBinder asBinder()
+{
+return this;
+}
+public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+{
+switch (code)
+{
+case INTERFACE_TRANSACTION:
+{
+reply.writeString(DESCRIPTOR);
+return true;
+}
+case TRANSACTION_checkLicense:
+{
+data.enforceInterface(DESCRIPTOR);
+long _arg0;
+_arg0 = data.readLong();
+java.lang.String _arg1;
+_arg1 = data.readString();
+com.google.android.vending.licensing.ILicenseResultListener _arg2;
+_arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder());
+this.checkLicense(_arg0, _arg1, _arg2);
+return true;
+}
+}
+return super.onTransact(code, data, reply, flags);
+}
+private static class Proxy implements com.google.android.vending.licensing.ILicensingService
+{
+private android.os.IBinder mRemote;
+Proxy(android.os.IBinder remote)
+{
+mRemote = remote;
+}
+public android.os.IBinder asBinder()
+{
+return mRemote;
+}
+public java.lang.String getInterfaceDescriptor()
+{
+return DESCRIPTOR;
+}
+public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+_data.writeLong(nonce);
+_data.writeString(packageName);
+_data.writeStrongBinder((((listener!=null))?(listener.asBinder()):(null)));
+mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY);
+}
+finally {
+_data.recycle();
+}
+}
+}
+static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
+}
+public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException;
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseChecker.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseChecker.java
new file mode 100644
index 000000000..0b1c4b6cc
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseChecker.java
@@ -0,0 +1,351 @@
+/*
+ * 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.
+ *
+ * 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.
+ *
+ * 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
+ * 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.
+ *
+ * source string: "com.android.vending.licensing.ILicensingService"
+ *
+ * @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="))),
+ 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.
+ *
+ * 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 "";
+ }
+ }
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseCheckerCallback.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseCheckerCallback.java
new file mode 100644
index 000000000..b250a7147
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseCheckerCallback.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+/**
+ * Callback for the license checker library.
+ *
+ * Upon checking with the Market server and conferring with the {@link Policy},
+ * the library calls the appropriate callback method to communicate the result.
+ *
+ * The callback does not occur in the original checking thread. Your
+ * application should post to the appropriate handling thread or lock
+ * accordingly.
+ *
+ * The reason that is passed back with allow/dontAllow is the base status handed
+ * to the policy for allowed/disallowing the license. Policy.RETRY will call
+ * allow or dontAllow depending on other statistics associated with the policy,
+ * while in most cases Policy.NOT_LICENSED will call dontAllow and
+ * Policy.LICENSED will Allow.
+ */
+public interface LicenseCheckerCallback {
+
+ /**
+ * Allow use. App should proceed as normal.
+ *
+ * @param reason Policy.LICENSED or Policy.RETRY typically. (although in
+ * theory the policy can return Policy.NOT_LICENSED here as well)
+ */
+ public void allow(int reason);
+
+ /**
+ * Don't allow use. App should inform user and take appropriate action.
+ *
+ * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory
+ * the policy can return Policy.LICENSED here as well ---
+ * perhaps the call to the LVL took too long, for example)
+ */
+ public void dontAllow(int reason);
+
+ /** Application error codes. */
+ public static final int ERROR_INVALID_PACKAGE_NAME = 1;
+ public static final int ERROR_NON_MATCHING_UID = 2;
+ public static final int ERROR_NOT_MARKET_MANAGED = 3;
+ public static final int ERROR_CHECK_IN_PROGRESS = 4;
+ public static final int ERROR_INVALID_PUBLIC_KEY = 5;
+ public static final int ERROR_MISSING_PERMISSION = 6;
+
+ /**
+ * Error in application code. Caller did not call or set up license checker
+ * correctly. Should be considered fatal.
+ */
+ public void applicationError(int errorCode);
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseValidator.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseValidator.java
new file mode 100644
index 000000000..61d3c7e79
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/LicenseValidator.java
@@ -0,0 +1,224 @@
+/*
+ * 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.text.TextUtils;
+import android.util.Log;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+
+/**
+ * Contains data related to a licensing request and methods to verify
+ * and process the response.
+ */
+class LicenseValidator {
+ private static final String TAG = "LicenseValidator";
+
+ // Server response codes.
+ private static final int LICENSED = 0x0;
+ private static final int NOT_LICENSED = 0x1;
+ private static final int LICENSED_OLD_KEY = 0x2;
+ private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
+ private static final int ERROR_SERVER_FAILURE = 0x4;
+ private static final int ERROR_OVER_QUOTA = 0x5;
+
+ 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;
+
+ private final Policy mPolicy;
+ private final LicenseCheckerCallback mCallback;
+ private final int mNonce;
+ private final String mPackageName;
+ private final String mVersionCode;
+ private final DeviceLimiter mDeviceLimiter;
+
+ LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback,
+ int nonce, String packageName, String versionCode) {
+ mPolicy = policy;
+ mDeviceLimiter = deviceLimiter;
+ mCallback = callback;
+ mNonce = nonce;
+ mPackageName = packageName;
+ mVersionCode = versionCode;
+ }
+
+ public LicenseCheckerCallback getCallback() {
+ return mCallback;
+ }
+
+ public int getNonce() {
+ return mNonce;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+ /**
+ * Verifies the response from server and calls appropriate callback method.
+ *
+ * @param publicKey public key associated with the developer account
+ * @param responseCode server response code
+ * @param signedData signed data from server
+ * @param signature server signature
+ */
+ public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
+ String userId = null;
+ // Skip signature check for unsuccessful requests
+ ResponseData data = null;
+ if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
+ responseCode == LICENSED_OLD_KEY) {
+ // Verify signature.
+ try {
+ Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+ sig.initVerify(publicKey);
+ sig.update(signedData.getBytes());
+
+ if (!sig.verify(Base64.decode(signature))) {
+ Log.e(TAG, "Signature verification failed.");
+ handleInvalidResponse();
+ return;
+ }
+ } catch (NoSuchAlgorithmException e) {
+ // This can't happen on an Android compatible device.
+ throw new RuntimeException(e);
+ } catch (InvalidKeyException e) {
+ handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY);
+ return;
+ } catch (SignatureException e) {
+ throw new RuntimeException(e);
+ } catch (Base64DecoderException e) {
+ Log.e(TAG, "Could not Base64-decode signature.");
+ handleInvalidResponse();
+ return;
+ }
+
+ // Parse and validate response.
+ try {
+ data = ResponseData.parse(signedData);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Could not parse response.");
+ handleInvalidResponse();
+ return;
+ }
+
+ if (data.responseCode != responseCode) {
+ Log.e(TAG, "Response codes don't match.");
+ handleInvalidResponse();
+ return;
+ }
+
+ if (data.nonce != mNonce) {
+ Log.e(TAG, "Nonce doesn't match.");
+ handleInvalidResponse();
+ return;
+ }
+
+ if (!data.packageName.equals(mPackageName)) {
+ Log.e(TAG, "Package name doesn't match.");
+ handleInvalidResponse();
+ return;
+ }
+
+ if (!data.versionCode.equals(mVersionCode)) {
+ Log.e(TAG, "Version codes don't match.");
+ handleInvalidResponse();
+ return;
+ }
+
+ // Application-specific user identifier.
+ userId = data.userId;
+ if (TextUtils.isEmpty(userId)) {
+ Log.e(TAG, "User identifier is empty.");
+ handleInvalidResponse();
+ return;
+ }
+ }
+
+ switch (responseCode) {
+ case LICENSED:
+ case LICENSED_OLD_KEY:
+ int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
+ handleResponse(limiterResponse, data);
+ break;
+ case NOT_LICENSED:
+ handleResponse(Policy.NOT_LICENSED, data);
+ break;
+ case ERROR_CONTACTING_SERVER:
+ Log.w(TAG, "Error contacting licensing server.");
+ handleResponse(Policy.RETRY, data);
+ break;
+ case ERROR_SERVER_FAILURE:
+ Log.w(TAG, "An error has occurred on the licensing server.");
+ handleResponse(Policy.RETRY, data);
+ break;
+ case ERROR_OVER_QUOTA:
+ Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
+ handleResponse(Policy.RETRY, data);
+ break;
+ case ERROR_INVALID_PACKAGE_NAME:
+ handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME);
+ break;
+ case ERROR_NON_MATCHING_UID:
+ handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID);
+ break;
+ case ERROR_NOT_MARKET_MANAGED:
+ handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED);
+ break;
+ default:
+ Log.e(TAG, "Unknown response code for license check.");
+ handleInvalidResponse();
+ }
+ }
+
+ /**
+ * Confers with policy and calls appropriate callback method.
+ *
+ * @param response
+ * @param rawData
+ */
+ private void handleResponse(int response, ResponseData rawData) {
+ // Update policy data and increment retry counter (if needed)
+ mPolicy.processServerResponse(response, rawData);
+
+ // Given everything we know, including cached data, ask the policy if we should grant
+ // access.
+ if (mPolicy.allowAccess()) {
+ mCallback.allow(response);
+ } else {
+ mCallback.dontAllow(response);
+ }
+ }
+
+ private void handleApplicationError(int code) {
+ mCallback.applicationError(code);
+ }
+
+ private void handleInvalidResponse() {
+ mCallback.dontAllow(Policy.NOT_LICENSED);
+ }
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/NullDeviceLimiter.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/NullDeviceLimiter.java
new file mode 100644
index 000000000..d87af3153
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/NullDeviceLimiter.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * A DeviceLimiter that doesn't limit the number of devices that can use a
+ * given user's license.
+ *
+ * Unless you have reason to believe that your application is being pirated
+ * by multiple users using the same license (signing in to Market as the same
+ * user), we recommend you use this implementation.
+ */
+public class NullDeviceLimiter implements DeviceLimiter {
+
+ public int isDeviceAllowed(String userId) {
+ return Policy.LICENSED;
+ }
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Obfuscator.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Obfuscator.java
new file mode 100644
index 000000000..b5d510d72
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Obfuscator.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * Interface used as part of a {@link Policy} to allow application authors to obfuscate
+ * licensing data that will be stored into a SharedPreferences file.
+ *
+ * Any transformation scheme must be reversable. Implementing classes may optionally implement an
+ * integrity check to further prevent modification to preference data. Implementing classes
+ * should use device-specific information as a key in the obfuscation algorithm to prevent
+ * obfuscated preferences from being shared among devices.
+ */
+public interface Obfuscator {
+
+ /**
+ * Obfuscate a string that is being stored into shared preferences.
+ *
+ * @param original The data that is to be obfuscated.
+ * @param key The key for the data that is to be obfuscated.
+ * @return A transformed version of the original data.
+ */
+ String obfuscate(String original, String key);
+
+ /**
+ * Undo the transformation applied to data by the obfuscate() method.
+ *
+ * @param original The data that is to be obfuscated.
+ * @param key The key for the data that is to be obfuscated.
+ * @return A transformed version of the original data.
+ * @throws ValidationException Optionally thrown if a data integrity check fails.
+ */
+ String unobfuscate(String obfuscated, String key) throws ValidationException;
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Policy.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Policy.java
new file mode 100644
index 000000000..fa267fc71
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/Policy.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+/**
+ * Policy used by {@link LicenseChecker} to determine whether a user should have
+ * access to the application.
+ */
+public interface Policy {
+
+ /**
+ * Change these values to make it more difficult for tools to automatically
+ * strip LVL protection from your APK.
+ */
+
+ /**
+ * LICENSED means that the server returned back a valid license response
+ */
+ public static final int LICENSED = 0x0100;
+ /**
+ * NOT_LICENSED means that the server returned back a valid license response
+ * that indicated that the user definitively is not licensed
+ */
+ public static final int NOT_LICENSED = 0x0231;
+ /**
+ * RETRY means that the license response was unable to be determined ---
+ * perhaps as a result of faulty networking
+ */
+ public static final int RETRY = 0x0123;
+
+ /**
+ * Provide results from contact with the license server. Retry counts are
+ * incremented if the current value of response is RETRY. Results will be
+ * used for any future policy decisions.
+ *
+ * @param response the result from validating the server response
+ * @param rawData the raw server response data, can be null for RETRY
+ */
+ void processServerResponse(int response, ResponseData rawData);
+
+ /**
+ * Check if the user should be allowed access to the application.
+ */
+ boolean allowAccess();
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/PreferenceObfuscator.java
new file mode 100644
index 000000000..7c42bfc28
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/PreferenceObfuscator.java
@@ -0,0 +1,77 @@
+/*
+ * 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 android.content.SharedPreferences;
+import android.util.Log;
+
+/**
+ * An wrapper for SharedPreferences that transparently performs data obfuscation.
+ */
+public class PreferenceObfuscator {
+
+ private static final String TAG = "PreferenceObfuscator";
+
+ private final SharedPreferences mPreferences;
+ private final Obfuscator mObfuscator;
+ private SharedPreferences.Editor mEditor;
+
+ /**
+ * Constructor.
+ *
+ * @param sp A SharedPreferences instance provided by the system.
+ * @param o The Obfuscator to use when reading or writing data.
+ */
+ public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) {
+ mPreferences = sp;
+ mObfuscator = o;
+ mEditor = null;
+ }
+
+ public void putString(String key, String value) {
+ if (mEditor == null) {
+ mEditor = mPreferences.edit();
+ }
+ String obfuscatedValue = mObfuscator.obfuscate(value, key);
+ mEditor.putString(key, obfuscatedValue);
+ }
+
+ public String getString(String key, String defValue) {
+ String result;
+ String value = mPreferences.getString(key, null);
+ if (value != null) {
+ try {
+ result = mObfuscator.unobfuscate(value, key);
+ } catch (ValidationException e) {
+ // Unable to unobfuscate, data corrupt or tampered
+ Log.w(TAG, "Validation error while reading preference: " + key);
+ result = defValue;
+ }
+ } else {
+ // Preference not found
+ result = defValue;
+ }
+ return result;
+ }
+
+ public void commit() {
+ if (mEditor != null) {
+ mEditor.commit();
+ mEditor = null;
+ }
+ }
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ResponseData.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ResponseData.java
new file mode 100644
index 000000000..2adef3709
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ResponseData.java
@@ -0,0 +1,79 @@
+/*
+ * 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 java.util.regex.Pattern;
+
+import android.text.TextUtils;
+
+/**
+ * ResponseData from licensing server.
+ */
+public class ResponseData {
+
+ public int responseCode;
+ public int nonce;
+ public String packageName;
+ public String versionCode;
+ public String userId;
+ public long timestamp;
+ /** Response-specific data. */
+ public String extra;
+
+ /**
+ * Parses response string into ResponseData.
+ *
+ * @param responseData response data string
+ * @throws IllegalArgumentException upon parsing error
+ * @return ResponseData object
+ */
+ public static ResponseData parse(String responseData) {
+ // Must parse out main response data and response-specific data.
+ int index = responseData.indexOf(':');
+ String mainData, extraData;
+ if ( -1 == index ) {
+ mainData = responseData;
+ extraData = "";
+ } else {
+ mainData = responseData.substring(0, index);
+ extraData = index >= responseData.length() ? "" : responseData.substring(index+1);
+ }
+
+ String [] fields = TextUtils.split(mainData, Pattern.quote("|"));
+ if (fields.length < 6) {
+ throw new IllegalArgumentException("Wrong number of fields.");
+ }
+
+ ResponseData data = new ResponseData();
+ data.extra = extraData;
+ data.responseCode = Integer.parseInt(fields[0]);
+ data.nonce = Integer.parseInt(fields[1]);
+ data.packageName = fields[2];
+ data.versionCode = fields[3];
+ // Application-specific user identifier.
+ data.userId = fields[4];
+ data.timestamp = Long.parseLong(fields[5]);
+
+ return data;
+ }
+
+ @Override
+ public String toString() {
+ return TextUtils.join("|", new Object [] { responseCode, nonce, packageName, versionCode,
+ userId, timestamp });
+ }
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ServerManagedPolicy.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ServerManagedPolicy.java
new file mode 100644
index 000000000..fbf8cf6d0
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ServerManagedPolicy.java
@@ -0,0 +1,276 @@
+/*
+ * 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 org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+/**
+ * Default policy. All policy decisions are based off of response data received
+ * from the licensing service. Specifically, the licensing server sends the
+ * following information: response validity period, error retry period, and
+ * error retry count.
+ *
+ * These values will vary based on the the way the application is configured in
+ * the Android Market publishing console, such as whether the application is
+ * marked as free or is within its refund period, as well as how often an
+ * application is checking with the licensing service.
+ *
+ * Developers who need more fine grained control over their application's
+ * licensing policy should implement a custom Policy.
+ */
+public class ServerManagedPolicy implements Policy {
+
+ private static final String TAG = "ServerManagedPolicy";
+ private static final String PREFS_FILE = "com.android.vending.licensing.ServerManagedPolicy";
+ private static final String PREF_LAST_RESPONSE = "lastResponse";
+ private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
+ private static final String PREF_RETRY_UNTIL = "retryUntil";
+ private static final String PREF_MAX_RETRIES = "maxRetries";
+ private static final String PREF_RETRY_COUNT = "retryCount";
+ private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
+ private static final String DEFAULT_RETRY_UNTIL = "0";
+ private static final String DEFAULT_MAX_RETRIES = "0";
+ private static final String DEFAULT_RETRY_COUNT = "0";
+
+ private static final long MILLIS_PER_MINUTE = 60 * 1000;
+
+ private long mValidityTimestamp;
+ private long mRetryUntil;
+ private long mMaxRetries;
+ private long mRetryCount;
+ private long mLastResponseTime = 0;
+ private int mLastResponse;
+ private PreferenceObfuscator mPreferences;
+
+ /**
+ * @param context The context for the current application
+ * @param obfuscator An obfuscator to be used with preferences.
+ */
+ public ServerManagedPolicy(Context context, Obfuscator obfuscator) {
+ // Import old values
+ SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+ mPreferences = new PreferenceObfuscator(sp, obfuscator);
+ mLastResponse = Integer.parseInt(
+ mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
+ mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
+ DEFAULT_VALIDITY_TIMESTAMP));
+ mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
+ mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
+ mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
+ }
+
+ /**
+ * Process a new response from the license server.
+ *
+ * This data will be used for computing future policy decisions. The
+ * following parameters are processed:
+ *
+ * Using a non-caching policy ensures that there is no local preference data
+ * for malicious users to tamper with. As a side effect, applications
+ * will not be permitted to run while offline. Developers should carefully
+ * weigh the risks of using this Policy over one which implements caching,
+ * such as ServerManagedPolicy.
+ *
+ * Access to the application is only allowed if a LICESNED response is.
+ * received. All other responses (including RETRY) will deny access.
+ */
+public class StrictPolicy implements Policy {
+
+ private int mLastResponse;
+
+ public StrictPolicy() {
+ // Set default policy. This will force the application to check the policy on launch.
+ mLastResponse = Policy.RETRY;
+ }
+
+ /**
+ * Process a new response from the license server. Since we aren't
+ * performing any caching, this equates to reading the LicenseResponse.
+ * Any ResponseData provided is ignored.
+ *
+ * @param response the result from validating the server response
+ * @param rawData the raw server response data
+ */
+ public void processServerResponse(int response, ResponseData rawData) {
+ mLastResponse = response;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation allows access if and only if a LICENSED response
+ * was received the last time the server was contacted.
+ */
+ public boolean allowAccess() {
+ return (mLastResponse == Policy.LICENSED);
+ }
+
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ValidationException.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ValidationException.java
new file mode 100644
index 000000000..ee4df47c6
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/ValidationException.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Indicates that an error occurred while validating the integrity of data managed by an
+ * {@link Obfuscator}.}
+ */
+public class ValidationException extends Exception {
+ public ValidationException() {
+ super();
+ }
+
+ public ValidationException(String s) {
+ super(s);
+ }
+
+ private static final long serialVersionUID = 1L;
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64.java
new file mode 100644
index 000000000..a0d2779af
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64.java
@@ -0,0 +1,570 @@
+// Portions copyright 2002, Google, Inc.
+//
+// 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.util;
+
+// This code was converted from code at http://iharder.sourceforge.net/base64/
+// Lots of extraneous features were removed.
+/* The original code said:
+ *
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit
+ * http://iharder.net/xmlizable
+ * periodically to check for updates or to contribute improvements.
+ * Note {@link CharBase64} is a GWT-compatible implementation of this
+ * class.
+ */
+public class Base64 {
+ /** Specify encoding (value is {@code true}). */
+ public final static boolean ENCODE = true;
+
+ /** Specify decoding (value is {@code false}). */
+ public final static boolean DECODE = false;
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /** The new line character (\n) as a byte. */
+ private final static byte NEW_LINE = (byte) '\n';
+
+ /**
+ * The 64 valid Base64 values.
+ */
+ private final static byte[] ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '+', (byte) '/'};
+
+ /**
+ * The 64 valid web safe Base64 values.
+ */
+ private final static byte[] WEBSAFE_ALPHABET =
+ {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '-', (byte) '_'};
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value
+ * or a negative number indicating some other meaning.
+ **/
+ private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9, -9, -9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ /** The web safe decodabet */
+ private final static byte[] WEBSAFE_DECODABET =
+ {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
+ 62, // Dash '-' sign at decimal 45
+ -9, -9, // Decimal 46-47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+ -9, -9, -9, -9, // Decimal 91-94
+ 63, // Underscore '_' at decimal 95
+ -9, // Decimal 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+ -9, -9, -9, -9, -9 // Decimal 123 - 127
+ /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+ // Indicates white space in encoding
+ private final static byte WHITE_SPACE_ENC = -5;
+ // Indicates equals sign in encoding
+ private final static byte EQUALS_SIGN_ENC = -1;
+
+ /** Defeats instantiation. */
+ private Base64() {
+ }
+
+ /* ******** E N C O D I N G M E T H O D S ******** */
+
+ /**
+ * Encodes up to three bytes of the array source
+ * and writes the resulting four Base64 bytes to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 3 for
+ * the source array or destOffset + 4 for
+ * the destination array.
+ * The actual number of significant bytes in your array is
+ * given by numSigBytes.
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param alphabet is the encoding alphabet
+ * @return the destination array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(byte[] source, int srcOffset,
+ int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index alphabet
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an int.
+ int inBuff =
+ (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
+ return destination;
+ case 2:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ case 1:
+ destination[destOffset] = alphabet[(inBuff >>> 18)];
+ destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Equivalent to calling
+ * {@code encodeBytes(source, 0, source.length)}
+ *
+ * @param source The data to convert
+ * @since 1.4
+ */
+ public static String encode(byte[] source) {
+ return encode(source, 0, source.length, ALPHABET, true);
+ }
+
+ /**
+ * Encodes a byte array into web safe Base64 notation.
+ *
+ * @param source The data to convert
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ */
+ public static String encodeWebSafe(byte[] source, boolean doPadding) {
+ return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @param alphabet is the encoding alphabet
+ * @param doPadding is {@code true} to pad result with '=' chars
+ * if it does not fall on 3 byte boundaries
+ * @since 1.4
+ */
+ public static String encode(byte[] source, int off, int len, byte[] alphabet,
+ boolean doPadding) {
+ byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
+ int outLen = outBuff.length;
+
+ // If doPadding is false, set length to truncate '='
+ // padding characters
+ while (doPadding == false && outLen > 0) {
+ if (outBuff[outLen - 1] != '=') {
+ break;
+ }
+ outLen -= 1;
+ }
+
+ return new String(outBuff, 0, outLen);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @param alphabet is the encoding alphabet
+ * @param maxLineLength maximum length of one line.
+ * @return the BASE64-encoded byte array
+ */
+ public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
+ int maxLineLength) {
+ int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
+ int len43 = lenDiv3 * 4;
+ byte[] outBuff = new byte[len43 // Main 4:3
+ + (len43 / maxLineLength)]; // New lines
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for (; d < len2; d += 3, e += 4) {
+
+ // The following block of code is the same as
+ // encode3to4( source, d + off, 3, outBuff, e, alphabet );
+ // but inlined for faster encoding (~20% improvement)
+ int inBuff =
+ ((source[d + off] << 24) >>> 8)
+ | ((source[d + 1 + off] << 24) >>> 16)
+ | ((source[d + 2 + off] << 24) >>> 24);
+ outBuff[e] = alphabet[(inBuff >>> 18)];
+ outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+ outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+ outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // end for: each piece of array
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e, alphabet);
+
+ lineLength += 4;
+ if (lineLength == maxLineLength) {
+ // Add a last newline
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ }
+ e += 4;
+ }
+
+ assert (e == outBuff.length);
+ return outBuff;
+ }
+
+
+ /* ******** D E C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * Decodes four bytes from array source
+ * and writes the resulting bytes (up to three of them)
+ * to destination.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * srcOffset and destOffset.
+ * This method does not check to make sure your arrays
+ * are large enough to accommodate srcOffset + 4 for
+ * the source array or destOffset + 3 for
+ * the destination array.
+ * This method returns the actual number of bytes that
+ * were converted from the Base64 encoding.
+ *
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return the number of decoded bytes converted
+ * @since 1.3
+ */
+ private static int decode4to3(byte[] source, int srcOffset,
+ byte[] destination, int destOffset, byte[] decodabet) {
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ return 1;
+ } else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ // Example: DkL=
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ destination[destOffset + 1] = (byte) (outBuff >>> 8);
+ return 2;
+ } else {
+ // Example: DkLE
+ int outBuff =
+ ((decodabet[source[srcOffset]] << 24) >>> 6)
+ | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+ | ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
+ | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
+
+ destination[destOffset] = (byte) (outBuff >> 16);
+ destination[destOffset + 1] = (byte) (outBuff >> 8);
+ destination[destOffset + 2] = (byte) (outBuff);
+ return 3;
+ }
+ } // end decodeToBytes
+
+
+ /**
+ * Decodes data from Base64 notation.
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decode(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes data from web safe Base64 notation.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param s the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
+ byte[] bytes = s.getBytes();
+ return decodeWebSafe(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source The Base64 encoded data
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source) throws Base64DecoderException {
+ return decode(source, 0, source.length);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded data.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source the string to decode (decoded in default encoding)
+ * @return the decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source)
+ throws Base64DecoderException {
+ return decodeWebSafe(source, 0, source.length);
+ }
+
+ /**
+ * Decodes Base64 content in byte array format and returns
+ * the decoded byte array.
+ *
+ * @param source The Base64 encoded data
+ * @param off The offset of where to begin decoding
+ * @param len The length of characters to decode
+ * @return decoded data
+ * @since 1.3
+ * @throws Base64DecoderException
+ */
+ public static byte[] decode(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, DECODABET);
+ }
+
+ /**
+ * Decodes web safe Base64 content in byte array format and returns
+ * the decoded byte array.
+ * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+ *
+ * @param source The Base64 encoded data
+ * @param off The offset of where to begin decoding
+ * @param len The length of characters to decode
+ * @return decoded data
+ */
+ public static byte[] decodeWebSafe(byte[] source, int off, int len)
+ throws Base64DecoderException {
+ return decode(source, off, len, WEBSAFE_DECODABET);
+ }
+
+ /**
+ * Decodes Base64 content using the supplied decodabet and returns
+ * the decoded byte array.
+ *
+ * @param source The Base64 encoded data
+ * @param off The offset of where to begin decoding
+ * @param len The length of characters to decode
+ * @param decodabet the decodabet for decoding Base64 content
+ * @return decoded data
+ */
+ public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
+ throws Base64DecoderException {
+ int len34 = len * 3 / 4;
+ byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+ int i = 0;
+ byte sbiCrop = 0;
+ byte sbiDecode = 0;
+ for (i = 0; i < len; i++) {
+ sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
+ sbiDecode = decodabet[sbiCrop];
+
+ if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
+ if (sbiDecode >= EQUALS_SIGN_ENC) {
+ // An equals sign (for padding) must not occur at position 0 or 1
+ // and must be the last byte[s] in the encoded value
+ if (sbiCrop == EQUALS_SIGN) {
+ int bytesLeft = len - i;
+ byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
+ if (b4Posn == 0 || b4Posn == 1) {
+ throw new Base64DecoderException(
+ "invalid padding byte '=' at byte offset " + i);
+ } else if ((b4Posn == 3 && bytesLeft > 2)
+ || (b4Posn == 4 && bytesLeft > 1)) {
+ throw new Base64DecoderException(
+ "padding byte '=' falsely signals end of encoded value "
+ + "at offset " + i);
+ } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
+ throw new Base64DecoderException(
+ "encoded value has invalid trailing byte");
+ }
+ break;
+ }
+
+ b4[b4Posn++] = sbiCrop;
+ if (b4Posn == 4) {
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ b4Posn = 0;
+ }
+ }
+ } else {
+ throw new Base64DecoderException("Bad Base64 input character at " + i
+ + ": " + source[i + off] + "(decimal)");
+ }
+ }
+
+ // Because web safe encoding allows non padding base64 encodes, we
+ // need to pad the rest of the b4 buffer with equal signs when
+ // b4Posn != 0. There can be at most 2 equal signs at the end of
+ // four characters, so the b4 buffer must have two or three
+ // characters. This also catches the case where the input is
+ // padded with EQUALS_SIGN
+ if (b4Posn != 0) {
+ if (b4Posn == 1) {
+ throw new Base64DecoderException("single trailing character at offset "
+ + (len - 1));
+ }
+ b4[b4Posn++] = EQUALS_SIGN;
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+ }
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ }
+}
diff --git a/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64DecoderException.java b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64DecoderException.java
new file mode 100644
index 000000000..1aef1b54b
--- /dev/null
+++ b/platform/android/libs/play_licensing/src/com/google/android/vending/licensing/util/Base64DecoderException.java
@@ -0,0 +1,32 @@
+// Copyright 2002, Google, Inc.
+//
+// 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.util;
+
+/**
+ * Exception thrown when encountering an invalid Base64 input character.
+ *
+ * @author nelson
+ */
+public class Base64DecoderException extends Exception {
+ public Base64DecoderException() {
+ super();
+ }
+
+ public Base64DecoderException(String s) {
+ super(s);
+ }
+
+ private static final long serialVersionUID = 1L;
+}
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index 9d9da3750..3a101515d 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -34,7 +34,7 @@
#include "drivers/unix/dir_access_unix.h"
#include "servers/visual/visual_server_raster.h"
-
+#include "servers/visual/visual_server_wrap_mt.h"
#include "main/main.h"
#include "core/globals.h"
@@ -128,6 +128,10 @@ void OS_Android::initialize(const VideoMode& p_desired,int p_video_driver,int p_
}
visual_server = memnew( VisualServerRaster(rasterizer) );
+ if (get_render_thread_mode() != RENDER_THREAD_UNSAFE) {
+
+ visual_server = memnew(VisualServerWrapMT(visual_server, false));
+ };
visual_server->init();
visual_server->cursor_set_visible(false, 0);
@@ -672,8 +676,29 @@ String OS_Android::get_unique_ID() const {
return OS::get_unique_ID();
}
+Error OS_Android::native_video_play(String p_path) {
+ if (video_play_func)
+ video_play_func(p_path);
+ return OK;
+}
+
+bool OS_Android::native_video_is_playing() {
+ if (video_is_playing_func)
+ return video_is_playing_func();
+ return false;
+}
+
+void OS_Android::native_video_pause() {
+ if (video_pause_func)
+ video_pause_func();
+}
+
+void OS_Android::native_video_stop() {
+ if (video_stop_func)
+ video_stop_func();
+}
-OS_Android::OS_Android(GFXInitFunc p_gfx_init_func,void*p_gfx_init_ud, OpenURIFunc p_open_uri_func, GetDataDirFunc p_get_data_dir_func,GetLocaleFunc p_get_locale_func,GetModelFunc p_get_model_func, ShowVirtualKeyboardFunc p_show_vk, HideVirtualKeyboardFunc p_hide_vk, SetScreenOrientationFunc p_screen_orient,GetUniqueIDFunc p_get_unique_id) {
+OS_Android::OS_Android(GFXInitFunc p_gfx_init_func,void*p_gfx_init_ud, OpenURIFunc p_open_uri_func, GetDataDirFunc p_get_data_dir_func,GetLocaleFunc p_get_locale_func,GetModelFunc p_get_model_func, ShowVirtualKeyboardFunc p_show_vk, HideVirtualKeyboardFunc p_hide_vk, SetScreenOrientationFunc p_screen_orient,GetUniqueIDFunc p_get_unique_id, VideoPlayFunc p_video_play_func, VideoIsPlayingFunc p_video_is_playing_func, VideoPauseFunc p_video_pause_func, VideoStopFunc p_video_stop_func) {
default_videomode.width=800;
diff --git a/platform/android/os_android.h b/platform/android/os_android.h
index 93c672927..76139c8a2 100644
--- a/platform/android/os_android.h
+++ b/platform/android/os_android.h
@@ -41,16 +41,19 @@
#ifdef ANDROID_NATIVE_ACTIVITY
-#include "audio_driver_android.h"
+
#include
+ *
+ *
+ * @param response the result from validating the server response
+ * @param rawData the raw server response data
+ */
+ public void processServerResponse(int response,
+ com.google.android.vending.licensing.ResponseData rawData) {
+
+ // Update retry counter
+ if (response != Policy.RETRY) {
+ setRetryCount(0);
+ } else {
+ setRetryCount(mRetryCount + 1);
+ }
+
+ if (response == Policy.LICENSED) {
+ // Update server policy data
+ Map
+ *
+ *
+ */
+ public boolean allowAccess() {
+ long ts = System.currentTimeMillis();
+ if (mLastResponse == Policy.LICENSED) {
+ // Check if the LICENSED response occurred within the validity
+ // timeout.
+ if (ts <= mValidityTimestamp) {
+ // Cached LICENSED response is still valid.
+ return true;
+ }
+ } else if (mLastResponse == Policy.RETRY &&
+ ts < mLastResponseTime + MILLIS_PER_MINUTE) {
+ // Only allow access if we are within the retry period or we haven't
+ // used up our
+ // max retries.
+ return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
+ }
+ return false;
+ }
+
+ private Map
+ *
+ *
+ * @param response the result from validating the server response
+ * @param rawData the raw server response data
+ */
+ public void processServerResponse(int response, ResponseData rawData) {
+
+ // Update retry counter
+ if (response != Policy.RETRY) {
+ setRetryCount(0);
+ } else {
+ setRetryCount(mRetryCount + 1);
+ }
+
+ if (response == Policy.LICENSED) {
+ // Update server policy data
+ Map
+ *
+ *
+ */
+ public boolean allowAccess() {
+ long ts = System.currentTimeMillis();
+ if (mLastResponse == Policy.LICENSED) {
+ // Check if the LICENSED response occurred within the validity timeout.
+ if (ts <= mValidityTimestamp) {
+ // Cached LICENSED response is still valid.
+ return true;
+ }
+ } else if (mLastResponse == Policy.RETRY &&
+ ts < mLastResponseTime + MILLIS_PER_MINUTE) {
+ // Only allow access if we are within the retry period or we haven't used up our
+ // max retries.
+ return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
+ }
+ return false;
+ }
+
+ private Map