@@ -0,0 +1,280 @@
/*
* 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.app.PendingIntent;
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.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
* This class binds the service API to your application client. It contains the IDownloaderClient proxy,
* which is used to call functions in your client as well as the Stub, which is used to call functions
* in the client implementation of IDownloaderClient.
*
* <p>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.
*
* <p>Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}.
*
* <p>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.
*
* <p>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 {

private static final Logger LOG = LoggerFactory.getLogger("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.debug("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.
* <p>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);
}

}
@@ -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);
}

}
@@ -0,0 +1,311 @@
/*
* 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.Build;
import android.os.Environment;
import android.os.StatFs;
import android.os.SystemClock;

import com.android.vending.expansion.downloader.R;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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 {

private static final Logger LOG = LoggerFactory.getLogger("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.debug("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.warn("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.error("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.error("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.error("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 = Build.VERSION.SDK_INT >= 23 ? Constants.EXP_PATH_API23 : Constants.EXP_PATH;
return root.toString() + path + c.getPackageName();
}

/**
* 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;
}
}

}
@@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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);
}
@@ -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.
* <p>
* 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);
}
@@ -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.
* <p>
* 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().
* <p>
* 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);
}
@@ -0,0 +1,129 @@
/*
* 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;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Contains useful helper functions, typically tied to the application context.
*/
class SystemFacade {

private static final Logger LOG = LoggerFactory.getLogger("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.warn("couldn't get connectivity manager");
return null;
}

NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
if (activeInfo == null) {
if (Constants.LOGVV) {
LOG.info("network is not available");
}
return null;
}
return activeInfo.getType();
}

public boolean isNetworkRoaming() {
ConnectivityManager connectivity =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity == null) {
LOG.warn("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.warn("couldn't get telephony manager");
return false;
}
boolean isRoaming = isMobile && tm.isNetworkRoaming();
if (Constants.LOGVV && isRoaming) {
LOG.info("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();
}
}

Large diffs are not rendered by default.

@@ -0,0 +1,116 @@
/*
* 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.impl;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* This service differs from IntentService in a few minor ways/ It will not
* auto-stop itself after the intent is handled unless the target returns "true"
* in should stop. Since the goal of this service is to handle a single kind of
* intent, it does not queue up batches of intents of the same type.
*/
public abstract class CustomIntentService extends Service {

private static final Logger LOG = LoggerFactory.getLogger("CancellableIntentService");

private String mName;
private boolean mRedelivery;
private volatile ServiceHandler mServiceHandler;
private volatile Looper mServiceLooper;
private static final int WHAT_MESSAGE = -10;

public CustomIntentService(String paramString) {
this.mName = paramString;
}

@Override
public IBinder onBind(Intent paramIntent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();
HandlerThread localHandlerThread = new HandlerThread("IntentService["
+ this.mName + "]");
localHandlerThread.start();
this.mServiceLooper = localHandlerThread.getLooper();
this.mServiceHandler = new ServiceHandler(this.mServiceLooper);
}

@Override
public void onDestroy() {
Thread localThread = this.mServiceLooper.getThread();
if ((localThread != null) && (localThread.isAlive())) {
localThread.interrupt();
}
this.mServiceLooper.quit();
LOG.debug("onDestroy");
}

protected abstract void onHandleIntent(Intent paramIntent);

protected abstract boolean shouldStop();

@Override
public void onStart(Intent paramIntent, int startId) {
if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) {
Message localMessage = this.mServiceHandler.obtainMessage();
localMessage.arg1 = startId;
localMessage.obj = paramIntent;
localMessage.what = WHAT_MESSAGE;
this.mServiceHandler.sendMessage(localMessage);
}
}

@Override
public int onStartCommand(Intent paramIntent, int flags, int startId) {
onStart(paramIntent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

public void setIntentRedelivery(boolean enabled) {
this.mRedelivery = enabled;
}

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message paramMessage) {
CustomIntentService.this
.onHandleIntent((Intent) paramMessage.obj);
if (shouldStop()) {
LOG.debug("stopSelf");
CustomIntentService.this.stopSelf(paramMessage.arg1);
LOG.debug("afterStopSelf");
}
}
}
}
@@ -0,0 +1,30 @@
/*
* 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.impl;

/**
* Uses the class-loader model to utilize the updated notification builders in
* Honeycomb while maintaining a compatible version for older devices.
*/
public class CustomNotificationFactory {
static public DownloadNotification.ICustomNotification createCustomNotification() {
if (android.os.Build.VERSION.SDK_INT > 13)
return new V14CustomNotification();
else
throw new RuntimeException();
}
}
@@ -0,0 +1,58 @@
/*
* 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.impl;

import com.google.android.vending.expansion.downloader.Helpers;

/**
* Representation of information about an individual download from the database.
*/
public class DownloadInfo {
public String mUri;
public final int mIndex;
public final String mFileName;
public String mETag;
public long mTotalBytes;
public long mCurrentBytes;
public long mLastMod;
public int mStatus;
public int mControl;
public int mNumFailed;
public int mRetryAfter;
public int mRedirectCount;

boolean mInitialized;

public int mFuzz;

public DownloadInfo(int index, String fileName, String pkg) {
mFuzz = Helpers.sRandom.nextInt(1001);
mFileName = fileName;
mIndex = index;
}

public void resetDownload() {
mCurrentBytes = 0;
mETag = "";
mLastMod = 0;
mStatus = 0;
mControl = 0;
mNumFailed = 0;
mRetryAfter = 0;
mRedirectCount = 0;
}
}
@@ -0,0 +1,236 @@
/*
* 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.impl;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.os.Messenger;

import com.android.vending.expansion.downloader.R;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;

/**
* This class handles displaying the notification associated with the download
* queue going on in the download manager. It handles multiple status types;
* Some require user interaction and some do not. Some of the user interactions
* may be transient. (for example: the user is queried to continue the download
* on 3G when it started on WiFi, but then the phone locks onto WiFi again so
* the prompt automatically goes away)
* <p/>
* The application interface for the downloader also needs to understand and
* handle these transient states.
*/
public class DownloadNotification implements IDownloaderClient {

private int mState;
private final Context mContext;
private final NotificationManager mNotificationManager;
private String mCurrentTitle;

private IDownloaderClient mClientProxy;
final ICustomNotification mCustomNotification;
private Notification.Builder mNotificationBuilder;
private Notification.Builder mCurrentNotificationBuilder;
private CharSequence mLabel;
private String mCurrentText;
private PendingIntent mContentIntent;
private DownloadProgressInfo mProgressInfo;

static final String LOGTAG = "DownloadNotification";
static final int NOTIFICATION_ID = LOGTAG.hashCode();

public PendingIntent getClientIntent() {
return mContentIntent;
}

public void setClientIntent(PendingIntent mClientIntent) {
this.mContentIntent = mClientIntent;
}

public void resendState() {
if (null != mClientProxy) {
mClientProxy.onDownloadStateChanged(mState);
}
}

@Override
public void onDownloadStateChanged(int newState) {
boolean completed = false;
if (null != mClientProxy) {
mClientProxy.onDownloadStateChanged(newState);
}
if (newState != mState) {
mState = newState;
if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
return;
}
int stringDownloadID;
int iconResource;
boolean ongoingEvent;

// get the new title string and paused text
switch (newState) {
case 0:
iconResource = android.R.drawable.stat_sys_warning;
stringDownloadID = R.string.state_unknown;
ongoingEvent = false;
break;

case IDownloaderClient.STATE_DOWNLOADING:
iconResource = android.R.drawable.stat_sys_download;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = true;
break;

case IDownloaderClient.STATE_FETCHING_URL:
case IDownloaderClient.STATE_CONNECTING:
iconResource = android.R.drawable.stat_sys_download_done;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = true;
break;

case IDownloaderClient.STATE_COMPLETED:
completed = true;
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
iconResource = android.R.drawable.stat_sys_download_done;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = false;
break;

case IDownloaderClient.STATE_FAILED:
case IDownloaderClient.STATE_FAILED_CANCELED:
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
case IDownloaderClient.STATE_FAILED_UNLICENSED:
iconResource = android.R.drawable.stat_sys_warning;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = false;
break;

default:
iconResource = android.R.drawable.stat_sys_warning;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = true;
break;
}
if (completed) {
mNotificationManager.cancel(NOTIFICATION_ID);
} else {
mCurrentText = mContext.getString(stringDownloadID);
mCurrentTitle = mLabel.toString();
mCurrentNotificationBuilder.setTicker(mLabel + ": " + mCurrentText);
mCurrentNotificationBuilder.setSmallIcon(iconResource);
mCurrentNotificationBuilder.setContentTitle(mCurrentTitle);
mCurrentNotificationBuilder.setContentText(mCurrentText);
mCurrentNotificationBuilder.setContentIntent(mContentIntent);
mCurrentNotificationBuilder.setOngoing(ongoingEvent);
mCurrentNotificationBuilder.setAutoCancel(!ongoingEvent);
mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotificationBuilder.build());
}
}
}

@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
mProgressInfo = progress;
if (null != mClientProxy) {
mClientProxy.onDownloadProgress(progress);
}
if (progress.mOverallTotal <= 0) {
// we just show the text
mNotificationBuilder.setTicker(mCurrentTitle);
mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
mNotificationBuilder.setContentTitle(mCurrentTitle);
mNotificationBuilder.setContentText(mCurrentText);
mNotificationBuilder.setContentIntent(mContentIntent);
mCurrentNotificationBuilder = mNotificationBuilder;
} else {
mCustomNotification.setCurrentBytes(progress.mOverallProgress);
mCustomNotification.setTotalBytes(progress.mOverallTotal);
mCustomNotification.setIcon(android.R.drawable.stat_sys_download);
mCustomNotification.setPendingIntent(mContentIntent);
mCustomNotification.setTicker(mLabel + ": " + mCurrentText);
mCustomNotification.setTitle(mLabel);
mCustomNotification.setTimeRemaining(progress.mTimeRemaining);
mCurrentNotificationBuilder = mCustomNotification.updateNotification(mContext);
}
mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotificationBuilder.build());
}

public interface ICustomNotification {
void setTitle(CharSequence title);

void setTicker(CharSequence ticker);

void setPendingIntent(PendingIntent mContentIntent);

void setTotalBytes(long totalBytes);

void setCurrentBytes(long currentBytes);

void setIcon(int iconResource);

void setTimeRemaining(long timeRemaining);

Notification.Builder updateNotification(Context c);
}

/**
* Called in response to onClientUpdated. Creates a new proxy and notifies
* it of the current state.
*
* @param msg the client Messenger to notify
*/
public void setMessenger(Messenger msg) {
mClientProxy = DownloaderClientMarshaller.CreateProxy(msg);
if (null != mProgressInfo) {
mClientProxy.onDownloadProgress(mProgressInfo);
}
if (mState != -1) {
mClientProxy.onDownloadStateChanged(mState);
}
}

/**
* Constructor
*
* @param ctx The context to use to obtain access to the Notification
* Service
*/
DownloadNotification(Context ctx, CharSequence applicationLabel) {
mState = -1;
mContext = ctx;
mLabel = applicationLabel;
mNotificationManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mCustomNotification = CustomNotificationFactory
.createCustomNotification();
mNotificationBuilder = new Notification.Builder(ctx);
mCurrentNotificationBuilder = mNotificationBuilder;

}

@Override
public void onServiceConnected(Messenger m) {
}

}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

@@ -0,0 +1,200 @@
/*
* 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.impl;

import android.text.format.Time;

import java.util.Calendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Helper for parsing an HTTP date.
*/
public final class HttpDateTime {

/*
* Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT
* RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850,
* obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format
* with following variations Wdy, DD-Mon-YYYY HH:MM:SS GMT Wdy, (SP)D Mon
* YYYY HH:MM:SS GMT Wdy,DD Mon YYYY HH:MM:SS GMT Wdy, DD-Mon-YY HH:MM:SS
* GMT Wdy, DD Mon YYYY HH:MM:SS -HHMM Wdy, DD Mon YYYY HH:MM:SS Wdy Mon
* (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first
* digit is zero. Mon can be the full name of the month.
*/
private static final String HTTP_DATE_RFC_REGEXP =
"([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]"
+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])";

private static final String HTTP_DATE_ANSIC_REGEXP =
"[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]"
+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})";

/**
* The compiled version of the HTTP-date regular expressions.
*/
private static final Pattern HTTP_DATE_RFC_PATTERN =
Pattern.compile(HTTP_DATE_RFC_REGEXP);
private static final Pattern HTTP_DATE_ANSIC_PATTERN =
Pattern.compile(HTTP_DATE_ANSIC_REGEXP);

private static class TimeOfDay {
TimeOfDay(int h, int m, int s) {
this.hour = h;
this.minute = m;
this.second = s;
}

int hour;
int minute;
int second;
}

public static long parse(String timeString)
throws IllegalArgumentException {

int date = 1;
int month = Calendar.JANUARY;
int year = 1970;
TimeOfDay timeOfDay;

Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
if (rfcMatcher.find()) {
date = getDate(rfcMatcher.group(1));
month = getMonth(rfcMatcher.group(2));
year = getYear(rfcMatcher.group(3));
timeOfDay = getTime(rfcMatcher.group(4));
} else {
Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
if (ansicMatcher.find()) {
month = getMonth(ansicMatcher.group(1));
date = getDate(ansicMatcher.group(2));
timeOfDay = getTime(ansicMatcher.group(3));
year = getYear(ansicMatcher.group(4));
} else {
throw new IllegalArgumentException();
}
}

// FIXME: Y2038 BUG!
if (year >= 2038) {
year = 2038;
month = Calendar.JANUARY;
date = 1;
}

Time time = new Time(Time.TIMEZONE_UTC);
time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
month, year);
return time.toMillis(false /* use isDst */);
}

private static int getDate(String dateString) {
if (dateString.length() == 2) {
return (dateString.charAt(0) - '0') * 10
+ (dateString.charAt(1) - '0');
} else {
return (dateString.charAt(0) - '0');
}
}

/*
* jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0
* + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20
* + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19
* = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9
*/
private static int getMonth(String monthString) {
int hash = Character.toLowerCase(monthString.charAt(0)) +
Character.toLowerCase(monthString.charAt(1)) +
Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
switch (hash) {
case 22:
return Calendar.JANUARY;
case 10:
return Calendar.FEBRUARY;
case 29:
return Calendar.MARCH;
case 32:
return Calendar.APRIL;
case 36:
return Calendar.MAY;
case 42:
return Calendar.JUNE;
case 40:
return Calendar.JULY;
case 26:
return Calendar.AUGUST;
case 37:
return Calendar.SEPTEMBER;
case 35:
return Calendar.OCTOBER;
case 48:
return Calendar.NOVEMBER;
case 9:
return Calendar.DECEMBER;
default:
throw new IllegalArgumentException();
}
}

private static int getYear(String yearString) {
if (yearString.length() == 2) {
int year = (yearString.charAt(0) - '0') * 10
+ (yearString.charAt(1) - '0');
if (year >= 70) {
return year + 1900;
} else {
return year + 2000;
}
} else if (yearString.length() == 3) {
// According to RFC 2822, three digit years should be added to 1900.
int year = (yearString.charAt(0) - '0') * 100
+ (yearString.charAt(1) - '0') * 10
+ (yearString.charAt(2) - '0');
return year + 1900;
} else if (yearString.length() == 4) {
return (yearString.charAt(0) - '0') * 1000
+ (yearString.charAt(1) - '0') * 100
+ (yearString.charAt(2) - '0') * 10
+ (yearString.charAt(3) - '0');
} else {
return 1970;
}
}

private static TimeOfDay getTime(String timeString) {
// HH might be H
int i = 0;
int hour = timeString.charAt(i++) - '0';
if (timeString.charAt(i) != ':')
hour = hour * 10 + (timeString.charAt(i++) - '0');
// Skip ':'
i++;

int minute = (timeString.charAt(i++) - '0') * 10
+ (timeString.charAt(i++) - '0');
// Skip ':'
i++;

int second = (timeString.charAt(i++) - '0') * 10
+ (timeString.charAt(i++) - '0');

return new TimeOfDay(hour, minute, second);
}
}
@@ -0,0 +1,101 @@
/*
* 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.impl;

import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;

import com.android.vending.expansion.downloader.R;
import com.google.android.vending.expansion.downloader.Helpers;

public class V14CustomNotification implements DownloadNotification.ICustomNotification {

CharSequence mTitle;
CharSequence mTicker;
int mIcon;
long mTotalKB = -1;
long mCurrentKB = -1;
long mTimeRemaining;
PendingIntent mPendingIntent;

@Override
public void setIcon(int icon) {
mIcon = icon;
}

@Override
public void setTitle(CharSequence title) {
mTitle = title;
}

@Override
public void setTotalBytes(long totalBytes) {
mTotalKB = totalBytes;
}

@Override
public void setCurrentBytes(long currentBytes) {
mCurrentKB = currentBytes;
}

void setProgress(Notification.Builder builder) {

}

@Override
public Notification.Builder updateNotification(Context c) {
Notification.Builder builder = new Notification.Builder(c);
builder.setContentTitle(mTitle);
if (mTotalKB > 0 && -1 != mCurrentKB) {
builder.setProgress((int) (mTotalKB >> 8), (int) (mCurrentKB >> 8), false);
} else {
builder.setProgress(0, 0, true);
}
builder.setContentText(Helpers.getDownloadProgressString(mCurrentKB, mTotalKB));
builder.setContentInfo(c.getString(R.string.time_remaining_notification,
Helpers.getTimeRemaining(mTimeRemaining)));
if (mIcon != 0) {
builder.setSmallIcon(mIcon);
} else {
int iconResource = android.R.drawable.stat_sys_download;
builder.setSmallIcon(iconResource);
}
builder.setOngoing(true);
builder.setTicker(mTicker);
builder.setContentIntent(mPendingIntent);
builder.setOnlyAlertOnce(true);

return builder;
}

@Override
public void setPendingIntent(PendingIntent contentIntent) {
mPendingIntent = contentIntent;
}

@Override
public void setTicker(CharSequence ticker) {
mTicker = ticker;
}

@Override
public void setTimeRemaining(long timeRemaining) {
mTimeRemaining = timeRemaining;
}

}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2008, 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.
*/
-->

<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android">

<RelativeLayout
android:layout_width="35dp"
android:layout_height="fill_parent"
android:paddingTop="10dp"
android:paddingBottom="8dp" >

<ImageView
android:id="@+id/appIcon"
android:layout_width="fill_parent"
android:layout_height="25dp"
android:scaleType="centerInside"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:src="@android:drawable/stat_sys_download" />

<TextView
android:id="@+id/progress_text"
style="@style/NotificationText"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:layout_gravity="center_horizontal"
android:singleLine="true"
android:gravity="center" />
</RelativeLayout>

<RelativeLayout
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:clickable="true"
android:focusable="true"
android:paddingTop="10dp"
android:paddingRight="8dp"
android:paddingBottom="8dp" >

<TextView
android:id="@+id/title"
style="@style/NotificationTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:singleLine="true"/>

<TextView
android:id="@+id/time_remaining"
style="@style/NotificationText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:singleLine="true"/>
<!-- Only one of progress_bar and paused_text will be visible. -->

<FrameLayout
android:id="@+id/progress_bar_frame"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" >

<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingRight="25dp" />

<TextView
android:id="@+id/description"
style="@style/NotificationTextShadow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingRight="25dp"
android:singleLine="true" />
</FrameLayout>

</RelativeLayout>

</LinearLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NotificationTextSecondary" parent="NotificationText">
<item name="android:textSize">12sp</item>
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NotificationText" parent="android:TextAppearance.StatusBar.EventContent" />
<style name="NotificationTitle" parent="android:TextAppearance.StatusBar.EventContent.Title" />
</resources>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<!-- When a download completes, a notification is displayed, and this
string is used to indicate that the download successfully completed.
Note that such a download could have been initiated by a variety of
applications, including (but not limited to) the browser, an email
application, a content marketplace. -->
<string name="notification_download_complete">Download complete</string>

<!-- When a download completes, a notification is displayed, and this
string is used to indicate that the download failed.
Note that such a download could have been initiated by a variety of
applications, including (but not limited to) the browser, an email
application, a content marketplace. -->
<string name="notification_download_failed">Download unsuccessful</string>


<string name="state_unknown">Starting..."</string>
<string name="state_idle">Waiting for download to start</string>
<string name="state_fetching_url">Looking for resources to download</string>
<string name="state_connecting">Connecting to the download server</string>
<string name="state_downloading">Downloading resources</string>
<string name="state_completed">Download finished</string>
<string name="state_paused_network_unavailable">Download paused because no network is available</string>
<string name="state_paused_network_setup_failure">Download paused. Test a website in browser</string>
<string name="state_paused_by_request">Download paused</string>
<string name="state_paused_wifi_unavailable">Download paused because wifi is unavailable</string>
<string name="state_paused_wifi_disabled">Download paused because wifi is disabled</string>
<string name="state_paused_roaming">Download paused because you are roaming</string>
<string name="state_paused_sdcard_unavailable">Download paused because the external storage is unavailable</string>
<string name="state_failed_unlicensed">Download failed because you may not have purchased this app</string>
<string name="state_failed_fetching_url">Download failed because the resources could not be found</string>
<string name="state_failed_sdcard_full">Download failed because the external storage is full</string>
<string name="state_failed_cancelled">Download cancelled</string>
<string name="state_failed">Download failed</string>

<string name="kilobytes_per_second">%1$s KB/s</string>
<string name="time_remaining">Time remaining: %1$s</string>
<string name="time_remaining_notification">%1$s left</string>
</resources>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<style name="NotificationText">
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>

<style name="NotificationTextShadow" parent="NotificationText">
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:shadowColor">@android:color/background_dark</item>
<item name="android:shadowDx">1.0</item>
<item name="android:shadowDy">1.0</item>
<item name="android:shadowRadius">1</item>
</style>

<style name="NotificationTitle">
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:textStyle">bold</item>
</style>

<style name="ButtonBackground">
<item name="android:background">@android:color/background_dark</item>
</style>

</resources>
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.

"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:

(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and

(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and

(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and

(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.

You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright [yyyy] [name of copyright owner]

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.
@@ -0,0 +1,18 @@
apply plugin: 'com.android.library'

android {
compileSdkVersion 29

useLibrary 'org.apache.http.legacy'

defaultConfig {
minSdkVersion 19
targetSdkVersion 29
versionCode 2
versionName '1.7.0'
}
}

dependencies {
compile 'org.slf4j:slf4j-api:1.7.7'
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.vending.licensing"
android:versionCode="2"
android:versionName="1.5">
<!-- Devices >= 3 have version of Android Market that supports licensing. -->
<uses-sdk android:minSdkVersion="3" android:targetSdkVersion="15" />
<!-- Required permission to check licensing. -->
<uses-permission android:name="com.android.vending.CHECK_LICENSE" />
</manifest>
@@ -0,0 +1,23 @@
/*
* 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.android.vending.licensing;

// Android library projects do not yet support AIDL, so this has been
// precompiled into the src directory.
oneway interface ILicenseResultListener {
void verifyLicense(int responseCode, String signedData, String signature);
}
@@ -0,0 +1,26 @@
/*
* 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.android.vending.licensing;


import com.android.vending.licensing.ILicenseResultListener;

// Android library projects do not yet support AIDL, so this has been
// precompiled into the src directory.
oneway interface ILicensingService {
void checkLicense(long nonce, String packageName, in ILicenseResultListener listener);
}
@@ -0,0 +1,110 @@
/*
* 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 java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.spec.KeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

/**
* An Obfuscator that uses AES to encrypt data.
*/
public class AESObfuscator implements Obfuscator {
private static final String UTF8 = "UTF-8";
private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final byte[] IV =
{ 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
private static final String header = "com.android.vending.licensing.AESObfuscator-1|";

private Cipher mEncryptor;
private Cipher mDecryptor;

/**
* @param salt an array of random bytes to use for each (un)obfuscation
* @param applicationId application identifier, e.g. the package name
* @param deviceId device identifier. Use as many sources as possible to
* create this unique identifier.
*/
public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
KeySpec keySpec =
new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
SecretKey tmp = factory.generateSecret(keySpec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
} catch (GeneralSecurityException e) {
// This can't happen on a compatible Android device.
throw new RuntimeException("Invalid environment", e);
}
}

public String obfuscate(String original, String key) {
if (original == null) {
return null;
}
try {
// Header is appended as an integrity check
return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8)));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Invalid environment", e);
} catch (GeneralSecurityException e) {
throw new RuntimeException("Invalid environment", e);
}
}

public String unobfuscate(String obfuscated, String key) throws ValidationException {
if (obfuscated == null) {
return null;
}
try {
String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
// Check for presence of header. This serves as a final integrity check, for cases
// where the block size is correct during decryption.
int headerIndex = result.indexOf(header+key);
if (headerIndex != 0) {
throw new ValidationException("Header not found (invalid data or key)" + ":" +
obfuscated);
}
return result.substring(header.length()+key.length(), result.length());
} catch (Base64DecoderException e) {
throw new ValidationException(e.getMessage() + ":" + obfuscated);
} catch (IllegalBlockSizeException e) {
throw new ValidationException(e.getMessage() + ":" + obfuscated);
} catch (BadPaddingException e) {
throw new ValidationException(e.getMessage() + ":" + obfuscated);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Invalid environment", e);
}
}
}