/*
* 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.Constants;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
import com.google.android.vending.expansion.downloader.IDownloaderService;
import com.google.android.vending.expansion.downloader.IStub;
import com.google.android.vending.licensing.AESObfuscator;
import com.google.android.vending.licensing.APKExpansionPolicy;
import com.google.android.vending.licensing.LicenseChecker;
import com.google.android.vending.licensing.LicenseCheckerCallback;
import com.google.android.vending.licensing.Policy;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Messenger;
import android.os.SystemClock;
import android.provider.Settings.Secure;
import android.telephony.TelephonyManager;
import android.util.Log;
// -- GODOT start --
import android.annotation.SuppressLint;
// -- GODOT end --
import java.io.File;
/**
* Performs the background downloads requested by applications that use the
* Downloads provider. This service does not run as a foreground task, so
* Android may kill it off at will, but it will try to restart itself if it can.
* Note that Android by default will kill off any process that has an open file
* handle on the shared (SD Card) partition if the partition is unmounted.
*/
public abstract class DownloaderService extends CustomIntentService implements IDownloaderService {
public DownloaderService() {
super("LVLDownloadService");
}
private static final String LOG_TAG = "LVLDL";
// the following NETWORK_* constants are used to indicates specific reasons
// for disallowing a
// download from using a network, since specific causes can require special
// handling
/**
* The network is usable for the given download.
*/
public static final int NETWORK_OK = 1;
/**
* There is no network connectivity.
*/
public static final int NETWORK_NO_CONNECTION = 2;
/**
* The download exceeds the maximum size for this network.
*/
public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3;
/**
* The download exceeds the recommended maximum size for this network, the
* user must confirm for this download to proceed without WiFi.
*/
public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4;
/**
* The current connection is roaming, and the download can't proceed over a
* roaming connection.
*/
public static final int NETWORK_CANNOT_USE_ROAMING = 5;
/**
* The app requesting the download specific that it can't use the current
* network connection.
*/
public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6;
/**
* For intents used to notify the user that a download exceeds a size
* threshold, if this extra is true, WiFi is required for this download
* size; otherwise, it is only recommended.
*/
public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired";
public static final String EXTRA_FILE_NAME = "downloadId";
/**
* Used with DOWNLOAD_STATUS
*/
public static final String EXTRA_STATUS_STATE = "ESS";
public static final String EXTRA_STATUS_TOTAL_SIZE = "ETS";
public static final String EXTRA_STATUS_CURRENT_FILE_SIZE = "CFS";
public static final String EXTRA_STATUS_TOTAL_PROGRESS = "TFP";
public static final String EXTRA_STATUS_CURRENT_PROGRESS = "CFP";
public static final String ACTION_DOWNLOADS_CHANGED = "downloadsChanged";
/**
* Broadcast intent action sent by the download manager when a download
* completes.
*/
public final static String ACTION_DOWNLOAD_COMPLETE = "lvldownloader.intent.action.DOWNLOAD_COMPLETE";
/**
* Broadcast intent action sent by the download manager when download status
* changes.
*/
public final static String ACTION_DOWNLOAD_STATUS = "lvldownloader.intent.action.DOWNLOAD_STATUS";
/*
* Lists the states that the download manager can set on a download to
* notify applications of the download progress. The codes follow the HTTP
* families:<br> 1xx: informational<br> 2xx: success<br> 3xx: redirects (not
* used by the download manager)<br> 4xx: client errors<br> 5xx: server
* errors
*/
/**
* Returns whether the status is informational (i.e. 1xx).
*/
public static boolean isStatusInformational(int status) {
return (status >= 100 && status < 200);
}
/**
* Returns whether the status is a success (i.e. 2xx).
*/
public static boolean isStatusSuccess(int status) {
return (status >= 200 && status < 300);
}
/**
* Returns whether the status is an error (i.e. 4xx or 5xx).
*/
public static boolean isStatusError(int status) {
return (status >= 400 && status < 600);
}
/**
* Returns whether the status is a client error (i.e. 4xx).
*/
public static boolean isStatusClientError(int status) {
return (status >= 400 && status < 500);
}
/**
* Returns whether the status is a server error (i.e. 5xx).
*/
public static boolean isStatusServerError(int status) {
return (status >= 500 && status < 600);
}
/**
* Returns whether the download has completed (either with success or
* error).
*/
public static boolean isStatusCompleted(int status) {
return (status >= 200 && status < 300)
|| (status >= 400 && status < 600);
}
/**
* This download hasn't stated yet
*/
public static final int STATUS_PENDING = 190;
/**
* This download has started
*/
public static final int STATUS_RUNNING = 192;
/**
* This download has been paused by the owning app.
*/
public static final int STATUS_PAUSED_BY_APP = 193;
/**
* This download encountered some network error and is waiting before
* retrying the request.
*/
public static final int STATUS_WAITING_TO_RETRY = 194;
/**
* This download is waiting for network connectivity to proceed.
*/
public static final int STATUS_WAITING_FOR_NETWORK = 195;
/**
* This download is waiting for a Wi-Fi connection to proceed or for
* permission to download over cellular.
*/
public static final int STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION = 196;
/**
* This download is waiting for a Wi-Fi connection to proceed.
*/
public static final int STATUS_QUEUED_FOR_WIFI = 197;
/**
* This download has successfully completed. Warning: there might be other
* status values that indicate success in the future. Use isSucccess() to
* capture the entire category.
*
* @hide
*/
public static final int STATUS_SUCCESS = 200;
/**
* The requested URL is no longer available
*/
public static final int STATUS_FORBIDDEN = 403;
/**
* The file was delivered incorrectly
*/
public static final int STATUS_FILE_DELIVERED_INCORRECTLY = 487;
/**
* The requested destination file already exists.
*/
public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488;
/**
* Some possibly transient error occurred, but we can't resume the download.
*/
public static final int STATUS_CANNOT_RESUME = 489;
/**
* This download was canceled
*
* @hide
*/
public static final int STATUS_CANCELED = 490;
/**
* This download has completed with an error. Warning: there will be other
* status values that indicate errors in the future. Use isStatusError() to
* capture the entire category.
*/
public static final int STATUS_UNKNOWN_ERROR = 491;
/**
* This download couldn't be completed because of a storage issue.
* Typically, that's because the filesystem is missing or full. Use the more
* specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} and
* {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate.
*
* @hide
*/
public static final int STATUS_FILE_ERROR = 492;
/**
* This download couldn't be completed because of an HTTP redirect response
* that the download manager couldn't handle.
*
* @hide
*/
public static final int STATUS_UNHANDLED_REDIRECT = 493;
/**
* This download couldn't be completed because of an unspecified unhandled
* HTTP code.
*
* @hide
*/
public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
/**
* This download couldn't be completed because of an error receiving or
* processing data at the HTTP level.
*
* @hide
*/
public static final int STATUS_HTTP_DATA_ERROR = 495;
/**
* This download couldn't be completed because of an HttpException while
* setting up the request.
*
* @hide
*/
public static final int STATUS_HTTP_EXCEPTION = 496;
/**
* This download couldn't be completed because there were too many
* redirects.
*
* @hide
*/
public static final int STATUS_TOO_MANY_REDIRECTS = 497;
/**
* This download couldn't be completed due to insufficient storage space.
* Typically, this is because the SD card is full.
*
* @hide
*/
public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498;
/**
* This download couldn't be completed because no external storage device
* was found. Typically, this is because the SD card is not mounted.
*
* @hide
*/
public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
/**
* This download is allowed to run.
*
* @hide
*/
public static final int CONTROL_RUN = 0;
/**
* This download must pause at the first opportunity.
*
* @hide
*/
public static final int CONTROL_PAUSED = 1;
/**
* This download is visible but only shows in the notifications while it's
* in progress.
*
* @hide
*/
public static final int VISIBILITY_VISIBLE = 0;
/**
* This download is visible and shows in the notifications while in progress
* and after completion.
*
* @hide
*/
public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
/**
* This download doesn't show in the UI or in the notifications.
*
* @hide
*/
public static final int VISIBILITY_HIDDEN = 2;
/**
* Bit flag for setAllowedNetworkTypes corresponding to
* {@link ConnectivityManager#TYPE_MOBILE}.
*/
public static final int NETWORK_MOBILE = 1 << 0;
/**
* Bit flag for setAllowedNetworkTypes corresponding to
* {@link ConnectivityManager#TYPE_WIFI}.
*/
public static final int NETWORK_WIFI = 1 << 1;
private final static String TEMP_EXT = ".tmp";
/**
* Service thread status
*/
private static boolean sIsRunning;
@Override
public IBinder onBind(Intent paramIntent) {
Log.d(Constants.TAG, "Service Bound");
return this.mServiceMessenger.getBinder();
}
/**
* Network state.
*/
private boolean mIsConnected;
private boolean mIsFailover;
private boolean mIsCellularConnection;
private boolean mIsRoaming;
private boolean mIsAtLeast3G;
private boolean mIsAtLeast4G;
private boolean mStateChanged;
/**
* Download state
*/
private int mControl;
private int mStatus;
public boolean isWiFi() {
return mIsConnected && !mIsCellularConnection;
}
/**
* Bindings to important services
*/
private ConnectivityManager mConnectivityManager;
private WifiManager mWifiManager;
/**
* Package we are downloading for (defaults to package of application)
*/
private PackageInfo mPackageInfo;
/**
* Byte counts
*/
long mBytesSoFar;
long mTotalLength;
int mFileCount;
/**
* Used for calculating time remaining and speed
*/
long mBytesAtSample;
long mMillisecondsAtSample;
float mAverageDownloadSpeed;
/**
* Our binding to the network state broadcasts
*/
private BroadcastReceiver mConnReceiver;
final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this);
final private Messenger mServiceMessenger = mServiceStub.getMessenger();
private Messenger mClientMessenger;
private DownloadNotification mNotification;
private PendingIntent mPendingIntent;
private PendingIntent mAlarmIntent;
/**
* Updates the network type based upon the type and subtype returned from
* the connectivity manager. Subtype is only used for cellular signals.
*
* @param type
* @param subType
*/
private void updateNetworkType(int type, int subType) {
switch (type) {
case ConnectivityManager.TYPE_WIFI:
case ConnectivityManager.TYPE_ETHERNET:
case ConnectivityManager.TYPE_BLUETOOTH:
mIsCellularConnection = false;
mIsAtLeast3G = false;
mIsAtLeast4G = false;
break;
case ConnectivityManager.TYPE_WIMAX:
mIsCellularConnection = true;
mIsAtLeast3G = true;
mIsAtLeast4G = true;
break;
case ConnectivityManager.TYPE_MOBILE:
mIsCellularConnection = true;
switch (subType) {
case TelephonyManager.NETWORK_TYPE_1xRTT:
case TelephonyManager.NETWORK_TYPE_CDMA:
case TelephonyManager.NETWORK_TYPE_EDGE:
case TelephonyManager.NETWORK_TYPE_GPRS:
case TelephonyManager.NETWORK_TYPE_IDEN:
mIsAtLeast3G = false;
mIsAtLeast4G = false;
break;
case TelephonyManager.NETWORK_TYPE_HSDPA:
case TelephonyManager.NETWORK_TYPE_HSUPA:
case TelephonyManager.NETWORK_TYPE_HSPA:
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_EVDO_A:
case TelephonyManager.NETWORK_TYPE_UMTS:
mIsAtLeast3G = true;
mIsAtLeast4G = false;
break;
case TelephonyManager.NETWORK_TYPE_LTE: // 4G
case TelephonyManager.NETWORK_TYPE_EHRPD: // 3G ++ interop
// with 4G
case TelephonyManager.NETWORK_TYPE_HSPAP: // 3G ++ but
// marketed as
// 4G
mIsAtLeast3G = true;
mIsAtLeast4G = true;
break;
default:
mIsCellularConnection = false;
mIsAtLeast3G = false;
mIsAtLeast4G = false;
}
}
}
private void updateNetworkState(NetworkInfo info) {
boolean isConnected = mIsConnected;
boolean isFailover = mIsFailover;
boolean isCellularConnection = mIsCellularConnection;
boolean isRoaming = mIsRoaming;
boolean isAtLeast3G = mIsAtLeast3G;
if (null != info) {
mIsRoaming = info.isRoaming();
mIsFailover = info.isFailover();
mIsConnected = info.isConnected();
updateNetworkType(info.getType(), info.getSubtype());
} else {
mIsRoaming = false;
mIsFailover = false;
mIsConnected = false;
updateNetworkType(-1, -1);
}
mStateChanged = (mStateChanged || isConnected != mIsConnected
|| isFailover != mIsFailover
|| isCellularConnection != mIsCellularConnection
|| isRoaming != mIsRoaming || isAtLeast3G != mIsAtLeast3G);
if (Constants.LOGVV) {
if (mStateChanged) {
Log.v(LOG_TAG, "Network state changed: ");
Log.v(LOG_TAG, "Starting State: " +
(isConnected ? "Connected " : "Not Connected ") +
(isCellularConnection ? "Cellular " : "WiFi ") +
(isRoaming ? "Roaming " : "Local ") +
(isAtLeast3G ? "3G+ " : "<3G "));
Log.v(LOG_TAG, "Ending State: " +
(mIsConnected ? "Connected " : "Not Connected ") +
(mIsCellularConnection ? "Cellular " : "WiFi ") +
(mIsRoaming ? "Roaming " : "Local ") +
(mIsAtLeast3G ? "3G+ " : "<3G "));
if (isServiceRunning()) {
if (mIsRoaming) {
mStatus = STATUS_WAITING_FOR_NETWORK;
mControl = CONTROL_PAUSED;
} else if (mIsCellularConnection) {
DownloadsDB db = DownloadsDB.getDB(this);
int flags = db.getFlags();
if (0 == (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) {
mStatus = STATUS_QUEUED_FOR_WIFI;
mControl = CONTROL_PAUSED;
}
}
}
}
}
}
/**
* Polls the network state, setting the flags appropriately.
*/
void pollNetworkState() {
if (null == mConnectivityManager) {
mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
}
if (null == mWifiManager) {
mWifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
}
if (mConnectivityManager == null) {
Log.w(Constants.TAG,
"couldn't get connectivity manager to poll network state");
} else {
@SuppressLint("MissingPermission")
NetworkInfo activeInfo = mConnectivityManager
.getActiveNetworkInfo();
updateNetworkState(activeInfo);
}
}
public static final int NO_DOWNLOAD_REQUIRED = 0;
public static final int LVL_CHECK_REQUIRED = 1;
public static final int DOWNLOAD_REQUIRED = 2;
public static final String EXTRA_PACKAGE_NAME = "EPN";
public static final String EXTRA_PENDING_INTENT = "EPI";
public static final String EXTRA_MESSAGE_HANDLER = "EMH";
/**
* Returns true if the LVL check is required
*
* @param db a downloads DB synchronized with the latest state
* @param pi the package info for the project
* @return returns true if the filenames need to be returned
*/
private static boolean isLVLCheckRequired(DownloadsDB db, PackageInfo pi) {
// we need to update the LVL check and get a successful status to
// proceed
if (db.mVersionCode != pi.versionCode) {
return true;
}
return false;
}
/**
* Careful! Only use this internally.
*
* @return whether we think the service is running
*/
private static synchronized boolean isServiceRunning() {
return sIsRunning;
}
private static synchronized void setServiceRunning(boolean isRunning) {
sIsRunning = isRunning;
}
public static int startDownloadServiceIfRequired(Context context,
Intent intent, Class<?> serviceClass) throws NameNotFoundException {
final PendingIntent pendingIntent = (PendingIntent) intent
.getParcelableExtra(EXTRA_PENDING_INTENT);
return startDownloadServiceIfRequired(context, pendingIntent,
serviceClass);
}
public static int startDownloadServiceIfRequired(Context context,
PendingIntent pendingIntent, Class<?> serviceClass)
throws NameNotFoundException
{
String packageName = context.getPackageName();
String className = serviceClass.getName();
return startDownloadServiceIfRequired(context, pendingIntent,
packageName, className);
}
/**
* 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
* @param pendingIntent
* @return true if the app should wait for more guidance from the
* downloader, false if the app can continue
* @throws NameNotFoundException
*/
public static int startDownloadServiceIfRequired(Context context,
PendingIntent pendingIntent, String classPackage, String className)
throws NameNotFoundException {
// first: do we need to do an LVL update?
// we begin by getting our APK version from the package manager
final PackageInfo pi = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
int status = NO_DOWNLOAD_REQUIRED;
// the database automatically reads the metadata for version code
// and download status when the instance is created
DownloadsDB db = DownloadsDB.getDB(context);
// we need to update the LVL check and get a successful status to
// proceed
if (isLVLCheckRequired(db, pi)) {
status = LVL_CHECK_REQUIRED;
}
// we don't have to update LVL. do we still have a download to start?
if (db.mStatus == 0) {
DownloadInfo[] infos = db.getDownloads();
if (null != infos) {
for (DownloadInfo info : infos) {
if (!Helpers.doesFileExist(context, info.mFileName, info.mTotalBytes, true)) {
status = DOWNLOAD_REQUIRED;
db.updateStatus(-1);
break;
}
}
}
} else {
status = DOWNLOAD_REQUIRED;
}
switch (status) {
case DOWNLOAD_REQUIRED:
case LVL_CHECK_REQUIRED:
Intent fileIntent = new Intent();
fileIntent.setClassName(classPackage, className);
fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
context.startService(fileIntent);
break;
}
return status;
}
@Override
public void requestAbortDownload() {
mControl = CONTROL_PAUSED;
mStatus = STATUS_CANCELED;
}
@Override
public void requestPauseDownload() {
mControl = CONTROL_PAUSED;
mStatus = STATUS_PAUSED_BY_APP;
}
@Override
public void setDownloadFlags(int flags) {
DownloadsDB.getDB(this).updateFlags(flags);
}
@Override
public void requestContinueDownload() {
if (mControl == CONTROL_PAUSED) {
mControl = CONTROL_RUN;
}
Intent fileIntent = new Intent(this, this.getClass());
fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
this.startService(fileIntent);
}
public abstract String getPublicKey();
public abstract byte[] getSALT();
public abstract String getAlarmReceiverClassName();
private class LVLRunnable implements Runnable {
LVLRunnable(Context context, PendingIntent intent) {
mContext = context;
mPendingIntent = intent;
}
final Context mContext;
@Override
public void run() {
setServiceRunning(true);
mNotification.onDownloadStateChanged(IDownloaderClient.STATE_FETCHING_URL);
String deviceId = Secure.getString(mContext.getContentResolver(),
Secure.ANDROID_ID);
final APKExpansionPolicy aep = new APKExpansionPolicy(mContext,
new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId));
// reset our policy back to the start of the world to force a
// re-check
aep.resetPolicy();
// let's try and get the OBB file from LVL first
// Construct the LicenseChecker with a Policy.
final LicenseChecker checker = new LicenseChecker(mContext, aep,
getPublicKey() // Your public licensing key.
);
checker.checkAccess(new LicenseCheckerCallback() {
@Override
public void allow(int reason) {
try {
int count = aep.getExpansionURLCount();
DownloadsDB db = DownloadsDB.getDB(mContext);
int status = 0;
if (count != 0) {
for (int i = 0; i < count; i++) {
String currentFileName = aep
.getExpansionFileName(i);
if (null != currentFileName) {
DownloadInfo di = new DownloadInfo(i,
currentFileName, mContext.getPackageName());
long fileSize = aep.getExpansionFileSize(i);
if (handleFileUpdated(db, i, currentFileName,
fileSize)) {
status |= -1;
di.resetDownload();
di.mUri = aep.getExpansionURL(i);
di.mTotalBytes = fileSize;
di.mStatus = status;
db.updateDownload(di);
} else {
// we need to read the download
// information
// from
// the database
DownloadInfo dbdi = db
.getDownloadInfoByFileName(di.mFileName);
if (null == dbdi) {
// the file exists already and is
// the
// correct size
// was delivered by Market or
// through
// another mechanism
Log.d(LOG_TAG, "file " + di.mFileName
+ " found. Not downloading.");
di.mStatus = STATUS_SUCCESS;
di.mTotalBytes = fileSize;
di.mCurrentBytes = fileSize;
di.mUri = aep.getExpansionURL(i);
db.updateDownload(di);
} else if (dbdi.mStatus != STATUS_SUCCESS) {
// we just update the URL
dbdi.mUri = aep.getExpansionURL(i);
db.updateDownload(dbdi);
status |= -1;
}
}
}
}
}
// first: do we need to do an LVL update?
// we begin by getting our APK version from the package
// manager
PackageInfo pi;
try {
pi = mContext.getPackageManager().getPackageInfo(
mContext.getPackageName(), 0);
db.updateMetadata(pi.versionCode, status);
Class<?> serviceClass = DownloaderService.this.getClass();
switch (startDownloadServiceIfRequired(mContext, mPendingIntent,
serviceClass)) {
case NO_DOWNLOAD_REQUIRED:
mNotification
.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED);
break;
case LVL_CHECK_REQUIRED:
// DANGER WILL ROBINSON!
Log.e(LOG_TAG, "In LVL checking loop!");
mNotification
.onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED);
throw new RuntimeException(
"Error with LVL checking and database integrity");
case DOWNLOAD_REQUIRED:
// do nothing. the download will notify the
// application
// when things are done
break;
}
} catch (NameNotFoundException e1) {
e1.printStackTrace();
throw new RuntimeException(
"Error with getting information from package name");
}
} finally {
setServiceRunning(false);
}
}
@Override
public void dontAllow(int reason) {
try
{
switch (reason) {
case Policy.NOT_LICENSED:
mNotification
.onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED);
break;
case Policy.RETRY:
mNotification
.onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL);
break;
}
} finally {
setServiceRunning(false);
}
}
@Override
public void applicationError(int errorCode) {
try {
mNotification
.onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL);
} finally {
setServiceRunning(false);
}
}
});
}
};
/**
* Updates the LVL information from the server.
*
* @param context
*/
public void updateLVL(final Context context) {
Context c = context.getApplicationContext();
Handler h = new Handler(c.getMainLooper());
h.post(new LVLRunnable(c, mPendingIntent));
}
/**
* The APK has been updated and a filename has been sent down from the
* Market call. If the file has the same name as the previous file, we do
* nothing as the file is guaranteed to be the same. If the file does not
* have the same name, we download it if it hasn't already been delivered by
* Market.
*
* @param index the index of the file from market (0 = main, 1 = patch)
* @param filename the name of the new file
* @param fileSize the size of the new file
* @return
*/
public boolean handleFileUpdated(DownloadsDB db, int index,
String filename, long fileSize) {
DownloadInfo di = db.getDownloadInfoByFileName(filename);
if (null != di) {
String oldFile = di.mFileName;
// cleanup
if (null != oldFile) {
if (filename.equals(oldFile)) {
return false;
}
// remove partially downloaded file if it is there
String deleteFile = Helpers.generateSaveFileName(this, oldFile);
File f = new File(deleteFile);
if (f.exists())
f.delete();
}
}
return !Helpers.doesFileExist(this, filename, fileSize, true);
}
private void scheduleAlarm(long wakeUp) {
AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
if (alarms == null) {
Log.e(Constants.TAG, "couldn't get alarm manager");
return;
}
if (Constants.LOGV) {
Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
}
String className = getAlarmReceiverClassName();
Intent intent = new Intent(Constants.ACTION_RETRY);
intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
intent.setClassName(this.getPackageName(),
className);
mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent,
PendingIntent.FLAG_ONE_SHOT);
alarms.set(
AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + wakeUp, mAlarmIntent
);
}
private void cancelAlarms() {
if (null != mAlarmIntent) {
AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
if (alarms == null) {
Log.e(Constants.TAG, "couldn't get alarm manager");
return;
}
alarms.cancel(mAlarmIntent);
mAlarmIntent = null;
}
}
/**
* We use this to track network state, such as when WiFi, Cellular, etc. is
* enabled when downloads are paused or in progress.
*/
private class InnerBroadcastReceiver extends BroadcastReceiver {
final Service mService;
InnerBroadcastReceiver(Service service) {
mService = service;
}
@Override
public void onReceive(Context context, Intent intent) {
pollNetworkState();
if (mStateChanged
&& !isServiceRunning()) {
Log.d(Constants.TAG, "InnerBroadcastReceiver Called");
Intent fileIntent = new Intent(context, mService.getClass());
fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
// send a new intent to the service
context.startService(fileIntent);
}
}
};
/**
* This is the main thread for the Downloader. This thread is responsible
* for queuing up downloads and other goodness.
*/
@Override
protected void onHandleIntent(Intent intent) {
setServiceRunning(true);
try {
// the database automatically reads the metadata for version code
// and download status when the instance is created
DownloadsDB db = DownloadsDB.getDB(this);
final PendingIntent pendingIntent = (PendingIntent) intent
.getParcelableExtra(EXTRA_PENDING_INTENT);
if (null != pendingIntent)
{
mNotification.setClientIntent(pendingIntent);
mPendingIntent = pendingIntent;
} else if (null != mPendingIntent) {
mNotification.setClientIntent(mPendingIntent);
} else {
Log.e(LOG_TAG, "Downloader started in bad state without notification intent.");
return;
}
// when the LVL check completes, a successful response will update
// the service
if (isLVLCheckRequired(db, mPackageInfo)) {
updateLVL(this);
return;
}
// get each download
DownloadInfo[] infos = db.getDownloads();
mBytesSoFar = 0;
mTotalLength = 0;
mFileCount = infos.length;
for (DownloadInfo info : infos) {
// We do an (simple) integrity check on each file, just to make
// sure
if (info.mStatus == STATUS_SUCCESS) {
// verify that the file matches the state
if (!Helpers.doesFileExist(this, info.mFileName, info.mTotalBytes, true)) {
info.mStatus = 0;
info.mCurrentBytes = 0;
}
}
// get aggregate data
mTotalLength += info.mTotalBytes;
mBytesSoFar += info.mCurrentBytes;
}
// loop through all downloads and fetch them
pollNetworkState();
if (null == mConnReceiver) {
/**
* We use this to track network state, such as when WiFi,
* Cellular, etc. is enabled when downloads are paused or in
* progress.
*/
mConnReceiver = new InnerBroadcastReceiver(this);
IntentFilter intentFilter = new IntentFilter(
ConnectivityManager.CONNECTIVITY_ACTION);
intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
registerReceiver(mConnReceiver, intentFilter);
}
for (DownloadInfo info : infos) {
long startingCount = info.mCurrentBytes;
if (info.mStatus != STATUS_SUCCESS) {
DownloadThread dt = new DownloadThread(info, this, mNotification);
cancelAlarms();
scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG);
dt.run();
cancelAlarms();
}
db.updateFromDb(info);
boolean setWakeWatchdog = false;
int notifyStatus;
switch (info.mStatus) {
case STATUS_FORBIDDEN:
// the URL is out of date
updateLVL(this);
return;
case STATUS_SUCCESS:
mBytesSoFar += info.mCurrentBytes - startingCount;
db.updateMetadata(mPackageInfo.versionCode, 0);
continue;
case STATUS_FILE_DELIVERED_INCORRECTLY:
// we may be on a network that is returning us a web
// page on redirect
notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE;
info.mCurrentBytes = 0;
db.updateDownload(info);
setWakeWatchdog = true;
break;
case STATUS_PAUSED_BY_APP:
notifyStatus = IDownloaderClient.STATE_PAUSED_BY_REQUEST;
break;
case STATUS_WAITING_FOR_NETWORK:
case STATUS_WAITING_TO_RETRY:
notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE;
setWakeWatchdog = true;
break;
case STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION:
case STATUS_QUEUED_FOR_WIFI:
// look for more detail here
if (null != mWifiManager) {
if (!mWifiManager.isWifiEnabled()) {
notifyStatus = IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION;
setWakeWatchdog = true;
break;
}
}
notifyStatus = IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION;
setWakeWatchdog = true;
break;
case STATUS_CANCELED:
notifyStatus = IDownloaderClient.STATE_FAILED_CANCELED;
setWakeWatchdog = true;
break;
case STATUS_INSUFFICIENT_SPACE_ERROR:
notifyStatus = IDownloaderClient.STATE_FAILED_SDCARD_FULL;
setWakeWatchdog = true;
break;
case STATUS_DEVICE_NOT_FOUND_ERROR:
notifyStatus = IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE;
setWakeWatchdog = true;
break;
default:
notifyStatus = IDownloaderClient.STATE_FAILED;
break;
}
if (setWakeWatchdog) {
scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER);
} else {
cancelAlarms();
}
// failure or pause state
mNotification.onDownloadStateChanged(notifyStatus);
return;
}
// all downloads complete
mNotification.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED);
} finally {
setServiceRunning(false);
}
}
@Override
public void onDestroy() {
if (null != mConnReceiver) {
unregisterReceiver(mConnReceiver);
mConnReceiver = null;
}
mServiceStub.disconnect(this);
super.onDestroy();
}
public int getNetworkAvailabilityState(DownloadsDB db) {
if (mIsConnected) {
if (!mIsCellularConnection)
return NETWORK_OK;
int flags = db.mFlags;
if (mIsRoaming)
return NETWORK_CANNOT_USE_ROAMING;
if (0 != (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) {
return NETWORK_OK;
} else {
return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
}
}
return NETWORK_NO_CONNECTION;
}
@Override
public void onCreate() {
super.onCreate();
try {
mPackageInfo = getPackageManager().getPackageInfo(
getPackageName(), 0);
ApplicationInfo ai = getApplicationInfo();
CharSequence applicationLabel = getPackageManager().getApplicationLabel(ai);
mNotification = new DownloadNotification(this, applicationLabel);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
/**
* Exception thrown from methods called by generateSaveFile() for any fatal
* error.
*/
public static class GenerateSaveFileError extends Exception {
private static final long serialVersionUID = 3465966015408936540L;
int mStatus;
String mMessage;
public GenerateSaveFileError(int status, String message) {
mStatus = status;
mMessage = message;
}
}
/**
* Returns the filename (where the file should be saved) from info about a
* download
*/
public String generateTempSaveFileName(String fileName) {
String path = Helpers.getSaveFilePath(this)
+ File.separator + fileName + TEMP_EXT;
return path;
}
/**
* Creates a filename (where the file should be saved) from info about a
* download.
*/
public String generateSaveFile(String filename, long filesize)
throws GenerateSaveFileError {
String path = generateTempSaveFileName(filename);
File expPath = new File(path);
if (!Helpers.isExternalMediaMounted()) {
Log.d(Constants.TAG, "External media not mounted: " + path);
throw new GenerateSaveFileError(STATUS_DEVICE_NOT_FOUND_ERROR,
"external media is not yet mounted");
}
if (expPath.exists()) {
Log.d(Constants.TAG, "File already exists: " + path);
throw new GenerateSaveFileError(STATUS_FILE_ALREADY_EXISTS_ERROR,
"requested destination file already exists");
}
if (Helpers.getAvailableBytes(Helpers.getFilesystemRoot(path)) < filesize) {
throw new GenerateSaveFileError(STATUS_INSUFFICIENT_SPACE_ERROR,
"insufficient space on external storage");
}
return path;
}
/**
* @return a non-localized string appropriate for logging corresponding to
* one of the NETWORK_* constants.
*/
public String getLogMessageForNetworkError(int networkError) {
switch (networkError) {
case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
return "download size exceeds recommended limit for mobile network";
case NETWORK_UNUSABLE_DUE_TO_SIZE:
return "download size exceeds limit for mobile network";
case NETWORK_NO_CONNECTION:
return "no network connection available";
case NETWORK_CANNOT_USE_ROAMING:
return "download cannot use the current network connection because it is roaming";
case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
return "download was requested to not use the current network type";
default:
return "unknown error with network connectivity";
}
}
public int getControl() {
return mControl;
}
public int getStatus() {
return mStatus;
}
/**
* Calculating a moving average for the speed so we don't get jumpy
* calculations for time etc.
*/
static private final float SMOOTHING_FACTOR = 0.005f;
public void notifyUpdateBytes(long totalBytesSoFar) {
long timeRemaining;
long currentTime = SystemClock.uptimeMillis();
if (0 != mMillisecondsAtSample) {
// we have a sample.
long timePassed = currentTime - mMillisecondsAtSample;
long bytesInSample = totalBytesSoFar - mBytesAtSample;
float currentSpeedSample = (float) bytesInSample / (float) timePassed;
if (0 != mAverageDownloadSpeed) {
mAverageDownloadSpeed = SMOOTHING_FACTOR * currentSpeedSample
+ (1 - SMOOTHING_FACTOR) * mAverageDownloadSpeed;
} else {
mAverageDownloadSpeed = currentSpeedSample;
}
timeRemaining = (long) ((mTotalLength - totalBytesSoFar) / mAverageDownloadSpeed);
} else {
timeRemaining = -1;
}
mMillisecondsAtSample = currentTime;
mBytesAtSample = totalBytesSoFar;
mNotification.onDownloadProgress(
new DownloadProgressInfo(mTotalLength,
totalBytesSoFar,
timeRemaining,
mAverageDownloadSpeed)
);
}
@Override
protected boolean shouldStop() {
// the database automatically reads the metadata for version code
// and download status when the instance is created
DownloadsDB db = DownloadsDB.getDB(this);
if (db.mStatus == 0) {
return true;
}
return false;
}
@Override
public void requestDownloadStatus() {
mNotification.resendState();
}
@Override
public void onClientUpdated(Messenger clientMessenger) {
this.mClientMessenger = clientMessenger;
mNotification.setMessenger(mClientMessenger);
}
}