|
@@ -0,0 +1,1341 @@
|
|
|
+/*
|
|
|
+ * 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);
|
|
|
+ }
|
|
|
+
|
|
|
+}
|