/* * 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; 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 {@link #setAllowedNetworkTypes} corresponding to * {@link ConnectivityManager#TYPE_MOBILE}. */ public static final int NETWORK_MOBILE = 1 << 0; /** * Bit flag for {@link #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) getSystemService(Context.WIFI_SERVICE); } if (mConnectivityManager == null) { Log.w(Constants.TAG, "couldn't get connectivity manager to poll network state"); } else { 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 thisIntent * @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); } }