Changed java libraries for apk expensions file download with non official better-apk-expansion version 5.0.3 and better-licensing version 1.5.0

This commit is contained in:
FalsinSoft 2019-01-09 22:39:04 +01:00
parent 12e61b761b
commit b926f059f4
30 changed files with 1039 additions and 1306 deletions

View File

@ -1,29 +1,51 @@
/*
* MIT License
*
* Copyright (c) 2018 Fabio Falsini <falsinsoft@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <QGuiApplication> #include <QGuiApplication>
#include "QtAndroidApkExpansionFiles.h" #include "QAndroidApkExpansionFiles.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
JNIEXPORT jstring JNICALL Java_com_google_android_vending_expansion_downloader_Helpers_getDownloaderStringResourceFromState(JNIEnv *env, jobject thiz, jint state) JNIEXPORT jstring JNICALL Java_com_falsinsoft_qtandroidtools_ApkExpansionDownloader_getString(JNIEnv *env, jobject thiz, jint stringID)
{ {
QtAndroidApkExpansionFiles *pInstance = QtAndroidApkExpansionFiles::instance(); QAndroidApkExpansionFiles *pInstance = QAndroidApkExpansionFiles::instance();
QString TextInfo; QString TextString;
Q_UNUSED(thiz) Q_UNUSED(thiz)
if(pInstance != nullptr) if(pInstance != nullptr)
{ {
TextInfo = pInstance->stateTextInfo(state); TextString = pInstance->getString(stringID);
} }
return env->NewString(TextInfo.utf16(), TextInfo.length()); return env->NewString(TextString.utf16(), TextString.length());
} }
JNIEXPORT void JNICALL Java_com_falsinsoft_QtAndroidTools_ApkExpansionDownloader_downloadStateChanged(JNIEnv *env, jobject thiz, jint newState) JNIEXPORT void JNICALL Java_com_falsinsoft_qtandroidtools_ApkExpansionDownloader_downloadStateChanged(JNIEnv *env, jobject thiz, jint newState)
{ {
QtAndroidApkExpansionFiles *pInstance = QtAndroidApkExpansionFiles::instance(); QAndroidApkExpansionFiles *pInstance = QAndroidApkExpansionFiles::instance();
Q_UNUSED(env) Q_UNUSED(env)
Q_UNUSED(thiz) Q_UNUSED(thiz)
@ -34,9 +56,9 @@ JNIEXPORT void JNICALL Java_com_falsinsoft_QtAndroidTools_ApkExpansionDownloader
} }
} }
JNIEXPORT void JNICALL Java_com_falsinsoft_QtAndroidTools_ApkExpansionDownloader_downloadProgress(JNIEnv *env, jobject thiz, jlong overallTotal, jlong overallProgress, jlong timeRemaining, jfloat currentSpeed) JNIEXPORT void JNICALL Java_com_falsinsoft_qtandroidtools_ApkExpansionDownloader_downloadProgress(JNIEnv *env, jobject thiz, jlong overallTotal, jlong overallProgress, jlong timeRemaining, jfloat currentSpeed)
{ {
QtAndroidApkExpansionFiles *pInstance = QtAndroidApkExpansionFiles::instance(); QAndroidApkExpansionFiles *pInstance = QAndroidApkExpansionFiles::instance();
Q_UNUSED(env) Q_UNUSED(env)
Q_UNUSED(thiz) Q_UNUSED(thiz)
@ -51,52 +73,62 @@ JNIEXPORT void JNICALL Java_com_falsinsoft_QtAndroidTools_ApkExpansionDownloader
} }
#endif #endif
QtAndroidApkExpansionFiles *QtAndroidApkExpansionFiles::m_pInstance = nullptr; QAndroidApkExpansionFiles *QAndroidApkExpansionFiles::m_pInstance = nullptr;
QtAndroidApkExpansionFiles::QtAndroidApkExpansionFiles() : m_JavaApkExpansionDownloader("com/falsinsoft/QtAndroidTools/ApkExpansionDownloader", QAndroidApkExpansionFiles::QAndroidApkExpansionFiles() : m_JavaApkExpansionDownloader("com/falsinsoft/qtandroidtools/ApkExpansionDownloader",
"(Landroid/app/Activity;)V", "(Landroid/app/Activity;)V",
QtAndroid::androidActivity().object<jobject>()) QtAndroid::androidActivity().object<jobject>())
{ {
connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QtAndroidApkExpansionFiles::ApplicationStateChanged);
qRegisterMetaType<ExpansionFileInfo>();
m_pInstance = this; m_pInstance = this;
connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QAndroidApkExpansionFiles::ApplicationStateChanged);
qRegisterMetaType<ExpansionFileInfo>();
SetNewAppState(APP_STATE_CREATE);
} }
QObject* QtAndroidApkExpansionFiles::qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine) QAndroidApkExpansionFiles::~QAndroidApkExpansionFiles()
{
SetNewAppState(APP_STATE_DESTROY);
}
QObject* QAndroidApkExpansionFiles::qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine)
{ {
Q_UNUSED(engine); Q_UNUSED(engine);
Q_UNUSED(scriptEngine); Q_UNUSED(scriptEngine);
return new QtAndroidApkExpansionFiles(); return new QAndroidApkExpansionFiles();
} }
QtAndroidApkExpansionFiles* QtAndroidApkExpansionFiles::instance() QAndroidApkExpansionFiles* QAndroidApkExpansionFiles::instance()
{ {
return m_pInstance; return m_pInstance;
} }
void QtAndroidApkExpansionFiles::ApplicationStateChanged(Qt::ApplicationState State) void QAndroidApkExpansionFiles::ApplicationStateChanged(Qt::ApplicationState State)
{ {
const bool StubConnect = (State == Qt::ApplicationActive) ? true : false; SetNewAppState((State == Qt::ApplicationActive) ? APP_STATE_START : APP_STATE_STOP);
}
void QAndroidApkExpansionFiles::SetNewAppState(APP_STATE NewState)
{
if(m_JavaApkExpansionDownloader.isValid()) if(m_JavaApkExpansionDownloader.isValid())
{ {
m_JavaApkExpansionDownloader.callMethod<void>("enableClientStubConnection", m_JavaApkExpansionDownloader.callMethod<void>("appStateChanged",
"(Z)V", "(I)V",
StubConnect NewState
); );
} }
} }
QtAndroidApkExpansionFiles::APKEF_STATE QtAndroidApkExpansionFiles::startDownloadFiles() QAndroidApkExpansionFiles::APKEF_STATE QAndroidApkExpansionFiles::startDownloadFiles()
{ {
if(m_JavaApkExpansionDownloader.isValid() == false) return APKEF_INVALID_JAVA_CLASS; if(m_JavaApkExpansionDownloader.isValid() == false) return APKEF_INVALID_JAVA_CLASS;
if(m_Base64PublicKey.count() == 0) return APKEF_INVALID_BASE64_PUBLIC_KEY; if(m_Base64PublicKey.count() == 0) return APKEF_INVALID_BASE64_PUBLIC_KEY;
if(m_SALT.count() != 20) return APKEF_INVALID_SALT; if(m_SALT.count() != 20) return APKEF_INVALID_SALT;
if(QtAndroid::androidSdkVersion() >= 23) if(QtAndroid::androidSdkVersion() >= 23)
{ {
if(QtAndroid::checkPermission("Manifest.permission.READ_EXTERNAL_STORAGE") != QtAndroid::PermissionResult::Granted) return APKEF_STORAGE_READ_PERMISSION_REQUIRED; if(QtAndroid::checkPermission("android.permission.READ_EXTERNAL_STORAGE") != QtAndroid::PermissionResult::Granted) return APKEF_STORAGE_READ_PERMISSION_REQUIRED;
if(QtAndroid::checkPermission("Manifest.permission.WRITE_EXTERNAL_STORAGE") != QtAndroid::PermissionResult::Granted) return APKEF_STORAGE_WRITE_PERMISSION_REQUIRED; if(QtAndroid::checkPermission("android.permission.WRITE_EXTERNAL_STORAGE") != QtAndroid::PermissionResult::Granted) return APKEF_STORAGE_WRITE_PERMISSION_REQUIRED;
} }
for(int i = 0; i < 2; i++) for(int i = 0; i < 2; i++)
@ -147,7 +179,7 @@ QtAndroidApkExpansionFiles::APKEF_STATE QtAndroidApkExpansionFiles::startDownloa
return APKEF_UNKNOWN_ERROR; return APKEF_UNKNOWN_ERROR;
} }
QString QtAndroidApkExpansionFiles::mainFileName() QString QAndroidApkExpansionFiles::mainFileName()
{ {
QString FileName; QString FileName;
@ -164,7 +196,7 @@ QString QtAndroidApkExpansionFiles::mainFileName()
return FileName; return FileName;
} }
QString QtAndroidApkExpansionFiles::patchFileName() QString QAndroidApkExpansionFiles::patchFileName()
{ {
QString FileName; QString FileName;
@ -181,111 +213,128 @@ QString QtAndroidApkExpansionFiles::patchFileName()
return FileName; return FileName;
} }
const QString& QtAndroidApkExpansionFiles::getBase64PublicKey() const void QAndroidApkExpansionFiles::sendRequest(REQUEST_ID requestID)
{
if(m_JavaApkExpansionDownloader.isValid())
{
m_JavaApkExpansionDownloader.callMethod<void>("sendRequest",
"(I)V",
requestID
);
}
}
const QString& QAndroidApkExpansionFiles::getBase64PublicKey() const
{ {
return m_Base64PublicKey; return m_Base64PublicKey;
} }
void QtAndroidApkExpansionFiles::setBase64PublicKey(const QString &Base64PublicKey) void QAndroidApkExpansionFiles::setBase64PublicKey(const QString &Base64PublicKey)
{ {
m_Base64PublicKey = Base64PublicKey; m_Base64PublicKey = Base64PublicKey;
} }
const QVector<int>& QtAndroidApkExpansionFiles::getSALT() const const QVector<int>& QAndroidApkExpansionFiles::getSALT() const
{ {
return m_SALT; return m_SALT;
} }
void QtAndroidApkExpansionFiles::setSALT(const QVector<int> &SALT) void QAndroidApkExpansionFiles::setSALT(const QVector<int> &SALT)
{ {
m_SALT = SALT; m_SALT = SALT;
} }
const ExpansionFileInfo& QtAndroidApkExpansionFiles::getMainExpansionFileInfo() const const ExpansionFileInfo& QAndroidApkExpansionFiles::getMainExpansionFileInfo() const
{ {
return m_ExpansionsFileInfo[0]; return m_ExpansionsFileInfo[0];
} }
void QtAndroidApkExpansionFiles::setMainExpansionFileInfo(const ExpansionFileInfo &MainExpansionFileInfo) void QAndroidApkExpansionFiles::setMainExpansionFileInfo(const ExpansionFileInfo &MainExpansionFileInfo)
{ {
m_ExpansionsFileInfo[0] = MainExpansionFileInfo; m_ExpansionsFileInfo[0] = MainExpansionFileInfo;
} }
const ExpansionFileInfo& QtAndroidApkExpansionFiles::getPatchExpansionFileInfo() const const ExpansionFileInfo& QAndroidApkExpansionFiles::getPatchExpansionFileInfo() const
{ {
return m_ExpansionsFileInfo[1]; return m_ExpansionsFileInfo[1];
} }
void QtAndroidApkExpansionFiles::setPatchExpansionFileInfo(const ExpansionFileInfo &PatchExpansionFileInfo) void QAndroidApkExpansionFiles::setPatchExpansionFileInfo(const ExpansionFileInfo &PatchExpansionFileInfo)
{ {
m_ExpansionsFileInfo[1] = PatchExpansionFileInfo; m_ExpansionsFileInfo[1] = PatchExpansionFileInfo;
} }
QString QtAndroidApkExpansionFiles::stateTextInfo(int state) QString QAndroidApkExpansionFiles::getString(int stringID)
{ {
QString TextInfo; QString TextString;
switch(state) switch(stringID)
{ {
case STATE_IDLE: case STRING_IDLE:
TextInfo = tr("Waiting for download to start"); TextString = tr("Waiting for download to start");
break; break;
case STATE_FETCHING_URL: case STRING_FETCHING_URL:
TextInfo = tr("Looking for resources to download"); TextString = tr("Looking for resources to download");
break; break;
case STATE_CONNECTING: case STRING_CONNECTING:
TextInfo = tr("Connecting to the download server"); TextString = tr("Connecting to the download server");
break; break;
case STATE_DOWNLOADING: case STRING_DOWNLOADING:
TextInfo = tr("Downloading resources"); TextString = tr("Downloading resources");
break; break;
case STATE_COMPLETED: case STRING_COMPLETED:
TextInfo = tr("Download finished"); TextString = tr("Download finished");
break; break;
case STATE_PAUSED_NETWORK_UNAVAILABLE: case STRING_PAUSED_NETWORK_UNAVAILABLE:
TextInfo = tr("Download paused because no network is available"); TextString = tr("Download paused because no network is available");
break; break;
case STATE_PAUSED_BY_REQUEST: case STRING_PAUSED_BY_REQUEST:
TextInfo = tr("Download paused"); TextString = tr("Download paused");
break; break;
case STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: case STRING_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
TextInfo = tr("Download paused because wifi is disabled"); TextString = tr("Download paused because wifi is disabled");
break; break;
case STATE_PAUSED_NEED_CELLULAR_PERMISSION: case STRING_PAUSED_NEED_CELLULAR_PERMISSION:
case STATE_PAUSED_NEED_WIFI: case STRING_PAUSED_NEED_WIFI:
TextInfo = tr("Download paused because wifi is unavailable"); TextString = tr("Download paused because wifi is unavailable");
break; break;
case STATE_PAUSED_WIFI_DISABLED: case STRING_PAUSED_WIFI_DISABLED:
TextInfo = tr("Download paused because wifi is disabled"); TextString = tr("Download paused because wifi is disabled");
break; break;
case STATE_PAUSED_ROAMING: case STRING_PAUSED_ROAMING:
TextInfo = tr("Download paused because you are roaming"); TextString = tr("Download paused because you are roaming");
break; break;
case STATE_PAUSED_NETWORK_SETUP_FAILURE: case STRING_PAUSED_NETWORK_SETUP_FAILURE:
TextInfo = tr("Download paused. Test a website in browser"); TextString = tr("Download paused. Test a website in browser");
break; break;
case STATE_PAUSED_SDCARD_UNAVAILABLE: case STRING_PAUSED_SDCARD_UNAVAILABLE:
TextInfo = tr("Download paused because the external storage is unavailable"); TextString = tr("Download paused because the external storage is unavailable");
break; break;
case STATE_FAILED_UNLICENSED: case STRING_FAILED_UNLICENSED:
TextInfo = tr("Download failed because you may not have purchased this app"); TextString = tr("Download failed because you may not have purchased this app");
break; break;
case STATE_FAILED_FETCHING_URL: case STRING_FAILED_FETCHING_URL:
TextInfo = tr("Download failed because the resources could not be found"); TextString = tr("Download failed because the resources could not be found");
break; break;
case STATE_FAILED_SDCARD_FULL: case STRING_FAILED_SDCARD_FULL:
TextInfo = tr("Download failed because the external storage is full"); TextString = tr("Download failed because the external storage is full");
break; break;
case STATE_FAILED_CANCELED: case STRING_FAILED_CANCELED:
TextInfo = tr("Download cancelled"); TextString = tr("Download cancelled");
break; break;
case STATE_UNKNOWN: case STRING_FAILED:
TextInfo = tr("Starting..."); TextString = tr("Download failed");
break; break;
case STATE_DOWNLOADING_TIME_LEFT: case STRING_UNKNOWN:
TextInfo = tr("Time left"); TextString = tr("Unknown error");
break;
case STRING_TIME_LEFT:
TextString = tr("Time left");
break;
case STRING_NOTIFICATION_CHANNEL_NAME:
TextString = tr("App data download");
break; break;
} }
return TextInfo; return TextString;
} }

View File

@ -1,3 +1,26 @@
/*
* MIT License
*
* Copyright (c) 2018 Fabio Falsini <falsinsoft@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once #pragma once
#include <QtAndroidExtras> #include <QtAndroidExtras>
@ -15,43 +38,45 @@ public:
}; };
Q_DECLARE_METATYPE(ExpansionFileInfo) Q_DECLARE_METATYPE(ExpansionFileInfo)
class QtAndroidApkExpansionFiles : public QObject class QAndroidApkExpansionFiles : public QObject
{ {
Q_PROPERTY(ExpansionFileInfo main READ getMainExpansionFileInfo WRITE setMainExpansionFileInfo) Q_PROPERTY(ExpansionFileInfo main READ getMainExpansionFileInfo WRITE setMainExpansionFileInfo)
Q_PROPERTY(ExpansionFileInfo patch READ getPatchExpansionFileInfo WRITE setPatchExpansionFileInfo) Q_PROPERTY(ExpansionFileInfo patch READ getPatchExpansionFileInfo WRITE setPatchExpansionFileInfo)
Q_PROPERTY(QString base64PublicKey READ getBase64PublicKey WRITE setBase64PublicKey) Q_PROPERTY(QString base64PublicKey READ getBase64PublicKey WRITE setBase64PublicKey)
Q_PROPERTY(QVector<int> salt READ getSALT WRITE setSALT) Q_PROPERTY(QVector<int> salt READ getSALT WRITE setSALT)
Q_DISABLE_COPY(QtAndroidApkExpansionFiles) Q_DISABLE_COPY(QAndroidApkExpansionFiles)
Q_ENUMS(DOWNLOAD_STATE) Q_ENUMS(DOWNLOAD_STATE)
Q_ENUMS(APKEF_STATE) Q_ENUMS(APKEF_STATE)
Q_ENUMS(REQUEST_ID)
Q_ENUMS(STRING_ID)
Q_OBJECT Q_OBJECT
QtAndroidApkExpansionFiles(); QAndroidApkExpansionFiles();
public: public:
~QAndroidApkExpansionFiles();
enum DOWNLOAD_STATE enum DOWNLOAD_STATE
{ {
STATE_IDLE = 1, STATE_IDLE = 1,
STATE_FETCHING_URL = 2, STATE_FETCHING_URL,
STATE_CONNECTING = 3, STATE_CONNECTING,
STATE_DOWNLOADING = 4, STATE_DOWNLOADING,
STATE_COMPLETED = 5, STATE_COMPLETED,
STATE_PAUSED_NETWORK_UNAVAILABLE = 6, STATE_PAUSED_NETWORK_UNAVAILABLE,
STATE_PAUSED_BY_REQUEST = 7, STATE_PAUSED_BY_REQUEST,
STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8, STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION,
STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9, STATE_PAUSED_NEED_CELLULAR_PERMISSION,
STATE_PAUSED_WIFI_DISABLED = 10, STATE_PAUSED_WIFI_DISABLED,
STATE_PAUSED_NEED_WIFI = 11, STATE_PAUSED_NEED_WIFI,
STATE_PAUSED_ROAMING = 12, STATE_PAUSED_ROAMING,
STATE_PAUSED_NETWORK_SETUP_FAILURE = 13, STATE_PAUSED_NETWORK_SETUP_FAILURE,
STATE_PAUSED_SDCARD_UNAVAILABLE = 14, STATE_PAUSED_SDCARD_UNAVAILABLE,
STATE_FAILED_UNLICENSED = 15, STATE_FAILED_UNLICENSED,
STATE_FAILED_FETCHING_URL = 16, STATE_FAILED_FETCHING_URL,
STATE_FAILED_SDCARD_FULL = 17, STATE_FAILED_SDCARD_FULL,
STATE_FAILED_CANCELED = 18, STATE_FAILED_CANCELED,
STATE_FAILED = 19, STATE_FAILED
STATE_UNKNOWN = 20,
STATE_DOWNLOADING_TIME_LEFT = 21
}; };
enum APKEF_STATE enum APKEF_STATE
{ {
@ -65,14 +90,47 @@ public:
APKEF_INVALID_SALT, APKEF_INVALID_SALT,
APKEF_UNKNOWN_ERROR APKEF_UNKNOWN_ERROR
}; };
enum REQUEST_ID
{
REQUEST_ABORT_DOWNLOAD = 0,
REQUEST_PAUSE_DOWNLOAD,
REQUEST_CONTINUE_DOWNLOAD,
REQUEST_DOWNLOAD_STATUS
};
enum STRING_ID
{
STRING_IDLE = 0,
STRING_FETCHING_URL,
STRING_CONNECTING,
STRING_DOWNLOADING,
STRING_COMPLETED,
STRING_PAUSED_NETWORK_UNAVAILABLE,
STRING_PAUSED_BY_REQUEST,
STRING_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION,
STRING_PAUSED_NEED_CELLULAR_PERMISSION,
STRING_PAUSED_WIFI_DISABLED,
STRING_PAUSED_NEED_WIFI,
STRING_PAUSED_ROAMING,
STRING_PAUSED_NETWORK_SETUP_FAILURE,
STRING_PAUSED_SDCARD_UNAVAILABLE,
STRING_FAILED_UNLICENSED,
STRING_FAILED_FETCHING_URL,
STRING_FAILED_SDCARD_FULL,
STRING_FAILED_CANCELED,
STRING_FAILED,
STRING_UNKNOWN,
STRING_TIME_LEFT,
STRING_NOTIFICATION_CHANNEL_NAME
};
static QObject* qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine); static QObject* qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine);
static QtAndroidApkExpansionFiles* instance(); static QAndroidApkExpansionFiles* instance();
Q_INVOKABLE QString mainFileName(); Q_INVOKABLE QString mainFileName();
Q_INVOKABLE QString patchFileName(); Q_INVOKABLE QString patchFileName();
Q_INVOKABLE APKEF_STATE startDownloadFiles(); Q_INVOKABLE APKEF_STATE startDownloadFiles();
Q_INVOKABLE QString stateTextInfo(int state); Q_INVOKABLE QString getString(int stringID);
Q_INVOKABLE void sendRequest(REQUEST_ID requestID);
const QString& getBase64PublicKey() const; const QString& getBase64PublicKey() const;
void setBase64PublicKey(const QString &Base64PublicKey); void setBase64PublicKey(const QString &Base64PublicKey);
@ -92,8 +150,17 @@ private slots:
private: private:
const QAndroidJniObject m_JavaApkExpansionDownloader; const QAndroidJniObject m_JavaApkExpansionDownloader;
static QtAndroidApkExpansionFiles *m_pInstance; static QAndroidApkExpansionFiles *m_pInstance;
ExpansionFileInfo m_ExpansionsFileInfo[2]; ExpansionFileInfo m_ExpansionsFileInfo[2];
QString m_Base64PublicKey; QString m_Base64PublicKey;
QVector<int> m_SALT; QVector<int> m_SALT;
enum APP_STATE
{
APP_STATE_CREATE = 0,
APP_STATE_START = 1,
APP_STATE_STOP = 2,
APP_STATE_DESTROY = 3
};
void SetNewAppState(APP_STATE NewState);
}; };

View File

@ -1,41 +1,85 @@
/*
* MIT License
*
* Copyright (c) 2018 Fabio Falsini <falsinsoft@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "QAndroidAppPermissions.h"
#include "QtAndroidAppPermissions.h" QAndroidAppPermissions::QAndroidAppPermissions()
QtAndroidAppPermissions::QtAndroidAppPermissions()
{ {
} }
QObject* QtAndroidAppPermissions::qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine) QObject* QAndroidAppPermissions::qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine)
{ {
Q_UNUSED(engine); Q_UNUSED(engine);
Q_UNUSED(scriptEngine); Q_UNUSED(scriptEngine);
return new QtAndroidAppPermissions(); return new QAndroidAppPermissions();
} }
void QtAndroidAppPermissions::requestPermissions(const QStringList &permissionsNameList) void QAndroidAppPermissions::requestPermissions(const QStringList &permissionsNameList)
{
for(int i = 0; i < permissionsNameList.count(); i++)
{
requestPermission(permissionsNameList[i]);
}
}
void QtAndroidAppPermissions::requestPermission(const QString &permissionName)
{ {
if(QtAndroid::androidSdkVersion() >= 23) if(QtAndroid::androidSdkVersion() >= 23)
{ {
if(QtAndroid::checkPermission(permissionName) != QtAndroid::PermissionResult::Granted) QStringList PermissionsNotGrantedList;
for(int i = 0; i < permissionsNameList.count(); i++)
{ {
QtAndroid::requestPermissions(QStringList() << permissionName, std::bind(&QtAndroidAppPermissions::RequestPermissionResults, this, std::placeholders::_1)); if(QtAndroid::checkPermission(permissionsNameList[i]) != QtAndroid::PermissionResult::Granted)
return; {
PermissionsNotGrantedList << permissionsNameList[i];
}
else
{
emit requestPermissionsResults(permissionsNameList[i], true);
}
}
if(PermissionsNotGrantedList.count() > 0)
{
QtAndroid::requestPermissions(PermissionsNotGrantedList, std::bind(&QAndroidAppPermissions::RequestPermissionResults, this, std::placeholders::_1));
}
}
else
{
for(int i = 0; i < permissionsNameList.count(); i++)
{
emit requestPermissionsResults(permissionsNameList[i], true);
} }
} }
emit requestPermissionsResults(permissionName, true);
} }
bool QtAndroidAppPermissions::shouldShowRequestPermissionInfo(const QString &permissionName) void QAndroidAppPermissions::requestPermission(const QString &permissionName)
{
if(QtAndroid::androidSdkVersion() >= 23 && QtAndroid::checkPermission(permissionName) != QtAndroid::PermissionResult::Granted)
{
QtAndroid::requestPermissions(QStringList() << permissionName, std::bind(&QAndroidAppPermissions::RequestPermissionResults, this, std::placeholders::_1));
}
else
{
emit requestPermissionsResults(permissionName, true);
}
}
bool QAndroidAppPermissions::shouldShowRequestPermissionInfo(const QString &permissionName)
{ {
if(QtAndroid::androidSdkVersion() >= 23) if(QtAndroid::androidSdkVersion() >= 23)
{ {
@ -45,7 +89,7 @@ bool QtAndroidAppPermissions::shouldShowRequestPermissionInfo(const QString &per
return false; return false;
} }
void QtAndroidAppPermissions::RequestPermissionResults(const QtAndroid::PermissionResultMap &ResultMap) void QAndroidAppPermissions::RequestPermissionResults(const QtAndroid::PermissionResultMap &ResultMap)
{ {
if(ResultMap.count() > 0) if(ResultMap.count() > 0)
{ {

View File

@ -1,13 +1,36 @@
/*
* MIT License
*
* Copyright (c) 2018 Fabio Falsini <falsinsoft@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once #pragma once
#include <QtAndroidExtras> #include <QtAndroidExtras>
#include <QQmlEngine> #include <QQmlEngine>
class QtAndroidAppPermissions : public QObject class QAndroidAppPermissions : public QObject
{ {
Q_OBJECT Q_OBJECT
QtAndroidAppPermissions(); QAndroidAppPermissions();
public: public:
static QObject* qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine); static QObject* qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine);

View File

@ -1,14 +1,31 @@
package com.falsinsoft.QtAndroidTools; /*
* MIT License
*
* Copyright (c) 2018 Fabio Falsini <falsinsoft@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import com.google.android.vending.expansion.downloader.Constants; package com.falsinsoft.qtandroidtools;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; import com.google.android.vending.expansion.downloader.*;
import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; import com.google.android.vending.expansion.downloader.impl.*;
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.expansion.downloader.impl.DownloaderService;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
@ -20,87 +37,124 @@ import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import android.os.Build;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import java.util.Arrays; import java.util.Arrays;
public class ApkExpansionDownloader implements IDownloaderClient public class ApkExpansionDownloader
{ {
private final Activity m_ActivityInstance; private final String NOTIFICATION_CHANNEL_ID;
private IDownloaderService m_RemoteService; private final DownloaderClient mDownloaderClient;
private IStub m_DownloaderClientStub; private final DownloaderProxy mDownloaderProxy;
private final Activity mActivityInstance;
public ApkExpansionDownloader(Activity ActivityInstance) public ApkExpansionDownloader(Activity ActivityInstance)
{ {
final IDownloaderClient Client = this; NOTIFICATION_CHANNEL_ID = ActivityInstance.getClass().getName();
ActivityInstance.runOnUiThread(new Runnable() mDownloaderClient = new DownloaderClient();
{ mDownloaderProxy = new DownloaderProxy(ActivityInstance);
public void run() mActivityInstance = ActivityInstance;
{
m_DownloaderClientStub = DownloaderClientMarshaller.CreateStub(Client, ApkExpansionDownloaderService.class);
}
});
m_ActivityInstance = ActivityInstance;
} }
public boolean isAPKFileDelivered(boolean IsMain, int FileVersion, int FileSize) public boolean isAPKFileDelivered(boolean IsMain, int FileVersion, int FileSize)
{ {
final String FileName = Helpers.getExpansionAPKFileName(m_ActivityInstance, IsMain, FileVersion); final String FileName = Helpers.getExpansionAPKFileName(mActivityInstance, IsMain, FileVersion);
return Helpers.doesFileExist(m_ActivityInstance, FileName, FileSize, false); return Helpers.doesFileExist(mActivityInstance, FileName, FileSize, false);
} }
public String getExpansionAPKFileName(boolean IsMain, int FileVersion) public String getExpansionAPKFileName(boolean IsMain, int FileVersion)
{ {
final String FileName = Helpers.getExpansionAPKFileName(m_ActivityInstance, IsMain, FileVersion); final String FileName = Helpers.getExpansionAPKFileName(mActivityInstance, IsMain, FileVersion);
return Helpers.generateSaveFileName(m_ActivityInstance, FileName); return Helpers.generateSaveFileName(mActivityInstance, FileName);
} }
public void enableClientStubConnection(boolean ConnectionEnabled) public void sendRequest(int requestID)
{ {
if(ConnectionEnabled == true) mDownloaderProxy.connect();
m_DownloaderClientStub.connect(m_ActivityInstance);
else switch(requestID)
m_DownloaderClientStub.disconnect(m_ActivityInstance); {
case REQUEST_ABORT_DOWNLOAD:
mDownloaderProxy.requestAbortDownload();
break;
case REQUEST_PAUSE_DOWNLOAD:
mDownloaderProxy.requestPauseDownload();
break;
case REQUEST_CONTINUE_DOWNLOAD:
mDownloaderProxy.requestContinueDownload();
break;
case REQUEST_DOWNLOAD_STATUS:
mDownloaderProxy.requestDownloadStatus();
break;
}
mDownloaderProxy.disconnect();
}
public void appStateChanged(int newState)
{
switch(newState)
{
case APP_STATE_CREATE:
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
NotificationManager Manager = mActivityInstance.getSystemService(NotificationManager.class);
NotificationChannel Channel;
Channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
getString(STRING_NOTIFICATION_CHANNEL_NAME),
NotificationManager.IMPORTANCE_DEFAULT
);
Manager.createNotificationChannel(Channel);
}
mDownloaderClient.register(mActivityInstance);
break;
case APP_STATE_START:
mDownloaderClient.register(mActivityInstance);
break;
case APP_STATE_STOP:
mDownloaderClient.unregister(mActivityInstance);
break;
case APP_STATE_DESTROY:
/*
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
NotificationManager Manager = mActivityInstance.getSystemService(NotificationManager.class);
Manager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
}
*/
mDownloaderClient.unregister(mActivityInstance);
break;
}
} }
public int startDownloader(String BASE64_PUBLIC_KEY, byte[] SALT) public int startDownloader(String BASE64_PUBLIC_KEY, byte[] SALT)
{ {
int DownloadResult = -1; int DownloadResult = -1;
ApkExpansionDownloaderService.BASE64_PUBLIC_KEY = BASE64_PUBLIC_KEY;
ApkExpansionDownloaderService.SALT = SALT;
try try
{ {
Intent LaunchIntent, IntentToLaunchThisActivityFromNotification; Intent IntentToLaunchThisActivityFromNotification;
PendingIntent PendingActivity; PendingIntent PendingActivity;
LaunchIntent = m_ActivityInstance.getIntent(); IntentToLaunchThisActivityFromNotification = new Intent(mActivityInstance, mActivityInstance.getClass());
IntentToLaunchThisActivityFromNotification = new Intent(m_ActivityInstance, m_ActivityInstance.getClass());
IntentToLaunchThisActivityFromNotification.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); IntentToLaunchThisActivityFromNotification.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
IntentToLaunchThisActivityFromNotification.setAction(LaunchIntent.getAction());
PendingActivity = PendingIntent.getActivity(mActivityInstance,
if(LaunchIntent.getCategories() != null)
{
for(String Category : LaunchIntent.getCategories())
{
IntentToLaunchThisActivityFromNotification.addCategory(Category);
}
}
PendingActivity = PendingIntent.getActivity(m_ActivityInstance,
0, 0,
IntentToLaunchThisActivityFromNotification, IntentToLaunchThisActivityFromNotification,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
); );
DownloadResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(m_ActivityInstance, PendingActivity, ApkExpansionDownloaderService.class); DownloadResult = DownloaderService.startDownloadServiceIfRequired(mActivityInstance,
NOTIFICATION_CHANNEL_ID,
if(DownloadResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) PendingActivity,
{ SALT,
enableClientStubConnection(true); BASE64_PUBLIC_KEY
} );
} }
catch(NameNotFoundException e) catch(NameNotFoundException e)
{ {
@ -110,26 +164,56 @@ public class ApkExpansionDownloader implements IDownloaderClient
return DownloadResult; return DownloadResult;
} }
@Override private class DownloaderClient extends BroadcastDownloaderClient
public void onServiceConnected(Messenger m)
{ {
m_RemoteService = DownloaderServiceMarshaller.CreateProxy(m); @Override
m_RemoteService.onClientUpdated(m_DownloaderClientStub.getMessenger()); public void onDownloadStateChanged(int newState)
} {
downloadStateChanged(newState);
}
@Override @Override
public void onDownloadStateChanged(int newState) public void onDownloadProgress(DownloadProgressInfo progress)
{ {
downloadStateChanged(newState); downloadProgress(progress.mOverallTotal, progress.mOverallProgress, progress.mTimeRemaining, progress.mCurrentSpeed);
}
} }
public static final int STRING_IDLE = 0;
public static final int STRING_FETCHING_URL = 1;
public static final int STRING_CONNECTING = 2;
public static final int STRING_DOWNLOADING = 3;
public static final int STRING_COMPLETED = 4;
public static final int STRING_PAUSED_NETWORK_UNAVAILABLE = 5;
public static final int STRING_PAUSED_BY_REQUEST = 6;
public static final int STRING_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 7;
public static final int STRING_PAUSED_NEED_CELLULAR_PERMISSION = 8;
public static final int STRING_PAUSED_WIFI_DISABLED = 9;
public static final int STRING_PAUSED_NEED_WIFI = 10;
public static final int STRING_PAUSED_ROAMING = 11;
public static final int STRING_PAUSED_NETWORK_SETUP_FAILURE = 12;
public static final int STRING_PAUSED_SDCARD_UNAVAILABLE = 13;
public static final int STRING_FAILED_UNLICENSED = 14;
public static final int STRING_FAILED_FETCHING_URL = 15;
public static final int STRING_FAILED_SDCARD_FULL = 16;
public static final int STRING_FAILED_CANCELED = 17;
public static final int STRING_FAILED = 18;
public static final int STRING_UNKNOWN = 19;
public static final int STRING_TIME_LEFT = 20;
public static final int STRING_NOTIFICATION_CHANNEL_NAME = 21;
@Override private static final int APP_STATE_CREATE = 0;
public void onDownloadProgress(DownloadProgressInfo progress) private static final int APP_STATE_START = 1;
{ private static final int APP_STATE_STOP = 2;
downloadProgress(progress.mOverallTotal, progress.mOverallProgress, progress.mTimeRemaining, progress.mCurrentSpeed); private static final int APP_STATE_DESTROY = 3;
}
private static final int REQUEST_ABORT_DOWNLOAD = 0;
private static final int REQUEST_PAUSE_DOWNLOAD = 1;
private static final int REQUEST_CONTINUE_DOWNLOAD = 2;
private static final int REQUEST_DOWNLOAD_STATUS = 3;
private static native void downloadStateChanged(int newState); private static native void downloadStateChanged(int newState);
private static native void downloadProgress(long overallTotal, long overallProgress, long timeRemaining, float currentSpeed); private static native void downloadProgress(long overallTotal, long overallProgress, long timeRemaining, float currentSpeed);
public static native String getString(int stringID);
} }

View File

@ -1,25 +0,0 @@
package com.falsinsoft.QtAndroidTools;
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
public class ApkExpansionDownloaderAlarmReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
try
{
DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent, ApkExpansionDownloaderService.class);
}
catch (NameNotFoundException e)
{
e.printStackTrace();
}
}
}

View File

@ -1,28 +0,0 @@
package com.falsinsoft.QtAndroidTools;
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
public class ApkExpansionDownloaderService extends DownloaderService
{
public static String BASE64_PUBLIC_KEY = null;
public static byte[] SALT = null;
@Override
public String getPublicKey()
{
return BASE64_PUBLIC_KEY;
}
@Override
public byte[] getSALT()
{
return SALT;
}
@Override
public String getAlarmReceiverClassName()
{
return ApkExpansionDownloaderAlarmReceiver.class.getName();
}
}

View File

@ -1,277 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
/**
* This class binds the service API to your application client. It contains the IDownloaderClient proxy,
* which is used to call functions in your client as well as the Stub, which is used to call functions
* in the client implementation of IDownloaderClient.
*
* <p>The IPC is implemented using an Android Messenger and a service Binder. The connect method
* should be called whenever the client wants to bind to the service. It opens up a service connection
* that ends up calling the onServiceConnected client API that passes the service messenger
* in. If the client wants to be notified by the service, it is responsible for then passing its
* messenger to the service in a separate call.
*
* <p>Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}.
*
* <p>When your application first starts, you should first check whether your app's expansion files are
* already on the device. If not, you should then call {@link #startDownloadServiceIfRequired}, which
* starts your {@link impl.DownloaderService} to download the expansion files if necessary. The method
* returns a value indicating whether download is required or not.
*
* <p>If a download is required, {@link #startDownloadServiceIfRequired} begins the download through
* the specified service and you should then call {@link #CreateStub} to instantiate a member {@link
* IStub} object that you need in order to receive calls through your {@link IDownloaderClient}
* interface.
*/
public class DownloaderClientMarshaller {
public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10;
public static final int MSG_ONDOWNLOADPROGRESS = 11;
public static final int MSG_ONSERVICECONNECTED = 12;
public static final String PARAM_NEW_STATE = "newState";
public static final String PARAM_PROGRESS = "progress";
public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED;
public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED;
public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED;
private static class Proxy implements IDownloaderClient {
private Messenger mServiceMessenger;
@Override
public void onDownloadStateChanged(int newState) {
Bundle params = new Bundle(1);
params.putInt(PARAM_NEW_STATE, newState);
send(MSG_ONDOWNLOADSTATE_CHANGED, params);
}
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
Bundle params = new Bundle(1);
params.putParcelable(PARAM_PROGRESS, progress);
send(MSG_ONDOWNLOADPROGRESS, params);
}
private void send(int method, Bundle params) {
Message m = Message.obtain(null, method);
m.setData(params);
try {
mServiceMessenger.send(m);
} catch (RemoteException e) {
e.printStackTrace();
}
}
public Proxy(Messenger msg) {
mServiceMessenger = msg;
}
@Override
public void onServiceConnected(Messenger m) {
/**
* This is never called through the proxy.
*/
}
}
private static class Stub implements IStub {
private IDownloaderClient mItf = null;
private Class<?> mDownloaderServiceClass;
private boolean mBound;
private Messenger mServiceMessenger;
private Context mContext;
/**
* Target we publish for clients to send messages to IncomingHandler.
*/
final Messenger mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ONDOWNLOADPROGRESS:
Bundle bun = msg.getData();
if ( null != mContext ) {
bun.setClassLoader(mContext.getClassLoader());
DownloadProgressInfo dpi = (DownloadProgressInfo) msg.getData()
.getParcelable(PARAM_PROGRESS);
mItf.onDownloadProgress(dpi);
}
break;
case MSG_ONDOWNLOADSTATE_CHANGED:
mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
break;
case MSG_ONSERVICECONNECTED:
mItf.onServiceConnected(
(Messenger) msg.getData().getParcelable(PARAM_MESSENGER));
break;
}
}
});
public Stub(IDownloaderClient itf, Class<?> downloaderService) {
mItf = itf;
mDownloaderServiceClass = downloaderService;
}
/**
* Class for interacting with the main interface of the service.
*/
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
// This is called when the connection with the service has been
// established, giving us the object we can use to
// interact with the service. We are communicating with the
// service using a Messenger, so here we get a client-side
// representation of that from the raw IBinder object.
mServiceMessenger = new Messenger(service);
mItf.onServiceConnected(
mServiceMessenger);
}
public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mServiceMessenger = null;
}
};
@Override
public void connect(Context c) {
mContext = c;
Intent bindIntent = new Intent(c, mDownloaderServiceClass);
bindIntent.putExtra(PARAM_MESSENGER, mMessenger);
if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) {
if ( Constants.LOGVV ) {
Log.d(Constants.TAG, "Service Unbound");
}
} else {
mBound = true;
}
}
@Override
public void disconnect(Context c) {
if (mBound) {
c.unbindService(mConnection);
mBound = false;
}
mContext = null;
}
@Override
public Messenger getMessenger() {
return mMessenger;
}
}
/**
* Returns a proxy that will marshal calls to IDownloaderClient methods
*
* @param msg
* @return
*/
public static IDownloaderClient CreateProxy(Messenger msg) {
return new Proxy(msg);
}
/**
* Returns a stub object that, when connected, will listen for marshaled
* {@link IDownloaderClient} methods and translate them into calls to the supplied
* interface.
*
* @param itf An implementation of IDownloaderClient that will be called
* when remote method calls are unmarshaled.
* @param downloaderService The class for your implementation of {@link
* impl.DownloaderService}.
* @return The {@link IStub} that allows you to connect to the service such that
* your {@link IDownloaderClient} receives status updates.
*/
public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) {
return new Stub(itf, downloaderService);
}
/**
* Starts the download if necessary. This function starts a flow that does `
* many things. 1) Checks to see if the APK version has been checked and
* the metadata database updated 2) If the APK version does not match,
* checks the new LVL status to see if a new download is required 3) If the
* APK version does match, then checks to see if the download(s) have been
* completed 4) If the downloads have been completed, returns
* NO_DOWNLOAD_REQUIRED The idea is that this can be called during the
* startup of an application to quickly ascertain if the application needs
* to wait to hear about any updated APK expansion files. Note that this does
* mean that the application MUST be run for the first time with a network
* connection, even if Market delivers all of the files.
*
* @param context Your application Context.
* @param notificationClient A PendingIntent to start the Activity in your application
* that shows the download progress and which will also start the application when download
* completes.
* @param serviceClass the class of your {@link imp.DownloaderService} implementation
* @return whether the service was started and the reason for starting the service.
* Either {@link #NO_DOWNLOAD_REQUIRED}, {@link #LVL_CHECK_REQUIRED}, or {@link
* #DOWNLOAD_REQUIRED}.
* @throws NameNotFoundException
*/
public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient,
Class<?> serviceClass)
throws NameNotFoundException {
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
serviceClass);
}
/**
* This version assumes that the intent contains the pending intent as a parameter. This
* is used for responding to alarms.
* <p>The pending intent must be in an extra with the key {@link
* impl.DownloaderService#EXTRA_PENDING_INTENT}.
*
* @param context
* @param notificationClient
* @param serviceClass the class of the service to start
* @return
* @throws NameNotFoundException
*/
public static int startDownloadServiceIfRequired(Context context, Intent notificationClient,
Class<?> serviceClass)
throws NameNotFoundException {
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
serviceClass);
}
}

View File

@ -1,181 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
/**
* This class is used by the client activity to proxy requests to the Downloader
* Service.
*
* Most importantly, you must call {@link #CreateProxy} during the {@link
* IDownloaderClient#onServiceConnected} callback in your activity in order to instantiate
* an {@link IDownloaderService} object that you can then use to issue commands to the {@link
* DownloaderService} (such as to pause and resume downloads).
*/
public class DownloaderServiceMarshaller {
public static final int MSG_REQUEST_ABORT_DOWNLOAD =
1;
public static final int MSG_REQUEST_PAUSE_DOWNLOAD =
2;
public static final int MSG_SET_DOWNLOAD_FLAGS =
3;
public static final int MSG_REQUEST_CONTINUE_DOWNLOAD =
4;
public static final int MSG_REQUEST_DOWNLOAD_STATE =
5;
public static final int MSG_REQUEST_CLIENT_UPDATE =
6;
public static final String PARAMS_FLAGS = "flags";
public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
private static class Proxy implements IDownloaderService {
private Messenger mMsg;
private void send(int method, Bundle params) {
Message m = Message.obtain(null, method);
m.setData(params);
try {
mMsg.send(m);
} catch (RemoteException e) {
e.printStackTrace();
}
}
public Proxy(Messenger msg) {
mMsg = msg;
}
@Override
public void requestAbortDownload() {
send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
}
@Override
public void requestPauseDownload() {
send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
}
@Override
public void setDownloadFlags(int flags) {
Bundle params = new Bundle();
params.putInt(PARAMS_FLAGS, flags);
send(MSG_SET_DOWNLOAD_FLAGS, params);
}
@Override
public void requestContinueDownload() {
send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
}
@Override
public void requestDownloadStatus() {
send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
}
@Override
public void onClientUpdated(Messenger clientMessenger) {
Bundle bundle = new Bundle(1);
bundle.putParcelable(PARAM_MESSENGER, clientMessenger);
send(MSG_REQUEST_CLIENT_UPDATE, bundle);
}
}
private static class Stub implements IStub {
private IDownloaderService mItf = null;
final Messenger mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REQUEST_ABORT_DOWNLOAD:
mItf.requestAbortDownload();
break;
case MSG_REQUEST_CONTINUE_DOWNLOAD:
mItf.requestContinueDownload();
break;
case MSG_REQUEST_PAUSE_DOWNLOAD:
mItf.requestPauseDownload();
break;
case MSG_SET_DOWNLOAD_FLAGS:
mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
break;
case MSG_REQUEST_DOWNLOAD_STATE:
mItf.requestDownloadStatus();
break;
case MSG_REQUEST_CLIENT_UPDATE:
mItf.onClientUpdated((Messenger) msg.getData().getParcelable(
PARAM_MESSENGER));
break;
}
}
});
public Stub(IDownloaderService itf) {
mItf = itf;
}
@Override
public Messenger getMessenger() {
return mMessenger;
}
@Override
public void connect(Context c) {
}
@Override
public void disconnect(Context c) {
}
}
/**
* Returns a proxy that will marshall calls to IDownloaderService methods
*
* @param ctx
* @return
*/
public static IDownloaderService CreateProxy(Messenger msg) {
return new Proxy(msg);
}
/**
* Returns a stub object that, when connected, will listen for marshalled
* IDownloaderService methods and translate them into calls to the supplied
* interface.
*
* @param itf An implementation of IDownloaderService that will be called
* when remote method calls are unmarshalled.
* @return
*/
public static IStub CreateStub(IDownloaderService itf) {
return new Stub(itf);
}
}

View File

@ -22,8 +22,12 @@ import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.StatFs; import android.os.StatFs;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.annotation.StringRes;
import android.util.Log; import android.util.Log;
//import com.android.vending.expansion.downloader.R;
import com.falsinsoft.qtandroidtools.ApkExpansionDownloader;
import java.io.File; import java.io.File;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@ -266,7 +270,7 @@ public class Helpers {
* *
* @param c the app/activity/service context * @param c the app/activity/service context
* @param fileName the name (sans path) of the file to query * @param fileName the name (sans path) of the file to query
* @return true if it does exist, false otherwise * @return value representing whether the file exists and is readable
*/ */
static public int getFileStatus(Context c, String fileName) { static public int getFileStatus(Context c, String fileName) {
// the file may have been delivered by Play --- let's make sure // the file may have been delivered by Play --- let's make sure
@ -312,5 +316,48 @@ public class Helpers {
* @param state One of the STATE_* constants from {@link IDownloaderClient}. * @param state One of the STATE_* constants from {@link IDownloaderClient}.
* @return string resource ID for the corresponding string. * @return string resource ID for the corresponding string.
*/ */
public static native String getDownloaderStringResourceFromState(int state); @StringRes
static public int getDownloaderStringResourceIDFromState(int state) {
switch (state) {
case IDownloaderClient.STATE_IDLE:
return ApkExpansionDownloader.STRING_IDLE;
case IDownloaderClient.STATE_FETCHING_URL:
return ApkExpansionDownloader.STRING_FETCHING_URL;
case IDownloaderClient.STATE_CONNECTING:
return ApkExpansionDownloader.STRING_CONNECTING;
case IDownloaderClient.STATE_DOWNLOADING:
return ApkExpansionDownloader.STRING_DOWNLOADING;
case IDownloaderClient.STATE_COMPLETED:
return ApkExpansionDownloader.STRING_COMPLETED;
case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE:
return ApkExpansionDownloader.STRING_PAUSED_NETWORK_UNAVAILABLE;
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
return ApkExpansionDownloader.STRING_PAUSED_BY_REQUEST;
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
return ApkExpansionDownloader.STRING_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION;
case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
return ApkExpansionDownloader.STRING_PAUSED_NEED_CELLULAR_PERMISSION;
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED:
return ApkExpansionDownloader.STRING_PAUSED_WIFI_DISABLED;
case IDownloaderClient.STATE_PAUSED_NEED_WIFI:
return ApkExpansionDownloader.STRING_PAUSED_NEED_WIFI;
case IDownloaderClient.STATE_PAUSED_ROAMING:
return ApkExpansionDownloader.STRING_PAUSED_ROAMING;
case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE:
return ApkExpansionDownloader.STRING_PAUSED_NETWORK_SETUP_FAILURE;
case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
return ApkExpansionDownloader.STRING_PAUSED_SDCARD_UNAVAILABLE;
case IDownloaderClient.STATE_FAILED_UNLICENSED:
return ApkExpansionDownloader.STRING_FAILED_UNLICENSED;
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
return ApkExpansionDownloader.STRING_FAILED_FETCHING_URL;
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
return ApkExpansionDownloader.STRING_FAILED_SDCARD_FULL;
case IDownloaderClient.STATE_FAILED_CANCELED:
return ApkExpansionDownloader.STRING_FAILED_CANCELED;
default:
return ApkExpansionDownloader.STRING_UNKNOWN;
}
}
} }

View File

@ -16,8 +16,6 @@
package com.google.android.vending.expansion.downloader; package com.google.android.vending.expansion.downloader;
import android.os.Messenger;
/** /**
* This interface should be implemented by the client activity for the * This interface should be implemented by the client activity for the
* downloader. It is used to pass status from the service to the client. * downloader. It is used to pass status from the service to the client.
@ -72,28 +70,6 @@ public interface IDownloaderClient {
static final int STATE_FAILED_CANCELED = 18; static final int STATE_FAILED_CANCELED = 18;
static final int STATE_FAILED = 19; static final int STATE_FAILED = 19;
static final int STATE_UNKNOWN = 20;
static final int STATE_DOWNLOADING_TIME_LEFT = 21;
/**
* Called internally by the stub when the service is bound to the client.
* <p>
* Critical implementation detail. In onServiceConnected we create the
* remote service and marshaler. This is how we pass the client information
* back to the service so the client can be properly notified of changes. We
* must do this every time we reconnect to the service.
* <p>
* That is, when you receive this callback, you should call
* {@link DownloaderServiceMarshaller#CreateProxy} to instantiate a member
* instance of {@link IDownloaderService}, then call
* {@link IDownloaderService#onClientUpdated} with the Messenger retrieved
* from your {@link IStub} proxy object.
*
* @param m the service Messenger. This Messenger is used to call the
* service API from the client.
*/
void onServiceConnected(Messenger m);
/** /**
* Called when the download state changes. Depending on the state, there may * Called when the download state changes. Depending on the state, there may

View File

@ -17,7 +17,6 @@
package com.google.android.vending.expansion.downloader; package com.google.android.vending.expansion.downloader;
import com.google.android.vending.expansion.downloader.impl.DownloaderService; import com.google.android.vending.expansion.downloader.impl.DownloaderService;
import android.os.Messenger;
/** /**
* This interface is implemented by the DownloaderService and by the * This interface is implemented by the DownloaderService and by the
@ -70,14 +69,4 @@ public interface IDownloaderService {
* Requests that the download status be sent to the client. * Requests that the download status be sent to the client.
*/ */
void requestDownloadStatus(); void requestDownloadStatus();
/**
* Call this when you get {@link
* IDownloaderClient.onServiceConnected(Messenger m)} from the
* DownloaderClient to register the client with the service. It will
* automatically send the current status to the client.
*
* @param clientMessenger
*/
void onClientUpdated(Messenger clientMessenger);
} }

View File

@ -1,41 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import android.content.Context;
import android.os.Messenger;
/**
* This is the interface that is used to connect/disconnect from the downloader
* service.
* <p>
* You should get a proxy object that implements this interface by calling
* {@link DownloaderClientMarshaller#CreateStub} in your activity when the
* downloader service starts. Then, call {@link #connect} during your activity's
* onResume() and call {@link #disconnect} during onStop().
* <p>
* Then during the {@link IDownloaderClient#onServiceConnected} callback, you
* should call {@link #getMessenger} to pass the stub's Messenger object to
* {@link IDownloaderService#onClientUpdated}.
*/
public interface IStub {
Messenger getMessenger();
void connect(Context c);
void disconnect(Context c);
}

View File

@ -1,123 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.vending.expansion.downloader;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.telephony.TelephonyManager;
import android.util.Log;
/**
* Contains useful helper functions, typically tied to the application context.
*/
class SystemFacade {
private Context mContext;
private NotificationManager mNotificationManager;
public SystemFacade(Context context) {
mContext = context;
mNotificationManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
public long currentTimeMillis() {
return System.currentTimeMillis();
}
public Integer getActiveNetworkType() {
ConnectivityManager connectivity =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity == null) {
Log.w(Constants.TAG, "couldn't get connectivity manager");
return null;
}
NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
if (activeInfo == null) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "network is not available");
}
return null;
}
return activeInfo.getType();
}
public boolean isNetworkRoaming() {
ConnectivityManager connectivity =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity == null) {
Log.w(Constants.TAG, "couldn't get connectivity manager");
return false;
}
NetworkInfo info = connectivity.getActiveNetworkInfo();
boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
TelephonyManager tm = (TelephonyManager) mContext
.getSystemService(Context.TELEPHONY_SERVICE);
if (null == tm) {
Log.w(Constants.TAG, "couldn't get telephony manager");
return false;
}
boolean isRoaming = isMobile && tm.isNetworkRoaming();
if (Constants.LOGVV && isRoaming) {
Log.v(Constants.TAG, "network is roaming");
}
return isRoaming;
}
public Long getMaxBytesOverMobile() {
return (long) Integer.MAX_VALUE;
}
public Long getRecommendedMaxBytesOverMobile() {
return 2097152L;
}
public void sendBroadcast(Intent intent) {
mContext.sendBroadcast(intent);
}
public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
}
public void postNotification(long id, Notification notification) {
/**
* TODO: The system notification manager takes ints, not longs, as IDs,
* but the download manager uses IDs take straight from the database,
* which are longs. This will have to be dealt with at some point.
*/
mNotificationManager.notify((int) id, notification);
}
public void cancelNotification(long id) {
mNotificationManager.cancel((int) id);
}
public void cancelAllNotifications() {
mNotificationManager.cancelAll();
}
public void startThread(Thread thread) {
thread.start();
}
}

View File

@ -0,0 +1,59 @@
package com.google.android.vending.expansion.downloader.impl;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v4.content.LocalBroadcastManager;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
import com.google.android.vending.expansion.downloader.IDownloaderService;
/**
* This class is used by client to receive download events. The current
* implementation uses {@link LocalBroadcastManager}, so client should
* subscribe for the updates within the application context.
*
* <br /> <br />
*
* <b>Note:</b> You should preferably register the receiver through the
* methods {@link #register(Context)} and {@link #unregister(Context)}.
* The broadcast is not sticky, so client should use
* {@link IDownloaderService#requestDownloadStatus()} to receive the
* current download status immediately (see {@link DownloaderProxy}).
*
*
* @since 5.0.0
*/
public abstract class BroadcastDownloaderClient extends BroadcastReceiver implements IDownloaderClient {
public static final String ACTION_STATE_CHANGED = "com.google.android.vending.expansion.downloader.ACTION_STATE_CHANGED";
public static final String ACTION_PROGRESS = "com.google.android.vending.expansion.downloader.ACTION_PROGRESS";
public static final String EXTRA_NEW_STATE = "newState";
public static final String EXTRA_PROGRESS = "progress";
public void register(Context context) {
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
filter.addAction(ACTION_PROGRESS);
LocalBroadcastManager.getInstance(context).registerReceiver(this, filter);
}
public void unregister(Context context) {
LocalBroadcastManager.getInstance(context).unregisterReceiver(this);
}
@Override public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_STATE_CHANGED:
int state = intent.getIntExtra(EXTRA_NEW_STATE, -1);
onDownloadStateChanged(state);
break;
case ACTION_PROGRESS:
DownloadProgressInfo info = intent.getParcelableExtra(EXTRA_PROGRESS);
onDownloadProgress(info);
break;
}
}
}

View File

@ -0,0 +1,42 @@
package com.google.android.vending.expansion.downloader.impl;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
/**
* Internal class used for communication with client. In current
* implementation the communication is done via {@link LocalBroadcastManager}.
* Commands are received by the {@link BroadcastDownloaderClient}.
*/
class ClientProxy implements IDownloaderClient {
private LocalBroadcastManager mBroadcastManager;
ClientProxy(Context ctx) {
mBroadcastManager = LocalBroadcastManager.getInstance(ctx);
}
@Override
public void onDownloadStateChanged(int newState) {
Bundle params = new Bundle(1);
params.putInt(BroadcastDownloaderClient.EXTRA_NEW_STATE, newState);
send(BroadcastDownloaderClient.ACTION_STATE_CHANGED, params);
}
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
Bundle params = new Bundle(1);
params.putParcelable(BroadcastDownloaderClient.EXTRA_PROGRESS, progress);
send(BroadcastDownloaderClient.ACTION_PROGRESS, params);
}
private void send(String action, Bundle params) {
Intent intent = new Intent(action);
intent.putExtras(params);
mBroadcastManager.sendBroadcast(intent);
}
}

View File

@ -18,20 +18,16 @@ package com.google.android.vending.expansion.downloader.impl;
import android.app.Service; import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.os.Handler; import android.os.*;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Log; import android.util.Log;
/** /**
* This service differs from IntentService in a few minor ways/ It will not * This service differs from IntentService in a few minor ways/ It will not
* auto-stop itself after the intent is handled unless the target returns "true" * auto-stop itself after the intent is handled unless the target returns "true"
* in should stop. Since the goal of this service is to handle a single kind of * in shouldStop(). Since the goal of this service is to handle a single kind of
* intent, it does not queue up batches of intents of the same type. * intent, it does not queue up batches of intents of the same type.
*/ */
public abstract class CustomIntentService extends Service { abstract class CustomIntentService extends Service {
private String mName; private String mName;
private boolean mRedelivery; private boolean mRedelivery;
private volatile ServiceHandler mServiceHandler; private volatile ServiceHandler mServiceHandler;

View File

@ -16,15 +16,14 @@
package com.google.android.vending.expansion.downloader.impl; package com.google.android.vending.expansion.downloader.impl;
import android.util.Log;
import com.google.android.vending.expansion.downloader.Constants; import com.google.android.vending.expansion.downloader.Constants;
import com.google.android.vending.expansion.downloader.Helpers; import com.google.android.vending.expansion.downloader.Helpers;
import android.util.Log;
/** /**
* Representation of information about an individual download from the database. * Representation of information about an individual download from the database.
*/ */
public class DownloadInfo { class DownloadInfo {
public String mUri; public String mUri;
public final int mIndex; public final int mIndex;
public final String mFileName; public final String mFileName;

View File

@ -16,18 +16,19 @@
package com.google.android.vending.expansion.downloader.impl; package com.google.android.vending.expansion.downloader.impl;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.support.annotation.StringRes;
import android.os.Messenger;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
//import com.android.vending.expansion.downloader.R;
import com.falsinsoft.qtandroidtools.ApkExpansionDownloader;
import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
/** /**
* This class handles displaying the notification associated with the download * This class handles displaying the notification associated with the download
* queue going on in the download manager. It handles multiple status types; * queue going on in the download manager. It handles multiple status types;
@ -39,155 +40,24 @@ import android.support.v4.app.NotificationCompat;
* The application interface for the downloader also needs to understand and * The application interface for the downloader also needs to understand and
* handle these transient states. * handle these transient states.
*/ */
public class DownloadNotification implements IDownloaderClient { class DownloadNotification {
private int mState; private int mState;
private final Context mContext; private final Context mContext;
private final NotificationManager mNotificationManager; private final NotificationManager mNotificationManager;
private CharSequence mCurrentTitle; private final IDownloaderClient mClientProxy;
private IDownloaderClient mClientProxy; private CharSequence mCurrentTitle;
private NotificationCompat.Builder mActiveDownloadBuilder; private NotificationCompat.Builder mActiveDownloadBuilder;
private NotificationCompat.Builder mBuilder; private NotificationCompat.Builder mBuilder;
private NotificationCompat.Builder mCurrentBuilder; private NotificationCompat.Builder mCurrentBuilder;
private CharSequence mLabel; private CharSequence mLabel;
private String mCurrentText; private String mCurrentText;
private DownloadProgressInfo mProgressInfo;
private PendingIntent mContentIntent; private PendingIntent mContentIntent;
static final String LOGTAG = "DownloadNotification"; static final String LOGTAG = "DownloadNotification";
static final int NOTIFICATION_ID = LOGTAG.hashCode(); static final int NOTIFICATION_ID = LOGTAG.hashCode();
public PendingIntent getClientIntent() {
return mContentIntent;
}
public void setClientIntent(PendingIntent clientIntent) {
this.mBuilder.setContentIntent(clientIntent);
this.mActiveDownloadBuilder.setContentIntent(clientIntent);
this.mContentIntent = clientIntent;
}
public void resendState() {
if (null != mClientProxy) {
mClientProxy.onDownloadStateChanged(mState);
}
}
@Override
public void onDownloadStateChanged(int newState) {
if (null != mClientProxy) {
mClientProxy.onDownloadStateChanged(newState);
}
if (newState != mState) {
mState = newState;
if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
return;
}
int iconResource;
boolean ongoingEvent;
// get the new title string and paused text
switch (newState) {
case 0:
iconResource = android.R.drawable.stat_sys_warning;
mCurrentText = Helpers.getDownloaderStringResourceFromState(IDownloaderClient.STATE_UNKNOWN);
ongoingEvent = false;
break;
case IDownloaderClient.STATE_DOWNLOADING:
iconResource = android.R.drawable.stat_sys_download;
mCurrentText = Helpers.getDownloaderStringResourceFromState(newState);
ongoingEvent = true;
break;
case IDownloaderClient.STATE_FETCHING_URL:
case IDownloaderClient.STATE_CONNECTING:
iconResource = android.R.drawable.stat_sys_download_done;
mCurrentText = Helpers.getDownloaderStringResourceFromState(newState);
ongoingEvent = true;
break;
case IDownloaderClient.STATE_COMPLETED:
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
iconResource = android.R.drawable.stat_sys_download_done;
mCurrentText = Helpers.getDownloaderStringResourceFromState(newState);
ongoingEvent = false;
break;
case IDownloaderClient.STATE_FAILED:
case IDownloaderClient.STATE_FAILED_CANCELED:
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
case IDownloaderClient.STATE_FAILED_UNLICENSED:
iconResource = android.R.drawable.stat_sys_warning;
mCurrentText = Helpers.getDownloaderStringResourceFromState(newState);
ongoingEvent = false;
break;
default:
iconResource = android.R.drawable.stat_sys_warning;
mCurrentText = Helpers.getDownloaderStringResourceFromState(newState);
ongoingEvent = true;
break;
}
mCurrentTitle = mLabel;
mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText);
mCurrentBuilder.setSmallIcon(iconResource);
mCurrentBuilder.setContentTitle(mCurrentTitle);
mCurrentBuilder.setContentText(mCurrentText);
if (ongoingEvent) {
mCurrentBuilder.setOngoing(true);
} else {
mCurrentBuilder.setOngoing(false);
mCurrentBuilder.setAutoCancel(true);
}
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
}
}
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
mProgressInfo = progress;
if (null != mClientProxy) {
mClientProxy.onDownloadProgress(progress);
}
if (progress.mOverallTotal <= 0) {
// we just show the text
mBuilder.setTicker(mCurrentTitle);
mBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
mBuilder.setContentTitle(mCurrentTitle);
mBuilder.setContentText(mCurrentText);
mCurrentBuilder = mBuilder;
} else {
mActiveDownloadBuilder.setProgress((int) progress.mOverallTotal, (int) progress.mOverallProgress, false);
mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal));
mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText);
mActiveDownloadBuilder.setContentTitle(mLabel);
mActiveDownloadBuilder.setContentInfo(Helpers.getDownloaderStringResourceFromState(IDownloaderClient.STATE_DOWNLOADING_TIME_LEFT) + ": " + Helpers.getTimeRemaining(progress.mTimeRemaining));
mCurrentBuilder = mActiveDownloadBuilder;
}
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
}
/**
* Called in response to onClientUpdated. Creates a new proxy and notifies
* it of the current state.
*
* @param msg the client Messenger to notify
*/
public void setMessenger(Messenger msg) {
mClientProxy = DownloaderClientMarshaller.CreateProxy(msg);
if (null != mProgressInfo) {
mClientProxy.onDownloadProgress(mProgressInfo);
}
if (mState != -1) {
mClientProxy.onDownloadStateChanged(mState);
}
}
/** /**
* Constructor * Constructor
* *
@ -200,6 +70,7 @@ public class DownloadNotification implements IDownloaderClient {
mLabel = applicationLabel; mLabel = applicationLabel;
mNotificationManager = (NotificationManager) mNotificationManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE); mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mClientProxy = new ClientProxy(ctx);
mActiveDownloadBuilder = new NotificationCompat.Builder(ctx); mActiveDownloadBuilder = new NotificationCompat.Builder(ctx);
mBuilder = new NotificationCompat.Builder(ctx); mBuilder = new NotificationCompat.Builder(ctx);
@ -214,8 +85,125 @@ public class DownloadNotification implements IDownloaderClient {
mCurrentBuilder = mBuilder; mCurrentBuilder = mBuilder;
} }
@Override public PendingIntent getClientIntent() {
public void onServiceConnected(Messenger m) { return mContentIntent;
}
public void setClientIntent(PendingIntent clientIntent) {
this.mBuilder.setContentIntent(clientIntent);
this.mActiveDownloadBuilder.setContentIntent(clientIntent);
this.mContentIntent = clientIntent;
}
public void setChannelId(String channelId) {
this.mBuilder.setChannelId(channelId);
this.mActiveDownloadBuilder.setChannelId(channelId);
}
public void resendState() {
mClientProxy.onDownloadStateChanged(mState);
}
void onDownloadStateChanged(int newState) {
mClientProxy.onDownloadStateChanged(newState);
if (newState != mState) {
mState = newState;
if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
return;
}
@StringRes int stringDownloadID;
int iconResource;
boolean ongoingEvent;
mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
// get the new title string and paused text
switch (newState) {
case 0:
iconResource = android.R.drawable.stat_sys_warning;
stringDownloadID = ApkExpansionDownloader.STRING_UNKNOWN;
ongoingEvent = false;
break;
case IDownloaderClient.STATE_DOWNLOADING:
iconResource = android.R.drawable.stat_sys_download;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = true;
break;
case IDownloaderClient.STATE_FETCHING_URL:
case IDownloaderClient.STATE_CONNECTING:
iconResource = android.R.drawable.stat_sys_download_done;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = true;
break;
case IDownloaderClient.STATE_COMPLETED:
// show notification without progress
mCurrentBuilder = mBuilder;
mBuilder.setPriority(NotificationCompat.PRIORITY_MAX);
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
iconResource = android.R.drawable.stat_sys_download_done;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = false;
break;
case IDownloaderClient.STATE_FAILED:
case IDownloaderClient.STATE_FAILED_CANCELED:
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
case IDownloaderClient.STATE_FAILED_UNLICENSED:
iconResource = android.R.drawable.stat_sys_warning;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = false;
break;
default:
iconResource = android.R.drawable.stat_sys_warning;
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
ongoingEvent = true;
break;
}
mCurrentText = ApkExpansionDownloader.getString(stringDownloadID);
mCurrentTitle = mLabel;
mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText);
mCurrentBuilder.setSmallIcon(iconResource);
mCurrentBuilder.setContentTitle(mCurrentTitle);
mCurrentBuilder.setContentText(mCurrentText);
if (ongoingEvent) {
mCurrentBuilder.setOngoing(true);
mCurrentBuilder.setOnlyAlertOnce(true);
} else {
mCurrentBuilder.setOnlyAlertOnce(false);
mCurrentBuilder.setOngoing(false);
mCurrentBuilder.setAutoCancel(true);
}
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
}
}
void onDownloadProgress(DownloadProgressInfo progress) {
mClientProxy.onDownloadProgress(progress);
if (progress.mOverallTotal <= 0) {
// we just show the text
mBuilder.setTicker(mCurrentTitle);
mBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
mBuilder.setContentTitle(mCurrentTitle);
mBuilder.setContentText(mCurrentText);
mCurrentBuilder = mBuilder;
} else {
mActiveDownloadBuilder.setProgress((int) progress.mOverallTotal, (int) progress.mOverallProgress, false);
mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal));
mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText);
mActiveDownloadBuilder.setContentTitle(mLabel);
mActiveDownloadBuilder.setContentInfo(ApkExpansionDownloader.getString(ApkExpansionDownloader.STRING_TIME_LEFT) + ": " + Helpers.getTimeRemaining(progress.mTimeRemaining));
mActiveDownloadBuilder.setOngoing(true);
mActiveDownloadBuilder.setOnlyAlertOnce(true);
mCurrentBuilder = mActiveDownloadBuilder;
}
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
} }
} }

View File

@ -16,21 +16,15 @@
package com.google.android.vending.expansion.downloader.impl; package com.google.android.vending.expansion.downloader.impl;
import com.google.android.vending.expansion.downloader.Constants;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
import android.content.Context; import android.content.Context;
import android.os.PowerManager; import android.os.PowerManager;
import android.os.Process; import android.os.Process;
import android.util.Log; import android.util.Log;
import com.google.android.vending.expansion.downloader.Constants;
import com.google.android.vending.expansion.downloader.Helpers;
import com.google.android.vending.expansion.downloader.IDownloaderClient;
import java.io.File; import java.io.*;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SyncFailedException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Locale; import java.util.Locale;
@ -38,7 +32,7 @@ import java.util.Locale;
/** /**
* Runs an actual download * Runs an actual download
*/ */
public class DownloadThread { class DownloadThread {
private Context mContext; private Context mContext;
private DownloadInfo mInfo; private DownloadInfo mInfo;
@ -47,7 +41,7 @@ public class DownloadThread {
private final DownloadNotification mNotification; private final DownloadNotification mNotification;
private String mUserAgent; private String mUserAgent;
public DownloadThread(DownloadInfo info, DownloaderService service, DownloadThread(DownloadInfo info, DownloaderService service,
DownloadNotification notification) { DownloadNotification notification) {
mContext = service; mContext = service;
mInfo = info; mInfo = info;
@ -135,7 +129,7 @@ public class DownloadThread {
} }
/** /**
* Executes the download in a separate thread * Executes the download
*/ */
public void run() { public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

View File

@ -0,0 +1,125 @@
package com.google.android.vending.expansion.downloader.impl;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.*;
import android.util.Log;
import com.google.android.vending.expansion.downloader.Constants;
import com.google.android.vending.expansion.downloader.IDownloaderService;
import java.util.LinkedList;
import java.util.Queue;
import static com.google.android.vending.expansion.downloader.impl
.ServiceHandler.*;
/**
* This class is used by the client to issue commands to the {@link
* DownloaderService} (such as to pause and resume downloads).
* <p>
* Most importantly, you must first call {@link #connect()} method to
* establish the connection with the service. All the calls to {@link
* IDownloaderService} that happen when connection is being
* established will be queued and delivered when connection is ready.
*/
public final class DownloaderProxy implements IDownloaderService,
ServiceConnection {
private final Context mContext;
private Messenger mMessenger;
private boolean mConnected;
private boolean connectCalled;
private final Queue<Message> mMessages = new LinkedList<>();
public DownloaderProxy(Context context) {
this.mContext = context;
}
@Override
public final void onServiceConnected(ComponentName name, IBinder service) {
mMessenger = new Messenger(service);
mConnected = true;
drainMessages();
}
@Override
public final void onServiceDisconnected(ComponentName name) {
mMessenger = null;
mConnected = false;
}
@Override
public void requestAbortDownload() {
send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
}
@Override
public void requestPauseDownload() {
send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
}
@Override
public void setDownloadFlags(int flags) {
Bundle params = new Bundle();
params.putInt(PARAMS_FLAGS, flags);
send(MSG_SET_DOWNLOAD_FLAGS, params);
}
@Override
public void requestContinueDownload() {
send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
}
@Override
public void requestDownloadStatus() {
send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
}
private void drainMessages() {
while (mMessenger != null && !mMessages.isEmpty()) {
try {
mMessenger.send(mMessages.peek());
mMessages.remove();
} catch (RemoteException e) {
Log.e(Constants.TAG, "send: ", e);
}
}
}
private void send(int method, Bundle params) {
if (!connectCalled) {
throw new IllegalStateException("connect() method was not called");
}
Message m = Message.obtain(null, method);
m.setData(params);
mMessages.add(m);
if (mConnected) {
drainMessages();
}
}
public void connect() {
Intent bindIntent = new Intent(mContext.getApplicationContext(),
DownloaderService.class);
if (!mContext.getApplicationContext().bindService(bindIntent, this,
Context.BIND_DEBUG_UNBIND)) {
Log.w(Constants.TAG, "Service not bound. Check Manifest.xml " +
"declaration");
} else {
connectCalled = true;
}
}
public void disconnect() {
if (mConnected) {
mContext.getApplicationContext().unbindService(this);
connectCalled = false;
}
}
}

View File

@ -16,19 +16,6 @@
package com.google.android.vending.expansion.downloader.impl; 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.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
@ -38,17 +25,17 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Handler; import android.os.*;
import android.os.IBinder;
import android.os.Messenger;
import android.os.SystemClock;
import android.provider.Settings.Secure; import android.provider.Settings.Secure;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
import com.google.android.vending.expansion.downloader.*;
import com.google.android.vending.licensing.*;
import java.io.File; import java.io.File;
@ -59,7 +46,7 @@ import java.io.File;
* Note that Android by default will kill off any process that has an open file * 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. * handle on the shared (SD Card) partition if the partition is unmounted.
*/ */
public abstract class DownloaderService extends CustomIntentService implements IDownloaderService { public class DownloaderService extends CustomIntentService implements IDownloaderService {
public DownloaderService() { public DownloaderService() {
super("LVLDownloadService"); super("LVLDownloadService");
@ -446,9 +433,7 @@ public abstract class DownloaderService extends CustomIntentService implements I
* Our binding to the network state broadcasts * Our binding to the network state broadcasts
*/ */
private BroadcastReceiver mConnReceiver; private BroadcastReceiver mConnReceiver;
final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this); final private Messenger mServiceMessenger = new Messenger(new ServiceHandler(this));
final private Messenger mServiceMessenger = mServiceStub.getMessenger();
private Messenger mClientMessenger;
private DownloadNotification mNotification; private DownloadNotification mNotification;
private PendingIntent mPendingIntent; private PendingIntent mPendingIntent;
private PendingIntent mAlarmIntent; private PendingIntent mAlarmIntent;
@ -588,9 +573,10 @@ public abstract class DownloaderService extends CustomIntentService implements I
public static final int LVL_CHECK_REQUIRED = 1; public static final int LVL_CHECK_REQUIRED = 1;
public static final int DOWNLOAD_REQUIRED = 2; 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_PENDING_INTENT = "EPI";
public static final String EXTRA_MESSAGE_HANDLER = "EMH"; public static final String EXTRA_CHANNEL_ID = "ECI";
public static final String EXTRA_SALT = "ESALT";
public static final String EXTRA_PUBLIC_KEY = "EPK";
/** /**
* Returns true if the LVL check is required * Returns true if the LVL check is required
@ -621,25 +607,6 @@ public abstract class DownloaderService extends CustomIntentService implements I
sIsRunning = 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 ` * 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 * many things. 1) Checks to see if the APK version has been checked and the
@ -654,14 +621,18 @@ public abstract class DownloaderService extends CustomIntentService implements I
* network connection, even if Market delivers all of the files. * network connection, even if Market delivers all of the files.
* *
* @param context * @param context
* @param channelId The Channel ID to use for download progress
* notifications on Android O+
* @param pendingIntent * @param pendingIntent
* @return true if the app should wait for more guidance from the * @return true if the app should wait for more guidance from the
* downloader, false if the app can continue * downloader, false if the app can continue
* @throws NameNotFoundException * @throws NameNotFoundException
*/ */
public static int startDownloadServiceIfRequired(Context context, public static int startDownloadServiceIfRequired(Context context,
PendingIntent pendingIntent, String classPackage, String className) String channelId,
PendingIntent pendingIntent, byte[] salt, String publicKey)
throws NameNotFoundException { throws NameNotFoundException {
// first: do we need to do an LVL update? // first: do we need to do an LVL update?
// we begin by getting our APK version from the package manager // we begin by getting our APK version from the package manager
final PackageInfo pi = context.getPackageManager().getPackageInfo( final PackageInfo pi = context.getPackageManager().getPackageInfo(
@ -696,10 +667,12 @@ public abstract class DownloaderService extends CustomIntentService implements I
switch (status) { switch (status) {
case DOWNLOAD_REQUIRED: case DOWNLOAD_REQUIRED:
case LVL_CHECK_REQUIRED: case LVL_CHECK_REQUIRED:
Intent fileIntent = new Intent(); Intent downloadIntent = new Intent(context, DownloaderService.class);
fileIntent.setClassName(classPackage, className); downloadIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent); downloadIntent.putExtra(EXTRA_CHANNEL_ID, channelId);
context.startService(fileIntent); downloadIntent.putExtra(EXTRA_SALT, salt);
downloadIntent.putExtra(EXTRA_PUBLIC_KEY, publicKey);
context.startService(downloadIntent);
break; break;
} }
return status; return status;
@ -732,19 +705,19 @@ public abstract class DownloaderService extends CustomIntentService implements I
this.startService(fileIntent); this.startService(fileIntent);
} }
public abstract String getPublicKey();
public abstract byte[] getSALT();
public abstract String getAlarmReceiverClassName();
private class LVLRunnable implements Runnable { private class LVLRunnable implements Runnable {
LVLRunnable(Context context, PendingIntent intent) {
mContext = context;
mPendingIntent = intent;
}
final Context mContext; final Context mContext;
private final String mChannelId;
private final byte[] mSalt;
private final String mPublicKey;
LVLRunnable(Context context, String channelId, byte[] salt, String publicKey) {
mContext = context;
mChannelId = channelId;
mSalt = salt;
mPublicKey = publicKey;
}
@Override @Override
public void run() { public void run() {
@ -754,7 +727,7 @@ public abstract class DownloaderService extends CustomIntentService implements I
Secure.ANDROID_ID); Secure.ANDROID_ID);
final APKExpansionPolicy aep = new APKExpansionPolicy(mContext, final APKExpansionPolicy aep = new APKExpansionPolicy(mContext,
new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId)); new AESObfuscator(mSalt, mContext.getPackageName(), deviceId));
// reset our policy back to the start of the world to force a // reset our policy back to the start of the world to force a
// re-check // re-check
@ -763,7 +736,7 @@ public abstract class DownloaderService extends CustomIntentService implements I
// let's try and get the OBB file from LVL first // let's try and get the OBB file from LVL first
// Construct the LicenseChecker with a Policy. // Construct the LicenseChecker with a Policy.
final LicenseChecker checker = new LicenseChecker(mContext, aep, final LicenseChecker checker = new LicenseChecker(mContext, aep,
getPublicKey() // Your public licensing key. mPublicKey // Your public licensing key.
); );
checker.checkAccess(new LicenseCheckerCallback() { checker.checkAccess(new LicenseCheckerCallback() {
@ -829,9 +802,8 @@ public abstract class DownloaderService extends CustomIntentService implements I
pi = mContext.getPackageManager().getPackageInfo( pi = mContext.getPackageManager().getPackageInfo(
mContext.getPackageName(), 0); mContext.getPackageName(), 0);
db.updateMetadata(pi.versionCode, status); db.updateMetadata(pi.versionCode, status);
Class<?> serviceClass = DownloaderService.this.getClass(); Class<? extends DownloaderService> serviceClass = DownloaderService.this.getClass();
switch (startDownloadServiceIfRequired(mContext, mPendingIntent, switch (startDownloadServiceIfRequired(mContext, mChannelId, mPendingIntent, mSalt, mPublicKey)) {
serviceClass)) {
case NO_DOWNLOAD_REQUIRED: case NO_DOWNLOAD_REQUIRED:
mNotification mNotification
.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); .onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED);
@ -900,10 +872,10 @@ public abstract class DownloaderService extends CustomIntentService implements I
* *
* @param context * @param context
*/ */
public void updateLVL(final Context context) { public void updateLVL(final Context context, String channelId, byte[] salt, String publicKey) {
Context c = context.getApplicationContext(); Context c = context.getApplicationContext();
Handler h = new Handler(c.getMainLooper()); Handler h = new Handler(c.getMainLooper());
h.post(new LVLRunnable(c, mPendingIntent)); h.post(new LVLRunnable(c, channelId, salt, publicKey));
} }
/** /**
@ -939,7 +911,7 @@ public abstract class DownloaderService extends CustomIntentService implements I
return !Helpers.doesFileExist(this, filename, fileSize, true); return !Helpers.doesFileExist(this, filename, fileSize, true);
} }
private void scheduleAlarm(long wakeUp) { private void scheduleAlarm(long wakeUp, boolean repeated, Bundle callerExtras) {
AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
if (alarms == null) { if (alarms == null) {
Log.e(Constants.TAG, "couldn't get alarm manager"); Log.e(Constants.TAG, "couldn't get alarm manager");
@ -950,17 +922,22 @@ public abstract class DownloaderService extends CustomIntentService implements I
Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
} }
String className = getAlarmReceiverClassName(); // put original extras to the wake up intent
Intent intent = new Intent(Constants.ACTION_RETRY); Intent intent = new Intent(this, AlarmReceiver.class);
intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); intent.setAction(Constants.ACTION_RETRY);
intent.setClassName(this.getPackageName(), intent.putExtras(callerExtras);
className);
mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent, mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent,
PendingIntent.FLAG_ONE_SHOT); PendingIntent.FLAG_CANCEL_CURRENT);
alarms.set(
AlarmManager.RTC_WAKEUP, if (repeated) {
System.currentTimeMillis() + wakeUp, mAlarmIntent alarms.setRepeating(AlarmManager.RTC_WAKEUP,
); System.currentTimeMillis() + wakeUp, wakeUp, mAlarmIntent);
return;
}
alarms.set(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + wakeUp, mAlarmIntent);
} }
private void cancelAlarms() { private void cancelAlarms() {
@ -998,7 +975,31 @@ public abstract class DownloaderService extends CustomIntentService implements I
context.startService(fileIntent); context.startService(fileIntent);
} }
} }
}; }
/**
* Used to handle wake up calls from service watch dogs.
*/
public static class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
try {
final PendingIntent pendingIntent = (PendingIntent) intent
.getParcelableExtra(EXTRA_PENDING_INTENT);
startDownloadServiceIfRequired(
context,
intent.getStringExtra(EXTRA_CHANNEL_ID),
pendingIntent,
intent.getByteArrayExtra(EXTRA_SALT),
intent.getStringExtra(EXTRA_PUBLIC_KEY)
);
} catch (PackageManager.NameNotFoundException e) {
Log.e(getClass().getSimpleName(), "onReceive: ", e);
}
}
}
/** /**
* This is the main thread for the Downloader. This thread is responsible * This is the main thread for the Downloader. This thread is responsible
@ -1011,11 +1012,14 @@ public abstract class DownloaderService extends CustomIntentService implements I
// the database automatically reads the metadata for version code // the database automatically reads the metadata for version code
// and download status when the instance is created // and download status when the instance is created
DownloadsDB db = DownloadsDB.getDB(this); DownloadsDB db = DownloadsDB.getDB(this);
final PendingIntent pendingIntent = (PendingIntent) intent final PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT);
.getParcelableExtra(EXTRA_PENDING_INTENT); final String channelId = intent.getStringExtra(EXTRA_CHANNEL_ID);
final byte[] salt = intent.getByteArrayExtra(EXTRA_SALT);
final String publicKey = intent.getStringExtra(EXTRA_PUBLIC_KEY);
if (null != pendingIntent) mNotification.setChannelId(channelId);
{
if (null != pendingIntent) {
mNotification.setClientIntent(pendingIntent); mNotification.setClientIntent(pendingIntent);
mPendingIntent = pendingIntent; mPendingIntent = pendingIntent;
} else if (null != mPendingIntent) { } else if (null != mPendingIntent) {
@ -1028,7 +1032,7 @@ public abstract class DownloaderService extends CustomIntentService implements I
// when the LVL check completes, a successful response will update // when the LVL check completes, a successful response will update
// the service // the service
if (isLVLCheckRequired(db, mPackageInfo)) { if (isLVLCheckRequired(db, mPackageInfo)) {
updateLVL(this); updateLVL(this, channelId, salt, publicKey);
return; return;
} }
@ -1074,7 +1078,8 @@ public abstract class DownloaderService extends CustomIntentService implements I
if (info.mStatus != STATUS_SUCCESS) { if (info.mStatus != STATUS_SUCCESS) {
DownloadThread dt = new DownloadThread(info, this, mNotification); DownloadThread dt = new DownloadThread(info, this, mNotification);
cancelAlarms(); cancelAlarms();
scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG); // schedule repeated alarm to check if process is alive
scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG, true, intent.getExtras());
dt.run(); dt.run();
cancelAlarms(); cancelAlarms();
} }
@ -1084,7 +1089,7 @@ public abstract class DownloaderService extends CustomIntentService implements I
switch (info.mStatus) { switch (info.mStatus) {
case STATUS_FORBIDDEN: case STATUS_FORBIDDEN:
// the URL is out of date // the URL is out of date
updateLVL(this); updateLVL(this, channelId, salt, publicKey);
return; return;
case STATUS_SUCCESS: case STATUS_SUCCESS:
mBytesSoFar += info.mCurrentBytes - startingCount; mBytesSoFar += info.mCurrentBytes - startingCount;
@ -1139,7 +1144,7 @@ public abstract class DownloaderService extends CustomIntentService implements I
break; break;
} }
if (setWakeWatchdog) { if (setWakeWatchdog) {
scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER); scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER, false, intent.getExtras());
} else { } else {
cancelAlarms(); cancelAlarms();
} }
@ -1161,7 +1166,6 @@ public abstract class DownloaderService extends CustomIntentService implements I
unregisterReceiver(mConnReceiver); unregisterReceiver(mConnReceiver);
mConnReceiver = null; mConnReceiver = null;
} }
mServiceStub.disconnect(this);
super.onDestroy(); super.onDestroy();
} }
@ -1331,11 +1335,4 @@ public abstract class DownloaderService extends CustomIntentService implements I
public void requestDownloadStatus() { public void requestDownloadStatus() {
mNotification.resendState(); mNotification.resendState();
} }
@Override
public void onClientUpdated(Messenger clientMessenger) {
this.mClientMessenger = clientMessenger;
mNotification.setMessenger(mClientMessenger);
}
} }

View File

@ -0,0 +1,44 @@
package com.google.android.vending.expansion.downloader.impl;
import android.os.Handler;
import android.os.Message;
import com.google.android.vending.expansion.downloader.IDownloaderService;
class ServiceHandler extends Handler {
public static final String PARAMS_FLAGS = "flags";
public static final int MSG_REQUEST_ABORT_DOWNLOAD = 1;
public static final int MSG_REQUEST_PAUSE_DOWNLOAD = 2;
public static final int MSG_SET_DOWNLOAD_FLAGS = 3;
public static final int MSG_REQUEST_CONTINUE_DOWNLOAD = 4;
public static final int MSG_REQUEST_DOWNLOAD_STATE = 5;
private final IDownloaderService service;
ServiceHandler(IDownloaderService service) {
this.service = service;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REQUEST_ABORT_DOWNLOAD:
service.requestAbortDownload();
break;
case MSG_REQUEST_CONTINUE_DOWNLOAD:
service.requestContinueDownload();
break;
case MSG_REQUEST_PAUSE_DOWNLOAD:
service.requestPauseDownload();
break;
case MSG_SET_DOWNLOAD_FLAGS:
service.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
break;
case MSG_REQUEST_DOWNLOAD_STATE:
service.requestDownloadStatus();
break;
}
}
}

View File

@ -33,8 +33,8 @@ import java.util.Vector;
/** /**
* Default policy. All policy decisions are based off of response data received * Default policy. All policy decisions are based off of response data received
* from the licensing service. Specifically, the licensing server sends the * from the licensing service. Specifically, the licensing server sends the
* following information: response validity period, error retry period, * following information: response validity period, error retry period, and
* error retry count and a URL for restoring app access in unlicensed cases. * error retry count.
* <p> * <p>
* These values will vary based on the the way the application is configured in * These values will vary based on the the way the application is configured in
* the Google Play publishing console, such as whether the application is * the Google Play publishing console, such as whether the application is
@ -53,7 +53,6 @@ public class APKExpansionPolicy implements Policy {
private static final String PREF_RETRY_UNTIL = "retryUntil"; private static final String PREF_RETRY_UNTIL = "retryUntil";
private static final String PREF_MAX_RETRIES = "maxRetries"; private static final String PREF_MAX_RETRIES = "maxRetries";
private static final String PREF_RETRY_COUNT = "retryCount"; private static final String PREF_RETRY_COUNT = "retryCount";
private static final String PREF_LICENSING_URL = "licensingUrl";
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
private static final String DEFAULT_RETRY_UNTIL = "0"; private static final String DEFAULT_RETRY_UNTIL = "0";
private static final String DEFAULT_MAX_RETRIES = "0"; private static final String DEFAULT_MAX_RETRIES = "0";
@ -67,7 +66,6 @@ public class APKExpansionPolicy implements Policy {
private long mRetryCount; private long mRetryCount;
private long mLastResponseTime = 0; private long mLastResponseTime = 0;
private int mLastResponse; private int mLastResponse;
private String mLicensingUrl;
private PreferenceObfuscator mPreferences; private PreferenceObfuscator mPreferences;
private Vector<String> mExpansionURLs = new Vector<String>(); private Vector<String> mExpansionURLs = new Vector<String>();
private Vector<String> mExpansionFileNames = new Vector<String>(); private Vector<String> mExpansionFileNames = new Vector<String>();
@ -96,7 +94,6 @@ public class APKExpansionPolicy implements Policy {
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
} }
/** /**
@ -122,8 +119,6 @@ public class APKExpansionPolicy implements Policy {
* until * until
* <li>GT: the timestamp that the client should ignore retry errors until * <li>GT: the timestamp that the client should ignore retry errors until
* <li>GR: the number of retry errors that the client should ignore * <li>GR: the number of retry errors that the client should ignore
* <li>LU: a deep link URL that can enable access for unlicensed apps (e.g.
* buy app on the Play Store)
* </ul> * </ul>
* *
* @param response the result from validating the server response * @param response the result from validating the server response
@ -139,12 +134,10 @@ public class APKExpansionPolicy implements Policy {
setRetryCount(mRetryCount + 1); setRetryCount(mRetryCount + 1);
} }
// Update server policy data
Map<String, String> extras = decodeExtras(rawData);
if (response == Policy.LICENSED) { if (response == Policy.LICENSED) {
// Update server policy data
Map<String, String> extras = decodeExtras(rawData.extra);
mLastResponse = response; mLastResponse = response;
// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
setLicensingUrl(null);
setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE)); setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE));
Set<String> keys = extras.keySet(); Set<String> keys = extras.keySet();
for (String key : keys) { for (String key : keys) {
@ -166,12 +159,10 @@ public class APKExpansionPolicy implements Policy {
} }
} }
} else if (response == Policy.NOT_LICENSED) { } else if (response == Policy.NOT_LICENSED) {
// Clear out stale retry params // Clear out stale policy data
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
setRetryUntil(DEFAULT_RETRY_UNTIL); setRetryUntil(DEFAULT_RETRY_UNTIL);
setMaxRetries(DEFAULT_MAX_RETRIES); setMaxRetries(DEFAULT_MAX_RETRIES);
// Update the licensing URL
setLicensingUrl(extras.get("LU"));
} }
setLastResponse(response); setLastResponse(response);
@ -284,20 +275,6 @@ public class APKExpansionPolicy implements Policy {
return mMaxRetries; return mMaxRetries;
} }
/**
* Set the licensing URL that displays a Play Store UI for the user to regain app access.
*
* @param url the LU string received
*/
private void setLicensingUrl(String url) {
mLicensingUrl = url;
mPreferences.putString(PREF_LICENSING_URL, url);
}
public String getLicensingUrl() {
return mLicensingUrl;
}
/** /**
* Gets the count of expansion URLs. Since expansionURLs are not committed * Gets the count of expansion URLs. Since expansionURLs are not committed
* to preferences, this will return zero if there has been no LVL fetch * to preferences, this will return zero if there has been no LVL fetch
@ -395,15 +372,10 @@ public class APKExpansionPolicy implements Policy {
return false; return false;
} }
private Map<String, String> decodeExtras( private Map<String, String> decodeExtras(String extras) {
com.google.android.vending.licensing.ResponseData rawData) {
Map<String, String> results = new HashMap<String, String>(); Map<String, String> results = new HashMap<String, String>();
if (rawData == null) {
return results;
}
try { try {
URI rawExtras = new URI("?" + rawData.extra); URI rawExtras = new URI("?" + extras);
URIQueryDecoder.DecodeQuery(rawExtras, results); URIQueryDecoder.DecodeQuery(rawExtras, results);
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
Log.w(TAG, "Invalid syntax error while decoding extras data from server."); Log.w(TAG, "Invalid syntax error while decoding extras data from server.");

View File

@ -21,7 +21,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.IBinder; import android.os.IBinder;
@ -196,20 +195,6 @@ public class LicenseChecker implements ServiceConnection {
} }
} }
/**
* Triggers the last deep link licensing URL returned from the server, which redirects users to a
* page which enables them to gain access to the app. If no such URL is returned by the server, it
* will go to the details page of the app in the Play Store.
*/
public void followLastLicensingUrl(Context context) {
String licensingUrl = mPolicy.getLicensingUrl();
if (licensingUrl == null) {
licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName();
}
Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl));
context.startActivity(marketIntent);
}
private void runChecks() { private void runChecks() {
LicenseValidator validator; LicenseValidator validator;
while ((validator = mPendingChecks.poll()) != null) { while ((validator = mPendingChecks.poll()) != null) {

View File

@ -39,9 +39,9 @@ public interface Obfuscator {
/** /**
* Undo the transformation applied to data by the obfuscate() method. * Undo the transformation applied to data by the obfuscate() method.
* *
* @param obfuscated The data that is to be un-obfuscated. * @param original The data that is to be obfuscated.
* @param key The key for the data that is to be un-obfuscated. * @param key The key for the data that is to be obfuscated.
* @return The original data transformed by the obfuscate() method. * @return A transformed version of the original data.
* @throws ValidationException Optionally thrown if a data integrity check fails. * @throws ValidationException Optionally thrown if a data integrity check fails.
*/ */
String unobfuscate(String obfuscated, String key) throws ValidationException; String unobfuscate(String obfuscated, String key) throws ValidationException;

View File

@ -56,10 +56,4 @@ public interface Policy {
* Check if the user should be allowed access to the application. * Check if the user should be allowed access to the application.
*/ */
boolean allowAccess(); boolean allowAccess();
/**
* Gets the licensing URL returned by the server that can enable access for unlicensed apps (e.g.
* buy app on the Play Store).
*/
String getLicensingUrl();
} }

View File

@ -30,8 +30,8 @@ import com.google.android.vending.licensing.util.URIQueryDecoder;
/** /**
* Default policy. All policy decisions are based off of response data received * Default policy. All policy decisions are based off of response data received
* from the licensing service. Specifically, the licensing server sends the * from the licensing service. Specifically, the licensing server sends the
* following information: response validity period, error retry period, * following information: response validity period, error retry period, and
* error retry count and a URL for restoring app access in unlicensed cases. * error retry count.
* <p> * <p>
* These values will vary based on the the way the application is configured in * These values will vary based on the the way the application is configured in
* the Google Play publishing console, such as whether the application is * the Google Play publishing console, such as whether the application is
@ -50,7 +50,6 @@ public class ServerManagedPolicy implements Policy {
private static final String PREF_RETRY_UNTIL = "retryUntil"; private static final String PREF_RETRY_UNTIL = "retryUntil";
private static final String PREF_MAX_RETRIES = "maxRetries"; private static final String PREF_MAX_RETRIES = "maxRetries";
private static final String PREF_RETRY_COUNT = "retryCount"; private static final String PREF_RETRY_COUNT = "retryCount";
private static final String PREF_LICENSING_URL = "licensingUrl";
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
private static final String DEFAULT_RETRY_UNTIL = "0"; private static final String DEFAULT_RETRY_UNTIL = "0";
private static final String DEFAULT_MAX_RETRIES = "0"; private static final String DEFAULT_MAX_RETRIES = "0";
@ -64,7 +63,6 @@ public class ServerManagedPolicy implements Policy {
private long mRetryCount; private long mRetryCount;
private long mLastResponseTime = 0; private long mLastResponseTime = 0;
private int mLastResponse; private int mLastResponse;
private String mLicensingUrl;
private PreferenceObfuscator mPreferences; private PreferenceObfuscator mPreferences;
/** /**
@ -82,7 +80,6 @@ public class ServerManagedPolicy implements Policy {
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
} }
/** /**
@ -91,12 +88,10 @@ public class ServerManagedPolicy implements Policy {
* This data will be used for computing future policy decisions. The * This data will be used for computing future policy decisions. The
* following parameters are processed: * following parameters are processed:
* <ul> * <ul>
* <li>VT: the timestamp that the client should consider the response valid * <li>VT: the timestamp that the client should consider the response
* until * valid until
* <li>GT: the timestamp that the client should ignore retry errors until * <li>GT: the timestamp that the client should ignore retry errors until
* <li>GR: the number of retry errors that the client should ignore * <li>GR: the number of retry errors that the client should ignore
* <li>LU: a deep link URL that can enable access for unlicensed apps (e.g.
* buy app on the Play Store)
* </ul> * </ul>
* *
* @param response the result from validating the server response * @param response the result from validating the server response
@ -111,22 +106,18 @@ public class ServerManagedPolicy implements Policy {
setRetryCount(mRetryCount + 1); setRetryCount(mRetryCount + 1);
} }
// Update server policy data
Map<String, String> extras = decodeExtras(rawData);
if (response == Policy.LICENSED) { if (response == Policy.LICENSED) {
// Update server policy data
Map<String, String> extras = decodeExtras(rawData.extra);
mLastResponse = response; mLastResponse = response;
// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
setLicensingUrl(null);
setValidityTimestamp(extras.get("VT")); setValidityTimestamp(extras.get("VT"));
setRetryUntil(extras.get("GT")); setRetryUntil(extras.get("GT"));
setMaxRetries(extras.get("GR")); setMaxRetries(extras.get("GR"));
} else if (response == Policy.NOT_LICENSED) { } else if (response == Policy.NOT_LICENSED) {
// Clear out stale retry params // Clear out stale policy data
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
setRetryUntil(DEFAULT_RETRY_UNTIL); setRetryUntil(DEFAULT_RETRY_UNTIL);
setMaxRetries(DEFAULT_MAX_RETRIES); setMaxRetries(DEFAULT_MAX_RETRIES);
// Update the licensing URL
setLicensingUrl(extras.get("LU"));
} }
setLastResponse(response); setLastResponse(response);
@ -239,21 +230,6 @@ public class ServerManagedPolicy implements Policy {
return mMaxRetries; return mMaxRetries;
} }
/**
* Set the license URL value (LU) as received from the server and add to preferences. You must
* manually call PreferenceObfuscator.commit() to commit these changes to disk.
*
* @param url the LU string received
*/
private void setLicensingUrl(String url) {
mLicensingUrl = url;
mPreferences.putString(PREF_LICENSING_URL, url);
}
public String getLicensingUrl() {
return mLicensingUrl;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
@ -281,15 +257,10 @@ public class ServerManagedPolicy implements Policy {
return false; return false;
} }
private Map<String, String> decodeExtras( private Map<String, String> decodeExtras(String extras) {
com.google.android.vending.licensing.ResponseData rawData) {
Map<String, String> results = new HashMap<String, String>(); Map<String, String> results = new HashMap<String, String>();
if (rawData == null) {
return results;
}
try { try {
URI rawExtras = new URI("?" + rawData.extra); URI rawExtras = new URI("?" + extras);
URIQueryDecoder.DecodeQuery(rawExtras, results); URIQueryDecoder.DecodeQuery(rawExtras, results);
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
Log.w(TAG, "Invalid syntax error while decoding extras data from server."); Log.w(TAG, "Invalid syntax error while decoding extras data from server.");

View File

@ -16,13 +16,6 @@
package com.google.android.vending.licensing; package com.google.android.vending.licensing;
import android.util.Log;
import com.google.android.vending.licensing.util.URIQueryDecoder;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
/** /**
* Non-caching policy. All requests will be sent to the licensing service, * Non-caching policy. All requests will be sent to the licensing service,
* and no local caching is performed. * and no local caching is performed.
@ -33,38 +26,28 @@ import java.util.Map;
* weigh the risks of using this Policy over one which implements caching, * weigh the risks of using this Policy over one which implements caching,
* such as ServerManagedPolicy. * such as ServerManagedPolicy.
* <p> * <p>
* Access to the application is only allowed if a LICENSED response is. * Access to the application is only allowed if a LICESNED response is.
* received. All other responses (including RETRY) will deny access. * received. All other responses (including RETRY) will deny access.
*/ */
public class StrictPolicy implements Policy { public class StrictPolicy implements Policy {
private static final String TAG = "StrictPolicy";
private int mLastResponse; private int mLastResponse;
private String mLicensingUrl;
public StrictPolicy() { public StrictPolicy() {
// Set default policy. This will force the application to check the policy on launch. // Set default policy. This will force the application to check the policy on launch.
mLastResponse = Policy.RETRY; mLastResponse = Policy.RETRY;
mLicensingUrl = null;
} }
/** /**
* Process a new response from the license server. Since we aren't * Process a new response from the license server. Since we aren't
* performing any caching, this equates to reading the LicenseResponse. * performing any caching, this equates to reading the LicenseResponse.
* Any cache-related ResponseData is ignored, but the licensing URL * Any ResponseData provided is ignored.
* extra is still extracted in cases where the app is unlicensed.
* *
* @param response the result from validating the server response * @param response the result from validating the server response
* @param rawData the raw server response data * @param rawData the raw server response data
*/ */
public void processServerResponse(int response, ResponseData rawData) { public void processServerResponse(int response, ResponseData rawData) {
mLastResponse = response; mLastResponse = response;
if (response == Policy.NOT_LICENSED) {
Map<String, String> extras = decodeExtras(rawData);
mLicensingUrl = extras.get("LU");
}
} }
/** /**
@ -77,24 +60,4 @@ public class StrictPolicy implements Policy {
return (mLastResponse == Policy.LICENSED); return (mLastResponse == Policy.LICENSED);
} }
public String getLicensingUrl() {
return mLicensingUrl;
}
private Map<String, String> decodeExtras(
com.google.android.vending.licensing.ResponseData rawData) {
Map<String, String> results = new HashMap<String, String>();
if (rawData == null) {
return results;
}
try {
URI rawExtras = new URI("?" + rawData.extra);
URIQueryDecoder.DecodeQuery(rawExtras, results);
} catch (URISyntaxException e) {
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
}
return results;
}
} }

View File

@ -64,8 +64,8 @@
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices --> <!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
<service android:name="com.falsinsoft.QtAndroidTools.ApkExpansionDownloaderService"/> <service android:name="com.google.android.vending.expansion.downloader.impl.DownloaderService" android:enabled="true"/>
<receiver android:name="com.falsinsoft.QtAndroidTools.ApkExpansionDownloaderAlarmReceiver"/> <receiver android:name="com.google.android.vending.expansion.downloader.impl.DownloaderService$AlarmReceiver" android:enabled="true"/>
</application> </application>