From 57dd8c37300046c1ce568181594a7a6eb8a4e8af Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 23 Jan 2017 16:03:05 -0800 Subject: [PATCH 001/142] Start version 0.5.1 --- versions.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/versions.gradle b/versions.gradle index 55d08190f1..cb86b8576f 100644 --- a/versions.gradle +++ b/versions.gradle @@ -1,8 +1,8 @@ // Version constants ext { - versionCode = 13 - versionName = '0.5.0' + versionCode = 14 + versionName = '0.5.1' minSdkVersion = 15 targetSdkVersion = 25 compileSdkVersion = 25 From 8309bf3278e407d103f0c345711fa2ce15aa8453 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 24 Jan 2017 18:16:41 -0800 Subject: [PATCH 002/142] In app update work in progress --- apps/sasquatch/build.gradle | 1 + .../sasquatch/activities/MainActivity.java | 5 +- sdk/mobile-center-analytics/build.gradle | 3 - sdk/mobile-center-crashes/build.gradle | 3 - sdk/mobile-center-updates/build.gradle | 6 + sdk/mobile-center-updates/proguard-rules.pro | 4 + .../src/main/AndroidManifest.xml | 22 +++ .../mobile/updates/LoginCallbackActivity.java | 27 +++ .../azure/mobile/updates/Updates.java | 157 ++++++++++++++++++ sdk/mobile-center/build.gradle | 3 - .../mobile/AbstractMobileCenterService.java | 31 ++-- settings.gradle | 1 + versions.gradle | 2 +- 13 files changed, 238 insertions(+), 27 deletions(-) create mode 100644 sdk/mobile-center-updates/build.gradle create mode 100644 sdk/mobile-center-updates/proguard-rules.pro create mode 100644 sdk/mobile-center-updates/src/main/AndroidManifest.xml create mode 100644 sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java create mode 100644 sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java diff --git a/apps/sasquatch/build.gradle b/apps/sasquatch/build.gradle index ece33e37ba..8c4bb7e06c 100644 --- a/apps/sasquatch/build.gradle +++ b/apps/sasquatch/build.gradle @@ -28,4 +28,5 @@ dependencies { projectDependencyCompile project(':sdk:mobile-center-crashes') jcenterDependencyCompile "com.microsoft.azure.mobile:mobile-center-analytics:${version}" jcenterDependencyCompile "com.microsoft.azure.mobile:mobile-center-crashes:${version}" + compile project(':sdk:mobile-center-updates') } \ No newline at end of file diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index f6e3ed1e91..70e69c36da 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -25,14 +25,15 @@ import com.microsoft.azure.mobile.sasquatch.R; import com.microsoft.azure.mobile.sasquatch.features.TestFeatures; import com.microsoft.azure.mobile.sasquatch.features.TestFeaturesListAdapter; +import com.microsoft.azure.mobile.updates.Updates; public class MainActivity extends AppCompatActivity { - private static final String LOG_TAG = "MobileCenterSasquatch"; static final String APP_SECRET = "45d1d9f6-2492-4e68-bd44-7190351eb5f3"; static final String APP_SECRET_KEY = "appSecret"; static final String SERVER_URL_KEY = "serverUrl"; + private static final String LOG_TAG = "MobileCenterSasquatch"; static SharedPreferences sSharedPreferences; @Override @@ -49,7 +50,7 @@ protected void onCreate(Bundle savedInstanceState) { } MobileCenter.setLogLevel(Log.VERBOSE); Crashes.setListener(getCrashesListener()); - MobileCenter.start(getApplication(), getAppSecret(), Analytics.class, Crashes.class); + MobileCenter.start(getApplication(), getAppSecret(), Analytics.class, Crashes.class, Updates.class); Log.i(LOG_TAG, "Crashes.hasCrashedInLastSession=" + Crashes.hasCrashedInLastSession()); Crashes.getLastSessionCrashReport(new ResultCallback() { diff --git a/sdk/mobile-center-analytics/build.gradle b/sdk/mobile-center-analytics/build.gradle index 6a0d77d284..10ffcef4cb 100644 --- a/sdk/mobile-center-analytics/build.gradle +++ b/sdk/mobile-center-analytics/build.gradle @@ -1,6 +1,3 @@ -// -// :sdk:analytics -// description = 'This package contains functionalities to collect session, device properties, events etc... for your application.' evaluationDependsOn(':sdk') diff --git a/sdk/mobile-center-crashes/build.gradle b/sdk/mobile-center-crashes/build.gradle index 2779f5c5a1..6d09a0bb6e 100644 --- a/sdk/mobile-center-crashes/build.gradle +++ b/sdk/mobile-center-crashes/build.gradle @@ -1,6 +1,3 @@ -// -// :sdk:crashes -// project.description = 'This package contains functionalities to collect crash reports for your application.' evaluationDependsOn(':sdk') diff --git a/sdk/mobile-center-updates/build.gradle b/sdk/mobile-center-updates/build.gradle new file mode 100644 index 0000000000..6576612646 --- /dev/null +++ b/sdk/mobile-center-updates/build.gradle @@ -0,0 +1,6 @@ +description = 'This package contains functionalities to get in app updates for your application.' +evaluationDependsOn(':sdk') + +dependencies { + compile project(':sdk:mobile-center') +} \ No newline at end of file diff --git a/sdk/mobile-center-updates/proguard-rules.pro b/sdk/mobile-center-updates/proguard-rules.pro new file mode 100644 index 0000000000..7f265ec541 --- /dev/null +++ b/sdk/mobile-center-updates/proguard-rules.pro @@ -0,0 +1,4 @@ +# The following options are set by default. +# Make sure they are always set, even if the default proguard config changes. +-dontskipnonpubliclibraryclasses +-verbose \ No newline at end of file diff --git a/sdk/mobile-center-updates/src/main/AndroidManifest.xml b/sdk/mobile-center-updates/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8da44ee87d --- /dev/null +++ b/sdk/mobile-center-updates/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java new file mode 100644 index 0000000000..9ddfbaa67d --- /dev/null +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java @@ -0,0 +1,27 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import com.microsoft.azure.mobile.utils.MobileCenterLog; + +import static com.microsoft.azure.mobile.updates.Updates.LOG_TAG; + +public class LoginCallbackActivity extends Activity { + + private static final String EXTRA_COOKIE = "cookie"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + String cookie = intent.getStringExtra(EXTRA_COOKIE); + MobileCenterLog.debug(LOG_TAG, "LoginCallbackActivity.getIntent()=" + intent); + MobileCenterLog.verbose(LOG_TAG, "LoginCallbackActivity.getIntent()#S.cookie=" + cookie); + if (isTaskRoot()) { + startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName())); + } + finish(); + } +} diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java new file mode 100644 index 0000000000..fc980b0840 --- /dev/null +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -0,0 +1,157 @@ +package com.microsoft.azure.mobile.updates; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; + +import com.microsoft.azure.mobile.AbstractMobileCenterService; +import com.microsoft.azure.mobile.MobileCenter; +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.utils.MobileCenterLog; + +import java.util.List; + +public class Updates extends AbstractMobileCenterService { + + static final String LOG_TAG = MobileCenter.LOG_TAG + "Updates"; + + private static final String GOOGLE_CHROME_URL_SCHEME = "googlechrome://navigate?url="; + + private static final String GENERIC_BROWSER_URL_SCHEME = "http://"; + + private static final String DEFAULT_LOGIN_PAGE_URL = "10.123.212.163:8080/default.htm"; + + /** + * Shared instance. + */ + @SuppressLint("StaticFieldLeak") + private static Updates sInstance = null; + + private Activity mForegroundActivity; + + private boolean mLoginChecked; + + /** + * Get shared instance. + * + * @return shared instance. + */ + public static synchronized Updates getInstance() { + if (sInstance == null) { + sInstance = new Updates(); + } + return sInstance; + } + + @VisibleForTesting + static synchronized void unsetInstance() { + sInstance = null; + } + + @Override + protected String getGroupName() { + return null; + } + + @Override + protected String getServiceName() { + return "Updates"; + } + + @Override + protected String getLoggerTag() { + return LOG_TAG; + } + + @Override + public synchronized void onChannelReady(@NonNull Context context, @NonNull Channel channel) { + super.onChannelReady(context, channel); + checkLogin(); + } + + @Override + public void onActivityResumed(Activity activity) { + mForegroundActivity = activity; + checkLogin(); + } + + @Override + public void onActivityPaused(Activity activity) { + mForegroundActivity = null; + } + + private void checkLogin() { + if (mForegroundActivity != null && !mLoginChecked) { + String baseUrl = DEFAULT_LOGIN_PAGE_URL + "?package=" + mForegroundActivity.getPackageName(); + Intent intent = new Intent(Intent.ACTION_VIEW); + + /* Try to force using Chrome first, we want fall back url support for intent. */ + try { + intent.setData(Uri.parse(GOOGLE_CHROME_URL_SCHEME + baseUrl)); + mForegroundActivity.startActivity(intent); + } catch (ActivityNotFoundException e) { + + /* Fall back using a browser but we don't want a chooser U.I. to pop. */ + MobileCenterLog.debug(LOG_TAG, "Google Chrome not found, pick another one."); + intent.setData(Uri.parse(GENERIC_BROWSER_URL_SCHEME + baseUrl)); + List browsers = mForegroundActivity.getPackageManager().queryIntentActivities(intent, 0); + if (browsers.isEmpty()) { + MobileCenterLog.error(LOG_TAG, "No browser found on device, abort login."); + } else { + + /* + * Check the default browser is not the picker, + * last thing we want is app to start and suddenly asks user to pick + * between 2 browsers without explaining why. + */ + String defaultBrowserPackageName = null; + String defaultBrowserClassName = null; + ResolveInfo defaultBrowser = mForegroundActivity.getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (defaultBrowser != null) { + ActivityInfo activityInfo = defaultBrowser.activityInfo; + defaultBrowserPackageName = activityInfo.packageName; + defaultBrowserClassName = activityInfo.name; + MobileCenterLog.debug(LOG_TAG, "Default browser seems to be " + defaultBrowserPackageName + "/" + defaultBrowserClassName); + } + String selectedPackageName = null; + String selectedClassName = null; + for (ResolveInfo browser : browsers) { + ActivityInfo activityInfo = browser.activityInfo; + if (activityInfo.packageName.equals(defaultBrowserPackageName) && activityInfo.name.equals(defaultBrowserClassName)) { + selectedPackageName = defaultBrowserPackageName; + selectedClassName = defaultBrowserClassName; + MobileCenterLog.debug(LOG_TAG, "And its not the picker."); + break; + } + } + if (defaultBrowser != null && selectedPackageName == null) { + MobileCenterLog.debug(LOG_TAG, "Default browser is actually a picker..."); + } + + /* If no default browser found, pick first one we can find. */ + if (selectedPackageName == null) { + MobileCenterLog.debug(LOG_TAG, "Picking first browser in list."); + ResolveInfo browser = browsers.iterator().next(); + ActivityInfo activityInfo = browser.activityInfo; + selectedPackageName = activityInfo.packageName; + selectedClassName = activityInfo.name; + } + + /* Launch generic browser. */ + MobileCenterLog.debug(LOG_TAG, "Launch browser=" + selectedPackageName + "/" + selectedClassName); + intent.setClassName(selectedPackageName, selectedClassName); + mForegroundActivity.startActivity(intent); + } + } + mLoginChecked = true; + } + } +} diff --git a/sdk/mobile-center/build.gradle b/sdk/mobile-center/build.gradle index 43cf57b21d..9ffe102404 100644 --- a/sdk/mobile-center/build.gradle +++ b/sdk/mobile-center/build.gradle @@ -1,5 +1,2 @@ -// -// :sdk:mobilecenter -// description = 'This package contains the basic functionalities that all Mobile Center services use to communicate with the backend.' evaluationDependsOn(':sdk') \ No newline at end of file diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java index 32356d21eb..7182cd863e 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java @@ -90,38 +90,39 @@ else if (enabled == isInstanceEnabled()) { } /* If channel initialized. */ - if (mChannel != null) { + String groupName = getGroupName(); + if (groupName != null && mChannel != null) { /* Register service to channel on enabling. */ if (enabled) - mChannel.addGroup(getGroupName(), getTriggerCount(), getTriggerInterval(), getTriggerMaxParallelRequests(), getChannelListener()); + mChannel.addGroup(groupName, getTriggerCount(), getTriggerInterval(), getTriggerMaxParallelRequests(), getChannelListener()); /* Otherwise, clear all persisted logs and remove a group for the service. */ else { - /* TODO: Expose a method and do this in one place. */ - mChannel.clear(getGroupName()); - mChannel.removeGroup(getGroupName()); + mChannel.clear(groupName); + mChannel.removeGroup(groupName); } } /* Save new state. */ StorageHelper.PreferencesStorage.putBoolean(getEnabledPreferenceKey(), enabled); - MobileCenterLog.info(getLoggerTag(), String.format("%s service has been %s.", getServiceName(), enabled ? "enabled" : "disabled")); } @Override public synchronized void onChannelReady(@NonNull Context context, @NonNull Channel channel) { - channel.removeGroup(getGroupName()); - - /* Add a group to the channel if the service is enabled */ - if (isInstanceEnabled()) - channel.addGroup(getGroupName(), getTriggerCount(), getTriggerInterval(), getTriggerMaxParallelRequests(), getChannelListener()); + String groupName = getGroupName(); + if (groupName != null) { + channel.removeGroup(groupName); - /* Otherwise, clear all persisted logs for the service. */ - else - channel.clear(getGroupName()); + /* Add a group to the channel if the service is enabled */ + if (isInstanceEnabled()) + channel.addGroup(groupName, getTriggerCount(), getTriggerInterval(), getTriggerMaxParallelRequests(), getChannelListener()); + /* Otherwise, clear all persisted logs for the service. */ + else + channel.clear(groupName); + } mChannel = channel; } @@ -154,7 +155,7 @@ public Map getLogFactories() { @SuppressWarnings("WeakerAccess") @NonNull protected String getEnabledPreferenceKey() { - return KEY_ENABLED + PREFERENCE_KEY_SEPARATOR + getGroupName(); + return KEY_ENABLED + PREFERENCE_KEY_SEPARATOR + getServiceName(); } /** diff --git a/settings.gradle b/settings.gradle index 6e610e848e..ae23b1fc29 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ include ':sdk' include ':sdk:mobile-center' include ':sdk:mobile-center-crashes' include ':sdk:mobile-center-analytics' +include ':sdk:mobile-center-updates' // common test code include ':test' diff --git a/versions.gradle b/versions.gradle index cb86b8576f..e7df4f1342 100644 --- a/versions.gradle +++ b/versions.gradle @@ -2,7 +2,7 @@ ext { versionCode = 14 - versionName = '0.5.1' + versionName = '0.6.0' minSdkVersion = 15 targetSdkVersion = 25 compileSdkVersion = 25 From 8b32e81622b0597342cb899050f0336d20391e68 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 26 Jan 2017 17:01:25 -0800 Subject: [PATCH 003/142] Store token and don't launch browser if we already stored it --- .../mobile/updates/LoginCallbackActivity.java | 22 ++++++-- .../azure/mobile/updates/Updates.java | 52 +++++++++++++++---- .../mobile/utils/storage/StorageHelper.java | 6 ++- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java index 9ddfbaa67d..7696550e91 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java @@ -6,19 +6,31 @@ import com.microsoft.azure.mobile.utils.MobileCenterLog; +import static com.microsoft.azure.mobile.updates.Updates.EXTRA_UPDATE_TOKEN; import static com.microsoft.azure.mobile.updates.Updates.LOG_TAG; public class LoginCallbackActivity extends Activity { - private static final String EXTRA_COOKIE = "cookie"; - @Override public void onCreate(Bundle savedInstanceState) { + + /* + * Get update token from intent. + * TODO protect intent: verifying signature with server public key in another field seems like a good way. + * But it would not protect against spamming intents to cause app to use CPU to verify fake signatures. + */ super.onCreate(savedInstanceState); Intent intent = getIntent(); - String cookie = intent.getStringExtra(EXTRA_COOKIE); - MobileCenterLog.debug(LOG_TAG, "LoginCallbackActivity.getIntent()=" + intent); - MobileCenterLog.verbose(LOG_TAG, "LoginCallbackActivity.getIntent()#S.cookie=" + cookie); + String updateToken = intent.getStringExtra(EXTRA_UPDATE_TOKEN); + MobileCenterLog.debug(LOG_TAG, getLocalClassName() + ".getIntent()=" + intent); + MobileCenterLog.verbose(LOG_TAG, getLocalClassName() + ".getIntent()#S.update_token=" + updateToken); + + /* Store update token. */ + if (updateToken != null) { + Updates.getInstance().storeUpdateToken(this, updateToken); + } + + /* Resume app to avoid staying on browser if no application task. */ if (isTaskRoot()) { startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName())); } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index fc980b0840..eea76d3599 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -16,18 +16,20 @@ import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.storage.StorageHelper; import java.util.List; public class Updates extends AbstractMobileCenterService { - static final String LOG_TAG = MobileCenter.LOG_TAG + "Updates"; - + static final String EXTRA_UPDATE_TOKEN = "update_token"; + private static final String SERVICE_NAME = "Updates"; + static final String LOG_TAG = MobileCenter.LOG_TAG + SERVICE_NAME; private static final String GOOGLE_CHROME_URL_SCHEME = "googlechrome://navigate?url="; - private static final String GENERIC_BROWSER_URL_SCHEME = "http://"; - private static final String DEFAULT_LOGIN_PAGE_URL = "10.123.212.163:8080/default.htm"; + private static final String PREFERENCE_PREFIX = SERVICE_NAME + "."; + private static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; /** * Shared instance. @@ -39,11 +41,14 @@ public class Updates extends AbstractMobileCenterService { private boolean mLoginChecked; + private boolean mUpdateChecked; + /** * Get shared instance. * * @return shared instance. */ + @SuppressWarnings("WeakerAccess") public static synchronized Updates getInstance() { if (sInstance == null) { sInstance = new Updates(); @@ -63,7 +68,7 @@ protected String getGroupName() { @Override protected String getServiceName() { - return "Updates"; + return SERVICE_NAME; } @Override @@ -74,13 +79,13 @@ protected String getLoggerTag() { @Override public synchronized void onChannelReady(@NonNull Context context, @NonNull Channel channel) { super.onChannelReady(context, channel); - checkLogin(); + checkAndFetchUpdateToken(); } @Override public void onActivityResumed(Activity activity) { mForegroundActivity = activity; - checkLogin(); + checkAndFetchUpdateToken(); } @Override @@ -88,8 +93,15 @@ public void onActivityPaused(Activity activity) { mForegroundActivity = null; } - private void checkLogin() { - if (mForegroundActivity != null && !mLoginChecked) { + private void checkAndFetchUpdateToken() { + if (mForegroundActivity != null && !mLoginChecked && !mUpdateChecked) { + + String updateToken = StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN); + if (updateToken != null && !mUpdateChecked) { + checkUpdate(updateToken); + return; + } + String baseUrl = DEFAULT_LOGIN_PAGE_URL + "?package=" + mForegroundActivity.getPackageName(); Intent intent = new Intent(Intent.ACTION_VIEW); @@ -154,4 +166,26 @@ private void checkLogin() { mLoginChecked = true; } } + + /* + * Store update token and possibly trigger application update check. + * TODO encrypt token, but where to store encryption key? If it's retrieved from server, + * how do we protect server call to get the key in the first place? + * Even having the encryption key temporarily in memory is risky as that can be heap dumped. + */ + void storeUpdateToken(@NonNull Context context, @NonNull String updateToken) { + if (mChannel == null) { + StorageHelper.initialize(context); + } + StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); + if (!mUpdateChecked) { + checkUpdate(updateToken); + } + } + + private void checkUpdate(@NonNull String updateToken) { + + /* TODO API call. */ + MobileCenterLog.error(LOG_TAG, "Update check not yet implemented."); + } } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/storage/StorageHelper.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/storage/StorageHelper.java index 2f0fc9399b..2ef516283c 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/storage/StorageHelper.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/storage/StorageHelper.java @@ -57,8 +57,10 @@ public class StorageHelper { * @param context The context of the application. */ public static void initialize(Context context) { - sContext = context; - sSharedPreferences = sContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); + if (sContext == null) { + sContext = context; + sSharedPreferences = sContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); + } } /** From da181ae1737eaaa3f9dabb09a220517c0d050d03 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 26 Jan 2017 19:21:48 -0800 Subject: [PATCH 004/142] Fix resuming app after login on emulator --- .../mobile/updates/LoginCallbackActivity.java | 27 +++++++++++++++---- .../azure/mobile/updates/Updates.java | 4 +++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java index 7696550e91..e49dc7764c 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java @@ -22,18 +22,35 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); String updateToken = intent.getStringExtra(EXTRA_UPDATE_TOKEN); - MobileCenterLog.debug(LOG_TAG, getLocalClassName() + ".getIntent()=" + intent); - MobileCenterLog.verbose(LOG_TAG, getLocalClassName() + ".getIntent()#S.update_token=" + updateToken); + MobileCenterLog.logAssert(LOG_TAG, getLocalClassName() + ".getIntent()=" + intent); + MobileCenterLog.logAssert(LOG_TAG, getLocalClassName() + ".getIntent()#S.update_token=" + updateToken); /* Store update token. */ if (updateToken != null) { Updates.getInstance().storeUpdateToken(this, updateToken); } - /* Resume app to avoid staying on browser if no application task. */ - if (isTaskRoot()) { + /* + * Resume app exactly where it was before with no activity duplicate, or starting the + * launcher if application task finished or killed (equivalent to clicking from launcher + * or activity history). + * + * The browser used in emulator don't set the NEW_TASK flag and when we receive the intent, + * isTaskRoot returns false even after API level 19 while application task was empty, + * none of the intent flags combination seems to do what we want in that case. + * + * So we restart the activity with the correct flag this time as Chrome would do + * and retry the isTaskRoot code and it will work correctly the second time... + * + * Also tried various finish() moveTaskToBack(true or false) combinations with no luck, + * only the following code seems to work. + */ + finish(); + if (!((getIntent().getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == Intent.FLAG_ACTIVITY_NEW_TASK)) { + MobileCenterLog.debug(LOG_TAG, "Using restart work around to correctly resume app."); + startActivity(intent.cloneFilter().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } else if (isTaskRoot()) { startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName())); } - finish(); } } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index eea76d3599..2a8e308d70 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -26,9 +26,13 @@ public class Updates extends AbstractMobileCenterService { private static final String SERVICE_NAME = "Updates"; static final String LOG_TAG = MobileCenter.LOG_TAG + SERVICE_NAME; private static final String GOOGLE_CHROME_URL_SCHEME = "googlechrome://navigate?url="; + /** + * TODO change to https once we have a real server. + */ private static final String GENERIC_BROWSER_URL_SCHEME = "http://"; private static final String DEFAULT_LOGIN_PAGE_URL = "10.123.212.163:8080/default.htm"; private static final String PREFERENCE_PREFIX = SERVICE_NAME + "."; + private static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; /** From b1898c91e26b0a63ed6702b3bdb2366676339e70 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 26 Jan 2017 19:24:03 -0800 Subject: [PATCH 005/142] Change log levels --- .../microsoft/azure/mobile/updates/LoginCallbackActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java index e49dc7764c..29bb324e3e 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java @@ -22,8 +22,8 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); String updateToken = intent.getStringExtra(EXTRA_UPDATE_TOKEN); - MobileCenterLog.logAssert(LOG_TAG, getLocalClassName() + ".getIntent()=" + intent); - MobileCenterLog.logAssert(LOG_TAG, getLocalClassName() + ".getIntent()#S.update_token=" + updateToken); + MobileCenterLog.debug(LOG_TAG, getLocalClassName() + ".getIntent()=" + intent); + MobileCenterLog.verbose(LOG_TAG, getLocalClassName() + ".getIntent()#S.update_token=" + updateToken); /* Store update token. */ if (updateToken != null) { From 675eafa82c8173668192cc6a035a281e71cb1a95 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 27 Jan 2017 19:52:20 -0800 Subject: [PATCH 006/142] Split logic between ingestion and http To be able to call other APIs and use the same retry/network logic. --- .../azure/mobile/analytics/AnalyticsTest.java | 2 +- .../azure/mobile/crashes/CrashesTest.java | 2 +- .../crashes/UncaughtExceptionHandlerTest.java | 2 +- .../LoginCallbackActivityAndroidTest.java | 10 + .../azure/mobile/updates/UpdatesTest.java | 10 + .../http/HttpUtilsAndroidTest.java | 4 +- .../azure/mobile/channel/DefaultChannel.java | 22 +- .../azure/mobile/http/DefaultHttpClient.java | 240 +++++++++++++ .../azure/mobile/http/HttpClient.java | 15 + .../mobile/http/HttpClientCallDecorator.java | 56 ++++ .../mobile/http/HttpClientDecorator.java | 17 + .../HttpClientNetworkStateHandler.java} | 34 +- .../HttpClientRetryer.java} | 25 +- .../{ingestion => }/http/HttpException.java | 2 +- .../{ingestion => }/http/HttpUtils.java | 2 +- .../{ingestion => http}/ServiceCall.java | 2 +- .../{ingestion => http}/ServiceCallback.java | 4 +- .../azure/mobile/ingestion/Ingestion.java | 6 +- .../azure/mobile/ingestion/IngestionHttp.java | 140 ++++++++ .../http/IngestionCallDecorator.java | 67 ---- .../ingestion/http/IngestionDecorator.java | 24 -- .../mobile/ingestion/http/IngestionHttp.java | 314 ------------------ .../AbstractMobileCenterServiceTest.java | 2 +- .../channel/AbstractDefaultChannelTest.java | 4 +- .../DefaultChannelRaceConditionTest.java | 9 +- .../mobile/channel/DefaultChannelTest.java | 16 +- .../DefaultHttpClientTest.java} | 187 +++++------ .../HttpClientNetworkStateHandlerTest.java} | 180 +++++----- .../HttpClientRetryerTest.java} | 80 ++--- .../http/HttpExceptionTest.java | 2 +- .../mobile/ingestion/IngestionHttpTest.java | 173 ++++++++++ 31 files changed, 932 insertions(+), 721 deletions(-) create mode 100644 sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/LoginCallbackActivityAndroidTest.java create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java rename sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/{ingestion => }/http/HttpUtilsAndroidTest.java (95%) create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/DefaultHttpClient.java create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClient.java create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientCallDecorator.java create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientDecorator.java rename sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/{ingestion/http/IngestionNetworkStateHandler.java => http/HttpClientNetworkStateHandler.java} (61%) rename sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/{ingestion/http/IngestionRetryer.java => http/HttpClientRetryer.java} (72%) rename sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/{ingestion => }/http/HttpException.java (97%) rename sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/{ingestion => }/http/HttpUtils.java (97%) rename sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/{ingestion => http}/ServiceCall.java (69%) rename sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/{ingestion => http}/ServiceCallback.java (81%) create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java delete mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionCallDecorator.java delete mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionDecorator.java delete mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java rename sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/{ingestion/http/IngestionHttpTest.java => http/DefaultHttpClientTest.java} (58%) rename sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/{ingestion/http/IngestionNetworkStateHandlerTest.java => http/HttpClientNetworkStateHandlerTest.java} (62%) rename sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/{ingestion/http/IngestionRetryerTest.java => http/HttpClientRetryerTest.java} (67%) rename sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/{ingestion => }/http/HttpExceptionTest.java (94%) create mode 100644 sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java diff --git a/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java b/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java index 9b37350ea8..6e774d21fe 100644 --- a/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java +++ b/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java @@ -60,7 +60,7 @@ @PrepareForTest({SystemClock.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class}) public class AnalyticsTest { - private static final String ANALYTICS_ENABLED_KEY = KEY_ENABLED + "_group_analytics"; + private static final String ANALYTICS_ENABLED_KEY = KEY_ENABLED + "_Analytics"; @Before public void setUp() { diff --git a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java index d75a04cc08..d013d34d37 100644 --- a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java +++ b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java @@ -81,7 +81,7 @@ public class CrashesTest { @SuppressWarnings("ThrowableInstanceNeverThrown") private static final Exception EXCEPTION = new Exception("This is a test exception."); - private static final String CRASHES_ENABLED_KEY = PrefStorageConstants.KEY_ENABLED + "_" + Crashes.getInstance().getGroupName(); + private static final String CRASHES_ENABLED_KEY = PrefStorageConstants.KEY_ENABLED + "_" + Crashes.getInstance().getServiceName(); @Rule public final TemporaryFolder errorStorageDirectory = new TemporaryFolder(); diff --git a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java index 4294006365..8a381fae0d 100644 --- a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java +++ b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java @@ -50,7 +50,7 @@ @PrepareForTest({SystemClock.class, StorageHelper.PreferencesStorage.class, StorageHelper.InternalStorage.class, Crashes.class, ErrorLogHelper.class, DeviceInfoHelper.class, UncaughtExceptionHandler.ShutdownHelper.class, MobileCenterLog.class, Process.class}) public class UncaughtExceptionHandlerTest { - private static final String CRASHES_ENABLED_KEY = PrefStorageConstants.KEY_ENABLED + "_" + Crashes.getInstance().getGroupName(); + private static final String CRASHES_ENABLED_KEY = PrefStorageConstants.KEY_ENABLED + "_" + Crashes.getInstance().getServiceName(); @Rule public PowerMockRule mPowerMockRule = new PowerMockRule(); diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/LoginCallbackActivityAndroidTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/LoginCallbackActivityAndroidTest.java new file mode 100644 index 0000000000..8c29bae2ff --- /dev/null +++ b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/LoginCallbackActivityAndroidTest.java @@ -0,0 +1,10 @@ +package com.microsoft.azure.mobile.updates; + +import org.junit.Test; + +public class LoginCallbackActivityAndroidTest { + + @Test + public void todo() { + } +} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java new file mode 100644 index 0000000000..5d2eb28fc1 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java @@ -0,0 +1,10 @@ +package com.microsoft.azure.mobile.updates; + +import org.junit.Test; + +public class UpdatesTest { + + @Test + public void todo() { + } +} diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/http/HttpUtilsAndroidTest.java similarity index 95% rename from sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java rename to sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/http/HttpUtilsAndroidTest.java index 2122da8fb3..f8750c8b18 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/http/HttpUtilsAndroidTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.ingestion.http; +package com.microsoft.azure.mobile.http; import org.junit.Test; @@ -15,7 +15,7 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; -import static com.microsoft.azure.mobile.ingestion.http.HttpUtils.isRecoverableError; +import static com.microsoft.azure.mobile.http.HttpUtils.isRecoverableError; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/DefaultChannel.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/DefaultChannel.java index 4ff2655954..9cbb2169b6 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/DefaultChannel.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/DefaultChannel.java @@ -7,12 +7,10 @@ import android.support.annotation.VisibleForTesting; import com.microsoft.azure.mobile.CancellationException; +import com.microsoft.azure.mobile.http.HttpUtils; +import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.ingestion.Ingestion; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.http.HttpUtils; -import com.microsoft.azure.mobile.ingestion.http.IngestionHttp; -import com.microsoft.azure.mobile.ingestion.http.IngestionNetworkStateHandler; -import com.microsoft.azure.mobile.ingestion.http.IngestionRetryer; +import com.microsoft.azure.mobile.ingestion.IngestionHttp; import com.microsoft.azure.mobile.ingestion.models.Device; import com.microsoft.azure.mobile.ingestion.models.Log; import com.microsoft.azure.mobile.ingestion.models.LogContainer; @@ -25,7 +23,6 @@ import com.microsoft.azure.mobile.utils.DeviceInfoHelper; import com.microsoft.azure.mobile.utils.IdHelper; import com.microsoft.azure.mobile.utils.MobileCenterLog; -import com.microsoft.azure.mobile.utils.NetworkStateHelper; import java.io.IOException; import java.util.ArrayList; @@ -123,7 +120,7 @@ public class DefaultChannel implements Channel { * @param logSerializer The log serializer. */ public DefaultChannel(@NonNull Context context, @NonNull String appSecret, @NonNull LogSerializer logSerializer) { - this(context, appSecret, buildDefaultPersistence(logSerializer), buildDefaultIngestion(context, logSerializer)); + this(context, appSecret, buildDefaultPersistence(logSerializer), new IngestionHttp(context, logSerializer)); } /** @@ -147,15 +144,6 @@ public DefaultChannel(@NonNull Context context, @NonNull String appSecret, @NonN mEnabled = true; } - /** - * Init ingestion for default constructor. - */ - private static Ingestion buildDefaultIngestion(@NonNull Context context, @NonNull LogSerializer logSerializer) { - IngestionHttp api = new IngestionHttp(logSerializer); - IngestionRetryer retryer = new IngestionRetryer(api); - return new IngestionNetworkStateHandler(retryer, NetworkStateHelper.getSharedInstance(context)); - } - /** * Init Persistence for default constructor. */ @@ -402,7 +390,7 @@ private synchronized void triggerIngestion(final String batchId, final GroupStat mIngestion.sendAsync(mAppSecret, mInstallId, logContainer, new ServiceCallback() { @Override - public void onCallSucceeded() { + public void onCallSucceeded(String payload) { handleSendingSuccess(groupState, stateSnapshot, batchId); } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/DefaultHttpClient.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/DefaultHttpClient.java new file mode 100644 index 0000000000..9c6df0ce3e --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/DefaultHttpClient.java @@ -0,0 +1,240 @@ +package com.microsoft.azure.mobile.http; + +import android.os.AsyncTask; +import android.support.annotation.VisibleForTesting; + +import com.microsoft.azure.mobile.utils.HandlerUtils; +import com.microsoft.azure.mobile.utils.MobileCenterLog; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.RejectedExecutionException; + +import static android.util.Log.VERBOSE; +import static com.microsoft.azure.mobile.MobileCenter.LOG_TAG; +import static java.lang.Math.max; + +public class DefaultHttpClient implements HttpClient { + + /** + * Application secret HTTP Header. + */ + public static final String APP_SECRET = "App-Secret"; + + public static final String METHOD_GET = "GET"; + + public static final String METHOD_POST = "METHOD_POST"; + + /** + * Content type header value. + */ + private static final String CONTENT_TYPE_VALUE = "application/json"; + + /** + * Default string builder capacity. + */ + private static final int DEFAULT_STRING_BUILDER_CAPACITY = 16; + + /** + * Content type header key. + */ + private static final String CONTENT_TYPE_KEY = "Content-Type"; + + /** + * Character encoding. + */ + private static final String CHARSET_NAME = "UTF-8"; + + /** + * Read buffer size. + */ + private static final int READ_BUFFER_SIZE = 1024; + + /** + * HTTP connection timeout. + */ + private static final int CONNECT_TIMEOUT = 60000; + + /** + * HTTP read timeout. + */ + private static final int READ_TIMEOUT = 20000; + + /** + * Maximum characters to be displayed in a log for application secret. + */ + private static final int MAX_CHARACTERS_DISPLAYED_FOR_APP_SECRET = 8; + + /** + * Dump stream to string. + * + * @param urlConnection URL connection. + * @return dumped string. + * @throws IOException if an error occurred. + */ + private static String dump(HttpURLConnection urlConnection) throws IOException { + + /* + * Though content length header value is less than actual payload length (gzip), we want to init + * buffer with a reasonable start size to optimize (default is 16 and is way too low for this + * use case). + */ + StringBuilder builder = new StringBuilder(max(urlConnection.getContentLength(), DEFAULT_STRING_BUILDER_CAPACITY)); + InputStream stream; + if (urlConnection.getResponseCode() < 400) + stream = urlConnection.getInputStream(); + else + stream = urlConnection.getErrorStream(); + InputStreamReader in = new InputStreamReader(stream, CHARSET_NAME); + char[] buffer = new char[READ_BUFFER_SIZE]; + int len; + while ((len = in.read(buffer)) > 0) + builder.append(buffer, 0, len); + return builder.toString(); + } + + private static String doCall(String urlString, String method, Map headers, CallTemplate callTemplate) throws Exception { + + /* HTTP session. */ + URL url = new URL(urlString); + MobileCenterLog.verbose(LOG_TAG, "Calling " + url + " ..."); + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + try { + + /* Configure connection timeouts. */ + urlConnection.setConnectTimeout(CONNECT_TIMEOUT); + urlConnection.setReadTimeout(READ_TIMEOUT); + + /* Set headers. */ + urlConnection.setRequestProperty(CONTENT_TYPE_KEY, CONTENT_TYPE_VALUE); + for (Map.Entry header : headers.entrySet()) { + urlConnection.setRequestProperty(header.getKey(), header.getValue()); + } + + /* Log headers. */ + if (MobileCenterLog.getLogLevel() <= VERBOSE) { + Map logHeaders = new HashMap<>(headers); + String appSecret = logHeaders.get(APP_SECRET); + if (appSecret != null) { + int hidingEndIndex = appSecret.length() - (appSecret.length() >= MAX_CHARACTERS_DISPLAYED_FOR_APP_SECRET ? MAX_CHARACTERS_DISPLAYED_FOR_APP_SECRET : 0); + char[] fill = new char[hidingEndIndex]; + Arrays.fill(fill, '*'); + appSecret = new String(fill) + appSecret.substring(hidingEndIndex); + logHeaders.put(APP_SECRET, appSecret); + } + MobileCenterLog.verbose(LOG_TAG, "Headers: " + logHeaders); + } + + /* Build payload. */ + if (method.equals(METHOD_POST)) { + String payload = callTemplate.buildRequestBody(); + MobileCenterLog.verbose(LOG_TAG, payload); + + /* Send payload through the wire. */ + byte[] binaryPayload = payload.getBytes(CHARSET_NAME); + urlConnection.setDoOutput(true); + urlConnection.setFixedLengthStreamingMode(binaryPayload.length); + OutputStream out = urlConnection.getOutputStream(); + out.write(binaryPayload); + out.close(); + } + + /* Read response. */ + int status = urlConnection.getResponseCode(); + String response = dump(urlConnection); + MobileCenterLog.verbose(LOG_TAG, "HTTP response status=" + status + " payload=" + response); + + /* Generate exception on failure. */ + if (status != 200) + throw new HttpException(status, response); + return response; + } finally { + + /* Release connection. */ + urlConnection.disconnect(); + } + } + + @Override + public ServiceCall callAsync(String url, String method, Map headers, CallTemplate callTemplate, final ServiceCallback serviceCallback) { + final Call call = new Call(url, method, headers, callTemplate, serviceCallback); + try { + call.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (final RejectedExecutionException e) { + + /* + * When executor saturated (shared with app), we should use the retry mechanism + * rather than creating more threads to avoid putting too much pressure on the hosting app. + * Also we need to return the method before calling the listener, + * so we post the callback on handler to make sure of that. + */ + HandlerUtils.runOnUiThread(new Runnable() { + + @Override + public void run() { + serviceCallback.onCallFailed(e); + } + }); + } + return new ServiceCall() { + + @Override + public void cancel() { + if (!call.isCancelled()) + call.cancel(true); + } + }; + } + + @Override + public void close() throws IOException { + + /* No-op. A decorator can take care of tracking calls to cancel. */ + } + + @VisibleForTesting + static class Call extends AsyncTask { + + private final String mUrl; + + private final String mMethod; + + private final Map mHeaders; + + private final CallTemplate mCallTemplate; + + private final ServiceCallback mServiceCallback; + + public Call(String url, String method, Map headers, CallTemplate callTemplate, ServiceCallback serviceCallback) { + mUrl = url; + mMethod = method; + mHeaders = headers; + mCallTemplate = callTemplate; + mServiceCallback = serviceCallback; + } + + @Override + protected Object doInBackground(Void... params) { + try { + return doCall(mUrl, mMethod, mHeaders, mCallTemplate); + } catch (Exception e) { + return e; + } + } + + @Override + protected void onPostExecute(Object result) { + if (result instanceof Exception) + mServiceCallback.onCallFailed((Exception) result); + else + mServiceCallback.onCallSucceeded(result.toString()); + } + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClient.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClient.java new file mode 100644 index 0000000000..d33a206a16 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClient.java @@ -0,0 +1,15 @@ +package com.microsoft.azure.mobile.http; + +import org.json.JSONException; + +import java.io.Closeable; +import java.util.Map; + +public interface HttpClient extends Closeable { + + ServiceCall callAsync(String url, String method, Map headers, CallTemplate callTemplate, ServiceCallback serviceCallback); + + interface CallTemplate { + String buildRequestBody() throws JSONException; + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientCallDecorator.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientCallDecorator.java new file mode 100644 index 0000000000..805596e682 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientCallDecorator.java @@ -0,0 +1,56 @@ +package com.microsoft.azure.mobile.http; + +import java.util.Map; + +/** + * Helper class used to share logic with multiple decorators. + */ +abstract class HttpClientCallDecorator implements Runnable, ServiceCall, ServiceCallback { + + /** + * Decorated API. + */ + final HttpClient mDecoratedApi; + + final String mUrl; + + final String mMethod; + + final Map mHeaders; + + final HttpClient.CallTemplate mCallTemplate; + + /** + * Callback. + */ + final ServiceCallback mServiceCallback; + + /** + * Call. + */ + ServiceCall mServiceCall; + + HttpClientCallDecorator(HttpClient decoratedApi, String url, String method, Map headers, HttpClient.CallTemplate callTemplate, ServiceCallback serviceCallback) { + mDecoratedApi = decoratedApi; + mUrl = url; + mMethod = method; + mHeaders = headers; + mCallTemplate = callTemplate; + mServiceCallback = serviceCallback; + } + + @Override + public synchronized void cancel() { + mServiceCall.cancel(); + } + + @Override + public synchronized void run() { + mServiceCall = mDecoratedApi.callAsync(mUrl, mMethod, mHeaders, mCallTemplate, this); + } + + @Override + public void onCallSucceeded(String payload) { + mServiceCallback.onCallSucceeded(payload); + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientDecorator.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientDecorator.java new file mode 100644 index 0000000000..13b54f3b1d --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientDecorator.java @@ -0,0 +1,17 @@ +package com.microsoft.azure.mobile.http; + +import java.io.IOException; + +abstract class HttpClientDecorator implements HttpClient { + + final HttpClient mDecoratedApi; + + HttpClientDecorator(HttpClient decoratedApi) { + mDecoratedApi = decoratedApi; + } + + @Override + public void close() throws IOException { + mDecoratedApi.close(); + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandler.java similarity index 61% rename from sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandler.java rename to sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandler.java index 470bd53a7b..106df1ef4a 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandler.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandler.java @@ -1,20 +1,16 @@ -package com.microsoft.azure.mobile.ingestion.http; +package com.microsoft.azure.mobile.http; -import com.microsoft.azure.mobile.ingestion.Ingestion; -import com.microsoft.azure.mobile.ingestion.ServiceCall; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.models.LogContainer; import com.microsoft.azure.mobile.utils.NetworkStateHelper; import java.io.IOException; import java.util.HashSet; +import java.util.Map; import java.util.Set; -import java.util.UUID; /** * Decorator pausing calls while network is down. */ -public class IngestionNetworkStateHandler extends IngestionDecorator implements NetworkStateHelper.Listener { +public class HttpClientNetworkStateHandler extends HttpClientDecorator implements NetworkStateHelper.Listener { /** * Network state helper. @@ -32,15 +28,15 @@ public class IngestionNetworkStateHandler extends IngestionDecorator implements * @param decoratedApi decorated API. * @param networkStateHelper network state helper. */ - public IngestionNetworkStateHandler(Ingestion decoratedApi, NetworkStateHelper networkStateHelper) { + public HttpClientNetworkStateHandler(HttpClient decoratedApi, NetworkStateHelper networkStateHelper) { super(decoratedApi); mNetworkStateHelper = networkStateHelper; mNetworkStateHelper.addListener(this); } @Override - public synchronized ServiceCall sendAsync(String appSecret, UUID installId, LogContainer logContainer, ServiceCallback serviceCallback) throws IllegalArgumentException { - Call ingestionCall = new Call(mDecoratedApi, appSecret, installId, logContainer, serviceCallback); + public synchronized ServiceCall callAsync(String url, String method, Map headers, CallTemplate callTemplate, ServiceCallback serviceCallback) { + Call ingestionCall = new Call(mDecoratedApi, url, method, headers, callTemplate, serviceCallback); mCalls.add(ingestionCall); if (mNetworkStateHelper.isNetworkConnected()) ingestionCall.run(); @@ -66,7 +62,7 @@ public synchronized void onNetworkStateUpdated(boolean connected) { } private synchronized void callRunAsync(Call call) { - call.mServiceCall = call.mDecoratedApi.sendAsync(call.mAppSecret, call.mInstallId, call.mLogContainer, call); + call.mServiceCall = call.mDecoratedApi.callAsync(call.mUrl, call.mMethod, call.mHeaders, call.mCallTemplate, call); } private synchronized void cancelCall(Call call) { @@ -82,9 +78,9 @@ private synchronized void pauseCall(Call call) { /** * Guard against multiple calls since this call can be retried on network state change. */ - private synchronized void onCallSucceeded(Call call) { + private synchronized void onCallSucceeded(Call call, String payload) { if (mCalls.contains(call)) { - call.mServiceCallback.onCallSucceeded(); + call.mServiceCallback.onCallSucceeded(payload); mCalls.remove(call); } } @@ -102,10 +98,10 @@ private synchronized void onCallFailed(Call call, Exception e) { /** * Call wrapper logic. */ - private class Call extends IngestionCallDecorator implements Runnable, ServiceCallback { + private class Call extends HttpClientCallDecorator implements Runnable, ServiceCallback { - Call(Ingestion decoratedApi, String appSecret, UUID installId, LogContainer logContainer, ServiceCallback serviceCallback) { - super(decoratedApi, appSecret, installId, logContainer, serviceCallback); + Call(HttpClient decoratedApi, String url, String method, Map headers, CallTemplate callTemplate, ServiceCallback serviceCallback) { + super(decoratedApi, url, method, headers, callTemplate, serviceCallback); } @Override @@ -119,13 +115,13 @@ public void cancel() { } @Override - public void onCallSucceeded() { - IngestionNetworkStateHandler.this.onCallSucceeded(this); + public void onCallSucceeded(String payload) { + HttpClientNetworkStateHandler.this.onCallSucceeded(this, payload); } @Override public void onCallFailed(Exception e) { - IngestionNetworkStateHandler.this.onCallFailed(this, e); + HttpClientNetworkStateHandler.this.onCallFailed(this, e); } } } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryer.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientRetryer.java similarity index 72% rename from sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryer.java rename to sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientRetryer.java index 6e03b097e9..79b0b8f54b 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryer.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClientRetryer.java @@ -1,25 +1,21 @@ -package com.microsoft.azure.mobile.ingestion.http; +package com.microsoft.azure.mobile.http; import android.os.Handler; import android.os.Looper; import android.support.annotation.VisibleForTesting; import com.microsoft.azure.mobile.MobileCenter; -import com.microsoft.azure.mobile.ingestion.Ingestion; -import com.microsoft.azure.mobile.ingestion.ServiceCall; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.models.LogContainer; import com.microsoft.azure.mobile.utils.MobileCenterLog; import java.net.UnknownHostException; +import java.util.Map; import java.util.Random; -import java.util.UUID; import java.util.concurrent.TimeUnit; /** * Decorator managing retries. */ -public class IngestionRetryer extends IngestionDecorator { +public class HttpClientRetryer extends HttpClientDecorator { /** * Retry intervals to use, array index is to use the value for each retry. When we used all the array values, we give up and forward the last error. @@ -46,7 +42,7 @@ public class IngestionRetryer extends IngestionDecorator { * * @param decoratedApi API to decorate. */ - public IngestionRetryer(Ingestion decoratedApi) { + public HttpClientRetryer(HttpClient decoratedApi) { this(decoratedApi, new Handler(Looper.getMainLooper())); } @@ -57,16 +53,17 @@ public IngestionRetryer(Ingestion decoratedApi) { * @param handler handler for timed retries. */ @VisibleForTesting - IngestionRetryer(Ingestion decoratedApi, Handler handler) { + HttpClientRetryer(HttpClient decoratedApi, Handler handler) { super(decoratedApi); mHandler = handler; } + @Override - public ServiceCall sendAsync(String appSecret, UUID installId, LogContainer logContainer, ServiceCallback serviceCallback) throws IllegalArgumentException { + public ServiceCall callAsync(String url, String method, Map headers, CallTemplate callTemplate, ServiceCallback serviceCallback) { /* Wrap the call with the retry logic and call delegate. */ - RetryableCall retryableCall = new RetryableCall(mDecoratedApi, appSecret, installId, logContainer, serviceCallback); + RetryableCall retryableCall = new RetryableCall(mDecoratedApi, url, method, headers, callTemplate, serviceCallback); retryableCall.run(); return retryableCall; } @@ -74,15 +71,15 @@ public ServiceCall sendAsync(String appSecret, UUID installId, LogContainer logC /** * Retry wrapper logic. */ - private class RetryableCall extends IngestionCallDecorator { + private class RetryableCall extends HttpClientCallDecorator { /** * Current retry counter. 0 means its the first try. */ private int mRetryCount; - RetryableCall(Ingestion decoratedApi, String appSecret, UUID installId, LogContainer logContainer, ServiceCallback serviceCallback) { - super(decoratedApi, appSecret, installId, logContainer, serviceCallback); + RetryableCall(HttpClient decoratedApi, String url, String method, Map headers, CallTemplate callTemplate, ServiceCallback serviceCallback) { + super(decoratedApi, url, method, headers, callTemplate, serviceCallback); } @Override diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpException.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpException.java similarity index 97% rename from sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpException.java rename to sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpException.java index a27b5e2479..653b58f5ff 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpException.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpException.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.ingestion.http; +package com.microsoft.azure.mobile.http; import android.support.annotation.NonNull; import android.text.TextUtils; diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java similarity index 97% rename from sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java rename to sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java index 93659e684a..6a18bf833f 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.ingestion.http; +package com.microsoft.azure.mobile.http; import android.support.annotation.VisibleForTesting; diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/ServiceCall.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCall.java similarity index 69% rename from sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/ServiceCall.java rename to sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCall.java index d05bf98339..23791a2640 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/ServiceCall.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCall.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.ingestion; +package com.microsoft.azure.mobile.http; public interface ServiceCall { diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/ServiceCallback.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCallback.java similarity index 81% rename from sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/ServiceCallback.java rename to sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCallback.java index 1f90c05a03..2f07bbb699 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/ServiceCallback.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCallback.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.ingestion; +package com.microsoft.azure.mobile.http; /** * The callback used for client side asynchronous operations. @@ -8,7 +8,7 @@ public interface ServiceCallback { /** * Implement this method to handle successful REST call results. */ - void onCallSucceeded(); + void onCallSucceeded(String payload); /** * Implement this method to handle REST call failures. diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java index 11e5ebc67c..4877291eea 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java @@ -1,17 +1,19 @@ package com.microsoft.azure.mobile.ingestion; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.ingestion.models.LogContainer; import java.io.Closeable; import java.util.UUID; /** - * The interface for Ingestion class. + * The interface for HttpClient class. */ public interface Ingestion extends Closeable { /** - * Send logs to the Ingestion service. + * Send logs to the HttpClient service. * * @param appSecret a unique and secret key used to identify the application. * @param installId install identifier. diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java new file mode 100644 index 0000000000..1c0d5e44c5 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java @@ -0,0 +1,140 @@ +package com.microsoft.azure.mobile.ingestion; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.microsoft.azure.mobile.http.DefaultHttpClient; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.HttpClientRetryer; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.ingestion.models.Log; +import com.microsoft.azure.mobile.ingestion.models.LogContainer; +import com.microsoft.azure.mobile.ingestion.models.json.LogSerializer; +import com.microsoft.azure.mobile.utils.NetworkStateHelper; + +import org.json.JSONException; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.microsoft.azure.mobile.http.DefaultHttpClient.APP_SECRET; +import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_POST; + +public class IngestionHttp implements Ingestion { + + /** + * Default base URL. + */ + private static final String DEFAULT_BASE_URL = "https://in.mobile.azure.com"; + + /** + * API Path. + */ + private static final String API_PATH = "/logs?api_version=1.0.0-preview20160914"; + + /** + * Installation identifier HTTP Header. + */ + private static final String INSTALL_ID = "Install-ID"; + + /** + * Log serializer. + */ + private final LogSerializer mLogSerializer; + + /** + * HTTP client. + */ + private final HttpClient mHttpClient; + + /** + * API base URL (scheme + authority). + */ + private String mBaseUrl; + + /** + * Init. + * + * @param context any context. + * @param logSerializer log serializer. + */ + public IngestionHttp(@NonNull Context context, @NonNull LogSerializer logSerializer) { + mLogSerializer = logSerializer; + HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); + NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(context); + mHttpClient = new HttpClientNetworkStateHandler(retryer, networkStateHelper); + mBaseUrl = DEFAULT_BASE_URL; + } + + /** + * Set the base url. + * + * @param baseUrl the base url. + */ + @Override + @SuppressWarnings("SameParameterValue") + public void setServerUrl(@NonNull String baseUrl) { + mBaseUrl = baseUrl; + } + + @Override + public ServiceCall sendAsync(String appSecret, UUID installId, LogContainer logContainer, final ServiceCallback serviceCallback) throws IllegalArgumentException { + Map headers = new HashMap<>(); + headers.put(INSTALL_ID, installId.toString()); + headers.put(APP_SECRET, appSecret); + HttpClient.CallTemplate callTemplate = new IngestionCallTemplate(mLogSerializer, logContainer); + return mHttpClient.callAsync(mBaseUrl + API_PATH, METHOD_POST, headers, callTemplate, serviceCallback); + } + + @Override + public void close() throws IOException { + mHttpClient.close(); + } + + /** + * Inner class is used to be able to mock System.currentTimeMillis, does not work if using anonymous inner class... + */ + private static class IngestionCallTemplate implements HttpClient.CallTemplate { + + private final LogSerializer mLogSerializer; + + private final LogContainer mLogContainer; + + IngestionCallTemplate(LogSerializer logSerializer, LogContainer logContainer) { + mLogSerializer = logSerializer; + mLogContainer = logContainer; + } + + @Override + public String buildRequestBody() throws JSONException { + + /* Timestamps need to be as accurate as possible so we convert absolute time to relative now. Save times. */ + List logs = mLogContainer.getLogs(); + int size = logs.size(); + long[] absoluteTimes = new long[size]; + for (int i = 0; i < size; i++) { + Log log = logs.get(i); + long toffset = log.getToffset(); + absoluteTimes[i] = toffset; + log.setToffset(System.currentTimeMillis() - toffset); + } + + /* Serialize payload. */ + String payload; + try { + payload = mLogSerializer.serializeContainer(mLogContainer); + } finally { + + /* Restore original times, could be retried later. */ + for (int i = 0; i < size; i++) + logs.get(i).setToffset(absoluteTimes[i]); + } + return payload; + } + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionCallDecorator.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionCallDecorator.java deleted file mode 100644 index df6cb9641c..0000000000 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionCallDecorator.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.microsoft.azure.mobile.ingestion.http; - -import com.microsoft.azure.mobile.ingestion.Ingestion; -import com.microsoft.azure.mobile.ingestion.ServiceCall; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.models.LogContainer; - -import java.util.UUID; - -/** - * Helper class used to share logic with multiple decorators. - */ -abstract class IngestionCallDecorator implements Runnable, ServiceCall, ServiceCallback { - - /** - * Decorated API. - */ - final Ingestion mDecoratedApi; - - /** - * Application secret. - */ - final String mAppSecret; - - /** - * Installation identifier. - */ - final UUID mInstallId; - - /** - * Log container. - */ - final LogContainer mLogContainer; - - /** - * Callback. - */ - final ServiceCallback mServiceCallback; - - /** - * Call. - */ - ServiceCall mServiceCall; - - IngestionCallDecorator(Ingestion decoratedApi, String appSecret, UUID installId, LogContainer logContainer, ServiceCallback serviceCallback) { - mDecoratedApi = decoratedApi; - mAppSecret = appSecret; - mInstallId = installId; - mLogContainer = logContainer; - mServiceCallback = serviceCallback; - } - - @Override - public synchronized void cancel() { - mServiceCall.cancel(); - } - - @Override - public synchronized void run() { - mServiceCall = mDecoratedApi.sendAsync(mAppSecret, mInstallId, mLogContainer, this); - } - - @Override - public void onCallSucceeded() { - mServiceCallback.onCallSucceeded(); - } -} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionDecorator.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionDecorator.java deleted file mode 100644 index 51ff6d8401..0000000000 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionDecorator.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.microsoft.azure.mobile.ingestion.http; - -import com.microsoft.azure.mobile.ingestion.Ingestion; - -import java.io.IOException; - -abstract class IngestionDecorator implements Ingestion { - - final Ingestion mDecoratedApi; - - IngestionDecorator(Ingestion decoratedApi) { - mDecoratedApi = decoratedApi; - } - - @Override - public void setServerUrl(String serverUrl) { - mDecoratedApi.setServerUrl(serverUrl); - } - - @Override - public void close() throws IOException { - mDecoratedApi.close(); - } -} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java deleted file mode 100644 index d54e75e284..0000000000 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java +++ /dev/null @@ -1,314 +0,0 @@ -package com.microsoft.azure.mobile.ingestion.http; - -import android.os.AsyncTask; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; - -import com.microsoft.azure.mobile.ingestion.Ingestion; -import com.microsoft.azure.mobile.ingestion.ServiceCall; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.models.Log; -import com.microsoft.azure.mobile.ingestion.models.LogContainer; -import com.microsoft.azure.mobile.ingestion.models.json.LogSerializer; -import com.microsoft.azure.mobile.utils.HandlerUtils; -import com.microsoft.azure.mobile.utils.MobileCenterLog; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.RejectedExecutionException; - -import static android.util.Log.VERBOSE; -import static com.microsoft.azure.mobile.MobileCenter.LOG_TAG; -import static java.lang.Math.max; - -public class IngestionHttp implements Ingestion { - - /** - * Default base URL. - */ - private static final String DEFAULT_BASE_URL = "https://in.mobile.azure.com"; - - /** - * API Path. - */ - private static final String API_PATH = "/logs?api_version=1.0.0-preview20160914"; - - /** - * Content type header value. - */ - private static final String CONTENT_TYPE_VALUE = "application/json"; - - /** - * Application secret HTTP Header. - */ - private static final String APP_SECRET = "App-Secret"; - - /** - * Installation identifier HTTP Header. - */ - private static final String INSTALL_ID = "Install-ID"; - - /** - * Default string builder capacity. - */ - private static final int DEFAULT_STRING_BUILDER_CAPACITY = 16; - - /** - * Content type header key. - */ - private static final String CONTENT_TYPE_KEY = "Content-Type"; - - /** - * Character encoding. - */ - private static final String CHARSET_NAME = "UTF-8"; - - /** - * Read buffer size. - */ - private static final int READ_BUFFER_SIZE = 1024; - - /** - * HTTP connection timeout. - */ - private static final int CONNECT_TIMEOUT = 60000; - - /** - * HTTP read timeout. - */ - private static final int READ_TIMEOUT = 20000; - - /** - * Maximum characters to be displayed in a log for application secret. - */ - private static final int MAX_CHARACTERS_DISPLAYED_FOR_APP_SECRET = 8; - - /** - * Log serializer. - */ - private final LogSerializer mLogSerializer; - - /** - * API base URL (scheme + authority). - */ - private String mBaseUrl; - - /** - * Init. - * - * @param logSerializer log serializer. - */ - public IngestionHttp(@NonNull LogSerializer logSerializer) { - mLogSerializer = logSerializer; - mBaseUrl = DEFAULT_BASE_URL; - } - - /** - * Do the HTTP call now. - * - * @param baseUrl API base URL (scheme + authority). - * @param logSerializer log serializer. - * @param appSecret a unique and secret key used to identify the application. - * @param installId install identifier. - * @param logContainer payload. @throws Exception if an error occurs. - */ - private static void doCall(String baseUrl, LogSerializer logSerializer, String appSecret, UUID installId, LogContainer logContainer) throws Exception { - - /* HTTP session. */ - URL url = new URL(baseUrl + API_PATH); - MobileCenterLog.verbose(LOG_TAG, "Calling " + url + " ..."); - HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - try { - - /* Configure connection timeouts. */ - urlConnection.setConnectTimeout(CONNECT_TIMEOUT); - urlConnection.setReadTimeout(READ_TIMEOUT); - - /* Set headers. */ - urlConnection.setRequestProperty(CONTENT_TYPE_KEY, CONTENT_TYPE_VALUE); - urlConnection.setRequestProperty(APP_SECRET, appSecret); - urlConnection.setRequestProperty(INSTALL_ID, installId.toString()); - - /* Log headers. */ - if (MobileCenterLog.getLogLevel() <= VERBOSE) { - int hidingEndIndex = appSecret.length() - (appSecret.length() >= MAX_CHARACTERS_DISPLAYED_FOR_APP_SECRET ? MAX_CHARACTERS_DISPLAYED_FOR_APP_SECRET : 0); - char[] fill = new char[hidingEndIndex]; - Arrays.fill(fill, '*'); - String header = "Headers: " + CONTENT_TYPE_KEY + '=' + CONTENT_TYPE_VALUE + - ", " + APP_SECRET + '=' + new String(fill) + appSecret.substring(hidingEndIndex) + - ", " + INSTALL_ID + '=' + installId.toString(); - MobileCenterLog.verbose(LOG_TAG, header); - } - - /* Timestamps need to be as accurate as possible so we convert absolute time to relative now. Save times. */ - List logs = logContainer.getLogs(); - int size = logs.size(); - long[] absoluteTimes = new long[size]; - for (int i = 0; i < size; i++) { - Log log = logs.get(i); - long toffset = log.getToffset(); - absoluteTimes[i] = toffset; - log.setToffset(System.currentTimeMillis() - toffset); - } - - /* Serialize payload. */ - String payload; - try { - payload = logSerializer.serializeContainer(logContainer); - } finally { - - /* Restore original times, could be retried later. */ - for (int i = 0; i < size; i++) - logs.get(i).setToffset(absoluteTimes[i]); - } - MobileCenterLog.verbose(LOG_TAG, payload); - - /* Send payload through the wire. */ - byte[] binaryPayload = payload.getBytes(CHARSET_NAME); - urlConnection.setDoOutput(true); - urlConnection.setFixedLengthStreamingMode(binaryPayload.length); - OutputStream out = urlConnection.getOutputStream(); - out.write(binaryPayload); - out.close(); - - /* Read response. */ - int status = urlConnection.getResponseCode(); - String response = dump(urlConnection); - MobileCenterLog.verbose(LOG_TAG, "HTTP response status=" + status + " payload=" + response); - - /* Generate exception on failure. */ - if (status != 200) - throw new HttpException(status, response); - } finally { - - /* Release connection. */ - urlConnection.disconnect(); - } - } - - /** - * Dump stream to string. - * - * @param urlConnection URL connection. - * @return dumped string. - * @throws IOException if an error occurred. - */ - private static String dump(HttpURLConnection urlConnection) throws IOException { - - /* - * Though content length header value is less than actual payload length (gzip), we want to init - * buffer with a reasonable start size to optimize (default is 16 and is way too low for this - * use case). - */ - StringBuilder builder = new StringBuilder(max(urlConnection.getContentLength(), DEFAULT_STRING_BUILDER_CAPACITY)); - InputStream stream; - if (urlConnection.getResponseCode() < 400) - stream = urlConnection.getInputStream(); - else - stream = urlConnection.getErrorStream(); - InputStreamReader in = new InputStreamReader(stream, CHARSET_NAME); - char[] buffer = new char[READ_BUFFER_SIZE]; - int len; - while ((len = in.read(buffer)) > 0) - builder.append(buffer, 0, len); - return builder.toString(); - } - - /** - * Set the base url. - * - * @param baseUrl the base url. - */ - @Override - @SuppressWarnings("SameParameterValue") - public void setServerUrl(@NonNull String baseUrl) { - mBaseUrl = baseUrl; - } - - @Override - public ServiceCall sendAsync(String appSecret, UUID installId, LogContainer logContainer, final ServiceCallback serviceCallback) throws IllegalArgumentException { - final Call call = new Call(mBaseUrl, mLogSerializer, appSecret, installId, logContainer, serviceCallback); - try { - call.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } catch (final RejectedExecutionException e) { - - /* - * When executor saturated (shared with app), we should use the retry mechanism - * rather than creating more threads to avoid putting too much pressure on the hosting app. - * Also we need to return the method before calling the listener, - * so we post the callback on handler to make sure of that. - */ - HandlerUtils.runOnUiThread(new Runnable() { - - @Override - public void run() { - serviceCallback.onCallFailed(e); - } - }); - } - return new ServiceCall() { - - @Override - public void cancel() { - if (!call.isCancelled()) - call.cancel(true); - } - }; - } - - @Override - public void close() throws IOException { - - /* No-op. A decorator can take care of tracking calls to cancel. */ - } - - @VisibleForTesting - static class Call extends AsyncTask { - - private final String mBaseUrl; - - private final LogSerializer mLogSerializer; - - private final String mAppSecret; - - private final UUID mInstallId; - - private final LogContainer mLogContainer; - - private final ServiceCallback mServiceCallback; - - Call(String baseUrl, LogSerializer logSerializer, String appSecret, UUID installId, LogContainer logContainer, ServiceCallback serviceCallback) { - mBaseUrl = baseUrl; - mLogSerializer = logSerializer; - mAppSecret = appSecret; - mInstallId = installId; - mLogContainer = logContainer; - mServiceCallback = serviceCallback; - } - - @Override - protected Exception doInBackground(Void... params) { - try { - doCall(mBaseUrl, mLogSerializer, mAppSecret, mInstallId, mLogContainer); - } catch (Exception e) { - return e; - } - return null; - } - - @Override - protected void onPostExecute(Exception e) { - if (e == null) - mServiceCallback.onCallSucceeded(); - else - mServiceCallback.onCallFailed(e); - } - } -} diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java index 12402b9023..dffb8b0287 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java @@ -33,7 +33,7 @@ @PrepareForTest({StorageHelper.PreferencesStorage.class, MobileCenter.class}) public class AbstractMobileCenterServiceTest { - private static final String SERVICE_ENABLED_KEY = KEY_ENABLED + "_group_test"; + private static final String SERVICE_ENABLED_KEY = KEY_ENABLED + "_Test"; private AbstractMobileCenterService service; diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/AbstractDefaultChannelTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/AbstractDefaultChannelTest.java index b791b018d7..f960742326 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/AbstractDefaultChannelTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/AbstractDefaultChannelTest.java @@ -5,7 +5,7 @@ import android.os.HandlerThread; import android.os.Looper; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; +import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.ingestion.models.Device; import com.microsoft.azure.mobile.ingestion.models.Log; import com.microsoft.azure.mobile.persistence.DatabasePersistenceAsync; @@ -82,7 +82,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); if (args[3] instanceof ServiceCallback) { if (e == null) - ((ServiceCallback) invocation.getArguments()[3]).onCallSucceeded(); + ((ServiceCallback) invocation.getArguments()[3]).onCallSucceeded(""); else ((ServiceCallback) invocation.getArguments()[3]).onCallFailed(e); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelRaceConditionTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelRaceConditionTest.java index d73d851751..4ddbd68c88 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelRaceConditionTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelRaceConditionTest.java @@ -3,10 +3,10 @@ import android.content.Context; import com.microsoft.azure.mobile.CancellationException; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.ingestion.Ingestion; -import com.microsoft.azure.mobile.ingestion.ServiceCall; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.http.IngestionHttp; +import com.microsoft.azure.mobile.ingestion.IngestionHttp; import com.microsoft.azure.mobile.ingestion.models.Log; import com.microsoft.azure.mobile.ingestion.models.LogContainer; import com.microsoft.azure.mobile.persistence.DatabasePersistenceAsync; @@ -30,6 +30,7 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.notNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -370,7 +371,7 @@ public Object answer(final InvocationOnMock invocation) throws Throwable { @Override public void run() { beforeCallSemaphore.acquireUninterruptibly(); - ((ServiceCallback) invocation.getArguments()[3]).onCallSucceeded(); + ((ServiceCallback) invocation.getArguments()[3]).onCallSucceeded(notNull(String.class)); afterCallSemaphore.release(); } }.start(); diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java index 288fcf81ba..55a4901e6d 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java @@ -6,10 +6,10 @@ import com.microsoft.azure.mobile.CancellationException; import com.microsoft.azure.mobile.MobileCenter; +import com.microsoft.azure.mobile.http.HttpException; +import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.ingestion.Ingestion; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.http.HttpException; -import com.microsoft.azure.mobile.ingestion.http.IngestionHttp; +import com.microsoft.azure.mobile.ingestion.IngestionHttp; import com.microsoft.azure.mobile.ingestion.models.Device; import com.microsoft.azure.mobile.ingestion.models.Log; import com.microsoft.azure.mobile.ingestion.models.LogContainer; @@ -211,7 +211,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { verify(mockPersistence, never()).deleteLogs(any(String.class), any(String.class)); /* Make 1 of the call succeed. Verify log deleted. */ - callbacks.get(0).onCallSucceeded(); + callbacks.get(0).onCallSucceeded(""); verify(mockPersistence).deleteLogs(any(String.class), any(String.class)); /* The request N+1 is now unlocked. */ @@ -219,7 +219,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { /* Unlock all requests and check logs deleted. */ for (int i = 1; i < 4; i++) - callbacks.get(i).onCallSucceeded(); + callbacks.get(i).onCallSucceeded(""); verify(mockPersistence, times(4)).deleteLogs(any(String.class), any(String.class)); /* The counter should be 0 now as we sent data. */ @@ -261,7 +261,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { verify(mockPersistence, never()).deleteLogs(any(String.class), any(String.class)); /* Make 1 of the call succeed. Verify log deleted. */ - callbacks.get(0).onCallSucceeded(); + callbacks.get(0).onCallSucceeded(""); verify(mockPersistence).deleteLogs(any(String.class), any(String.class)); /* The request N+1 is now unlocked. */ @@ -269,7 +269,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { /* Unlock all requests and check logs deleted. */ for (int i = 1; i < 4; i++) - callbacks.get(i).onCallSucceeded(); + callbacks.get(i).onCallSucceeded(""); verify(mockPersistence, times(4)).deleteLogs(any(String.class), any(String.class)); /* The counter should be 0 now as we sent data. */ @@ -683,7 +683,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { /* Simulate a service disabled in the middle of network transaction. */ ServiceCallback callback = (ServiceCallback) invocation.getArguments()[3]; channel.removeGroup(TEST_GROUP); - callback.onCallSucceeded(); + callback.onCallSucceeded(""); return null; } }); diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttpTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/DefaultHttpClientTest.java similarity index 58% rename from sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttpTest.java rename to sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/DefaultHttpClientTest.java index 3cea4ec85e..e766b9db77 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttpTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/DefaultHttpClientTest.java @@ -1,11 +1,6 @@ -package com.microsoft.azure.mobile.ingestion.http; +package com.microsoft.azure.mobile.http; import com.microsoft.azure.mobile.MobileCenter; -import com.microsoft.azure.mobile.ingestion.ServiceCall; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.models.Log; -import com.microsoft.azure.mobile.ingestion.models.LogContainer; -import com.microsoft.azure.mobile.ingestion.models.json.LogSerializer; import com.microsoft.azure.mobile.utils.HandlerUtils; import com.microsoft.azure.mobile.utils.UUIDUtils; @@ -22,17 +17,19 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.Semaphore; -import static android.util.Log.INFO; import static android.util.Log.VERBOSE; +import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; +import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_POST; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.notNull; import static org.mockito.Mockito.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -47,8 +44,8 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("unused") -@PrepareForTest(IngestionHttp.class) -public class IngestionHttpTest { +@PrepareForTest(DefaultHttpClient.class) +public class DefaultHttpClientTest { @Rule public PowerMockRule rule = new PowerMockRule(); @@ -59,16 +56,18 @@ public class IngestionHttpTest { private static void mockCall() throws Exception { /* Mock AsyncTask... */ - whenNew(IngestionHttp.Call.class).withAnyArguments().thenAnswer(new Answer() { + whenNew(DefaultHttpClient.Call.class).withAnyArguments().thenAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - final IngestionHttp.Call call = new IngestionHttp.Call(invocation.getArguments()[0].toString(), (LogSerializer) invocation.getArguments()[1], (String) invocation.getArguments()[2], (UUID) invocation.getArguments()[3], (LogContainer) invocation.getArguments()[4], (ServiceCallback) invocation.getArguments()[5]); - IngestionHttp.Call spyCall = spy(call); - when(spyCall.executeOnExecutor(any(Executor.class))).then(new Answer() { + + @SuppressWarnings("unchecked") + final DefaultHttpClient.Call call = new DefaultHttpClient.Call(invocation.getArguments()[0].toString(), invocation.getArguments()[1].toString(), (Map) invocation.getArguments()[2], (HttpClient.CallTemplate) invocation.getArguments()[3], (ServiceCallback) invocation.getArguments()[4]); + DefaultHttpClient.Call spyCall = spy(call); + when(spyCall.executeOnExecutor(any(Executor.class))).then(new Answer() { @Override - public IngestionHttp.Call answer(InvocationOnMock invocation) throws Throwable { + public DefaultHttpClient.Call answer(InvocationOnMock invocation) throws Throwable { call.onPostExecute(call.doInBackground()); return call; } @@ -79,28 +78,15 @@ public IngestionHttp.Call answer(InvocationOnMock invocation) throws Throwable { } @Test - public void success() throws Exception { + public void post200() throws Exception { /* Set log level to verbose to test shorter app secret as well. */ MobileCenter.setLogLevel(VERBOSE); - /* Build some payload. */ - LogContainer container = new LogContainer(); - Log log = mock(Log.class); - long logAbsoluteTime = 123L; - when(log.getToffset()).thenReturn(logAbsoluteTime); - List logs = new ArrayList<>(); - logs.add(log); - container.setLogs(logs); - - /* Stable time. */ - mockStatic(System.class); - long now = 456L; - when(System.currentTimeMillis()).thenReturn(now); - /* Configure mock HTTP. */ + String urlString = "http://mock/logs?api_version=1.0.0-preview20160914"; URL url = mock(URL.class); - whenNew(URL.class).withArguments("http://mock/logs?api_version=1.0.0-preview20160914").thenReturn(url); + whenNew(URL.class).withArguments(urlString).thenReturn(url); HttpURLConnection urlConnection = mock(HttpURLConnection.class); when(url.openConnection()).thenReturn(urlConnection); when(urlConnection.getResponseCode()).thenReturn(200); @@ -109,18 +95,20 @@ public void success() throws Exception { when(urlConnection.getInputStream()).thenReturn(new ByteArrayInputStream("OK".getBytes())); /* Configure API client. */ - LogSerializer serializer = mock(LogSerializer.class); - when(serializer.serializeContainer(any(LogContainer.class))).thenReturn("mockPayload"); - IngestionHttp httpClient = new IngestionHttp(serializer); - httpClient.setServerUrl("http://mock"); + HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); + when(callTemplate.buildRequestBody()).thenReturn("mockPayload"); + DefaultHttpClient httpClient = new DefaultHttpClient(); /* Test calling code. Use shorter but valid app secret. */ String appSecret = "SHORT"; UUID installId = UUIDUtils.randomUUID(); + Map headers = new HashMap<>(); + headers.put("App-Secret", appSecret); + headers.put("Install-ID", installId.toString()); ServiceCallback serviceCallback = mock(ServiceCallback.class); mockCall(); - httpClient.sendAsync(appSecret, installId, container, serviceCallback); - verify(serviceCallback).onCallSucceeded(); + httpClient.callAsync(urlString, METHOD_POST, headers, callTemplate, serviceCallback); + verify(serviceCallback).onCallSucceeded("OK"); verifyNoMoreInteractions(serviceCallback); verify(urlConnection).setRequestProperty("Content-Type", "application/json"); verify(urlConnection).setRequestProperty("App-Secret", appSecret); @@ -128,32 +116,56 @@ public void success() throws Exception { verify(urlConnection).disconnect(); httpClient.close(); - /* Verify payload and toffset manipulation. */ + /* Verify payload. */ String sentPayload = buffer.toString("UTF-8"); assertEquals("mockPayload", sentPayload); - verify(log).setToffset(now - logAbsoluteTime); - verify(log).setToffset(logAbsoluteTime); + } + + @Test + public void get200() throws Exception { + + /* Set log level to verbose to test shorter app secret as well. */ + MobileCenter.setLogLevel(VERBOSE); + + /* Configure mock HTTP. */ + String urlString = "http://mock/get"; + URL url = mock(URL.class); + whenNew(URL.class).withArguments(urlString).thenReturn(url); + HttpURLConnection urlConnection = mock(HttpURLConnection.class); + when(url.openConnection()).thenReturn(urlConnection); + when(urlConnection.getResponseCode()).thenReturn(200); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + when(urlConnection.getOutputStream()).thenReturn(buffer); + when(urlConnection.getInputStream()).thenReturn(new ByteArrayInputStream("OK".getBytes())); + + /* Configure API client. */ + HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); + DefaultHttpClient httpClient = new DefaultHttpClient(); + + /* Test calling code. */ + String appSecret = UUIDUtils.randomUUID().toString(); + UUID installId = UUIDUtils.randomUUID(); + Map headers = new HashMap<>(); + headers.put("App-Secret", appSecret); + headers.put("Install-ID", installId.toString()); + ServiceCallback serviceCallback = mock(ServiceCallback.class); + mockCall(); + httpClient.callAsync(urlString, METHOD_GET, headers, callTemplate, serviceCallback); + verify(serviceCallback).onCallSucceeded("OK"); + verifyNoMoreInteractions(serviceCallback); + verify(urlConnection).setRequestProperty("Content-Type", "application/json"); + verify(urlConnection).setRequestProperty("App-Secret", appSecret); + verify(urlConnection).setRequestProperty("Install-ID", installId.toString()); + verify(urlConnection).disconnect(); + verify(callTemplate, never()).buildRequestBody(); + httpClient.close(); } @Test public void error503() throws Exception { /* Set log level to verbose to test shorter app secret as well. */ - MobileCenter.setLogLevel(INFO); - - /* Build some payload. */ - LogContainer container = new LogContainer(); - Log log = mock(Log.class); - long logAbsoluteTime = 123L; - when(log.getToffset()).thenReturn(logAbsoluteTime); - List logs = new ArrayList<>(); - logs.add(log); - container.setLogs(logs); - - /* Stable time. */ - mockStatic(System.class); - long now = 456L; - when(System.currentTimeMillis()).thenReturn(now); + MobileCenter.setLogLevel(android.util.Log.INFO); /* Configure mock HTTP. */ URL url = mock(URL.class); @@ -166,33 +178,34 @@ public void error503() throws Exception { when(urlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream("Busy".getBytes())); /* Configure API client. */ - LogSerializer logSerializer = mock(LogSerializer.class); - when(logSerializer.serializeContainer(container)).thenReturn(""); - IngestionHttp httpClient = new IngestionHttp(logSerializer); + HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); + when(callTemplate.buildRequestBody()).thenReturn("mockPayload"); + DefaultHttpClient httpClient = new DefaultHttpClient(); /* Test calling code. */ String appSecret = UUIDUtils.randomUUID().toString(); UUID installId = UUIDUtils.randomUUID(); + Map headers = new HashMap<>(); + headers.put("App-Secret", appSecret); + headers.put("Install-ID", installId.toString()); ServiceCallback serviceCallback = mock(ServiceCallback.class); mockCall(); - httpClient.sendAsync(appSecret, installId, container, serviceCallback); + httpClient.callAsync("", METHOD_POST, headers, callTemplate, serviceCallback); verify(serviceCallback).onCallFailed(new HttpException(503, "Busy")); verifyNoMoreInteractions(serviceCallback); verify(urlConnection).disconnect(); - verify(log).setToffset(now - logAbsoluteTime); - verify(log).setToffset(logAbsoluteTime); } @Test public void cancel() throws Exception { /* Mock AsyncTask... */ - IngestionHttp.Call mockCall = mock(IngestionHttp.Call.class); - whenNew(IngestionHttp.Call.class).withAnyArguments().thenReturn(mockCall); + DefaultHttpClient.Call mockCall = mock(DefaultHttpClient.Call.class); + whenNew(DefaultHttpClient.Call.class).withAnyArguments().thenReturn(mockCall); when(mockCall.isCancelled()).thenReturn(false).thenReturn(true); - IngestionHttp httpClient = new IngestionHttp(mock(LogSerializer.class)); + DefaultHttpClient httpClient = new DefaultHttpClient(); ServiceCallback serviceCallback = mock(ServiceCallback.class); - ServiceCall call = httpClient.sendAsync(UUIDUtils.randomUUID().toString(), UUIDUtils.randomUUID(), new LogContainer(), serviceCallback); + ServiceCall call = httpClient.callAsync("", "", new HashMap(), mock(HttpClient.CallTemplate.class), serviceCallback); /* Cancel and verify. */ call.cancel(); @@ -210,31 +223,19 @@ public void failedConnection() throws Exception { whenNew(URL.class).withAnyArguments().thenReturn(url); IOException exception = new IOException("mock"); when(url.openConnection()).thenThrow(exception); + HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); ServiceCallback serviceCallback = mock(ServiceCallback.class); - IngestionHttp httpClient = new IngestionHttp(mock(LogSerializer.class)); + DefaultHttpClient httpClient = new DefaultHttpClient(); mockCall(); - httpClient.sendAsync(UUIDUtils.randomUUID().toString(), UUIDUtils.randomUUID(), new LogContainer(), serviceCallback); + httpClient.callAsync("", "", new HashMap(), callTemplate, serviceCallback); verify(serviceCallback).onCallFailed(exception); + verifyZeroInteractions(callTemplate); verifyZeroInteractions(serviceCallback); } @Test public void failedSerialization() throws Exception { - /* Build some payload. */ - LogContainer container = new LogContainer(); - Log log = mock(Log.class); - long logAbsoluteTime = 123L; - when(log.getToffset()).thenReturn(logAbsoluteTime); - List logs = new ArrayList<>(); - logs.add(log); - container.setLogs(logs); - - /* Stable time. */ - mockStatic(System.class); - long now = 456L; - when(System.currentTimeMillis()).thenReturn(now); - /* Configure mock HTTP. */ URL url = mock(URL.class); whenNew(URL.class).withAnyArguments().thenReturn(url); @@ -242,22 +243,18 @@ public void failedSerialization() throws Exception { when(url.openConnection()).thenReturn(urlConnection); /* Configure API client. */ - LogSerializer serializer = mock(LogSerializer.class); + HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); JSONException exception = new JSONException("mock"); - when(serializer.serializeContainer(any(LogContainer.class))).thenThrow(exception); - IngestionHttp httpClient = new IngestionHttp(serializer); + when(callTemplate.buildRequestBody()).thenThrow(exception); + DefaultHttpClient httpClient = new DefaultHttpClient(); /* Test calling code. */ - String appSecret = UUID.randomUUID().toString(); - UUID installId = UUID.randomUUID(); ServiceCallback serviceCallback = mock(ServiceCallback.class); mockCall(); - httpClient.sendAsync(appSecret, installId, container, serviceCallback); + httpClient.callAsync("", METHOD_POST, new HashMap(), callTemplate, serviceCallback); verify(serviceCallback).onCallFailed(exception); verifyNoMoreInteractions(serviceCallback); verify(urlConnection).disconnect(); - verify(log).setToffset(now - logAbsoluteTime); - verify(log).setToffset(logAbsoluteTime); } @Test @@ -285,19 +282,19 @@ public void run() { HandlerUtils.runOnUiThread(any(Runnable.class)); /* Mock ingestion to fail on saturated executor in AsyncTask. */ - IngestionHttp.Call call = mock(IngestionHttp.Call.class); - whenNew(IngestionHttp.Call.class).withAnyArguments().thenReturn(call); + DefaultHttpClient.Call call = mock(DefaultHttpClient.Call.class); + whenNew(DefaultHttpClient.Call.class).withAnyArguments().thenReturn(call); RejectedExecutionException exception = new RejectedExecutionException(); when(call.executeOnExecutor(any(Executor.class))).thenThrow(exception); - IngestionHttp httpClient = new IngestionHttp(mock(LogSerializer.class)); + DefaultHttpClient httpClient = new DefaultHttpClient(); /* Test. */ ServiceCallback serviceCallback = mock(ServiceCallback.class); - assertNotNull(httpClient.sendAsync("", UUID.randomUUID(), mock(LogContainer.class), serviceCallback)); + assertNotNull(httpClient.callAsync("", "", new HashMap(), mock(HttpClient.CallTemplate.class), serviceCallback)); /* Verify the callback call from "main" thread. */ semaphore.acquireUninterruptibly(); verify(serviceCallback).onCallFailed(exception); - verify(serviceCallback, never()).onCallSucceeded(); + verify(serviceCallback, never()).onCallSucceeded(notNull(String.class)); } } \ No newline at end of file diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandlerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java similarity index 62% rename from sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandlerTest.java rename to sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java index 14fe370550..87b707bc1d 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandlerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java @@ -1,11 +1,6 @@ -package com.microsoft.azure.mobile.ingestion.http; +package com.microsoft.azure.mobile.http; -import com.microsoft.azure.mobile.ingestion.Ingestion; -import com.microsoft.azure.mobile.ingestion.ServiceCall; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.models.LogContainer; import com.microsoft.azure.mobile.utils.NetworkStateHelper; -import com.microsoft.azure.mobile.utils.UUIDUtils; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; @@ -13,9 +8,11 @@ import java.io.IOException; import java.net.SocketException; -import java.util.UUID; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; @@ -26,171 +23,171 @@ import static org.mockito.Mockito.when; @SuppressWarnings("unused") -public class IngestionNetworkStateHandlerTest { +public class HttpClientNetworkStateHandlerTest { @Test public void success() throws IOException { /* Configure mock wrapped API. */ - String appSecret = UUIDUtils.randomUUID().toString(); - UUID installId = UUIDUtils.randomUUID(); - LogContainer container = mock(LogContainer.class); + String url = "http://mock/call"; + Map headers = new HashMap<>(); + final HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); final ServiceCallback callback = mock(ServiceCallback.class); final ServiceCall call = mock(ServiceCall.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ServiceCallback serviceCallback = (ServiceCallback) invocationOnMock.getArguments()[3]; - serviceCallback.onCallSucceeded(); - serviceCallback.onCallSucceeded(); + ServiceCallback serviceCallback = (ServiceCallback) invocationOnMock.getArguments()[4]; + serviceCallback.onCallSucceeded(""); + serviceCallback.onCallSucceeded(""); return call; } - }).when(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + }).when(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); /* Simulate network is initially up. */ NetworkStateHelper networkStateHelper = mock(NetworkStateHelper.class); when(networkStateHelper.isNetworkConnected()).thenReturn(true); /* Test call. */ - Ingestion decorator = new IngestionNetworkStateHandler(ingestion, networkStateHelper); - decorator.sendAsync(appSecret, installId, container, callback); - verify(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); - verify(callback).onCallSucceeded(); + HttpClient decorator = new HttpClientNetworkStateHandler(httpClient, networkStateHelper); + decorator.callAsync(url, METHOD_GET, headers, callTemplate, callback); + verify(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); + verify(callback).onCallSucceeded(""); verifyNoMoreInteractions(callback); /* Close. */ decorator.close(); - verify(ingestion).close(); + verify(httpClient).close(); } @Test public void failure() throws IOException { /* Configure mock wrapped API. */ - String appSecret = UUIDUtils.randomUUID().toString(); - UUID installId = UUIDUtils.randomUUID(); - LogContainer container = mock(LogContainer.class); + String url = "http://mock/call"; + Map headers = new HashMap<>(); + final HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); final ServiceCallback callback = mock(ServiceCallback.class); final ServiceCall call = mock(ServiceCall.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ServiceCallback serviceCallback = (ServiceCallback) invocationOnMock.getArguments()[3]; + ServiceCallback serviceCallback = (ServiceCallback) invocationOnMock.getArguments()[4]; serviceCallback.onCallFailed(new HttpException(503)); serviceCallback.onCallFailed(new SocketException()); return call; } - }).when(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + }).when(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); /* Simulate network is initially up. */ NetworkStateHelper networkStateHelper = mock(NetworkStateHelper.class); when(networkStateHelper.isNetworkConnected()).thenReturn(true); /* Test call. */ - Ingestion decorator = new IngestionNetworkStateHandler(ingestion, networkStateHelper); - decorator.sendAsync(appSecret, installId, container, callback); - verify(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + HttpClient decorator = new HttpClientNetworkStateHandler(httpClient, networkStateHelper); + decorator.callAsync(url, METHOD_GET, headers, callTemplate, callback); + verify(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); verify(callback).onCallFailed(new HttpException(503)); verifyNoMoreInteractions(callback); /* Close. */ decorator.close(); - verify(ingestion).close(); + verify(httpClient).close(); } @Test public void networkDownBecomesUp() throws IOException { /* Configure mock wrapped API. */ - String appSecret = UUIDUtils.randomUUID().toString(); - UUID installId = UUIDUtils.randomUUID(); - LogContainer container = mock(LogContainer.class); + String url = "http://mock/call"; + Map headers = new HashMap<>(); + final HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); final ServiceCallback callback = mock(ServiceCallback.class); final ServiceCall call = mock(ServiceCall.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallSucceeded(); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); return call; } - }).when(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + }).when(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); /* Simulate network down then becomes up. */ NetworkStateHelper networkStateHelper = mock(NetworkStateHelper.class); when(networkStateHelper.isNetworkConnected()).thenReturn(false).thenReturn(true); /* Test call. */ - IngestionNetworkStateHandler decorator = new IngestionNetworkStateHandler(ingestion, networkStateHelper); - decorator.sendAsync(appSecret, installId, container, callback); + HttpClientNetworkStateHandler decorator = new HttpClientNetworkStateHandler(httpClient, networkStateHelper); + decorator.callAsync(url, METHOD_GET, headers, callTemplate, callback); /* Network is down: no call to target API must be done. */ - verify(ingestion, times(0)).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); - verify(callback, times(0)).onCallSucceeded(); + verify(httpClient, times(0)).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); + verify(callback, times(0)).onCallSucceeded(""); /* Network now up: call must be done and succeed. */ decorator.onNetworkStateUpdated(true); - verify(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); - verify(callback).onCallSucceeded(); + verify(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); + verify(callback).onCallSucceeded(""); /* Close. */ decorator.close(); - verify(ingestion).close(); + verify(httpClient).close(); } @Test public void networkDownCancelBeforeUp() throws IOException { /* Configure mock wrapped API. */ - String appSecret = UUIDUtils.randomUUID().toString(); - UUID installId = UUIDUtils.randomUUID(); - LogContainer container = mock(LogContainer.class); + String url = "http://mock/call"; + Map headers = new HashMap<>(); + final HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); final ServiceCallback callback = mock(ServiceCallback.class); final ServiceCall call = mock(ServiceCall.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallSucceeded(); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); return call; } - }).when(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + }).when(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); /* Simulate network down then becomes up. */ NetworkStateHelper networkStateHelper = mock(NetworkStateHelper.class); when(networkStateHelper.isNetworkConnected()).thenReturn(false).thenReturn(true); /* Test call and cancel right away. */ - IngestionNetworkStateHandler decorator = new IngestionNetworkStateHandler(ingestion, networkStateHelper); - decorator.sendAsync(appSecret, installId, container, callback).cancel(); + HttpClientNetworkStateHandler decorator = new HttpClientNetworkStateHandler(httpClient, networkStateHelper); + decorator.callAsync(url, METHOD_GET, headers, callTemplate, callback).cancel(); /* Network now up, verify no interaction with anything. */ decorator.onNetworkStateUpdated(true); - verifyNoMoreInteractions(ingestion); + verifyNoMoreInteractions(httpClient); verifyNoMoreInteractions(call); verifyNoMoreInteractions(callback); /* Close. */ decorator.close(); - verify(ingestion).close(); + verify(httpClient).close(); } @Test public void cancelRunningCall() throws InterruptedException, IOException { /* Configure mock wrapped API. */ - String appSecret = UUIDUtils.randomUUID().toString(); - UUID installId = UUIDUtils.randomUUID(); - LogContainer container = mock(LogContainer.class); + String url = "http://mock/call"; + Map headers = new HashMap<>(); + final HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); final ServiceCallback callback = mock(ServiceCallback.class); final ServiceCall call = mock(ServiceCall.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); final AtomicReference threadRef = new AtomicReference<>(); doAnswer(new Answer() { @@ -202,7 +199,7 @@ public ServiceCall answer(final InvocationOnMock invocationOnMock) throws Throwa public void run() { try { sleep(200); - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallSucceeded(); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); } catch (InterruptedException e) { e.printStackTrace(); } @@ -212,7 +209,7 @@ public void run() { threadRef.set(thread); return call; } - }).when(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + }).when(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); doAnswer(new Answer() { @Override @@ -227,8 +224,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { when(networkStateHelper.isNetworkConnected()).thenReturn(true); /* Test call. */ - IngestionNetworkStateHandler decorator = new IngestionNetworkStateHandler(ingestion, networkStateHelper); - ServiceCall decoratorCall = decorator.sendAsync(appSecret, installId, container, callback); + HttpClientNetworkStateHandler decorator = new HttpClientNetworkStateHandler(httpClient, networkStateHelper); + ServiceCall decoratorCall = decorator.callAsync(url, METHOD_GET, headers, callTemplate, callback); /* Wait some time. */ Thread.sleep(100); @@ -237,25 +234,25 @@ public Object answer(InvocationOnMock invocation) throws Throwable { decoratorCall.cancel(); /* Verify that the call was attempted then canceled. */ - verify(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + verify(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); verify(call).cancel(); verifyNoMoreInteractions(callback); /* Close. */ decorator.close(); - verify(ingestion).close(); + verify(httpClient).close(); } @Test public void cancelRunningCallByClosing() throws InterruptedException, IOException { /* Configure mock wrapped API. */ - String appSecret = UUIDUtils.randomUUID().toString(); - UUID installId = UUIDUtils.randomUUID(); - LogContainer container = mock(LogContainer.class); + String url = "http://mock/call"; + Map headers = new HashMap<>(); + final HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); final ServiceCallback callback = mock(ServiceCallback.class); final ServiceCall call = mock(ServiceCall.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); final AtomicReference threadRef = new AtomicReference<>(); doAnswer(new Answer() { @@ -267,7 +264,7 @@ public ServiceCall answer(final InvocationOnMock invocationOnMock) throws Throwa public void run() { try { sleep(200); - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallSucceeded(); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); } catch (InterruptedException e) { e.printStackTrace(); } @@ -277,7 +274,7 @@ public void run() { threadRef.set(thread); return call; } - }).when(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + }).when(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); doAnswer(new Answer() { @Override @@ -285,15 +282,15 @@ public Object answer(InvocationOnMock invocation) throws Throwable { threadRef.get().interrupt(); return null; } - }).when(ingestion).close(); + }).when(httpClient).close(); /* Simulate network down then becomes up. */ NetworkStateHelper networkStateHelper = mock(NetworkStateHelper.class); when(networkStateHelper.isNetworkConnected()).thenReturn(true); /* Test call. */ - IngestionNetworkStateHandler decorator = new IngestionNetworkStateHandler(ingestion, networkStateHelper); - decorator.sendAsync(appSecret, installId, container, callback); + HttpClientNetworkStateHandler decorator = new HttpClientNetworkStateHandler(httpClient, networkStateHelper); + decorator.callAsync(url, METHOD_GET, headers, callTemplate, callback); /* Wait some time. */ Thread.sleep(100); @@ -302,8 +299,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { decorator.close(); /* Verify that the call was attempted then canceled. */ - verify(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); - verify(ingestion).close(); + verify(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); + verify(httpClient).close(); verify(call).cancel(); verifyNoMoreInteractions(callback); } @@ -312,12 +309,12 @@ public Object answer(InvocationOnMock invocation) throws Throwable { public void networkLossDuringCall() throws InterruptedException, IOException { /* Configure mock wrapped API. */ - String appSecret = UUIDUtils.randomUUID().toString(); - UUID installId = UUIDUtils.randomUUID(); - LogContainer container = mock(LogContainer.class); + String url = "http://mock/call"; + Map headers = new HashMap<>(); + final HttpClient.CallTemplate callTemplate = mock(HttpClient.CallTemplate.class); final ServiceCallback callback = mock(ServiceCallback.class); final ServiceCall call = mock(ServiceCall.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); final AtomicReference threadRef = new AtomicReference<>(); doAnswer(new Answer() { @@ -329,7 +326,7 @@ public ServiceCall answer(final InvocationOnMock invocationOnMock) throws Throwa public void run() { try { sleep(200); - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallSucceeded(); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); } catch (InterruptedException e) { e.printStackTrace(); } @@ -339,7 +336,7 @@ public void run() { threadRef.set(thread); return call; } - }).when(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + }).when(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); doAnswer(new Answer() { @Override @@ -354,8 +351,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { when(networkStateHelper.isNetworkConnected()).thenReturn(true).thenReturn(false).thenReturn(true); /* Test call. */ - IngestionNetworkStateHandler decorator = new IngestionNetworkStateHandler(ingestion, networkStateHelper); - decorator.sendAsync(appSecret, installId, container, callback); + HttpClientNetworkStateHandler decorator = new HttpClientNetworkStateHandler(httpClient, networkStateHelper); + decorator.callAsync(url, METHOD_GET, headers, callTemplate, callback); /* Wait some time. */ Thread.sleep(100); @@ -364,28 +361,19 @@ public Object answer(InvocationOnMock invocation) throws Throwable { decorator.onNetworkStateUpdated(false); /* Verify that the call was attempted then canceled. */ - verify(ingestion).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + verify(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); verify(call).cancel(); verifyNoMoreInteractions(callback); /* Then up again. */ decorator.onNetworkStateUpdated(true); - verify(ingestion, times(2)).sendAsync(eq(appSecret), eq(installId), eq(container), any(ServiceCallback.class)); + verify(httpClient, times(2)).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); Thread.sleep(300); - verify(callback).onCallSucceeded(); + verify(callback).onCallSucceeded(""); verifyNoMoreInteractions(callback); /* Close. */ decorator.close(); - verify(ingestion).close(); - } - - @Test - public void setServerUrl() { - Ingestion ingestion = mock(Ingestion.class); - Ingestion retryer = new IngestionNetworkStateHandler(ingestion, mock(NetworkStateHelper.class)); - String serverUrl = "http://someServerUrl"; - retryer.setServerUrl(serverUrl); - verify(ingestion).setServerUrl(serverUrl); + verify(httpClient).close(); } } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java similarity index 67% rename from sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryerTest.java rename to sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java index 7d429c939e..caf4a0659d 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java @@ -1,12 +1,7 @@ -package com.microsoft.azure.mobile.ingestion.http; +package com.microsoft.azure.mobile.http; import android.os.Handler; -import com.microsoft.azure.mobile.ingestion.Ingestion; -import com.microsoft.azure.mobile.ingestion.ServiceCall; -import com.microsoft.azure.mobile.ingestion.ServiceCallback; -import com.microsoft.azure.mobile.ingestion.models.LogContainer; - import org.junit.Test; import org.mockito.ArgumentMatcher; import org.mockito.invocation.InvocationOnMock; @@ -14,9 +9,9 @@ import java.net.SocketException; import java.net.UnknownHostException; -import java.util.UUID; import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.longThat; import static org.mockito.Mockito.any; @@ -26,7 +21,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; @SuppressWarnings("unused") -public class IngestionRetryerTest { +public class HttpClientRetryerTest { private static void simulateRetryAfterDelay(Handler handler) { doAnswer(new Answer() { @@ -46,7 +41,7 @@ private static void verifyDelay(Handler handler, final int retryIndex) { @Override public boolean matches(Object argument) { long interval = (Long) argument; - long retryInterval = IngestionRetryer.RETRY_INTERVALS[retryIndex]; + long retryInterval = HttpClientRetryer.RETRY_INTERVALS[retryIndex]; return interval >= retryInterval / 2 && interval <= retryInterval; } })); @@ -56,18 +51,18 @@ public boolean matches(Object argument) { public void success() { final ServiceCall call = mock(ServiceCall.class); final ServiceCallback callback = mock(ServiceCallback.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallSucceeded(); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); return call; } - }).when(ingestion).sendAsync(anyString(), any(UUID.class), any(LogContainer.class), any(ServiceCallback.class)); - Ingestion retryer = new IngestionRetryer(ingestion); - retryer.sendAsync(null, null, null, callback); - verify(callback).onCallSucceeded(); + }).when(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + HttpClientRetryer retryer = new HttpClientRetryer(httpClient); + retryer.callAsync(null, null, null, null, callback); + verify(callback).onCallSucceeded(""); verifyNoMoreInteractions(callback); verifyNoMoreInteractions(call); } @@ -75,29 +70,29 @@ public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { @Test public void successAfterOneRetry() { final ServiceCallback callback = mock(ServiceCallback.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallFailed(new SocketException()); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallFailed(new SocketException()); return mock(ServiceCall.class); } }).doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallSucceeded(); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); return mock(ServiceCall.class); } - }).when(ingestion).sendAsync(anyString(), any(UUID.class), any(LogContainer.class), any(ServiceCallback.class)); + }).when(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); Handler handler = mock(Handler.class); - Ingestion retryer = new IngestionRetryer(ingestion, handler); + HttpClient retryer = new HttpClientRetryer(httpClient, handler); simulateRetryAfterDelay(handler); - retryer.sendAsync(null, null, null, callback); + retryer.callAsync(null, null, null, null, callback); verifyDelay(handler, 0); verifyNoMoreInteractions(handler); - verify(callback).onCallSucceeded(); + verify(callback).onCallSucceeded(""); verifyNoMoreInteractions(callback); } @@ -105,26 +100,26 @@ public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { public void retryOnceThenFail() { final HttpException expectedException = new HttpException(403); final ServiceCallback callback = mock(ServiceCallback.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallFailed(new UnknownHostException()); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallFailed(new UnknownHostException()); return mock(ServiceCall.class); } }).doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallFailed(expectedException); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallFailed(expectedException); return mock(ServiceCall.class); } - }).when(ingestion).sendAsync(anyString(), any(UUID.class), any(LogContainer.class), any(ServiceCallback.class)); + }).when(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); Handler handler = mock(Handler.class); - Ingestion retryer = new IngestionRetryer(ingestion, handler); + HttpClient retryer = new HttpClientRetryer(httpClient, handler); simulateRetryAfterDelay(handler); - retryer.sendAsync(null, null, null, callback); + retryer.callAsync(null, null, null, null, callback); verifyDelay(handler, 0); verifyNoMoreInteractions(handler); verify(callback).onCallFailed(any(Exception.class)); @@ -136,19 +131,19 @@ public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { public void exhaustRetries() { final ServiceCall call = mock(ServiceCall.class); ServiceCallback callback = mock(ServiceCallback.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallFailed(new HttpException(429)); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallFailed(new HttpException(429)); return call; } - }).when(ingestion).sendAsync(anyString(), any(UUID.class), any(LogContainer.class), any(ServiceCallback.class)); + }).when(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); Handler handler = mock(Handler.class); - Ingestion retryer = new IngestionRetryer(ingestion, handler); + HttpClient retryer = new HttpClientRetryer(httpClient, handler); simulateRetryAfterDelay(handler); - retryer.sendAsync(null, null, null, callback); + retryer.callAsync(null, null, null, null, callback); verifyDelay(handler, 0); verifyDelay(handler, 1); verifyDelay(handler, 2); @@ -162,29 +157,20 @@ public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { public void cancel() throws InterruptedException { final ServiceCall call = mock(ServiceCall.class); ServiceCallback callback = mock(ServiceCallback.class); - Ingestion ingestion = mock(Ingestion.class); + HttpClient httpClient = mock(HttpClient.class); doAnswer(new Answer() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[3]).onCallFailed(new HttpException(503)); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallFailed(new HttpException(503)); return call; } - }).when(ingestion).sendAsync(anyString(), any(UUID.class), any(LogContainer.class), any(ServiceCallback.class)); + }).when(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); Handler handler = mock(Handler.class); - Ingestion retryer = new IngestionRetryer(ingestion, handler); - retryer.sendAsync(null, null, null, callback).cancel(); + HttpClient retryer = new HttpClientRetryer(httpClient, handler); + retryer.callAsync(null, null, null, null, callback).cancel(); Thread.sleep(500); verifyNoMoreInteractions(callback); verify(call).cancel(); } - - @Test - public void setServerUrl() { - Ingestion ingestion = mock(Ingestion.class); - Ingestion retryer = new IngestionRetryer(ingestion, mock(Handler.class)); - String serverUrl = "http://someServerUrl"; - retryer.setServerUrl(serverUrl); - verify(ingestion).setServerUrl(serverUrl); - } } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/HttpExceptionTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpExceptionTest.java similarity index 94% rename from sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/HttpExceptionTest.java rename to sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpExceptionTest.java index 4dac6eaefa..044c839b24 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/HttpExceptionTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpExceptionTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.ingestion.http; +package com.microsoft.azure.mobile.http; import org.junit.Test; diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java new file mode 100644 index 0000000000..e7b6024564 --- /dev/null +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java @@ -0,0 +1,173 @@ +package com.microsoft.azure.mobile.ingestion; + +import android.content.Context; + +import com.microsoft.azure.mobile.http.DefaultHttpClient; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.ingestion.models.Log; +import com.microsoft.azure.mobile.ingestion.models.LogContainer; +import com.microsoft.azure.mobile.ingestion.models.json.LogSerializer; +import com.microsoft.azure.mobile.utils.UUIDUtils; + +import org.json.JSONException; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_POST; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.notNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@SuppressWarnings("unused") +@PrepareForTest(IngestionHttp.class) +public class IngestionHttpTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Test + public void sendAsync() throws Exception { + + /* Build some payload. */ + LogContainer container = new LogContainer(); + Log log = mock(Log.class); + long logAbsoluteTime = 123L; + when(log.getToffset()).thenReturn(logAbsoluteTime); + List logs = new ArrayList<>(); + logs.add(log); + container.setLogs(logs); + LogSerializer serializer = mock(LogSerializer.class); + when(serializer.serializeContainer(any(LogContainer.class))).thenReturn("mockPayload"); + + /* Stable time. */ + mockStatic(System.class); + long now = 456L; + when(System.currentTimeMillis()).thenReturn(now); + + /* Configure mock HTTP. */ + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + final ServiceCall call = mock(ServiceCall.class); + final AtomicReference callTemplate = new AtomicReference<>(); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).then(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + callTemplate.set((HttpClient.CallTemplate) invocation.getArguments()[3]); + return call; + } + }); + + /* Test calling code. */ + IngestionHttp ingestionHttp = new IngestionHttp(mock(Context.class), serializer); + ingestionHttp.setServerUrl("http://mock"); + String appSecret = UUIDUtils.randomUUID().toString(); + UUID installId = UUIDUtils.randomUUID(); + ServiceCallback serviceCallback = mock(ServiceCallback.class); + assertEquals(call, ingestionHttp.sendAsync(appSecret, installId, container, serviceCallback)); + + /* Verify call to http client. */ + HashMap expectedHeaders = new HashMap<>(); + expectedHeaders.put(DefaultHttpClient.APP_SECRET, appSecret); + expectedHeaders.put("Install-ID", installId.toString()); + verify(httpClient).callAsync(eq("http://mock/logs?api_version=1.0.0-preview20160914"), eq(METHOD_POST), eq(expectedHeaders), notNull(HttpClient.CallTemplate.class), eq(serviceCallback)); + assertNotNull(callTemplate.get()); + assertEquals("mockPayload", callTemplate.get().buildRequestBody()); + + /* Verify toffset manipulation. */ + verify(log).setToffset(now - logAbsoluteTime); + verify(log).setToffset(logAbsoluteTime); + + /* Verify close. */ + ingestionHttp.close(); + verify(httpClient).close(); + } + + @Test + public void failedSerialization() throws Exception { + + /* Build some payload. */ + LogContainer container = new LogContainer(); + Log log = mock(Log.class); + long logAbsoluteTime = 123L; + when(log.getToffset()).thenReturn(logAbsoluteTime); + List logs = new ArrayList<>(); + logs.add(log); + container.setLogs(logs); + LogSerializer serializer = mock(LogSerializer.class); + JSONException exception = new JSONException("mock"); + when(serializer.serializeContainer(any(LogContainer.class))).thenThrow(exception); + + /* Stable time. */ + mockStatic(System.class); + long now = 456L; + when(System.currentTimeMillis()).thenReturn(now); + + /* Configure mock HTTP. */ + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + final ServiceCall call = mock(ServiceCall.class); + final AtomicReference callTemplate = new AtomicReference<>(); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).then(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + callTemplate.set((HttpClient.CallTemplate) invocation.getArguments()[3]); + return call; + } + }); + + /* Test calling code. */ + IngestionHttp ingestionHttp = new IngestionHttp(mock(Context.class), serializer); + ingestionHttp.setServerUrl("http://mock"); + String appSecret = UUIDUtils.randomUUID().toString(); + UUID installId = UUIDUtils.randomUUID(); + ServiceCallback serviceCallback = mock(ServiceCallback.class); + assertEquals(call, ingestionHttp.sendAsync(appSecret, installId, container, serviceCallback)); + + /* Verify call to http client. */ + HashMap expectedHeaders = new HashMap<>(); + expectedHeaders.put(DefaultHttpClient.APP_SECRET, appSecret); + expectedHeaders.put("Install-ID", installId.toString()); + verify(httpClient).callAsync(eq("http://mock/logs?api_version=1.0.0-preview20160914"), eq(METHOD_POST), eq(expectedHeaders), notNull(HttpClient.CallTemplate.class), eq(serviceCallback)); + assertNotNull(callTemplate.get()); + + try { + callTemplate.get().buildRequestBody(); + Assert.fail("Expected json exception"); + } catch (JSONException e) { + e.printStackTrace(); + } + + /* Verify toffset manipulation. */ + verify(log).setToffset(now - logAbsoluteTime); + verify(log).setToffset(logAbsoluteTime); + + /* Verify close. */ + ingestionHttp.close(); + verify(httpClient).close(); + } +} \ No newline at end of file From 165a1813b8457f126b254beebfbeb42dc5920a0f Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 27 Jan 2017 19:57:11 -0800 Subject: [PATCH 007/142] Fix refactoring mistakes in http logic --- .../java/com/microsoft/azure/mobile/ingestion/Ingestion.java | 4 ++-- .../azure/mobile/channel/DefaultChannelRaceConditionTest.java | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java index 4877291eea..7dcb6413db 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java @@ -8,12 +8,12 @@ import java.util.UUID; /** - * The interface for HttpClient class. + * Interface to send logs to the Ingestion service. */ public interface Ingestion extends Closeable { /** - * Send logs to the HttpClient service. + * Send logs to the Ingestion service. * * @param appSecret a unique and secret key used to identify the application. * @param installId install identifier. diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelRaceConditionTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelRaceConditionTest.java index 4ddbd68c88..b446b84565 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelRaceConditionTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelRaceConditionTest.java @@ -30,7 +30,6 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.notNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -371,7 +370,7 @@ public Object answer(final InvocationOnMock invocation) throws Throwable { @Override public void run() { beforeCallSemaphore.acquireUninterruptibly(); - ((ServiceCallback) invocation.getArguments()[3]).onCallSucceeded(notNull(String.class)); + ((ServiceCallback) invocation.getArguments()[3]).onCallSucceeded(""); afterCallSemaphore.release(); } }.start(); From f2cafc0878cfd37aba2e9c933f92b452d5d04d4b Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 30 Jan 2017 11:18:09 -0800 Subject: [PATCH 008/142] Fix licence badge By changing color, it defeats the invalid cache of github... --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd8b2a4cb1..522b1737b8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![codecov](https://codecov.io/gh/Microsoft/mobile-center-sdk-android/branch/develop/graph/badge.svg?token=YwMZRPnYK3)](https://codecov.io/gh/Microsoft/mobile-center-sdk-android) [![GitHub Release](https://img.shields.io/github/release/Microsoft/mobile-center-sdk-android.svg)](https://github.com/Microsoft/mobile-center-sdk-android/releases/latest) [![Bintray](https://api.bintray.com/packages/mobile-center/mobile-center/mobile-center/images/download.svg)](https://bintray.com/mobile-center/mobile-center) -[![license](https://img.shields.io/badge/license-MIT%20License-yellow.svg)](https://github.com/Microsoft/mobile-center-sdk-android/blob/develop/license.txt) +[![license](https://img.shields.io/badge/license-MIT%20License-00AAAA.svg)](https://github.com/Microsoft/mobile-center-sdk-android/blob/develop/license.txt) # Mobile Center SDK for Android From fb017e3c8c5bb8081b5242958799e2e60080bf67 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 30 Jan 2017 12:33:26 -0800 Subject: [PATCH 009/142] Test coverage for optional channel group in abstract service --- .../mobile/AbstractMobileCenterService.java | 1 + .../AbstractMobileCenterServiceTest.java | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java index 7182cd863e..dabf756add 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java @@ -31,6 +31,7 @@ public abstract class AbstractMobileCenterService implements MobileCenterService * Maximum time interval in milliseconds after which a synchronize will be triggered, regardless of queue size. */ private static final int DEFAULT_TRIGGER_INTERVAL = 3 * 1000; + /** * Maximum number of requests being sent for the group. */ diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java index dffb8b0287..3a3d0335f7 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyStatic; @@ -167,4 +168,29 @@ public void onChannelReadyDisabledThenEnable() { public void getGroupName() { Assert.assertEquals("group_test", service.getGroupName()); } + + @Test + public void optionalGroup() { + service = new AbstractMobileCenterService() { + @Override + protected String getGroupName() { + return null; + } + + @Override + protected String getServiceName() { + return "Test"; + } + + @Override + protected String getLoggerTag() { + return "TestLog"; + } + }; + Channel channel = mock(Channel.class); + service.onChannelReady(mock(Context.class), channel); + service.setInstanceEnabled(false); + service.setInstanceEnabled(true); + verifyZeroInteractions(channel); + } } From f8918e3f1f4dcaa2f40d66f593b28237b90da1ec Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 30 Jan 2017 15:20:54 -0800 Subject: [PATCH 010/142] Pass appSecret to services * Updates need it. * onChannelReady is thus renamed onStarted. --- .../analytics/AnalyticsAndroidTest.java | 4 +- .../azure/mobile/analytics/Analytics.java | 4 +- .../azure/mobile/analytics/AnalyticsTest.java | 12 ++--- .../mobile/crashes/CrashesAndroidTest.java | 18 ++++---- .../azure/mobile/crashes/Crashes.java | 4 +- .../azure/mobile/crashes/CrashesTest.java | 46 +++++++++---------- .../azure/mobile/updates/Updates.java | 4 +- .../mobile/AbstractMobileCenterService.java | 2 +- .../microsoft/azure/mobile/MobileCenter.java | 11 ++++- .../azure/mobile/MobileCenterService.java | 9 ++-- .../AbstractMobileCenterServiceTest.java | 6 +-- .../azure/mobile/MobileCenterTest.java | 44 ++++++++++-------- 12 files changed, 88 insertions(+), 76 deletions(-) diff --git a/sdk/mobile-center-analytics/src/androidTest/java/com/microsoft/azure/mobile/analytics/AnalyticsAndroidTest.java b/sdk/mobile-center-analytics/src/androidTest/java/com/microsoft/azure/mobile/analytics/AnalyticsAndroidTest.java index a2994477e7..d9f6615db3 100644 --- a/sdk/mobile-center-analytics/src/androidTest/java/com/microsoft/azure/mobile/analytics/AnalyticsAndroidTest.java +++ b/sdk/mobile-center-analytics/src/androidTest/java/com/microsoft/azure/mobile/analytics/AnalyticsAndroidTest.java @@ -58,7 +58,7 @@ public void testAnalyticsListener() { AnalyticsListener analyticsListener = mock(AnalyticsListener.class); Analytics.setListener(analyticsListener); Channel channel = mock(Channel.class); - Analytics.getInstance().onChannelReady(sContext, channel); + Analytics.getInstance().onStarted(sContext, "", channel); Analytics.trackEvent("event"); /* First process: enqueue log but network is down... */ @@ -84,7 +84,7 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { }).when(channel).addGroup(anyString(), anyInt(), anyInt(), anyInt(), any(Channel.GroupListener.class)); Analytics.unsetInstance(); Analytics.setListener(analyticsListener); - Analytics.getInstance().onChannelReady(sContext, channel); + Analytics.getInstance().onStarted(sContext, "", channel); assertNotNull(groupListener.get()); groupListener.get().onSuccess(log); verify(channel, never()).enqueue(any(Log.class), anyString()); diff --git a/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java b/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java index 166b460113..45bd05e9d1 100644 --- a/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java +++ b/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java @@ -246,8 +246,8 @@ public Map getLogFactories() { } @Override - public synchronized void onChannelReady(@NonNull Context context, @NonNull Channel channel) { - super.onChannelReady(context, channel); + public synchronized void onStarted(@NonNull Context context, @NonNull String appSecret, @NonNull Channel channel) { + super.onStarted(context, appSecret, channel); applyEnabledState(isInstanceEnabled()); } diff --git a/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java b/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java index 6e774d21fe..d1f985da82 100644 --- a/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java +++ b/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java @@ -124,7 +124,7 @@ public void notInit() { private void activityResumed(final String expectedName, android.app.Activity activity) { Analytics analytics = Analytics.getInstance(); Channel channel = mock(Channel.class); - analytics.onChannelReady(mock(Context.class), channel); + analytics.onStarted(mock(Context.class), "", channel); analytics.onActivityResumed(activity); analytics.onActivityPaused(activity); verify(channel).enqueue(argThat(new ArgumentMatcher() { @@ -162,7 +162,7 @@ public void disableAutomaticPageTracking() { Analytics.setAutoPageTrackingEnabled(false); assertFalse(Analytics.isAutoPageTrackingEnabled()); Channel channel = mock(Channel.class); - analytics.onChannelReady(mock(Context.class), channel); + analytics.onStarted(mock(Context.class), "", channel); analytics.onActivityResumed(new MyActivity()); verify(channel).enqueue(argThat(new ArgumentMatcher() { @@ -198,7 +198,7 @@ public boolean matches(Object item) { public void trackEvent() { Analytics analytics = Analytics.getInstance(); Channel channel = mock(Channel.class); - analytics.onChannelReady(mock(Context.class), channel); + analytics.onStarted(mock(Context.class), "", channel); final String name = "testEvent"; final HashMap properties = new HashMap<>(); properties.put("a", "b"); @@ -225,7 +225,7 @@ public void setEnabled() { assertTrue(Analytics.isEnabled()); Analytics.setEnabled(false); assertFalse(Analytics.isEnabled()); - analytics.onChannelReady(mock(Context.class), channel); + analytics.onStarted(mock(Context.class), "", channel); verify(channel).clear(analytics.getGroupName()); verify(channel).removeGroup(eq(analytics.getGroupName())); Analytics.trackEvent("test"); @@ -272,7 +272,7 @@ public void startSessionAfterUserApproval() { */ Analytics analytics = Analytics.getInstance(); Channel channel = mock(Channel.class); - analytics.onChannelReady(mock(Context.class), channel); + analytics.onStarted(mock(Context.class), "", channel); Analytics.setEnabled(false); /* App in foreground: no log yet, we are disabled. */ @@ -316,7 +316,7 @@ public void startSessionAfterUserApprovalWeakReference() { */ Analytics analytics = Analytics.getInstance(); Channel channel = mock(Channel.class); - analytics.onChannelReady(mock(Context.class), channel); + analytics.onStarted(mock(Context.class), "", channel); Analytics.setEnabled(false); /* App in foreground: no log yet, we are disabled. */ diff --git a/sdk/mobile-center-crashes/src/androidTest/java/com/microsoft/azure/mobile/crashes/CrashesAndroidTest.java b/sdk/mobile-center-crashes/src/androidTest/java/com/microsoft/azure/mobile/crashes/CrashesAndroidTest.java index 6ff9b00320..8e367ae84c 100644 --- a/sdk/mobile-center-crashes/src/androidTest/java/com/microsoft/azure/mobile/crashes/CrashesAndroidTest.java +++ b/sdk/mobile-center-crashes/src/androidTest/java/com/microsoft/azure/mobile/crashes/CrashesAndroidTest.java @@ -78,7 +78,7 @@ public void getLastSessionCrashReport() throws InterruptedException { Thread.UncaughtExceptionHandler uncaughtExceptionHandler = mock(Thread.UncaughtExceptionHandler.class); Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler); Channel channel = mock(Channel.class); - Crashes.getInstance().onChannelReady(sContext, channel); + Crashes.getInstance().onStarted(sContext, "", channel); final Error exception = generateStackOverflowError(); assertTrue(exception.getStackTrace().length > ErrorLogHelper.FRAME_LIMIT); final Thread thread = new Thread() { @@ -93,7 +93,7 @@ public void run() { /* Get last session crash on 2nd process. */ Crashes.unsetInstance(); - Crashes.getInstance().onChannelReady(sContext, channel); + Crashes.getInstance().onStarted(sContext, "", channel); assertNotNull(Crashes.getLastSessionCrashReport()); /* Try to get last session crash after Crashes service completed processing. */ @@ -109,7 +109,7 @@ public void testNoDuplicateCallbacksOrSending() throws InterruptedException { Thread.UncaughtExceptionHandler uncaughtExceptionHandler = mock(Thread.UncaughtExceptionHandler.class); Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler); Channel channel = mock(Channel.class); - Crashes.getInstance().onChannelReady(sContext, channel); + Crashes.getInstance().onStarted(sContext, "", channel); CrashesListener crashesListener = mock(CrashesListener.class); when(crashesListener.shouldProcess(any(ErrorReport.class))).thenReturn(true); when(crashesListener.shouldAwaitUserConfirmation()).thenReturn(true); @@ -143,7 +143,7 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { }).when(channel).enqueue(any(Log.class), anyString()); Crashes.unsetInstance(); Crashes.setListener(crashesListener); - Crashes.getInstance().onChannelReady(sContext, channel); + Crashes.getInstance().onStarted(sContext, "", channel); waitForCrashesHandlerTasksToComplete(); /* Check last session error report. */ @@ -192,7 +192,7 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { }).when(channel).addGroup(anyString(), anyInt(), anyInt(), anyInt(), any(Channel.GroupListener.class)); Crashes.unsetInstance(); Crashes.setListener(crashesListener); - Crashes.getInstance().onChannelReady(sContext, channel); + Crashes.getInstance().onStarted(sContext, "", channel); waitForCrashesHandlerTasksToComplete(); assertFalse(Crashes.hasCrashedInLastSession()); Crashes.getLastSessionCrashReport(new ResultCallback() { @@ -227,7 +227,7 @@ public void cleanupFilesOnDisable() throws InterruptedException { android.util.Log.i(TAG, "Process 1"); Thread.UncaughtExceptionHandler uncaughtExceptionHandler = mock(Thread.UncaughtExceptionHandler.class); Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler); - Crashes.getInstance().onChannelReady(sContext, mock(Channel.class)); + Crashes.getInstance().onStarted(sContext, "", mock(Channel.class)); final RuntimeException exception = new RuntimeException(); final Thread thread = new Thread() { @@ -252,7 +252,7 @@ public void wrapperSdkOverrideLog() throws InterruptedException { Thread.UncaughtExceptionHandler uncaughtExceptionHandler = mock(Thread.UncaughtExceptionHandler.class); Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler); Channel channel = mock(Channel.class); - Crashes.getInstance().onChannelReady(sContext, channel); + Crashes.getInstance().onStarted(sContext, "", channel); Crashes.WrapperSdkListener wrapperSdkListener = mock(Crashes.WrapperSdkListener.class); Crashes.getInstance().setWrapperSdkListener(wrapperSdkListener); doAnswer(new Answer() { @@ -277,7 +277,7 @@ public void run() { thread.join(); verify(wrapperSdkListener).onCrashCaptured(notNull(ManagedErrorLog.class)); Crashes.unsetInstance(); - Crashes.getInstance().onChannelReady(sContext, channel); + Crashes.getInstance().onStarted(sContext, "", channel); waitForCrashesHandlerTasksToComplete(); Crashes.getLastSessionCrashReport(new ResultCallback() { @@ -297,7 +297,7 @@ public void setEnabledWhileAlreadyEnabledShouldNotDuplicateCrashReport() throws Thread.UncaughtExceptionHandler uncaughtExceptionHandler = mock(Thread.UncaughtExceptionHandler.class); Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler); Channel channel = mock(Channel.class); - Crashes.getInstance().onChannelReady(sContext, channel); + Crashes.getInstance().onStarted(sContext, "", channel); Crashes.setEnabled(true); final RuntimeException exception = new RuntimeException(); final Thread thread = new Thread() { diff --git a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java index 8183802c9c..2fd5388c73 100644 --- a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java +++ b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java @@ -339,8 +339,8 @@ public synchronized void setInstanceEnabled(boolean enabled) { } @Override - public synchronized void onChannelReady(@NonNull Context context, @NonNull Channel channel) { - super.onChannelReady(context, channel); + public synchronized void onStarted(@NonNull Context context, @NonNull String appSecret, @NonNull Channel channel) { + super.onStarted(context, appSecret, channel); mContext = context; initialize(); if (isInstanceEnabled()) { diff --git a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java index d013d34d37..3318eb58a2 100644 --- a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java +++ b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java @@ -184,7 +184,7 @@ public void initializeWhenDisabled() { when(dir.listFiles()).thenReturn(new File[]{file1, file2}); crashes.setUncaughtExceptionHandler(mockHandler); crashes.setInstanceEnabled(false); - crashes.onChannelReady(mock(Context.class), mock(Channel.class)); + crashes.onStarted(mock(Context.class), "", mock(Channel.class)); /* Test. */ assertFalse(Crashes.isEnabled()); @@ -240,7 +240,7 @@ public void setEnabled() { Crashes.setEnabled(false); assertFalse(Crashes.isEnabled()); verify(mMockLooper).quit(); - crashes.onChannelReady(mock(Context.class), mockChannel); + crashes.onStarted(mock(Context.class), "", mockChannel); verify(mockChannel).clear(crashes.getGroupName()); verify(mockChannel).removeGroup(eq(crashes.getGroupName())); assertEquals(crashes.getInitializeTimestamp(), -1); @@ -306,7 +306,7 @@ public void queuePendingCrashesShouldProcess() throws IOException, ClassNotFound crashes.setLogSerializer(logSerializer); crashes.setInstanceListener(mockListener); - crashes.onChannelReady(mockContext, mockChannel); + crashes.onStarted(mockContext, "", mockChannel); verify(mockListener).shouldProcess(report); verify(mockListener).shouldAwaitUserConfirmation(); @@ -344,7 +344,7 @@ public void queuePendingCrashesShouldNotProcess() throws IOException, ClassNotFo crashes.setLogSerializer(logSerializer); crashes.setInstanceListener(mockListener); - crashes.onChannelReady(mockContext, mockChannel); + crashes.onStarted(mockContext, "", mockChannel); verify(mockListener).shouldProcess(report); verify(mockListener, never()).shouldAwaitUserConfirmation(); @@ -378,7 +378,7 @@ public void queuePendingCrashesAlwaysSend() throws IOException, ClassNotFoundExc crashes.setLogSerializer(logSerializer); crashes.setInstanceListener(mockListener); - crashes.onChannelReady(mockContext, mockChannel); + crashes.onStarted(mockContext, "", mockChannel); verify(mockListener).shouldProcess(report); verify(mockListener, never()).shouldAwaitUserConfirmation(); @@ -409,7 +409,7 @@ public void processPendingErrorsCorrupted() throws JSONException { crashes.setInstanceListener(listener); Channel channel = mock(Channel.class); - crashes.onChannelReady(mock(Context.class), channel); + crashes.onStarted(mock(Context.class), "", channel); verifyZeroInteractions(listener); verify(channel, never()).enqueue(any(Log.class), anyString()); } @@ -445,7 +445,7 @@ public String answer(InvocationOnMock invocation) throws Throwable { Crashes crashes = Crashes.getInstance(); crashes.setLogSerializer(logSerializer); crashes.setInstanceListener(listener); - crashes.onChannelReady(mock(Context.class), channel); + crashes.onStarted(mock(Context.class), "", channel); verify(channel, never()).enqueue(any(Log.class), anyString()); verify(listener).shouldProcess(errorReport); @@ -457,7 +457,7 @@ public String answer(InvocationOnMock invocation) throws Throwable { crashes = Crashes.getInstance(); crashes.setLogSerializer(logSerializer); crashes.setInstanceListener(listener); - crashes.onChannelReady(mock(Context.class), channel); + crashes.onStarted(mock(Context.class), "", channel); verify(channel, never()).enqueue(any(Log.class), anyString()); verify(listener, times(2)).shouldProcess(errorReport); @@ -498,7 +498,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { Crashes crashes = Crashes.getInstance(); crashes.setLogSerializer(logSerializer); crashes.setInstanceListener(listener); - crashes.onChannelReady(mock(Context.class), channel); + crashes.onStarted(mock(Context.class), "", channel); verify(mMockLooper).quit(); verify(listener, times(2)).shouldProcess(any(ErrorReport.class)); @@ -520,7 +520,7 @@ public void noQueueingWhenDisabled() { Crashes.setEnabled(false); Crashes crashes = Crashes.getInstance(); - crashes.onChannelReady(mock(Context.class), mock(Channel.class)); + crashes.onStarted(mock(Context.class), "", mock(Channel.class)); verifyStatic(); ErrorLogHelper.getErrorStorageDirectory(); @@ -540,7 +540,7 @@ public void noQueueNullLog() throws JSONException { when(logSerializer.deserializeLog(anyString())).thenReturn(null); crashes.setLogSerializer(logSerializer); - crashes.onChannelReady(mockContext, mockChannel); + crashes.onStarted(mockContext, "", mockChannel); verify(mockChannel, never()).enqueue(any(Log.class), anyString()); } @@ -560,7 +560,7 @@ public void printErrorOnJSONException() throws JSONException { when(logSerializer.deserializeLog(anyString())).thenThrow(jsonException); crashes.setLogSerializer(logSerializer); - crashes.onChannelReady(mockContext, mockChannel); + crashes.onStarted(mockContext, "", mockChannel); verify(mockChannel, never()).enqueue(any(Log.class), anyString()); @@ -585,7 +585,7 @@ public void trackException() { /* Track exception test. */ Crashes crashes = Crashes.getInstance(); Channel mockChannel = mock(Channel.class); - crashes.onChannelReady(mock(Context.class), mockChannel); + crashes.onStarted(mock(Context.class), "", mockChannel); Crashes.trackException(EXCEPTION); verify(mockChannel).enqueue(argThat(new ArgumentMatcher() { @@ -626,7 +626,7 @@ public void trackExceptionForWrapperSdk() { Crashes.getInstance().trackException(exception); verify(mockChannel, never()).enqueue(any(Log.class), eq(crashes.getGroupName())); - crashes.onChannelReady(mock(Context.class), mockChannel); + crashes.onStarted(mock(Context.class), "", mockChannel); Crashes.getInstance().trackException(exception); verify(mockChannel).enqueue(argThat(new ArgumentMatcher() { @@ -717,7 +717,7 @@ public void handleUserConfirmationDoNotSend() throws IOException, ClassNotFoundE crashes.setLogSerializer(logSerializer); crashes.setInstanceListener(mockListener); - crashes.onChannelReady(mock(Context.class), mock(Channel.class)); + crashes.onStarted(mock(Context.class), "", mock(Channel.class)); Crashes.notifyUserConfirmation(Crashes.DONT_SEND); @@ -755,7 +755,7 @@ public void handleUserConfirmationAlwaysSend() throws IOException, ClassNotFound crashes.setLogSerializer(logSerializer); crashes.setInstanceListener(mockListener); - crashes.onChannelReady(mock(Context.class), mock(Channel.class)); + crashes.onStarted(mock(Context.class), "", mock(Channel.class)); Crashes.notifyUserConfirmation(Crashes.ALWAYS_SEND); @@ -878,7 +878,7 @@ public void onResult(ErrorReport data) { }; Crashes.getLastSessionCrashReport(callback); - Crashes.getInstance().onChannelReady(mock(Context.class), mock(Channel.class)); + Crashes.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); assertFalse(Crashes.hasCrashedInLastSession()); Crashes.getLastSessionCrashReport(callback); } @@ -938,7 +938,7 @@ public String answer(InvocationOnMock invocation) throws Throwable { * Here the service is enabled by default but we are waiting channel to be ready, simulate that. */ assertTrue(Crashes.isEnabled()); - Crashes.getInstance().onChannelReady(mock(Context.class), mock(Channel.class)); + Crashes.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); assertTrue(Crashes.hasCrashedInLastSession()); @@ -1010,7 +1010,7 @@ public void onResult(ErrorReport data) { */ assertTrue(Crashes.isEnabled()); Crashes.getLastSessionCrashReport(callback); - Crashes.getInstance().onChannelReady(mock(Context.class), mock(Channel.class)); + Crashes.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); assertFalse(Crashes.hasCrashedInLastSession()); Crashes.getLastSessionCrashReport(callback); @@ -1029,7 +1029,7 @@ public void crashInLastSessionCorrupted() throws IOException { File file = errorStorageDirectory.newFile("last-error-log.json"); when(ErrorLogHelper.getStoredErrorLogFiles()).thenReturn(new File[]{file}); when(ErrorLogHelper.getLastErrorLogFile()).thenReturn(file); - Crashes.getInstance().onChannelReady(mock(Context.class), mock(Channel.class)); + Crashes.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); assertFalse(Crashes.hasCrashedInLastSession()); Crashes.getLastSessionCrashReport(new ResultCallback() { @@ -1057,7 +1057,7 @@ public void onResult(ErrorReport data) { /* Call twice for multiple callbacks before initialize. */ Crashes.getLastSessionCrashReport(callback); Crashes.getLastSessionCrashReport(callback); - Crashes.getInstance().onChannelReady(mock(Context.class), mock(Channel.class)); + Crashes.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); assertFalse(Crashes.hasCrashedInLastSession()); } @@ -1071,7 +1071,7 @@ public void getLastSessionCrashReportInterrupted() throws Exception { when(ErrorLogHelper.getLastErrorLogFile()).thenReturn(mock(File.class)); when(ErrorLogHelper.getStoredErrorLogFiles()).thenReturn(new File[0]); - Crashes.getInstance().onChannelReady(mock(Context.class), mock(Channel.class)); + Crashes.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); verifyStatic(); MobileCenterLog.error(anyString(), anyString()); @@ -1100,7 +1100,7 @@ public String answer(InvocationOnMock invocation) throws Throwable { } }); - Crashes.getInstance().onChannelReady(mock(Context.class), mock(Channel.class)); + Crashes.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); } @Test diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 2a8e308d70..76f72501cc 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -81,8 +81,8 @@ protected String getLoggerTag() { } @Override - public synchronized void onChannelReady(@NonNull Context context, @NonNull Channel channel) { - super.onChannelReady(context, channel); + public synchronized void onStarted(@NonNull Context context, @NonNull String appSecret, @NonNull Channel channel) { + super.onStarted(context, appSecret, channel); checkAndFetchUpdateToken(); } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java index dabf756add..ec6ba79a8c 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java @@ -111,7 +111,7 @@ else if (enabled == isInstanceEnabled()) { } @Override - public synchronized void onChannelReady(@NonNull Context context, @NonNull Channel channel) { + public synchronized void onStarted(@NonNull Context context, @NonNull String appSecret, @NonNull Channel channel) { String groupName = getGroupName(); if (groupName != null) { channel.removeGroup(groupName); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index 5f433e1d78..e97f0a0b15 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -56,6 +56,11 @@ public class MobileCenter { */ private Application mApplication; + /** + * Application secret. + */ + private String mAppSecret; + /** * Configured services. */ @@ -65,7 +70,6 @@ public class MobileCenter { * Log serializer. */ private LogSerializer mLogSerializer; - /** * Channel. */ @@ -286,7 +290,10 @@ private synchronized boolean instanceConfigure(Application application, String a } else if (appSecret == null || appSecret.isEmpty()) { MobileCenterLog.error(LOG_TAG, "appSecret may not be null or empty"); } else { + + /* Store state. */ mApplication = application; + mAppSecret = appSecret; /* If parameters are valid, init context related resources. */ StorageHelper.initialize(application); @@ -350,7 +357,7 @@ private synchronized void startService(@NonNull MobileCenterService service) { mLogSerializer.addLogFactory(logFactory.getKey(), logFactory.getValue()); } mServices.add(service); - service.onChannelReady(mApplication, mChannel); + service.onStarted(mApplication, mAppSecret, mChannel); if (isInstanceEnabled()) mApplication.registerActivityLifecycleCallbacks(service); MobileCenterLog.info(LOG_TAG, service.getClass().getSimpleName() + " service started."); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java index c1f15502c1..3e497010fb 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java @@ -39,10 +39,11 @@ public interface MobileCenterService extends Application.ActivityLifecycleCallba Map getLogFactories(); /** - * Called when the channel is ready to be used. This is called even when the service is disabled. + * Called when the service has been started (disregarding if enabled or disabled). * - * @param context application context. - * @param channel channel. + * @param context application context. + * @param appSecret application secret. + * @param channel channel. */ - void onChannelReady(@NonNull Context context, @NonNull Channel channel); + void onStarted(@NonNull Context context, @NonNull String appSecret, @NonNull Channel channel); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java index 3a3d0335f7..4bb4c428b6 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java @@ -137,7 +137,7 @@ public void getLogFactories() { @Test public void onChannelReadyEnabledThenDisable() { Channel channel = mock(Channel.class); - service.onChannelReady(mock(Context.class), channel); + service.onStarted(mock(Context.class), "", channel); verify(channel).removeGroup(service.getGroupName()); verify(channel).addGroup(service.getGroupName(), service.getTriggerCount(), service.getTriggerInterval(), service.getTriggerMaxParallelRequests(), service.getChannelListener()); verifyNoMoreInteractions(channel); @@ -153,7 +153,7 @@ public void onChannelReadyEnabledThenDisable() { public void onChannelReadyDisabledThenEnable() { Channel channel = mock(Channel.class); service.setInstanceEnabled(false); - service.onChannelReady(mock(Context.class), channel); + service.onStarted(mock(Context.class), "", channel); verify(channel).removeGroup(service.getGroupName()); verify(channel).clear(service.getGroupName()); verifyNoMoreInteractions(channel); @@ -188,7 +188,7 @@ protected String getLoggerTag() { } }; Channel channel = mock(Channel.class); - service.onChannelReady(mock(Context.class), channel); + service.onStarted(mock(Context.class), "", channel); service.setInstanceEnabled(false); service.setInstanceEnabled(true); verifyZeroInteractions(channel); diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index fbc36ad5ca..265eceff43 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -164,7 +164,7 @@ public void useDummyServiceTest() { DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @@ -180,28 +180,28 @@ public void useDummyServiceTestSplitCall() { DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @Test public void configureAndStartTwiceTest() { MobileCenter.start(application, DUMMY_APP_SECRET, DummyService.class); - MobileCenter.start(application, DUMMY_APP_SECRET, AnotherDummyService.class); //ignored + MobileCenter.start(application, DUMMY_APP_SECRET + "a", AnotherDummyService.class); //ignored /* Verify that single service has been loaded and configured */ assertEquals(1, MobileCenter.getInstance().getServices().size()); DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @Test public void configureTwiceTest() { MobileCenter.configure(application, DUMMY_APP_SECRET); - MobileCenter.configure(application, DUMMY_APP_SECRET); //ignored + MobileCenter.configure(application, DUMMY_APP_SECRET + "a"); //ignored MobileCenter.start(DummyService.class); /* Verify that single service has been loaded and configured */ @@ -209,7 +209,7 @@ public void configureTwiceTest() { DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @@ -223,13 +223,13 @@ public void startTwoServicesTest() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -244,13 +244,13 @@ public void startTwoServicesSplit() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -266,13 +266,13 @@ public void startTwoServicesSplitEvenMore() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -286,13 +286,13 @@ public void startTwoServicesWithSomeInvalidReferences() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -308,13 +308,13 @@ public void startTwoServicesWithSomeInvalidReferencesSplit() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -331,7 +331,7 @@ public void startServiceTwice() { DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); /* Start twice, this call is ignored. */ @@ -340,7 +340,7 @@ public void startServiceTwice() { /* Verify that single service has been loaded and configured (only once interaction). */ assertEquals(1, MobileCenter.getInstance().getServices().size()); verify(service).getLogFactories(); - verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @@ -451,9 +451,9 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { /* Check factories / channel only once interactions. */ verify(dummyService).getLogFactories(); - verify(dummyService).onChannelReady(any(Context.class), any(Channel.class)); + verify(dummyService).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(anotherDummyService).getLogFactories(); - verify(anotherDummyService).onChannelReady(any(Context.class), any(Channel.class)); + verify(anotherDummyService).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); } @Test @@ -528,6 +528,7 @@ public void invalidServiceTest() { @Test public void nullApplicationTest() { MobileCenter.start(null, DUMMY_APP_SECRET, DummyService.class); + verify(DummyService.getInstance(), never()).onStarted(any(Context.class), anyString(), any(Channel.class)); PowerMockito.verifyStatic(); MobileCenterLog.error(eq(MobileCenter.LOG_TAG), anyString()); } @@ -535,6 +536,7 @@ public void nullApplicationTest() { @Test public void nullAppIdentifierTest() { MobileCenter.start(application, null, DummyService.class); + verify(DummyService.getInstance(), never()).onStarted(any(Context.class), anyString(), any(Channel.class)); PowerMockito.verifyStatic(); MobileCenterLog.error(eq(MobileCenter.LOG_TAG), anyString()); } @@ -542,6 +544,7 @@ public void nullAppIdentifierTest() { @Test public void emptyAppIdentifierTest() { MobileCenter.start(application, "", DummyService.class); + verify(DummyService.getInstance(), never()).onStarted(any(Context.class), anyString(), any(Channel.class)); PowerMockito.verifyStatic(); MobileCenterLog.error(eq(MobileCenter.LOG_TAG), anyString()); } @@ -551,6 +554,7 @@ public void duplicateServiceTest() { MobileCenter.start(application, DUMMY_APP_SECRET, DummyService.class, DummyService.class); /* Verify that only one service has been loaded and configured */ + verify(DummyService.getInstance()).onStarted(notNull(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); assertEquals(1, MobileCenter.getInstance().getServices().size()); } From 7abdfb0a8a8e282d3aaf36eaa9641de2aa7b4b00 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 30 Jan 2017 19:48:39 -0800 Subject: [PATCH 011/142] Manage API call and download apk without confirmation This is just a work in progress, there will be UI to confirm. Add other TODO mentions... --- .../azure/mobile/updates/ReleaseDetails.java | 162 ++++++++++++++ .../azure/mobile/updates/Updates.java | 202 ++++++++++++++++-- 2 files changed, 351 insertions(+), 13 deletions(-) create mode 100644 sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java new file mode 100644 index 0000000000..c9926463c7 --- /dev/null +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java @@ -0,0 +1,162 @@ +package com.microsoft.azure.mobile.updates; + +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; + +class ReleaseDetails { + + private static final String VERSION = "version"; + + private static final String SHORT_VERSION = "short_version"; + + private static final String RELEASE_NOTES = "release_notes"; + + private static final String MIN_OS = "min_os"; + + private static final String FINGERPRINT = "fingerprint"; + + private static final String DOWNLOAD_URL = "download_url"; + + /** + * The release's version.
+ * For iOS: CFBundleVersion from info.plist. + * For Android: android:versionCode from AppManifest.xml. + */ + private int version; + + /** + * The release's short version.
+ * For iOS: CFBundleShortVersionString from info.plist. + * For Android: android:versionName from AppManifest.xml. + */ + private String shortVersion; + + /** + * The release's release notes. + */ + private String releaseNotes; + + /** + * The release's minimum required operating system. + */ + private String minOs; + + /** + * Checksum of the release binary. + */ + private String fingerprint; + + /** + * The URL that hosts the binary for this release. + */ + private Uri downloadUrl; + + /** + * Parse a JSON string describing release details. + * + * @param json a string. + * @return parsed release details. + * @throws JSONException if JSON is invalid. + */ + static ReleaseDetails parse(String json) throws JSONException { + JSONObject object = new JSONObject(json); + ReleaseDetails releaseDetails = new ReleaseDetails(); + try { + releaseDetails.version = Integer.parseInt(object.getString(VERSION)); + } catch (NumberFormatException e) { + throw new JSONException(e.getMessage()); + } + releaseDetails.shortVersion = object.getString(SHORT_VERSION); + releaseDetails.releaseNotes = object.optString(RELEASE_NOTES, null); + releaseDetails.minOs = object.getString(MIN_OS); + releaseDetails.fingerprint = object.getString(FINGERPRINT); + releaseDetails.downloadUrl = Uri.parse(object.getString(DOWNLOAD_URL)); + return releaseDetails; + } + + /** + * Get the version value. + * + * @return the version value + */ + int getVersion() { + return this.version; + } + + /** + * Get the shortVersion value. + * + * @return the shortVersion value + */ + String getShortVersion() { + return shortVersion; + } + + /** + * Get the releaseNotes value. + * + * @return the releaseNotes value + */ + String getReleaseNotes() { + return this.releaseNotes; + } + + /** + * Get the minOs value. + * + * @return the minOs value + */ + String getMinOs() { + return this.minOs; + } + + /** + * Get the fingerprint value. + * + * @return the fingerprint value + */ + String getFingerprint() { + return this.fingerprint; + } + + /** + * Get the downloadUrl value. + * + * @return the downloadUrl value + */ + Uri getDownloadUrl() { + return this.downloadUrl; + } + + @Override + @SuppressWarnings("SimplifiableIfStatement") + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ReleaseDetails that = (ReleaseDetails) o; + + if (version != that.version) return false; + if (shortVersion != null ? !shortVersion.equals(that.shortVersion) : that.shortVersion != null) + return false; + if (releaseNotes != null ? !releaseNotes.equals(that.releaseNotes) : that.releaseNotes != null) + return false; + if (minOs != null ? !minOs.equals(that.minOs) : that.minOs != null) return false; + if (fingerprint != null ? !fingerprint.equals(that.fingerprint) : that.fingerprint != null) + return false; + return downloadUrl != null ? downloadUrl.equals(that.downloadUrl) : that.downloadUrl == null; + } + + @Override + public int hashCode() { + int result = version; + result = 31 * result + (shortVersion != null ? shortVersion.hashCode() : 0); + result = 31 * result + (releaseNotes != null ? releaseNotes.hashCode() : 0); + result = 31 * result + (minOs != null ? minOs.hashCode() : 0); + result = 31 * result + (fingerprint != null ? fingerprint.hashCode() : 0); + result = 31 * result + (downloadUrl != null ? downloadUrl.hashCode() : 0); + return result; + } +} diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 76f72501cc..e6877ea4e4 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -2,23 +2,41 @@ import android.annotation.SuppressLint; import android.app.Activity; +import android.app.DownloadManager; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import com.microsoft.azure.mobile.AbstractMobileCenterService; import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.http.DefaultHttpClient; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.HttpClientRetryer; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.NetworkStateHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper; +import org.json.JSONException; + +import java.util.HashMap; import java.util.List; +import java.util.Map; + +import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; +import static android.content.Context.DOWNLOAD_SERVICE; +import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; public class Updates extends AbstractMobileCenterService { @@ -30,22 +48,38 @@ public class Updates extends AbstractMobileCenterService { * TODO change to https once we have a real server. */ private static final String GENERIC_BROWSER_URL_SCHEME = "http://"; - private static final String DEFAULT_LOGIN_PAGE_URL = "10.123.212.163:8080/default.htm"; + private static final String DEFAULT_LOGIN_PAGE_URL = "10.123.212.163:8080"; private static final String PREFERENCE_PREFIX = SERVICE_NAME + "."; private static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; + private static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; + + private static final String CHECK_UPDATE_SERVER_URL = "http://10.123.212.163:8080/apps/%s/releases/latest"; + + private static final String HEADER_UPDATE_TOKEN = "x-update-token"; + /** * Shared instance. */ @SuppressLint("StaticFieldLeak") private static Updates sInstance = null; + private Context mContext; + + private String mAppSecret; + private Activity mForegroundActivity; private boolean mLoginChecked; - private boolean mUpdateChecked; + private HttpClient mHttpClient; + + private AsyncTask mCheckReleaseTask; + + private ServiceCall mCheckReleaseApiCall; + + private Object mCheckReleaseCallId; /** * Get shared instance. @@ -83,6 +117,8 @@ protected String getLoggerTag() { @Override public synchronized void onStarted(@NonNull Context context, @NonNull String appSecret, @NonNull Channel channel) { super.onStarted(context, appSecret, channel); + mContext = context; + mAppSecret = appSecret; checkAndFetchUpdateToken(); } @@ -97,11 +133,32 @@ public void onActivityPaused(Activity activity) { mForegroundActivity = null; } + @Override + public synchronized void setInstanceEnabled(boolean enabled) { + super.setInstanceEnabled(enabled); + if (enabled) { + checkAndFetchUpdateToken(); + } else { + if (mCheckReleaseApiCall != null) { + mCheckReleaseApiCall.cancel(); + mCheckReleaseApiCall = null; + mCheckReleaseCallId = null; + } + if (mCheckReleaseTask != null) { + mCheckReleaseTask.cancel(true); + mCheckReleaseTask = null; + } + mLoginChecked = false; + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + } + } + private void checkAndFetchUpdateToken() { - if (mForegroundActivity != null && !mLoginChecked && !mUpdateChecked) { + if (mForegroundActivity != null && !mLoginChecked && mCheckReleaseCallId == null) { String updateToken = StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN); - if (updateToken != null && !mUpdateChecked) { + if (updateToken != null && mCheckReleaseCallId == null) { checkUpdate(updateToken); return; } @@ -177,19 +234,138 @@ private void checkAndFetchUpdateToken() { * how do we protect server call to get the key in the first place? * Even having the encryption key temporarily in memory is risky as that can be heap dumped. */ - void storeUpdateToken(@NonNull Context context, @NonNull String updateToken) { - if (mChannel == null) { - StorageHelper.initialize(context); + synchronized void storeUpdateToken(@NonNull Context context, @NonNull String updateToken) { + if (isInstanceEnabled()) { + if (mChannel == null) { + StorageHelper.initialize(context); + } + StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); + if (mCheckReleaseCallId == null) { + checkUpdate(updateToken); + } + // TODO else cancel request retry? } - StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); - if (!mUpdateChecked) { - checkUpdate(updateToken); + } + + private synchronized void checkUpdate(@NonNull String updateToken) { + if (mHttpClient == null) { + HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); + NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(mContext); + mHttpClient = new HttpClientNetworkStateHandler(retryer, networkStateHelper); } + String url = String.format(CHECK_UPDATE_SERVER_URL, mAppSecret); + Map headers = new HashMap<>(); + headers.put(HEADER_UPDATE_TOKEN, updateToken); + final Object releaseCallId = mCheckReleaseCallId = new Object(); + mCheckReleaseApiCall = mHttpClient.callAsync(url, METHOD_GET, headers, null, new ServiceCallback() { + + @Override + public void onCallSucceeded(String payload) { + try { + compareVersions(releaseCallId, ReleaseDetails.parse(payload)); + } catch (JSONException e) { + onCallFailed(e); + } + } + + @Override + public void onCallFailed(Exception e) { + MobileCenterLog.error(LOG_TAG, "Failed to check latest release:", e); + } + }); } - private void checkUpdate(@NonNull String updateToken) { + /** + * Query package manager and compute hash in background. + */ + private synchronized void compareVersions(Object releaseCallId, final ReleaseDetails releaseDetails) { - /* TODO API call. */ - MobileCenterLog.error(LOG_TAG, "Update check not yet implemented."); + /* Check if state did not change. */ + if (mCheckReleaseCallId == releaseCallId && isInstanceEnabled()) { + mCheckReleaseTask = new CheckReleaseDetails(releaseDetails).execute(); + } + } + + /** + * Persist download state. + * + * @param task current task to check race conditions. + * @param downloadRequestId download identifier. + */ + private synchronized void storeDownloadRequestId(CheckReleaseDetails task, long downloadRequestId) { + + /* Check for if state changed and task not canceled in time. */ + if (mCheckReleaseTask == task && isInstanceEnabled()) { + + /* No state change, let download proceed and store state. */ + StorageHelper.PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, downloadRequestId); + } else { + + /* State changed quickly, cancel download. */ + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + downloadManager.remove(downloadRequestId); + } + } + + /** + * Inspecting release details can take some time, especially if we have to compute a hash. + */ + private class CheckReleaseDetails extends AsyncTask { + + /** + * Release details to check. + */ + private final ReleaseDetails mReleaseDetails; + + /** + * Init. + * + * @param releaseDetails release details associated to this check. + */ + CheckReleaseDetails(ReleaseDetails releaseDetails) { + this.mReleaseDetails = releaseDetails; + } + + @Override + protected Void doInBackground(Void[] params) { + + /* TODO Check minimum API level, there is a spec problem currently on that on JSON. */ + + /* Check version code. */ + boolean isMoreRecent = false; + PackageManager packageManager = mContext.getPackageManager(); + try { + PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0); + if (mReleaseDetails.getVersion() > packageInfo.versionCode) { + isMoreRecent = true; + } else if (mReleaseDetails.getVersion() == packageInfo.versionCode) { + // FIXME check hash when version code is same + isMoreRecent = false; + } + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); + return null; + } + + /* Start download if build considered more recent. */ + if (isMoreRecent) { + DownloadManager.Request request = new DownloadManager.Request(mReleaseDetails.getDownloadUrl()); + request.setMimeType("application/vnd.android.package-archive"); // FIXME useless, see below + + /* + * TODO you can't have notification click working that way, it will fail to open file. + * We need anyway to listen for completion, upon completion either: + * - If we are in foreground, pop install UI ourselves. + * - If we are in background, place a notification in panel (that just resumes application). + * When clicking on it: launch install UI. + * Also pop install U.I. if application resumes if user did not action notification. + */ + request.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + long downloadRequestId = downloadManager.enqueue(request); + storeDownloadRequestId(this, downloadRequestId); + } + return null; + } } } From 13568a81bdfd066007702c9d3004d318f54f9410 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 31 Jan 2017 18:41:17 -0800 Subject: [PATCH 012/142] Prototype APK download and install --- .../src/main/AndroidManifest.xml | 13 +- .../updates/DownloadCompletionReceiver.java | 43 +++++ .../mobile/updates/LoginCallbackActivity.java | 7 +- .../azure/mobile/updates/Updates.java | 178 +++++++++++++++--- .../src/main/res/values/strings.xml | 5 + 5 files changed, 213 insertions(+), 33 deletions(-) create mode 100644 sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java create mode 100644 sdk/mobile-center-updates/src/main/res/values/strings.xml diff --git a/sdk/mobile-center-updates/src/main/AndroidManifest.xml b/sdk/mobile-center-updates/src/main/AndroidManifest.xml index 8da44ee87d..2eb2acbaf2 100644 --- a/sdk/mobile-center-updates/src/main/AndroidManifest.xml +++ b/sdk/mobile-center-updates/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + android:scheme="mobile.center"/> + + + + + + + \ No newline at end of file diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java new file mode 100644 index 0000000000..be491c5a6a --- /dev/null +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java @@ -0,0 +1,43 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; + +/** + * Process download manager callbacks. + */ +public class DownloadCompletionReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + + /* Check intent action. */ + switch (intent.getAction()) { + + /* + * Just resume app if clicking on pending download notification as + * it's always weird to click on a notification and nothing happening. + * Another option would be to open download list. + */ + case DownloadManager.ACTION_NOTIFICATION_CLICKED: + PackageManager packageManager = context.getPackageManager(); + Intent resumeIntent = packageManager.getLaunchIntentForPackage(context.getPackageName()); + if (resumeIntent != null) { + resumeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(resumeIntent); + } + break; + + /* + * Forward the download identifier to Updates for inspection. + */ + case DownloadManager.ACTION_DOWNLOAD_COMPLETE: + long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); + Updates.processCompletedDownload(context, downloadId); + break; + } + } +} diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java index 29bb324e3e..a8bbfd6e96 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java @@ -27,7 +27,7 @@ public void onCreate(Bundle savedInstanceState) { /* Store update token. */ if (updateToken != null) { - Updates.getInstance().storeUpdateToken(this, updateToken); + Updates.getInstance().storeUpdateToken(updateToken); } /* @@ -50,7 +50,10 @@ public void onCreate(Bundle savedInstanceState) { MobileCenterLog.debug(LOG_TAG, "Using restart work around to correctly resume app."); startActivity(intent.cloneFilter().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } else if (isTaskRoot()) { - startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName())); + Intent launchIntentForPackage = getPackageManager().getLaunchIntentForPackage(getPackageName()); + if (launchIntentForPackage != null) { + startActivity(launchIntentForPackage); + } } } } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index e6877ea4e4..3e44352484 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -3,15 +3,20 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.DownloadManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; @@ -34,7 +39,6 @@ import java.util.List; import java.util.Map; -import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; import static android.content.Context.DOWNLOAD_SERVICE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; @@ -55,6 +59,8 @@ public class Updates extends AbstractMobileCenterService { private static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; + private static final String PREFERENCE_KEY_DOWNLOAD_URI = PREFERENCE_PREFIX + "download_uri"; + private static final String CHECK_UPDATE_SERVER_URL = "http://10.123.212.163:8080/apps/%s/releases/latest"; private static final String HEADER_UPDATE_TOKEN = "x-update-token"; @@ -81,6 +87,8 @@ public class Updates extends AbstractMobileCenterService { private Object mCheckReleaseCallId; + private String mUpdateToken; + /** * Get shared instance. * @@ -99,6 +107,23 @@ static synchronized void unsetInstance() { sInstance = null; } + static void processCompletedDownload(Context context, long downloadId) { + getInstance().doProcessCompletedDownload(context, downloadId); + } + + @NonNull + private static Intent getInstallIntent(Uri fileUri) { + Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + intent.setData(fileUri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } + + private static int getNotificationId(long downloadId) { + return (Updates.class.getName() + downloadId).hashCode(); + } + @Override protected String getGroupName() { return null; @@ -119,13 +144,13 @@ public synchronized void onStarted(@NonNull Context context, @NonNull String app super.onStarted(context, appSecret, channel); mContext = context; mAppSecret = appSecret; - checkAndFetchUpdateToken(); + checkWhatToDoNext(); } @Override public void onActivityResumed(Activity activity) { mForegroundActivity = activity; - checkAndFetchUpdateToken(); + checkWhatToDoNext(); } @Override @@ -137,7 +162,7 @@ public void onActivityPaused(Activity activity) { public synchronized void setInstanceEnabled(boolean enabled) { super.setInstanceEnabled(enabled); if (enabled) { - checkAndFetchUpdateToken(); + checkWhatToDoNext(); } else { if (mCheckReleaseApiCall != null) { mCheckReleaseApiCall.cancel(); @@ -149,20 +174,54 @@ public synchronized void setInstanceEnabled(boolean enabled) { mCheckReleaseTask = null; } mLoginChecked = false; + long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); + if (downloadId > 0) { + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); + downloadManager.remove(downloadId); + NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(getNotificationId(downloadId)); + } StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); } } - private void checkAndFetchUpdateToken() { - if (mForegroundActivity != null && !mLoginChecked && mCheckReleaseCallId == null) { + private void checkWhatToDoNext() { + if (mForegroundActivity != null && !mLoginChecked) { + + /* If we received the update token before Mobile Center was started/enabled, process it now. */ + if (mUpdateToken != null) { + storeUpdateToken(mUpdateToken); + mUpdateToken = null; + return; + } + + /* If we have a download ready but we were in background, pop install UI now. */ + try { + long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); + Uri apkUri = Uri.parse(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI)); + NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(getNotificationId(downloadId)); + mForegroundActivity.startActivity(getInstallIntent(apkUri)); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + } catch (RuntimeException e) { + MobileCenterLog.debug(LOG_TAG, "No APK downloaded."); + } + /* Nothing more to do for now if we are already calling API to check release. */ + if (mCheckReleaseCallId != null) { + return; + } + + /* Check if we have previous stored the update token. */ String updateToken = StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN); - if (updateToken != null && mCheckReleaseCallId == null) { + if (updateToken != null) { checkUpdate(updateToken); return; } + /* If not, open browser to login. */ String baseUrl = DEFAULT_LOGIN_PAGE_URL + "?package=" + mForegroundActivity.getPackageName(); Intent intent = new Intent(Intent.ACTION_VIEW); @@ -234,11 +293,12 @@ private void checkAndFetchUpdateToken() { * how do we protect server call to get the key in the first place? * Even having the encryption key temporarily in memory is risky as that can be heap dumped. */ - synchronized void storeUpdateToken(@NonNull Context context, @NonNull String updateToken) { - if (isInstanceEnabled()) { - if (mChannel == null) { - StorageHelper.initialize(context); - } + synchronized void storeUpdateToken(@NonNull String updateToken) { + + /* Keep token for later if we are not started and enabled yet. */ + if (mContext == null) { + mUpdateToken = updateToken; + } else if (isInstanceEnabled()) { StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); if (mCheckReleaseCallId == null) { checkUpdate(updateToken); @@ -289,24 +349,93 @@ private synchronized void compareVersions(Object releaseCallId, final ReleaseDet /** * Persist download state. * + * @param downloadManager download manager. * @param task current task to check race conditions. * @param downloadRequestId download identifier. */ - private synchronized void storeDownloadRequestId(CheckReleaseDetails task, long downloadRequestId) { + private synchronized void storeDownloadRequestId(DownloadManager downloadManager, CheckReleaseDetails task, long downloadRequestId) { /* Check for if state changed and task not canceled in time. */ if (mCheckReleaseTask == task && isInstanceEnabled()) { - /* No state change, let download proceed and store state. */ + /* Delete previous download. */ + long previousDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); + if (previousDownloadId > 0) { + downloadManager.remove(previousDownloadId); + NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(getNotificationId(previousDownloadId)); + } + + /* Store new download identifier. */ StorageHelper.PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, downloadRequestId); } else { /* State changed quickly, cancel download. */ - DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); downloadManager.remove(downloadRequestId); } } + private void doProcessCompletedDownload(Context context, long downloadId) { + + /* Completion might be triggered before MobileCenter.start. */ + if (mContext == null) { + StorageHelper.initialize(context); + } + + /* Check intent data is what we expected. */ + long expectedDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, -1); + if (expectedDownloadId != downloadId) { + MobileCenterLog.warn(LOG_TAG, "Ignoring completion for a download we didn't expect, id=" + downloadId); + return; + } + + /* Check if download successful. */ + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + Uri uriForDownloadedFile = downloadManager.getUriForDownloadedFile(downloadId); + if (uriForDownloadedFile != null) { + + /* Build install intent. */ + Intent intent = getInstallIntent(uriForDownloadedFile); + + /* If foreground, execute now, otherwise post notification. */ + if (mForegroundActivity != null) { + mForegroundActivity.startActivity(intent); + } else { + + /* Remember we have a download ready. */ + StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, uriForDownloadedFile.toString()); + + /* And notify. */ + int icon; + try { + ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); + icon = applicationInfo.icon; + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.error(LOG_TAG, "Could not get application icon", e); + return; + } + Notification.Builder builder = new Notification.Builder(mContext) + .setContentTitle(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) + .setContentText(context.getString(R.string.mobile_center_updates_download_successful_notification_message)) + .setSmallIcon(icon) + .setContentIntent(PendingIntent.getActivities(context, 0, new Intent[]{intent}, 0)); + Notification notification; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + notification = builder.build(); + } else { + //noinspection deprecation + notification = builder.getNotification(); + } + notification.flags |= Notification.FLAG_AUTO_CANCEL; + int notificationId = getNotificationId(downloadId); + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(notificationId, notification); + } + } else { + MobileCenterLog.error(LOG_TAG, "Failed to download update."); + } + } + /** * Inspecting release details can take some time, especially if we have to compute a hash. */ @@ -347,23 +476,14 @@ protected Void doInBackground(Void[] params) { return null; } - /* Start download if build considered more recent. */ + /* Start download if build compatible with device and more recent. */ if (isMoreRecent) { - DownloadManager.Request request = new DownloadManager.Request(mReleaseDetails.getDownloadUrl()); - request.setMimeType("application/vnd.android.package-archive"); // FIXME useless, see below - - /* - * TODO you can't have notification click working that way, it will fail to open file. - * We need anyway to listen for completion, upon completion either: - * - If we are in foreground, pop install UI ourselves. - * - If we are in background, place a notification in panel (that just resumes application). - * When clicking on it: launch install UI. - * Also pop install U.I. if application resumes if user did not action notification. - */ - request.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + + /* Download file. */ DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + DownloadManager.Request request = new DownloadManager.Request(mReleaseDetails.getDownloadUrl()); long downloadRequestId = downloadManager.enqueue(request); - storeDownloadRequestId(this, downloadRequestId); + storeDownloadRequestId(downloadManager, this, downloadRequestId); } return null; } diff --git a/sdk/mobile-center-updates/src/main/res/values/strings.xml b/sdk/mobile-center-updates/src/main/res/values/strings.xml new file mode 100644 index 0000000000..79335d17a3 --- /dev/null +++ b/sdk/mobile-center-updates/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Application update downloaded. + Tap to install it now. + \ No newline at end of file From 90e1e9acf0f0c1f0f632b09173115d019de20ee1 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 31 Jan 2017 19:12:32 -0800 Subject: [PATCH 013/142] Fix some null pointer exceptions --- .../main/java/com/microsoft/azure/mobile/updates/Updates.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 3e44352484..feaafd2c2c 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -390,7 +390,7 @@ private void doProcessCompletedDownload(Context context, long downloadId) { } /* Check if download successful. */ - DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE); Uri uriForDownloadedFile = downloadManager.getUriForDownloadedFile(downloadId); if (uriForDownloadedFile != null) { @@ -414,7 +414,7 @@ private void doProcessCompletedDownload(Context context, long downloadId) { MobileCenterLog.error(LOG_TAG, "Could not get application icon", e); return; } - Notification.Builder builder = new Notification.Builder(mContext) + Notification.Builder builder = new Notification.Builder(context) .setContentTitle(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) .setContentText(context.getString(R.string.mobile_center_updates_download_successful_notification_message)) .setSmallIcon(icon) From 50c67849df4b86f67e3852676a73d2c28a30bf71 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 1 Feb 2017 15:11:14 -0800 Subject: [PATCH 014/142] Stabilize updates prototype --- .../activities/SettingsActivity.java | 15 ++++ .../src/main/res/values/settings.xml | 16 +++- apps/sasquatch/src/main/res/xml/settings.xml | 8 ++ .../azure/mobile/analytics/Analytics.java | 2 - .../updates/DownloadCompletionReceiver.java | 10 +-- .../azure/mobile/updates/Updates.java | 83 +++++++++++++++---- versions.gradle | 2 +- 7 files changed, 105 insertions(+), 31 deletions(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java index 9db8d5c5d3..a31c709844 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java @@ -20,6 +20,7 @@ import com.microsoft.azure.mobile.analytics.AnalyticsPrivateHelper; import com.microsoft.azure.mobile.crashes.Crashes; import com.microsoft.azure.mobile.sasquatch.R; +import com.microsoft.azure.mobile.updates.Updates; import com.microsoft.azure.mobile.utils.PrefStorageConstants; import com.microsoft.azure.mobile.utils.storage.StorageHelper; @@ -48,6 +49,7 @@ public void onCreate(Bundle savedInstanceState) { addPreferencesFromResource(R.xml.settings); final CheckBoxPreference analyticsEnabledPreference = (CheckBoxPreference) getPreferenceManager().findPreference(getString(R.string.mobile_center_analytics_state_key)); final CheckBoxPreference crashesEnabledPreference = (CheckBoxPreference) getPreferenceManager().findPreference(getString(R.string.mobile_center_crashes_state_key)); + final CheckBoxPreference updatesEnabledPreference = (CheckBoxPreference) getPreferenceManager().findPreference(getString(R.string.mobile_center_updates_state_key)); initCheckBoxSetting(R.string.mobile_center_state_key, MobileCenter.isEnabled(), R.string.mobile_center_state_summary_enabled, R.string.mobile_center_state_summary_disabled, new HasEnabled() { @Override @@ -88,6 +90,19 @@ public boolean isEnabled() { return Crashes.isEnabled(); } }); + initCheckBoxSetting(R.string.mobile_center_updates_state_key, Updates.isEnabled(), R.string.mobile_center_updates_state_summary_enabled, R.string.mobile_center_updates_state_summary_disabled, new HasEnabled() { + + @Override + public void setEnabled(boolean enabled) { + Updates.setEnabled(enabled); + updatesEnabledPreference.setChecked(Updates.isEnabled()); + } + + @Override + public boolean isEnabled() { + return Updates.isEnabled(); + } + }); initCheckBoxSetting(R.string.mobile_center_auto_page_tracking_key, AnalyticsPrivateHelper.isAutoPageTrackingEnabled(), R.string.mobile_center_auto_page_tracking_enabled, R.string.mobile_center_auto_page_tracking_disabled, new HasEnabled() { @Override diff --git a/apps/sasquatch/src/main/res/values/settings.xml b/apps/sasquatch/src/main/res/values/settings.xml index c9eed7ff6f..04125d60a5 100644 --- a/apps/sasquatch/src/main/res/values/settings.xml +++ b/apps/sasquatch/src/main/res/values/settings.xml @@ -10,8 +10,8 @@ mobile_center_analytics_state_key Analytics state - Analytics is enabled - Analytics is disabled + Analytics are enabled + Analytics are disabled mobile_center_auto_page_tracking_key Analytics automatic page tracking @@ -24,8 +24,16 @@ mobile_center_crashes_state_key Crashes state - Crashes is enabled - Crashes is disabled + Crashes are enabled + Crashes are disabled + + mobile_center_updates + Updates + + mobile_center_updates_state_key + Updates state + Updates are enabled + Updates are disabled application_info Application Information diff --git a/apps/sasquatch/src/main/res/xml/settings.xml b/apps/sasquatch/src/main/res/xml/settings.xml index 460eb49951..669aeac589 100644 --- a/apps/sasquatch/src/main/res/xml/settings.xml +++ b/apps/sasquatch/src/main/res/xml/settings.xml @@ -23,6 +23,14 @@ android:title="@string/mobile_center_crashes_state_title" /> + + + + diff --git a/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java b/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java index 45bd05e9d1..14f6f52c23 100644 --- a/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java +++ b/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java @@ -114,7 +114,6 @@ static synchronized void unsetInstance() { * * @return true if enabled, false otherwise. */ - @SuppressWarnings("WeakerAccess") public static boolean isEnabled() { return getInstance().isInstanceEnabled(); } @@ -124,7 +123,6 @@ public static boolean isEnabled() { * * @param enabled true to enable, false to disable. */ - @SuppressWarnings("WeakerAccess") public static void setEnabled(boolean enabled) { getInstance().setInstanceEnabled(enabled); } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java index be491c5a6a..f7db06bb7e 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java @@ -4,7 +4,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; /** * Process download manager callbacks. @@ -23,12 +22,7 @@ public void onReceive(Context context, Intent intent) { * Another option would be to open download list. */ case DownloadManager.ACTION_NOTIFICATION_CLICKED: - PackageManager packageManager = context.getPackageManager(); - Intent resumeIntent = packageManager.getLaunchIntentForPackage(context.getPackageName()); - if (resumeIntent != null) { - resumeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(resumeIntent); - } + Updates.getInstance().resumeApp(context); break; /* @@ -36,7 +30,7 @@ public void onReceive(Context context, Intent intent) { */ case DownloadManager.ACTION_DOWNLOAD_COMPLETE: long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); - Updates.processCompletedDownload(context, downloadId); + Updates.getInstance().processCompletedDownload(context, downloadId); break; } } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index feaafd2c2c..93a165ced7 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -77,7 +77,7 @@ public class Updates extends AbstractMobileCenterService { private Activity mForegroundActivity; - private boolean mLoginChecked; + private boolean mBrowserOpened; private HttpClient mHttpClient; @@ -107,8 +107,22 @@ static synchronized void unsetInstance() { sInstance = null; } - static void processCompletedDownload(Context context, long downloadId) { - getInstance().doProcessCompletedDownload(context, downloadId); + /** + * Check whether Updates service is enabled or not. + * + * @return true if enabled, false otherwise. + */ + public static boolean isEnabled() { + return getInstance().isInstanceEnabled(); + } + + /** + * Enable or disable Updates service. + * + * @param enabled true to enable, false to disable. + */ + public static void setEnabled(boolean enabled) { + getInstance().setInstanceEnabled(enabled); } @NonNull @@ -148,13 +162,13 @@ public synchronized void onStarted(@NonNull Context context, @NonNull String app } @Override - public void onActivityResumed(Activity activity) { + public synchronized void onActivityResumed(Activity activity) { mForegroundActivity = activity; checkWhatToDoNext(); } @Override - public void onActivityPaused(Activity activity) { + public synchronized void onActivityPaused(Activity activity) { mForegroundActivity = null; } @@ -173,9 +187,10 @@ public synchronized void setInstanceEnabled(boolean enabled) { mCheckReleaseTask.cancel(true); mCheckReleaseTask = null; } - mLoginChecked = false; + mBrowserOpened = false; long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); if (downloadId > 0) { + MobileCenterLog.debug(LOG_TAG, "Removing download and notification id=" + downloadId); DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); downloadManager.remove(downloadId); NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); @@ -187,11 +202,12 @@ public synchronized void setInstanceEnabled(boolean enabled) { } } - private void checkWhatToDoNext() { - if (mForegroundActivity != null && !mLoginChecked) { + private synchronized void checkWhatToDoNext() { + if (mForegroundActivity != null) { /* If we received the update token before Mobile Center was started/enabled, process it now. */ if (mUpdateToken != null) { + MobileCenterLog.debug(LOG_TAG, "Processing update token we kept in memory before onStarted"); storeUpdateToken(mUpdateToken); mUpdateToken = null; return; @@ -199,18 +215,21 @@ private void checkWhatToDoNext() { /* If we have a download ready but we were in background, pop install UI now. */ try { - long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); Uri apkUri = Uri.parse(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI)); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + MobileCenterLog.debug(LOG_TAG, "Now in foreground, remove notification and start install for APK uri=" + apkUri); + long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(getNotificationId(downloadId)); mForegroundActivity.startActivity(getInstallIntent(apkUri)); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + return; } catch (RuntimeException e) { - MobileCenterLog.debug(LOG_TAG, "No APK downloaded."); + MobileCenterLog.verbose(LOG_TAG, "No APK downloaded or user ignored it, proceed state check."); } /* Nothing more to do for now if we are already calling API to check release. */ if (mCheckReleaseCallId != null) { + MobileCenterLog.verbose(LOG_TAG, "Already checking or checked latest release."); return; } @@ -222,6 +241,10 @@ private void checkWhatToDoNext() { } /* If not, open browser to login. */ + if (mBrowserOpened) { + return; + } + MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to login."); String baseUrl = DEFAULT_LOGIN_PAGE_URL + "?package=" + mForegroundActivity.getPackageName(); Intent intent = new Intent(Intent.ACTION_VIEW); @@ -283,7 +306,7 @@ private void checkWhatToDoNext() { mForegroundActivity.startActivity(intent); } } - mLoginChecked = true; + mBrowserOpened = true; } } @@ -297,9 +320,11 @@ synchronized void storeUpdateToken(@NonNull String updateToken) { /* Keep token for later if we are not started and enabled yet. */ if (mContext == null) { + MobileCenterLog.debug(LOG_TAG, "Update token received before onStart, keep it in memory."); mUpdateToken = updateToken; } else if (isInstanceEnabled()) { StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); + MobileCenterLog.debug(LOG_TAG, "Stored update token."); if (mCheckReleaseCallId == null) { checkUpdate(updateToken); } @@ -308,6 +333,7 @@ synchronized void storeUpdateToken(@NonNull String updateToken) { } private synchronized void checkUpdate(@NonNull String updateToken) { + MobileCenterLog.debug(LOG_TAG, "Check latest release..."); if (mHttpClient == null) { HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(mContext); @@ -342,6 +368,7 @@ private synchronized void compareVersions(Object releaseCallId, final ReleaseDet /* Check if state did not change. */ if (mCheckReleaseCallId == releaseCallId && isInstanceEnabled()) { + MobileCenterLog.debug(LOG_TAG, "Schedule background version check..."); mCheckReleaseTask = new CheckReleaseDetails(releaseDetails).execute(); } } @@ -361,6 +388,7 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager /* Delete previous download. */ long previousDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); if (previousDownloadId > 0) { + MobileCenterLog.debug(LOG_TAG, "Delete previous download an notification id=" + previousDownloadId); downloadManager.remove(previousDownloadId); NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(getNotificationId(previousDownloadId)); @@ -371,14 +399,28 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager } else { /* State changed quickly, cancel download. */ + MobileCenterLog.debug(LOG_TAG, "State changed while downloading, cancel id=" + downloadRequestId); downloadManager.remove(downloadRequestId); } } - private void doProcessCompletedDownload(Context context, long downloadId) { + synchronized void resumeApp(Context context) { + if (mForegroundActivity == null) { + PackageManager packageManager = context.getPackageManager(); + Intent resumeIntent = packageManager.getLaunchIntentForPackage(context.getPackageName()); + if (resumeIntent != null) { + resumeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(resumeIntent); + } + } + } + + synchronized void processCompletedDownload(Context context, long downloadId) { /* Completion might be triggered before MobileCenter.start. */ + MobileCenterLog.debug(LOG_TAG, "Process download completion id=" + downloadId); if (mContext == null) { + MobileCenterLog.debug(LOG_TAG, "Called before onStart, init storage"); StorageHelper.initialize(context); } @@ -395,14 +437,17 @@ private void doProcessCompletedDownload(Context context, long downloadId) { if (uriForDownloadedFile != null) { /* Build install intent. */ + MobileCenterLog.debug(LOG_TAG, "Download was successful for id=" + downloadId + " uri=" + uriForDownloadedFile); Intent intent = getInstallIntent(uriForDownloadedFile); /* If foreground, execute now, otherwise post notification. */ if (mForegroundActivity != null) { + MobileCenterLog.debug(LOG_TAG, "We are in foreground, launch install UI now."); mForegroundActivity.startActivity(intent); } else { /* Remember we have a download ready. */ + MobileCenterLog.debug(LOG_TAG, "We are in background, post a notification."); StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, uriForDownloadedFile.toString()); /* And notify. */ @@ -418,6 +463,7 @@ private void doProcessCompletedDownload(Context context, long downloadId) { .setContentTitle(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) .setContentText(context.getString(R.string.mobile_center_updates_download_successful_notification_message)) .setSmallIcon(icon) + .setWhen(System.currentTimeMillis()) .setContentIntent(PendingIntent.getActivities(context, 0, new Intent[]{intent}, 0)); Notification notification; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { @@ -432,7 +478,7 @@ private void doProcessCompletedDownload(Context context, long downloadId) { notificationManager.notify(notificationId, notification); } } else { - MobileCenterLog.error(LOG_TAG, "Failed to download update."); + MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + downloadId); } } @@ -461,15 +507,18 @@ protected Void doInBackground(Void[] params) { /* TODO Check minimum API level, there is a spec problem currently on that on JSON. */ /* Check version code. */ + MobileCenterLog.debug(LOG_TAG, "Check version code."); boolean isMoreRecent = false; PackageManager packageManager = mContext.getPackageManager(); try { PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0); if (mReleaseDetails.getVersion() > packageInfo.versionCode) { + MobileCenterLog.debug(LOG_TAG, "Latest release version code is higher."); isMoreRecent = true; } else if (mReleaseDetails.getVersion() == packageInfo.versionCode) { // FIXME check hash when version code is same - isMoreRecent = false; + MobileCenterLog.debug(LOG_TAG, "Same version code, need to check hash TODO, for now we assume more recent."); + isMoreRecent = true; } } catch (PackageManager.NameNotFoundException e) { MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); @@ -480,8 +529,10 @@ protected Void doInBackground(Void[] params) { if (isMoreRecent) { /* Download file. */ + Uri downloadUrl = mReleaseDetails.getDownloadUrl(); + MobileCenterLog.debug(LOG_TAG, "Start downloading new release, url=" + downloadUrl); DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); - DownloadManager.Request request = new DownloadManager.Request(mReleaseDetails.getDownloadUrl()); + DownloadManager.Request request = new DownloadManager.Request(downloadUrl); long downloadRequestId = downloadManager.enqueue(request); storeDownloadRequestId(downloadManager, this, downloadRequestId); } diff --git a/versions.gradle b/versions.gradle index e7df4f1342..5472930481 100644 --- a/versions.gradle +++ b/versions.gradle @@ -7,5 +7,5 @@ ext { targetSdkVersion = 25 compileSdkVersion = 25 buildToolsVersion = '25.0.2' - supportLibVersion = '25.1.0' + supportLibVersion = '25.1.1' } From 04a84634e63c64ccb447b3beaf12905bb8c88b0c Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 1 Feb 2017 17:01:56 -0800 Subject: [PATCH 015/142] Fix strict mode in updates and refactoring Also prevent package manager from crashing if stacking previous install U.I. --- .../azure/mobile/updates/Updates.java | 429 +++++++++++++----- 1 file changed, 307 insertions(+), 122 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 93a165ced7..0c98aa59ca 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -42,52 +42,121 @@ import static android.content.Context.DOWNLOAD_SERVICE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; +/** + * Updates service. + */ public class Updates extends AbstractMobileCenterService { + /** + * Used for deep link intent from browser, string field for update token. + */ static final String EXTRA_UPDATE_TOKEN = "update_token"; + + /** + * Update service name. + */ private static final String SERVICE_NAME = "Updates"; + + /** + * Log tag for this service. + */ static final String LOG_TAG = MobileCenter.LOG_TAG + SERVICE_NAME; + + /** + * Scheme used to open URLs in Google Chrome instead of any browser. + */ private static final String GOOGLE_CHROME_URL_SCHEME = "googlechrome://navigate?url="; + /** - * TODO change to https once we have a real server. + * Scheme used to open URLs in any browser. TODO change to https once we have a real server. */ private static final String GENERIC_BROWSER_URL_SCHEME = "http://"; + + /** + * URL without scheme to open browser to login. + */ private static final String DEFAULT_LOGIN_PAGE_URL = "10.123.212.163:8080"; + + /** + * Full URL to call server to check latest release. + */ + private static final String CHECK_UPDATE_SERVER_URL = "http://10.123.212.163:8080/apps/%s/releases/latest"; + + /** + * Header used to pass token when checking latest release. + */ + private static final String HEADER_API_TOKEN = "x-api-token"; + + /** + * Base key for stored preferences. + */ private static final String PREFERENCE_PREFIX = SERVICE_NAME + "."; + /** + * Preference key to store token. + */ private static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; + /** + * Preference key to store the last download identifier. + */ private static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; + /** + * Preference key to store the last download file location on download manager. + */ private static final String PREFERENCE_KEY_DOWNLOAD_URI = PREFERENCE_PREFIX + "download_uri"; - private static final String CHECK_UPDATE_SERVER_URL = "http://10.123.212.163:8080/apps/%s/releases/latest"; - - private static final String HEADER_UPDATE_TOKEN = "x-update-token"; - /** * Shared instance. */ @SuppressLint("StaticFieldLeak") private static Updates sInstance = null; + /** + * Application context, if not null it means onStart was called. + */ private Context mContext; + /** + * Application secret. + */ private String mAppSecret; + /** + * If not null we are in foreground inside this activity. + */ private Activity mForegroundActivity; + /** + * Remember if we already opened browser to login. + */ private boolean mBrowserOpened; - private HttpClient mHttpClient; + /** + * In memory token if we receive deep link intent before onStart. + */ + private String mBeforeStartUpdateToken; - private AsyncTask mCheckReleaseTask; + /** + * Current API call identifier to check latest release from server, used for state check. + */ + private Object mCheckReleaseCallId; + /** + * Current API call to check latest release from server. + */ private ServiceCall mCheckReleaseApiCall; - private Object mCheckReleaseCallId; + /** + * Current task inspecting the latest release details that we fetched from server. + */ + private AsyncTask mInspectReleaseTask; - private String mUpdateToken; + /** + * Current task to process download completion. + */ + private AsyncTask mProcessDownloadCompletionTask; /** * Get shared instance. @@ -125,17 +194,29 @@ public static void setEnabled(boolean enabled) { getInstance().setInstanceEnabled(enabled); } + /** + * Get the intent used to open installation U.I. + * + * @param fileUri downloaded file URI from the download manager. + * @return intent to open installation U.I. + */ @NonNull private static Intent getInstallIntent(Uri fileUri) { Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); intent.setData(fileUri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); return intent; } - private static int getNotificationId(long downloadId) { - return (Updates.class.getName() + downloadId).hashCode(); + /** + * Get the notification identifier for downloads. + * + * @return notification identifier for downloads. + */ + private static int getNotificationId() { + return (Updates.class.getName()).hashCode(); } @Override @@ -158,13 +239,13 @@ public synchronized void onStarted(@NonNull Context context, @NonNull String app super.onStarted(context, appSecret, channel); mContext = context; mAppSecret = appSecret; - checkWhatToDoNext(); + resumeUpdateWorkflow(); } @Override public synchronized void onActivityResumed(Activity activity) { mForegroundActivity = activity; - checkWhatToDoNext(); + resumeUpdateWorkflow(); } @Override @@ -176,40 +257,56 @@ public synchronized void onActivityPaused(Activity activity) { public synchronized void setInstanceEnabled(boolean enabled) { super.setInstanceEnabled(enabled); if (enabled) { - checkWhatToDoNext(); + resumeUpdateWorkflow(); } else { - if (mCheckReleaseApiCall != null) { - mCheckReleaseApiCall.cancel(); - mCheckReleaseApiCall = null; - mCheckReleaseCallId = null; - } - if (mCheckReleaseTask != null) { - mCheckReleaseTask.cancel(true); - mCheckReleaseTask = null; - } + + /* Clean all state on disabling, cancel everything. */ mBrowserOpened = false; - long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); - if (downloadId > 0) { - MobileCenterLog.debug(LOG_TAG, "Removing download and notification id=" + downloadId); - DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); - downloadManager.remove(downloadId); - NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(getNotificationId(downloadId)); - } + cancelPreviousTasks(); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); } } - private synchronized void checkWhatToDoNext() { + /** + * Cancel everything. + */ + private synchronized void cancelPreviousTasks() { + if (mCheckReleaseApiCall != null) { + mCheckReleaseApiCall.cancel(); + mCheckReleaseApiCall = null; + mCheckReleaseCallId = null; + } + if (mInspectReleaseTask != null) { + mInspectReleaseTask.cancel(true); + mInspectReleaseTask = null; + } + if (mProcessDownloadCompletionTask != null) { + mProcessDownloadCompletionTask.cancel(true); + mProcessDownloadCompletionTask = null; + } + long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); + if (downloadId > 0) { + MobileCenterLog.debug(LOG_TAG, "Removing download and notification id=" + downloadId); + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); + downloadManager.remove(downloadId); + NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(getNotificationId()); + } + } + + /** + * Method that triggers the update workflow or proceed to the next step. + */ + private synchronized void resumeUpdateWorkflow() { if (mForegroundActivity != null) { /* If we received the update token before Mobile Center was started/enabled, process it now. */ - if (mUpdateToken != null) { + if (mBeforeStartUpdateToken != null) { MobileCenterLog.debug(LOG_TAG, "Processing update token we kept in memory before onStarted"); - storeUpdateToken(mUpdateToken); - mUpdateToken = null; + storeUpdateToken(mBeforeStartUpdateToken); + mBeforeStartUpdateToken = null; return; } @@ -218,9 +315,8 @@ private synchronized void checkWhatToDoNext() { Uri apkUri = Uri.parse(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI)); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); MobileCenterLog.debug(LOG_TAG, "Now in foreground, remove notification and start install for APK uri=" + apkUri); - long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(getNotificationId(downloadId)); + notificationManager.cancel(getNotificationId()); mForegroundActivity.startActivity(getInstallIntent(apkUri)); return; } catch (RuntimeException e) { @@ -236,7 +332,7 @@ private synchronized void checkWhatToDoNext() { /* Check if we have previous stored the update token. */ String updateToken = StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN); if (updateToken != null) { - checkUpdate(updateToken); + getLatestReleaseDetails(updateToken); return; } @@ -321,29 +417,30 @@ synchronized void storeUpdateToken(@NonNull String updateToken) { /* Keep token for later if we are not started and enabled yet. */ if (mContext == null) { MobileCenterLog.debug(LOG_TAG, "Update token received before onStart, keep it in memory."); - mUpdateToken = updateToken; + mBeforeStartUpdateToken = updateToken; } else if (isInstanceEnabled()) { StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); MobileCenterLog.debug(LOG_TAG, "Stored update token."); - if (mCheckReleaseCallId == null) { - checkUpdate(updateToken); - } - // TODO else cancel request retry? + cancelPreviousTasks(); + getLatestReleaseDetails(updateToken); } } - private synchronized void checkUpdate(@NonNull String updateToken) { - MobileCenterLog.debug(LOG_TAG, "Check latest release..."); - if (mHttpClient == null) { - HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); - NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(mContext); - mHttpClient = new HttpClientNetworkStateHandler(retryer, networkStateHelper); - } + /** + * Get latest release details from server. + * + * @param updateToken token to secure API call. + */ + private synchronized void getLatestReleaseDetails(@NonNull String updateToken) { + MobileCenterLog.debug(LOG_TAG, "Get latest release details..."); + HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); + NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(mContext); + HttpClient httpClient = new HttpClientNetworkStateHandler(retryer, networkStateHelper); String url = String.format(CHECK_UPDATE_SERVER_URL, mAppSecret); Map headers = new HashMap<>(); - headers.put(HEADER_UPDATE_TOKEN, updateToken); + headers.put(HEADER_API_TOKEN, updateToken); final Object releaseCallId = mCheckReleaseCallId = new Object(); - mCheckReleaseApiCall = mHttpClient.callAsync(url, METHOD_GET, headers, null, new ServiceCallback() { + mCheckReleaseApiCall = httpClient.callAsync(url, METHOD_GET, headers, null, new ServiceCallback() { @Override public void onCallSucceeded(String payload) { @@ -367,9 +464,9 @@ public void onCallFailed(Exception e) { private synchronized void compareVersions(Object releaseCallId, final ReleaseDetails releaseDetails) { /* Check if state did not change. */ - if (mCheckReleaseCallId == releaseCallId && isInstanceEnabled()) { + if (mCheckReleaseCallId == releaseCallId) { MobileCenterLog.debug(LOG_TAG, "Schedule background version check..."); - mCheckReleaseTask = new CheckReleaseDetails(releaseDetails).execute(); + mInspectReleaseTask = new CheckReleaseDetails(releaseDetails).execute(); } } @@ -383,7 +480,7 @@ private synchronized void compareVersions(Object releaseCallId, final ReleaseDet private synchronized void storeDownloadRequestId(DownloadManager downloadManager, CheckReleaseDetails task, long downloadRequestId) { /* Check for if state changed and task not canceled in time. */ - if (mCheckReleaseTask == task && isInstanceEnabled()) { + if (mInspectReleaseTask == task) { /* Delete previous download. */ long previousDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); @@ -391,7 +488,7 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager MobileCenterLog.debug(LOG_TAG, "Delete previous download an notification id=" + previousDownloadId); downloadManager.remove(previousDownloadId); NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(getNotificationId(previousDownloadId)); + notificationManager.cancel(getNotificationId()); } /* Store new download identifier. */ @@ -404,8 +501,17 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager } } - synchronized void resumeApp(Context context) { + /** + * Bring app to foreground if in background. + * + * @param context any application context. + */ + synchronized void resumeApp(@NonNull Context context) { + + /* Nothing to do if already in foreground. */ if (mForegroundActivity == null) { + + /* Start launcher activity. */ PackageManager packageManager = context.getPackageManager(); Intent resumeIntent = packageManager.getLaunchIntentForPackage(context.getPackageName()); if (resumeIntent != null) { @@ -415,70 +521,42 @@ synchronized void resumeApp(Context context) { } } - synchronized void processCompletedDownload(Context context, long downloadId) { + /** + * Check a download that just completed. + * + * @param context any application context. + * @param downloadId download identifier from DownloadManager. + */ + synchronized void processCompletedDownload(@NonNull Context context, long downloadId) { - /* Completion might be triggered before MobileCenter.start. */ - MobileCenterLog.debug(LOG_TAG, "Process download completion id=" + downloadId); - if (mContext == null) { - MobileCenterLog.debug(LOG_TAG, "Called before onStart, init storage"); - StorageHelper.initialize(context); - } + /* Querying download manager and even the start intent violate strict mode so do that in background. */ + mProcessDownloadCompletionTask = new ProcessDownloadCompletion(context, downloadId).execute(); + } - /* Check intent data is what we expected. */ - long expectedDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, -1); - if (expectedDownloadId != downloadId) { - MobileCenterLog.warn(LOG_TAG, "Ignoring completion for a download we didn't expect, id=" + downloadId); - return; + /** + * Used by task processing the download completion in background prior to showing install U.I to check if request was canceled. + * + * @param task task to check state for. + * @return foreground activity if any, if state is valid. + * @throws IllegalStateException if state changed. + */ + private synchronized Activity checkStateIsValidFor(ProcessDownloadCompletion task) throws IllegalStateException { + if (task == mProcessDownloadCompletionTask) { + return mForegroundActivity; } + throw new IllegalStateException(); + } - /* Check if download successful. */ - DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE); - Uri uriForDownloadedFile = downloadManager.getUriForDownloadedFile(downloadId); - if (uriForDownloadedFile != null) { - - /* Build install intent. */ - MobileCenterLog.debug(LOG_TAG, "Download was successful for id=" + downloadId + " uri=" + uriForDownloadedFile); - Intent intent = getInstallIntent(uriForDownloadedFile); - - /* If foreground, execute now, otherwise post notification. */ - if (mForegroundActivity != null) { - MobileCenterLog.debug(LOG_TAG, "We are in foreground, launch install UI now."); - mForegroundActivity.startActivity(intent); - } else { - - /* Remember we have a download ready. */ - MobileCenterLog.debug(LOG_TAG, "We are in background, post a notification."); - StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, uriForDownloadedFile.toString()); - - /* And notify. */ - int icon; - try { - ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); - icon = applicationInfo.icon; - } catch (PackageManager.NameNotFoundException e) { - MobileCenterLog.error(LOG_TAG, "Could not get application icon", e); - return; - } - Notification.Builder builder = new Notification.Builder(context) - .setContentTitle(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) - .setContentText(context.getString(R.string.mobile_center_updates_download_successful_notification_message)) - .setSmallIcon(icon) - .setWhen(System.currentTimeMillis()) - .setContentIntent(PendingIntent.getActivities(context, 0, new Intent[]{intent}, 0)); - Notification notification; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - notification = builder.build(); - } else { - //noinspection deprecation - notification = builder.getNotification(); - } - notification.flags |= Notification.FLAG_AUTO_CANCEL; - int notificationId = getNotificationId(downloadId); - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(notificationId, notification); - } - } else { - MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + downloadId); + /** + * Post notification about a completed download if state did not change. + * + * @param task task that prepared the notification to check state. + * @param notification notification to post. + */ + private synchronized void notifyDownload(ProcessDownloadCompletion task, Notification notification) { + if (task == mProcessDownloadCompletionTask) { + NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(getNotificationId(), notification); } } @@ -498,13 +576,13 @@ private class CheckReleaseDetails extends AsyncTask { * @param releaseDetails release details associated to this check. */ CheckReleaseDetails(ReleaseDetails releaseDetails) { - this.mReleaseDetails = releaseDetails; + mReleaseDetails = releaseDetails; } @Override protected Void doInBackground(Void[] params) { - /* TODO Check minimum API level, there is a spec problem currently on that on JSON. */ + /* Check minimum API level TODO not yet available from JSON. */ /* Check version code. */ MobileCenterLog.debug(LOG_TAG, "Check version code."); @@ -516,9 +594,10 @@ protected Void doInBackground(Void[] params) { MobileCenterLog.debug(LOG_TAG, "Latest release version code is higher."); isMoreRecent = true; } else if (mReleaseDetails.getVersion() == packageInfo.versionCode) { - // FIXME check hash when version code is same - MobileCenterLog.debug(LOG_TAG, "Same version code, need to check hash TODO, for now we assume more recent."); - isMoreRecent = true; + + /* Check hash code to see if it's a different build. TODO */ + MobileCenterLog.debug(LOG_TAG, "Same version code, need to check hash."); + isMoreRecent = false; } } catch (PackageManager.NameNotFoundException e) { MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); @@ -539,4 +618,110 @@ protected Void doInBackground(Void[] params) { return null; } } + + /** + * Inspect a completed download, this uses APIs that would trigger strict mode violation if used in U.I. thread. + */ + private class ProcessDownloadCompletion extends AsyncTask { + + /** + * Context. + */ + private final Context mContext; + + /** + * Download identifier to inspect. + */ + private final long mDownloadId; + + /** + * Init. + * + * @param context context. + * @param downloadId download identifier. + */ + ProcessDownloadCompletion(Context context, long downloadId) { + mContext = context; + mDownloadId = downloadId; + } + + @Override + protected Notification doInBackground(Void... params) { + + /* Completion might be triggered before MobileCenter.start. */ + MobileCenterLog.debug(LOG_TAG, "Process download completion id=" + mDownloadId); + if (Updates.this.mContext == null) { + MobileCenterLog.debug(LOG_TAG, "Called before onStart, init storage"); + StorageHelper.initialize(mContext); + } + + /* Check intent data is what we expected. */ + long expectedDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, -1); + if (expectedDownloadId != mDownloadId) { + MobileCenterLog.warn(LOG_TAG, "Ignoring completion for a download we didn't expect, id=" + mDownloadId); + return null; + } + + /* Check if download successful. */ + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + Uri uriForDownloadedFile = downloadManager.getUriForDownloadedFile(mDownloadId); + if (uriForDownloadedFile != null) { + + /* Build install intent. */ + MobileCenterLog.debug(LOG_TAG, "Download was successful for id=" + mDownloadId + " uri=" + uriForDownloadedFile); + Intent intent = getInstallIntent(uriForDownloadedFile); + + /* Exit check point. */ + Activity activity; + try { + activity = checkStateIsValidFor(this); + } catch (IllegalStateException e) { + + /* If we were canceled, exit now. */ + return null; + } + + /* If foreground, execute now, otherwise post notification. */ + if (activity != null) { + + /* This start call triggers strict mode violation in U.I. thread so it needs to be done here, and we can't synchronize anymore... */ + MobileCenterLog.debug(LOG_TAG, "Application is in foreground, launch install UI now."); + activity.startActivity(intent); + } else { + + /* Remember we have a download ready. */ + MobileCenterLog.debug(LOG_TAG, "Application is in background, post a notification."); + StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, uriForDownloadedFile.toString()); + + /* And notify. */ + int icon; + try { + ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(mContext.getPackageName(), 0); + icon = applicationInfo.icon; + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.error(LOG_TAG, "Could not get application icon", e); + return null; + } + Notification.Builder builder = new Notification.Builder(mContext) + .setContentTitle(mContext.getString(R.string.mobile_center_updates_download_successful_notification_title)) + .setContentText(mContext.getString(R.string.mobile_center_updates_download_successful_notification_message)) + .setSmallIcon(icon) + .setWhen(System.currentTimeMillis()) + .setContentIntent(PendingIntent.getActivities(mContext, 0, new Intent[]{intent}, 0)); + Notification notification; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + notification = builder.build(); + } else { + //noinspection deprecation + notification = builder.getNotification(); + } + notification.flags |= Notification.FLAG_AUTO_CANCEL; + notifyDownload(this, notification); + } + } else { + MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + mDownloadId); + } + return null; + } + } } From a7b5530339faa6871930721612c1eb07f6abcae6 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 1 Feb 2017 17:31:02 -0800 Subject: [PATCH 016/142] Fix resuming app and canceling/cleaning download --- .../src/main/AndroidManifest.xml | 2 +- ...ackActivity.java => DeepLinkActivity.java} | 6 ++++-- .../azure/mobile/updates/Updates.java | 21 ++++++++++--------- 3 files changed, 16 insertions(+), 13 deletions(-) rename sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/{LoginCallbackActivity.java => DeepLinkActivity.java} (93%) diff --git a/sdk/mobile-center-updates/src/main/AndroidManifest.xml b/sdk/mobile-center-updates/src/main/AndroidManifest.xml index 2eb2acbaf2..536ad10f49 100644 --- a/sdk/mobile-center-updates/src/main/AndroidManifest.xml +++ b/sdk/mobile-center-updates/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java similarity index 93% rename from sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java rename to sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java index a8bbfd6e96..5f7e52c649 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/LoginCallbackActivity.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java @@ -9,7 +9,10 @@ import static com.microsoft.azure.mobile.updates.Updates.EXTRA_UPDATE_TOKEN; import static com.microsoft.azure.mobile.updates.Updates.LOG_TAG; -public class LoginCallbackActivity extends Activity { +/** + * Generic activity used for deep linking in updates. + */ +public class DeepLinkActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { @@ -23,7 +26,6 @@ public void onCreate(Bundle savedInstanceState) { Intent intent = getIntent(); String updateToken = intent.getStringExtra(EXTRA_UPDATE_TOKEN); MobileCenterLog.debug(LOG_TAG, getLocalClassName() + ".getIntent()=" + intent); - MobileCenterLog.verbose(LOG_TAG, getLocalClassName() + ".getIntent()#S.update_token=" + updateToken); /* Store update token. */ if (updateToken != null) { diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 0c98aa59ca..02713eeff4 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -140,6 +140,7 @@ public class Updates extends AbstractMobileCenterService { /** * Current API call identifier to check latest release from server, used for state check. + * We can't use the ServiceCall object for that purpose because of a chicken and egg problem. */ private Object mCheckReleaseCallId; @@ -264,8 +265,6 @@ public synchronized void setInstanceEnabled(boolean enabled) { mBrowserOpened = false; cancelPreviousTasks(); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); } } @@ -294,6 +293,8 @@ private synchronized void cancelPreviousTasks() { NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(getNotificationId()); } + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); } /** @@ -511,13 +512,13 @@ synchronized void resumeApp(@NonNull Context context) { /* Nothing to do if already in foreground. */ if (mForegroundActivity == null) { - /* Start launcher activity. */ - PackageManager packageManager = context.getPackageManager(); - Intent resumeIntent = packageManager.getLaunchIntentForPackage(context.getPackageName()); - if (resumeIntent != null) { - resumeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(resumeIntent); - } + /* + * Use our deep link activity with no parameter just to resume app correctly + * without duplicating activities or clearing task. + */ + Intent intent = new Intent(context, DeepLinkActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); } } @@ -703,10 +704,10 @@ protected Notification doInBackground(Void... params) { return null; } Notification.Builder builder = new Notification.Builder(mContext) + .setTicker(mContext.getString(R.string.mobile_center_updates_download_successful_notification_title)) .setContentTitle(mContext.getString(R.string.mobile_center_updates_download_successful_notification_title)) .setContentText(mContext.getString(R.string.mobile_center_updates_download_successful_notification_message)) .setSmallIcon(icon) - .setWhen(System.currentTimeMillis()) .setContentIntent(PendingIntent.getActivities(mContext, 0, new Intent[]{intent}, 0)); Notification notification; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { From 35542e5bef1d87a31b671930ca6fae60c53a5532 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 1 Feb 2017 17:43:30 -0800 Subject: [PATCH 017/142] Fix a crash when receiving download intent if application exited --- .../java/com/microsoft/azure/mobile/updates/Updates.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 02713eeff4..08b24078d4 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -551,12 +551,13 @@ private synchronized Activity checkStateIsValidFor(ProcessDownloadCompletion tas /** * Post notification about a completed download if state did not change. * + * @param context context. * @param task task that prepared the notification to check state. * @param notification notification to post. */ - private synchronized void notifyDownload(ProcessDownloadCompletion task, Notification notification) { + private synchronized void notifyDownload(Context context, ProcessDownloadCompletion task, Notification notification) { if (task == mProcessDownloadCompletionTask) { - NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(getNotificationId(), notification); } } @@ -717,7 +718,7 @@ protected Notification doInBackground(Void... params) { notification = builder.getNotification(); } notification.flags |= Notification.FLAG_AUTO_CANCEL; - notifyDownload(this, notification); + notifyDownload(mContext, this, notification); } } else { MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + mDownloadId); From 47329325b851598dd96e2cd8eedd59cdc08893cc Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 1 Feb 2017 18:29:28 -0800 Subject: [PATCH 018/142] Fall back on another executor if AsyncTask thread pool is saturated --- .../azure/mobile/updates/Updates.java | 5 +-- .../azure/mobile/AsyncTaskUtils.java | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AsyncTaskUtils.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 08b24078d4..7baba0984f 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -21,6 +21,7 @@ import android.support.annotation.VisibleForTesting; import com.microsoft.azure.mobile.AbstractMobileCenterService; +import com.microsoft.azure.mobile.AsyncTaskUtils; import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.http.DefaultHttpClient; @@ -467,7 +468,7 @@ private synchronized void compareVersions(Object releaseCallId, final ReleaseDet /* Check if state did not change. */ if (mCheckReleaseCallId == releaseCallId) { MobileCenterLog.debug(LOG_TAG, "Schedule background version check..."); - mInspectReleaseTask = new CheckReleaseDetails(releaseDetails).execute(); + mInspectReleaseTask = AsyncTaskUtils.execute(LOG_TAG, new CheckReleaseDetails(releaseDetails)); } } @@ -531,7 +532,7 @@ synchronized void resumeApp(@NonNull Context context) { synchronized void processCompletedDownload(@NonNull Context context, long downloadId) { /* Querying download manager and even the start intent violate strict mode so do that in background. */ - mProcessDownloadCompletionTask = new ProcessDownloadCompletion(context, downloadId).execute(); + mProcessDownloadCompletionTask = AsyncTaskUtils.execute(LOG_TAG, new ProcessDownloadCompletion(context, downloadId)); } /** diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AsyncTaskUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AsyncTaskUtils.java new file mode 100644 index 0000000000..98f1688e45 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AsyncTaskUtils.java @@ -0,0 +1,35 @@ +package com.microsoft.azure.mobile; + +import android.os.AsyncTask; + +import com.microsoft.azure.mobile.utils.MobileCenterLog; + +import java.util.concurrent.RejectedExecutionException; + +/** + * AsyncTask utilities. + */ +public class AsyncTaskUtils { + + /** + * Execute a task using {@link AsyncTask#THREAD_POOL_EXECUTOR} and fall back + * using {@link AsyncTask#SERIAL_EXECUTOR} in case of {@link RejectedExecutionException}. + * + * @param logTag log tag to use for logging a warning about the fallback. + * @param asyncTask task to execute. + * @param params parameters. + * @param parameters type. + * @param progress type. + * @param result type. + * @return the task. + */ + @SafeVarargs + public static AsyncTask execute(String logTag, AsyncTask asyncTask, Params... params) { + try { + return asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); + } catch (RejectedExecutionException e) { + MobileCenterLog.warn(logTag, "THREAD_POOL_EXECUTOR saturated, fall back on SERIAL_EXECUTOR", e); + return asyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, params); + } + } +} From 9f644711c7c7fe0a68938e5113a5e8682ff6cc2e Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 1 Feb 2017 19:10:10 -0800 Subject: [PATCH 019/142] Move AsyncTaskUtils class and add tests --- .../azure/mobile/updates/Updates.java | 2 +- .../mobile/{ => utils}/AsyncTaskUtils.java | 13 ++-- .../mobile/utils/AsyncTaskUtilsTest.java | 59 +++++++++++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) rename sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/{ => utils}/AsyncTaskUtils.java (83%) create mode 100644 sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/AsyncTaskUtilsTest.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 7baba0984f..a69f623c1e 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -21,7 +21,6 @@ import android.support.annotation.VisibleForTesting; import com.microsoft.azure.mobile.AbstractMobileCenterService; -import com.microsoft.azure.mobile.AsyncTaskUtils; import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.http.DefaultHttpClient; @@ -30,6 +29,7 @@ import com.microsoft.azure.mobile.http.HttpClientRetryer; import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.utils.AsyncTaskUtils; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.NetworkStateHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper; diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AsyncTaskUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java similarity index 83% rename from sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AsyncTaskUtils.java rename to sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java index 98f1688e45..abfcf5fc01 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AsyncTaskUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java @@ -1,15 +1,20 @@ -package com.microsoft.azure.mobile; +package com.microsoft.azure.mobile.utils; import android.os.AsyncTask; - -import com.microsoft.azure.mobile.utils.MobileCenterLog; +import android.support.annotation.VisibleForTesting; import java.util.concurrent.RejectedExecutionException; /** * AsyncTask utilities. */ -public class AsyncTaskUtils { +public final class AsyncTaskUtils { + + @VisibleForTesting + AsyncTaskUtils() { + + /* Hide constructor in utils. */ + } /** * Execute a task using {@link AsyncTask#THREAD_POOL_EXECUTOR} and fall back diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/AsyncTaskUtilsTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/AsyncTaskUtilsTest.java new file mode 100644 index 0000000000..1e830d4f61 --- /dev/null +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/AsyncTaskUtilsTest.java @@ -0,0 +1,59 @@ +package com.microsoft.azure.mobile.utils; + +import android.os.AsyncTask; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(MobileCenterLog.class) +public class AsyncTaskUtilsTest { + + @Test + public void init() { + assertNotNull(new AsyncTaskUtils()); + } + + @Test + public void execute() { + + @SuppressWarnings("unchecked") + AsyncTask task = mock(AsyncTask.class); + when(task.executeOnExecutor(any(Executor.class), anyInt(), anyInt())).thenReturn(task); + assertSame(task, AsyncTaskUtils.execute("", task, 1, 2)); + verify(task).executeOnExecutor(any(Executor.class), eq(1), eq(2)); + } + + @Test + public void executeFallback() { + + @SuppressWarnings("unchecked") + AsyncTask task = mock(AsyncTask.class); + mockStatic(MobileCenterLog.class); + RejectedExecutionException exception = new RejectedExecutionException(); + when(task.executeOnExecutor(any(Executor.class), anyInt(), anyInt())).thenThrow(exception).thenReturn(task); + assertSame(task, AsyncTaskUtils.execute("", task, 1, 2)); + verify(task, times(2)).executeOnExecutor(any(Executor.class), eq(1), eq(2)); + verifyStatic(); + MobileCenterLog.warn(eq(""), anyString(), eq(exception)); + } + +} From 197774fae3157d80bb808991e6f7bdf0e414dfc1 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 2 Feb 2017 11:45:51 -0800 Subject: [PATCH 020/142] Implement request_id in deep link intent for updates --- .../mobile/updates/DeepLinkActivity.java | 14 +++---- .../azure/mobile/updates/Updates.java | 42 +++++++++++++++---- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java index 5f7e52c649..3d913d417c 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java @@ -6,6 +6,7 @@ import com.microsoft.azure.mobile.utils.MobileCenterLog; +import static com.microsoft.azure.mobile.updates.Updates.EXTRA_REQUEST_ID; import static com.microsoft.azure.mobile.updates.Updates.EXTRA_UPDATE_TOKEN; import static com.microsoft.azure.mobile.updates.Updates.LOG_TAG; @@ -17,19 +18,16 @@ public class DeepLinkActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { - /* - * Get update token from intent. - * TODO protect intent: verifying signature with server public key in another field seems like a good way. - * But it would not protect against spamming intents to cause app to use CPU to verify fake signatures. - */ + /* Check intent. */ super.onCreate(savedInstanceState); Intent intent = getIntent(); String updateToken = intent.getStringExtra(EXTRA_UPDATE_TOKEN); + String requestId = intent.getStringExtra(EXTRA_REQUEST_ID); MobileCenterLog.debug(LOG_TAG, getLocalClassName() + ".getIntent()=" + intent); - /* Store update token. */ - if (updateToken != null) { - Updates.getInstance().storeUpdateToken(updateToken); + /* Store update token if the parameters are correct. */ + if (updateToken != null && requestId != null) { + Updates.getInstance().storeUpdateToken(updateToken, requestId); } /* diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index a69f623c1e..1074f42e96 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -32,6 +32,7 @@ import com.microsoft.azure.mobile.utils.AsyncTaskUtils; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.NetworkStateHelper; +import com.microsoft.azure.mobile.utils.UUIDUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper; import org.json.JSONException; @@ -53,6 +54,11 @@ public class Updates extends AbstractMobileCenterService { */ static final String EXTRA_UPDATE_TOKEN = "update_token"; + /** + * Used for deep link intent from browser, string field for request identifier. + */ + static final String EXTRA_REQUEST_ID = "request_id"; + /** * Update service name. */ @@ -98,6 +104,11 @@ public class Updates extends AbstractMobileCenterService { */ private static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; + /** + * Preference key for request identifier to validate deep link intent. + */ + private static final String PREFERENCE_KEY_REQUEST_ID = PREFERENCE_PREFIX + EXTRA_REQUEST_ID; + /** * Preference key to store the last download identifier. */ @@ -139,6 +150,11 @@ public class Updates extends AbstractMobileCenterService { */ private String mBeforeStartUpdateToken; + /** + * In memory request identifier if we receive deep link intent before onStart. + */ + private String mBeforeStartRequestId; + /** * Current API call identifier to check latest release from server, used for state check. * We can't use the ServiceCall object for that purpose because of a chicken and egg problem. @@ -218,7 +234,7 @@ private static Intent getInstallIntent(Uri fileUri) { * @return notification identifier for downloads. */ private static int getNotificationId() { - return (Updates.class.getName()).hashCode(); + return Updates.class.getName().hashCode(); } @Override @@ -305,10 +321,11 @@ private synchronized void resumeUpdateWorkflow() { if (mForegroundActivity != null) { /* If we received the update token before Mobile Center was started/enabled, process it now. */ - if (mBeforeStartUpdateToken != null) { + if (mBeforeStartUpdateToken != null && mBeforeStartRequestId != null) { MobileCenterLog.debug(LOG_TAG, "Processing update token we kept in memory before onStarted"); - storeUpdateToken(mBeforeStartUpdateToken); + storeUpdateToken(mBeforeStartUpdateToken, mBeforeStartRequestId); mBeforeStartUpdateToken = null; + mBeforeStartRequestId = null; return; } @@ -343,18 +360,21 @@ private synchronized void resumeUpdateWorkflow() { return; } MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to login."); - String baseUrl = DEFAULT_LOGIN_PAGE_URL + "?package=" + mForegroundActivity.getPackageName(); + String url = DEFAULT_LOGIN_PAGE_URL + "?package=" + mForegroundActivity.getPackageName(); + String requestId = UUIDUtils.randomUUID().toString(); + url += "&request_id=" + requestId; + StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId); Intent intent = new Intent(Intent.ACTION_VIEW); /* Try to force using Chrome first, we want fall back url support for intent. */ try { - intent.setData(Uri.parse(GOOGLE_CHROME_URL_SCHEME + baseUrl)); + intent.setData(Uri.parse(GOOGLE_CHROME_URL_SCHEME + url)); mForegroundActivity.startActivity(intent); } catch (ActivityNotFoundException e) { /* Fall back using a browser but we don't want a chooser U.I. to pop. */ MobileCenterLog.debug(LOG_TAG, "Google Chrome not found, pick another one."); - intent.setData(Uri.parse(GENERIC_BROWSER_URL_SCHEME + baseUrl)); + intent.setData(Uri.parse(GENERIC_BROWSER_URL_SCHEME + url)); List browsers = mForegroundActivity.getPackageManager().queryIntentActivities(intent, 0); if (browsers.isEmpty()) { MobileCenterLog.error(LOG_TAG, "No browser found on device, abort login."); @@ -414,17 +434,23 @@ private synchronized void resumeUpdateWorkflow() { * how do we protect server call to get the key in the first place? * Even having the encryption key temporarily in memory is risky as that can be heap dumped. */ - synchronized void storeUpdateToken(@NonNull String updateToken) { + synchronized void storeUpdateToken(@NonNull String updateToken, @NonNull String requestId) { /* Keep token for later if we are not started and enabled yet. */ if (mContext == null) { MobileCenterLog.debug(LOG_TAG, "Update token received before onStart, keep it in memory."); mBeforeStartUpdateToken = updateToken; - } else if (isInstanceEnabled()) { + mBeforeStartRequestId = requestId; + } else if (!isInstanceEnabled()) { + MobileCenterLog.warn(LOG_TAG, "Ignoring update token as Updates are disabled."); + } else if (requestId.equals(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID))) { StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); MobileCenterLog.debug(LOG_TAG, "Stored update token."); cancelPreviousTasks(); getLatestReleaseDetails(updateToken); + } else { + MobileCenterLog.warn(LOG_TAG, "Ignoring update token as requestId is invalid."); } } From 239fd1c343cf04a318b0619d1499984e6389c04a Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 2 Feb 2017 17:20:55 -0800 Subject: [PATCH 021/142] Check release at each launcher activity onCreate (if not already checking/downloading) --- .../azure/mobile/updates/Updates.java | 121 ++++++++++++++---- 1 file changed, 96 insertions(+), 25 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 1074f42e96..189042e33f 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -7,6 +7,7 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -17,6 +18,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Build; +import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; @@ -115,7 +117,12 @@ public class Updates extends AbstractMobileCenterService { private static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; /** - * Preference key to store the last download file location on download manager. + * Preference key to store the last download file location on download manager if completed, + * empty string while download is in progress, null if we launched install U.I. + * If this is null and {@link #PREFERENCE_KEY_DOWNLOAD_ID} is not null, it's to remember we + * downloaded a file for later removal (when we disable SDK or prepare a new download). + *

+ * Rationale is that we keep the file in case the user chooses to install it from downloads U.I. */ private static final String PREFERENCE_KEY_DOWNLOAD_URI = PREFERENCE_PREFIX + "download_uri"; @@ -176,6 +183,17 @@ public class Updates extends AbstractMobileCenterService { */ private AsyncTask mProcessDownloadCompletionTask; + /** + * True when update workflow reached final state. + * This can be reset to check update again when app restarts. + */ + private boolean mWorkflowCompleted; + + /** + * Cache launch intent not to resolve it every time from package manager in every onCreate call. + */ + private String mLauncherActivityClassName; + /** * Get shared instance. * @@ -260,6 +278,31 @@ public synchronized void onStarted(@NonNull Context context, @NonNull String app resumeUpdateWorkflow(); } + @Override + public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) { + + /* Resolve launcher class name only once, use empty string to cache a failed resolution. */ + if (mLauncherActivityClassName == null) { + mLauncherActivityClassName = ""; + PackageManager packageManager = activity.getPackageManager(); + Intent intent = packageManager.getLaunchIntentForPackage(activity.getPackageName()); + if (intent != null) { + ComponentName component = intent.resolveActivity(packageManager); + if (component != null) { + mLauncherActivityClassName = component.getClassName(); + } + } + } + + /* Clear workflow finished state if launch recreated, to achieve check on "startup". */ + if (activity.getClass().getName().equals(mLauncherActivityClassName)) { + MobileCenterLog.info(LOG_TAG, "Launcher activity restarted."); + if (mWorkflowCompleted && StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI) == null) { + mWorkflowCompleted = false; + } + } + } + @Override public synchronized void onActivityResumed(Activity activity) { mForegroundActivity = activity; @@ -318,7 +361,7 @@ private synchronized void cancelPreviousTasks() { * Method that triggers the update workflow or proceed to the next step. */ private synchronized void resumeUpdateWorkflow() { - if (mForegroundActivity != null) { + if (mForegroundActivity != null && !mWorkflowCompleted) { /* If we received the update token before Mobile Center was started/enabled, process it now. */ if (mBeforeStartUpdateToken != null && mBeforeStartRequestId != null) { @@ -330,17 +373,25 @@ private synchronized void resumeUpdateWorkflow() { } /* If we have a download ready but we were in background, pop install UI now. */ - try { - Uri apkUri = Uri.parse(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI)); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - MobileCenterLog.debug(LOG_TAG, "Now in foreground, remove notification and start install for APK uri=" + apkUri); - NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(getNotificationId()); - mForegroundActivity.startActivity(getInstallIntent(apkUri)); + String downloadUri = StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI); + if ("".equals(downloadUri)) { + MobileCenterLog.verbose(LOG_TAG, "Download is still in progress..."); return; - } catch (RuntimeException e) { - MobileCenterLog.verbose(LOG_TAG, "No APK downloaded or user ignored it, proceed state check."); - } + } else if (downloadUri != null) + try { + Uri apkUri = Uri.parse(downloadUri); + MobileCenterLog.debug(LOG_TAG, "Now in foreground, remove notification and start install for APK uri=" + apkUri); + NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(getNotificationId()); + mForegroundActivity.startActivity(getInstallIntent(apkUri)); + completeWorkflow(); + return; + } catch (RuntimeException e) { + + /* Cleanup on exception and resume update workflow. */ + MobileCenterLog.warn(LOG_TAG, "Download uri was invalid", e); + cancelPreviousTasks(); + } /* Nothing more to do for now if we are already calling API to check release. */ if (mCheckReleaseCallId != null) { @@ -428,6 +479,16 @@ private synchronized void resumeUpdateWorkflow() { } } + /** + * Reset all variables that matter to restart checking a new release on launcher activity restart. + */ + private synchronized void completeWorkflow() { + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + mWorkflowCompleted = true; + mCheckReleaseApiCall = null; + mCheckReleaseCallId = null; + } + /* * Store update token and possibly trigger application update check. * TODO encrypt token, but where to store encryption key? If it's retrieved from server, @@ -473,7 +534,7 @@ private synchronized void getLatestReleaseDetails(@NonNull String updateToken) { @Override public void onCallSucceeded(String payload) { try { - compareVersions(releaseCallId, ReleaseDetails.parse(payload)); + handleApiCallSuccess(releaseCallId, ReleaseDetails.parse(payload)); } catch (JSONException e) { onCallFailed(e); } @@ -481,20 +542,28 @@ public void onCallSucceeded(String payload) { @Override public void onCallFailed(Exception e) { - MobileCenterLog.error(LOG_TAG, "Failed to check latest release:", e); + handleApiCallFailure(releaseCallId, e); } }); } + private synchronized void handleApiCallFailure(Object releaseCallId, Exception e) { + + /* Check if state did not change. */ + if (mCheckReleaseCallId == releaseCallId) { + MobileCenterLog.error(LOG_TAG, "Failed to check latest release:", e); + } + } + /** * Query package manager and compute hash in background. */ - private synchronized void compareVersions(Object releaseCallId, final ReleaseDetails releaseDetails) { + private synchronized void handleApiCallSuccess(Object releaseCallId, final ReleaseDetails releaseDetails) { /* Check if state did not change. */ if (mCheckReleaseCallId == releaseCallId) { - MobileCenterLog.debug(LOG_TAG, "Schedule background version check..."); - mInspectReleaseTask = AsyncTaskUtils.execute(LOG_TAG, new CheckReleaseDetails(releaseDetails)); + MobileCenterLog.debug(LOG_TAG, "Schedule background version/hash check..."); + mInspectReleaseTask = AsyncTaskUtils.execute(LOG_TAG, new InspectReleaseTask(releaseDetails)); } } @@ -505,7 +574,7 @@ private synchronized void compareVersions(Object releaseCallId, final ReleaseDet * @param task current task to check race conditions. * @param downloadRequestId download identifier. */ - private synchronized void storeDownloadRequestId(DownloadManager downloadManager, CheckReleaseDetails task, long downloadRequestId) { + private synchronized void storeDownloadRequestId(DownloadManager downloadManager, InspectReleaseTask task, long downloadRequestId) { /* Check for if state changed and task not canceled in time. */ if (mInspectReleaseTask == task) { @@ -521,6 +590,7 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager /* Store new download identifier. */ StorageHelper.PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, downloadRequestId); + StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, ""); } else { /* State changed quickly, cancel download. */ @@ -558,7 +628,7 @@ synchronized void resumeApp(@NonNull Context context) { synchronized void processCompletedDownload(@NonNull Context context, long downloadId) { /* Querying download manager and even the start intent violate strict mode so do that in background. */ - mProcessDownloadCompletionTask = AsyncTaskUtils.execute(LOG_TAG, new ProcessDownloadCompletion(context, downloadId)); + mProcessDownloadCompletionTask = AsyncTaskUtils.execute(LOG_TAG, new ProcessDownloadCompletionTask(context, downloadId)); } /** @@ -568,7 +638,7 @@ synchronized void processCompletedDownload(@NonNull Context context, long downlo * @return foreground activity if any, if state is valid. * @throws IllegalStateException if state changed. */ - private synchronized Activity checkStateIsValidFor(ProcessDownloadCompletion task) throws IllegalStateException { + private synchronized Activity checkStateIsValidFor(ProcessDownloadCompletionTask task) throws IllegalStateException { if (task == mProcessDownloadCompletionTask) { return mForegroundActivity; } @@ -582,7 +652,7 @@ private synchronized Activity checkStateIsValidFor(ProcessDownloadCompletion tas * @param task task that prepared the notification to check state. * @param notification notification to post. */ - private synchronized void notifyDownload(Context context, ProcessDownloadCompletion task, Notification notification) { + private synchronized void notifyDownload(Context context, ProcessDownloadCompletionTask task, Notification notification) { if (task == mProcessDownloadCompletionTask) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(getNotificationId(), notification); @@ -592,7 +662,7 @@ private synchronized void notifyDownload(Context context, ProcessDownloadComplet /** * Inspecting release details can take some time, especially if we have to compute a hash. */ - private class CheckReleaseDetails extends AsyncTask { + private class InspectReleaseTask extends AsyncTask { /** * Release details to check. @@ -604,7 +674,7 @@ private class CheckReleaseDetails extends AsyncTask { * * @param releaseDetails release details associated to this check. */ - CheckReleaseDetails(ReleaseDetails releaseDetails) { + InspectReleaseTask(ReleaseDetails releaseDetails) { mReleaseDetails = releaseDetails; } @@ -651,7 +721,7 @@ protected Void doInBackground(Void[] params) { /** * Inspect a completed download, this uses APIs that would trigger strict mode violation if used in U.I. thread. */ - private class ProcessDownloadCompletion extends AsyncTask { + private class ProcessDownloadCompletionTask extends AsyncTask { /** * Context. @@ -669,7 +739,7 @@ private class ProcessDownloadCompletion extends AsyncTask Date: Thu, 2 Feb 2017 19:27:09 -0800 Subject: [PATCH 022/142] Add hash utils --- .../azure/mobile/utils/HashUtils.java | 67 +++++++++++++++++++ .../azure/mobile/utils/HashUtilsTest.java | 59 ++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HashUtils.java create mode 100644 sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/HashUtilsTest.java diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HashUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HashUtils.java new file mode 100644 index 0000000000..ca964fd23b --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HashUtils.java @@ -0,0 +1,67 @@ +package com.microsoft.azure.mobile.utils; + +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Utility class relating to hashing. + */ +public class HashUtils { + + private static final char[] HEXADECIMAL_OUTPUT = "0123456789abcdef".toCharArray(); + + @VisibleForTesting + HashUtils() { + + /* Hide constructor in utils pattern. */ + } + + /** + * Hash data with sha256 and encodeHex output in hexadecimal. + * + * @param data data to hash. + * @return hashed data in hexadecimal output. + */ + @NonNull + public static String sha256(@NonNull String data) { + return sha256(data, "UTF-8"); + } + + @NonNull + @VisibleForTesting + static String sha256(@NonNull String data, String charsetName) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(data.getBytes(charsetName)); + return encodeHex(digest.digest()); + } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { + + /* + * Never happens as every device has UTF-8 support and SHA-256, + * but if it ever happens make sure we propagate exception as unchecked. + */ + throw new RuntimeException(e); + } + } + + /** + * Encode a byte array to a string (hexadecimal) representation. + * + * @param bytes the bytes to encodeHex. + * @return the hexadecimal representation. + */ + @NonNull + private static String encodeHex(@NonNull byte[] bytes) { + char[] output = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + output[j * 2] = HEXADECIMAL_OUTPUT[v >>> 4]; + output[j * 2 + 1] = HEXADECIMAL_OUTPUT[v & 0x0F]; + } + return new String(output); + } +} diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/HashUtilsTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/HashUtilsTest.java new file mode 100644 index 0000000000..1baa062972 --- /dev/null +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/HashUtilsTest.java @@ -0,0 +1,59 @@ +package com.microsoft.azure.mobile.utils; + +import org.junit.Rule; +import org.junit.Test; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; +import static org.powermock.api.mockito.PowerMockito.doThrow; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +public class HashUtilsTest { + + @Rule + public PowerMockRule mPowerMockRule = new PowerMockRule(); + + @Test + public void init() { + assertNotNull(new HashUtils()); + } + + @Test + public void sha256() { + assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", HashUtils.sha256("")); + assertEquals("7efd873c874fbf92d6c3eccc2f24f7eaa349d9d7b512d81ff3f1b44e896362fb", HashUtils.sha256("This hash function rocks!")); + } + + @Test(expected = RuntimeException.class) + @PrepareForTest(HashUtils.class) + public void algorithmNotFound() throws NoSuchAlgorithmException { + mockStatic(MessageDigest.class); + NoSuchAlgorithmException cause = new NoSuchAlgorithmException(); + doThrow(cause).when(MessageDigest.class); + MessageDigest.getInstance(anyString()); + try { + HashUtils.sha256(""); + } catch (RuntimeException e) { + assertEquals(cause, e.getCause()); + throw e; + } + } + + @Test(expected = RuntimeException.class) + public void utf8NotFound() throws UnsupportedEncodingException { + try { + HashUtils.sha256("", "Some Invalid Encoding"); + } catch (RuntimeException e) { + assertTrue(e.getCause() instanceof UnsupportedEncodingException); + throw e; + } + } +} From 18c2823726ea8ba7b8b833654972fcc4cd7f243c Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 3 Feb 2017 15:10:28 -0800 Subject: [PATCH 023/142] Implement latest login spec and version hashing Also fix workflow state when version is older. Remove toast in test app. --- .../sasquatch/activities/MainActivity.java | 1 - .../sasquatch/src/main/res/values/strings.xml | 1 - .../azure/mobile/updates/Updates.java | 128 +++++++++++++----- 3 files changed, 93 insertions(+), 37 deletions(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index 70e69c36da..3fd9a84074 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -94,7 +94,6 @@ private String getAppSecret() { editor.apply(); appSecret = sSharedPreferences.getString(APP_SECRET_KEY, null); } - Toast.makeText(this, String.format(getString(R.string.app_secret_toast), appSecret), Toast.LENGTH_SHORT).show(); return appSecret; } diff --git a/apps/sasquatch/src/main/res/values/strings.xml b/apps/sasquatch/src/main/res/values/strings.xml index a26170c6a3..9abda3d3c1 100644 --- a/apps/sasquatch/src/main/res/values/strings.xml +++ b/apps/sasquatch/src/main/res/values/strings.xml @@ -21,5 +21,4 @@ Properties Key Value - App Secret: %s diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 189042e33f..54b349f323 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -32,6 +32,7 @@ import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; +import com.microsoft.azure.mobile.utils.HashUtils; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.NetworkStateHelper; import com.microsoft.azure.mobile.utils.UUIDUtils; @@ -77,19 +78,49 @@ public class Updates extends AbstractMobileCenterService { private static final String GOOGLE_CHROME_URL_SCHEME = "googlechrome://navigate?url="; /** - * Scheme used to open URLs in any browser. TODO change to https once we have a real server. + * Host (and possibly port), to open browser. */ - private static final String GENERIC_BROWSER_URL_SCHEME = "http://"; + private static final String DEFAULT_LOGIN_HOST = "http://10.123.212.163:8080"; /** - * URL without scheme to open browser to login. + * Base URL to call server to check latest release. */ - private static final String DEFAULT_LOGIN_PAGE_URL = "10.123.212.163:8080"; + private static final String DEFAULT_CHECK_UPDATE_SERVER_URL = "http://10.123.212.163:8080"; /** - * Full URL to call server to check latest release. + * Login URL path. Trailing slash matters to avoid redirection that loses query string. */ - private static final String CHECK_UPDATE_SERVER_URL = "http://10.123.212.163:8080/apps/%s/releases/latest"; + private static final String LOGIN_PAGE_URL_PATH = "/apps/%s/update-setup/"; + + /** + * Check latest release API URL path. + */ + private static final String CHECK_UPDATE_URL_PATH = "/apps/%s/releases/latest"; + + /** + * API parameter for release hash. + */ + private static final String PARAMETER_RELEASE_HASH = "release_hash"; + + /** + * API parameter for redirect URL. + */ + private static final String PARAMETER_REDIRECT_ID = "redirect_id"; + + /** + * API parameter for request identifier. + */ + private static final String PARAMETER_REQUEST_ID = "request_id"; + + /** + * API parameter for platform. + */ + private static final String PARAMETER_PLATFORM = "platform"; + + /** + * API parameter value for this platform. + */ + private static final String PARAMETER_PLATFORM_VALUE = "Android"; /** * Header used to pass token when checking latest release. @@ -323,6 +354,7 @@ public synchronized void setInstanceEnabled(boolean enabled) { /* Clean all state on disabling, cancel everything. */ mBrowserOpened = false; + mWorkflowCompleted = false; cancelPreviousTasks(); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); } @@ -410,22 +442,36 @@ private synchronized void resumeUpdateWorkflow() { if (mBrowserOpened) { return; } - MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to login."); - String url = DEFAULT_LOGIN_PAGE_URL + "?package=" + mForegroundActivity.getPackageName(); + + /* Generate request identifier and store it. */ String requestId = UUIDUtils.randomUUID().toString(); - url += "&request_id=" + requestId; StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId); - Intent intent = new Intent(Intent.ACTION_VIEW); + + /* Compute hash. */ + String releaseHash; + try { + releaseHash = computeHash(mContext); + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.error(LOG_TAG, "Could not get package info", e); + return; + } + String url = DEFAULT_LOGIN_HOST; + url += String.format(LOGIN_PAGE_URL_PATH, mAppSecret); + url += "?" + PARAMETER_RELEASE_HASH + "=" + releaseHash; + url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); + url += "&" + PARAMETER_REQUEST_ID + "=" + requestId; + url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; + MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to login url=" + url); /* Try to force using Chrome first, we want fall back url support for intent. */ + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(GOOGLE_CHROME_URL_SCHEME + url)); try { - intent.setData(Uri.parse(GOOGLE_CHROME_URL_SCHEME + url)); mForegroundActivity.startActivity(intent); } catch (ActivityNotFoundException e) { /* Fall back using a browser but we don't want a chooser U.I. to pop. */ MobileCenterLog.debug(LOG_TAG, "Google Chrome not found, pick another one."); - intent.setData(Uri.parse(GENERIC_BROWSER_URL_SCHEME + url)); + intent.setData(Uri.parse(url)); List browsers = mForegroundActivity.getPackageManager().queryIntentActivities(intent, 0); if (browsers.isEmpty()) { MobileCenterLog.error(LOG_TAG, "No browser found on device, abort login."); @@ -479,6 +525,18 @@ private synchronized void resumeUpdateWorkflow() { } } + @NonNull + private String computeHash(@NonNull Context context) throws PackageManager.NameNotFoundException { + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); + return computeHash(context, packageInfo); + } + + @NonNull + private String computeHash(@NonNull Context context, @NonNull PackageInfo packageInfo) { + return HashUtils.sha256(context.getPackageName() + ":" + packageInfo.versionName + ":" + packageInfo.versionCode); + } + /** * Reset all variables that matter to restart checking a new release on launcher activity restart. */ @@ -525,7 +583,7 @@ private synchronized void getLatestReleaseDetails(@NonNull String updateToken) { HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(mContext); HttpClient httpClient = new HttpClientNetworkStateHandler(retryer, networkStateHelper); - String url = String.format(CHECK_UPDATE_SERVER_URL, mAppSecret); + String url = DEFAULT_CHECK_UPDATE_SERVER_URL + String.format(CHECK_UPDATE_URL_PATH, mAppSecret); Map headers = new HashMap<>(); headers.put(HEADER_API_TOKEN, updateToken); final Object releaseCallId = mCheckReleaseCallId = new Object(); @@ -683,38 +741,38 @@ protected Void doInBackground(Void[] params) { /* Check minimum API level TODO not yet available from JSON. */ - /* Check version code. */ - MobileCenterLog.debug(LOG_TAG, "Check version code."); - boolean isMoreRecent = false; + /* Check version code is equals or higher and hash is different. */ + MobileCenterLog.debug(LOG_TAG, "Check version code and hash."); PackageManager packageManager = mContext.getPackageManager(); try { PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0); - if (mReleaseDetails.getVersion() > packageInfo.versionCode) { - MobileCenterLog.debug(LOG_TAG, "Latest release version code is higher."); - isMoreRecent = true; - } else if (mReleaseDetails.getVersion() == packageInfo.versionCode) { - - /* Check hash code to see if it's a different build. TODO */ - MobileCenterLog.debug(LOG_TAG, "Same version code, need to check hash."); - isMoreRecent = false; + if (isMoreRecent(packageInfo)) { + + /* Download file. */ + Uri downloadUrl = mReleaseDetails.getDownloadUrl(); + MobileCenterLog.debug(LOG_TAG, "Start downloading new release, url=" + downloadUrl); + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + DownloadManager.Request request = new DownloadManager.Request(downloadUrl); + long downloadRequestId = downloadManager.enqueue(request); + storeDownloadRequestId(downloadManager, this, downloadRequestId); + return null; + } else { + MobileCenterLog.debug(LOG_TAG, "Latest server version is not more recent."); } } catch (PackageManager.NameNotFoundException e) { MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); - return null; } - /* Start download if build compatible with device and more recent. */ - if (isMoreRecent) { + /* If download was not started, complete workflow. */ + completeWorkflow(); + return null; + } - /* Download file. */ - Uri downloadUrl = mReleaseDetails.getDownloadUrl(); - MobileCenterLog.debug(LOG_TAG, "Start downloading new release, url=" + downloadUrl); - DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); - DownloadManager.Request request = new DownloadManager.Request(downloadUrl); - long downloadRequestId = downloadManager.enqueue(request); - storeDownloadRequestId(downloadManager, this, downloadRequestId); + private boolean isMoreRecent(PackageInfo packageInfo) throws PackageManager.NameNotFoundException { + if (mReleaseDetails.getVersion() == packageInfo.versionCode) { + return !mReleaseDetails.getFingerprint().equals(computeHash(mContext)); } - return null; + return mReleaseDetails.getVersion() > packageInfo.versionCode; } } From 433a12003bf09ff8642f26dc53f8328b6f86de6e Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 3 Feb 2017 15:22:51 -0800 Subject: [PATCH 024/142] Fix a comment --- .../main/java/com/microsoft/azure/mobile/updates/Updates.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 54b349f323..dc4b68d5dc 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -718,7 +718,7 @@ private synchronized void notifyDownload(Context context, ProcessDownloadComplet } /** - * Inspecting release details can take some time, especially if we have to compute a hash. + * Inspecting release details, the download manager API violates strict mode in U.I. thread. */ private class InspectReleaseTask extends AsyncTask { From f1307c4c99a5c0fba51f6efdfe1c1445a225be01 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 3 Feb 2017 15:37:47 -0800 Subject: [PATCH 025/142] Fix strict mode violation when disabling updates --- .../azure/mobile/updates/Updates.java | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index dc4b68d5dc..fbd23a9f6d 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -380,10 +380,7 @@ private synchronized void cancelPreviousTasks() { long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); if (downloadId > 0) { MobileCenterLog.debug(LOG_TAG, "Removing download and notification id=" + downloadId); - DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); - downloadManager.remove(downloadId); - NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(getNotificationId()); + removeDownload(downloadId); } StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); @@ -717,6 +714,33 @@ private synchronized void notifyDownload(Context context, ProcessDownloadComplet } } + /** + * Remove a previously downloaded file and any notification. + */ + private void removeDownload(long downloadId) { + MobileCenterLog.debug(LOG_TAG, "Delete previous notification downloadId=" + downloadId); + NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(getNotificationId()); + AsyncTaskUtils.execute(LOG_TAG, new RemoveDownloadTask(), downloadId); + } + + /** + * Removing a download violates strict mode in U.I. thread. + */ + private class RemoveDownloadTask extends AsyncTask { + + @Override + protected Void doInBackground(Long... params) { + + /* This special cleanup task does not require any cancellation on state change as a previous download will never be reused. */ + Long downloadId = params[0]; + MobileCenterLog.debug(LOG_TAG, "Delete previous download downloadId=" + downloadId); + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); + downloadManager.remove(downloadId); + return null; + } + } + /** * Inspecting release details, the download manager API violates strict mode in U.I. thread. */ From 2c44183a5c744b725013dea7038257558ca58391 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 3 Feb 2017 17:52:26 -0800 Subject: [PATCH 026/142] Handle confirmation dialog prior to downloading --- .../azure/mobile/updates/Updates.java | 193 +++++++++++++----- .../src/main/res/values/strings.xml | 4 + 2 files changed, 151 insertions(+), 46 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index fbd23a9f6d..c6c039b6d3 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -2,6 +2,7 @@ import android.annotation.SuppressLint; import android.app.Activity; +import android.app.AlertDialog; import android.app.DownloadManager; import android.app.Notification; import android.app.NotificationManager; @@ -9,6 +10,7 @@ import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -21,6 +23,8 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.text.TextUtils; import com.microsoft.azure.mobile.AbstractMobileCenterService; import com.microsoft.azure.mobile.MobileCenter; @@ -204,10 +208,20 @@ public class Updates extends AbstractMobileCenterService { */ private ServiceCall mCheckReleaseApiCall; + /** + * Latest release details waiting to be shown to user. + */ + private ReleaseDetails mReleaseDetails; + + /** + * Last update dialog that was shown. + */ + private AlertDialog mUpdateDialog; + /** * Current task inspecting the latest release details that we fetched from server. */ - private AsyncTask mInspectReleaseTask; + private AsyncTask mDownloadTask; /** * Current task to process download completion. @@ -219,7 +233,6 @@ public class Updates extends AbstractMobileCenterService { * This can be reset to check update again when app restarts. */ private boolean mWorkflowCompleted; - /** * Cache launch intent not to resolve it every time from package manager in every onCreate call. */ @@ -369,9 +382,11 @@ private synchronized void cancelPreviousTasks() { mCheckReleaseApiCall = null; mCheckReleaseCallId = null; } - if (mInspectReleaseTask != null) { - mInspectReleaseTask.cancel(true); - mInspectReleaseTask = null; + mUpdateDialog = null; + mReleaseDetails = null; + if (mDownloadTask != null) { + mDownloadTask.cancel(true); + mDownloadTask = null; } if (mProcessDownloadCompletionTask != null) { mProcessDownloadCompletionTask.cancel(true); @@ -422,6 +437,12 @@ private synchronized void resumeUpdateWorkflow() { cancelPreviousTasks(); } + /* If we were waiting after API call to resume app to show the dialog do it now. */ + if (mReleaseDetails != null) { + showUpdateDialog(); + return; + } + /* Nothing more to do for now if we are already calling API to check release. */ if (mCheckReleaseCallId != null) { MobileCenterLog.verbose(LOG_TAG, "Already checking or checked latest release."); @@ -534,14 +555,27 @@ private String computeHash(@NonNull Context context, @NonNull PackageInfo packag return HashUtils.sha256(context.getPackageName() + ":" + packageInfo.versionName + ":" + packageInfo.versionCode); } + /** + * Reset all variables that matter to restart checking a new release on launcher activity restart. + * + * @param releaseDetails to check if state changed and that the call should be ignored. + */ + private synchronized void completeWorkflow(ReleaseDetails releaseDetails) { + if (releaseDetails == mReleaseDetails) { + completeWorkflow(); + } + } + /** * Reset all variables that matter to restart checking a new release on launcher activity restart. */ - private synchronized void completeWorkflow() { + private void completeWorkflow() { StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - mWorkflowCompleted = true; mCheckReleaseApiCall = null; mCheckReleaseCallId = null; + mUpdateDialog = null; + mReleaseDetails = null; + mWorkflowCompleted = true; } /* @@ -613,12 +647,103 @@ private synchronized void handleApiCallFailure(Object releaseCallId, Exception e /** * Query package manager and compute hash in background. */ - private synchronized void handleApiCallSuccess(Object releaseCallId, final ReleaseDetails releaseDetails) { + private synchronized void handleApiCallSuccess(final Object releaseCallId, final ReleaseDetails releaseDetails) { /* Check if state did not change. */ if (mCheckReleaseCallId == releaseCallId) { + + /* Check version code is equals or higher and hash is different. */ + MobileCenterLog.debug(LOG_TAG, "Check version code and hash."); + PackageManager packageManager = mContext.getPackageManager(); + try { + PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0); + if (isMoreRecent(packageInfo, releaseDetails)) { + + /* Show update dialog. */ + mReleaseDetails = releaseDetails; + if (mForegroundActivity != null) { + showUpdateDialog(); + } + return; + } else { + MobileCenterLog.debug(LOG_TAG, "Latest server version is not more recent."); + } + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); + } + + /* If update dialog was not started, complete workflow. */ + completeWorkflow(); + } + } + + /** + * Check if the fetched release information should be installed. + * + * @param packageInfo current app version. + * @param releaseDetails latest release on server. + * @return true if latest release on server should be used. + */ + private boolean isMoreRecent(PackageInfo packageInfo, ReleaseDetails releaseDetails) { + if (releaseDetails.getVersion() == packageInfo.versionCode) { + return !releaseDetails.getFingerprint().equals(computeHash(mContext, packageInfo)); + } + return releaseDetails.getVersion() > packageInfo.versionCode; + } + + /** + * Show update dialog. This can be called multiple times if clicking on HOME and app resumed + * (it could be resumed in another activity covering the previous one). + */ + private synchronized void showUpdateDialog() { + + /* We could be in another activity now, refresh dialog. */ + MobileCenterLog.debug(LOG_TAG, "Show update dialog."); + if (mUpdateDialog != null) { + mUpdateDialog.hide(); + } + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mForegroundActivity); + dialogBuilder.setTitle(R.string.mobile_center_updates_update_dialog_title); + final ReleaseDetails releaseDetails = mReleaseDetails; + String releaseNotes = releaseDetails.getReleaseNotes(); + if (TextUtils.isEmpty(releaseNotes)) + dialogBuilder.setMessage(R.string.mobile_center_updates_update_dialog_message); + else + dialogBuilder.setMessage(releaseNotes); + dialogBuilder.setPositiveButton(R.string.mobile_center_updates_update_dialog_download, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + scheduleDownload(releaseDetails); + } + }); + dialogBuilder.setNegativeButton(R.string.mobile_center_updates_update_dialog_ignore, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + completeWorkflow(releaseDetails); + } + }); + dialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + + @Override + public void onCancel(DialogInterface dialog) { + completeWorkflow(releaseDetails); + } + }); + mUpdateDialog = dialogBuilder.create(); + mUpdateDialog.show(); + } + + /** + * Check state did not change and schedule download of the release. + * + * @param releaseDetails release details. + */ + private synchronized void scheduleDownload(ReleaseDetails releaseDetails) { + if (releaseDetails == mReleaseDetails) { MobileCenterLog.debug(LOG_TAG, "Schedule background version/hash check..."); - mInspectReleaseTask = AsyncTaskUtils.execute(LOG_TAG, new InspectReleaseTask(releaseDetails)); + mDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new DownloadTask(releaseDetails)); } } @@ -629,10 +754,11 @@ private synchronized void handleApiCallSuccess(Object releaseCallId, final Relea * @param task current task to check race conditions. * @param downloadRequestId download identifier. */ - private synchronized void storeDownloadRequestId(DownloadManager downloadManager, InspectReleaseTask task, long downloadRequestId) { + @WorkerThread + private synchronized void storeDownloadRequestId(DownloadManager downloadManager, DownloadTask task, long downloadRequestId) { /* Check for if state changed and task not canceled in time. */ - if (mInspectReleaseTask == task) { + if (mDownloadTask == task) { /* Delete previous download. */ long previousDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); @@ -742,9 +868,9 @@ protected Void doInBackground(Long... params) { } /** - * Inspecting release details, the download manager API violates strict mode in U.I. thread. + * The download manager API violates strict mode in U.I. thread. */ - private class InspectReleaseTask extends AsyncTask { + private class DownloadTask extends AsyncTask { /** * Release details to check. @@ -756,48 +882,23 @@ private class InspectReleaseTask extends AsyncTask { * * @param releaseDetails release details associated to this check. */ - InspectReleaseTask(ReleaseDetails releaseDetails) { + DownloadTask(ReleaseDetails releaseDetails) { mReleaseDetails = releaseDetails; } @Override protected Void doInBackground(Void[] params) { - /* Check minimum API level TODO not yet available from JSON. */ - - /* Check version code is equals or higher and hash is different. */ - MobileCenterLog.debug(LOG_TAG, "Check version code and hash."); - PackageManager packageManager = mContext.getPackageManager(); - try { - PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0); - if (isMoreRecent(packageInfo)) { - - /* Download file. */ - Uri downloadUrl = mReleaseDetails.getDownloadUrl(); - MobileCenterLog.debug(LOG_TAG, "Start downloading new release, url=" + downloadUrl); - DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); - DownloadManager.Request request = new DownloadManager.Request(downloadUrl); - long downloadRequestId = downloadManager.enqueue(request); - storeDownloadRequestId(downloadManager, this, downloadRequestId); - return null; - } else { - MobileCenterLog.debug(LOG_TAG, "Latest server version is not more recent."); - } - } catch (PackageManager.NameNotFoundException e) { - MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); - } - - /* If download was not started, complete workflow. */ - completeWorkflow(); + /* Download file. */ + Uri downloadUrl = mReleaseDetails.getDownloadUrl(); + MobileCenterLog.debug(LOG_TAG, "Start downloading new release, url=" + downloadUrl); + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + DownloadManager.Request request = new DownloadManager.Request(downloadUrl); + long downloadRequestId = downloadManager.enqueue(request); + storeDownloadRequestId(downloadManager, this, downloadRequestId); return null; } - private boolean isMoreRecent(PackageInfo packageInfo) throws PackageManager.NameNotFoundException { - if (mReleaseDetails.getVersion() == packageInfo.versionCode) { - return !mReleaseDetails.getFingerprint().equals(computeHash(mContext)); - } - return mReleaseDetails.getVersion() > packageInfo.versionCode; - } } /** diff --git a/sdk/mobile-center-updates/src/main/res/values/strings.xml b/sdk/mobile-center-updates/src/main/res/values/strings.xml index 79335d17a3..3b8c14c83a 100644 --- a/sdk/mobile-center-updates/src/main/res/values/strings.xml +++ b/sdk/mobile-center-updates/src/main/res/values/strings.xml @@ -2,4 +2,8 @@ Application update downloaded. Tap to install it now. + Update Available + No release notes was provided for this build. + Ignore + Download \ No newline at end of file From 987f2aab94d5845ec2950e401a203f1e3b0ed40c Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 3 Feb 2017 18:20:42 -0800 Subject: [PATCH 027/142] Add Updates.setLoginUrl and Updates.setApiUrl Reopen browser on restart in same process if deep link intent not fired. End update workflow on fatal http error on API call. --- .../sasquatch/activities/MainActivity.java | 2 + .../azure/mobile/updates/Updates.java | 59 ++++++++++++++++--- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index 3fd9a84074..23c0c92e72 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -50,6 +50,8 @@ protected void onCreate(Bundle savedInstanceState) { } MobileCenter.setLogLevel(Log.VERBOSE); Crashes.setListener(getCrashesListener()); + Updates.setLoginUrl("http://10.123.212.163:8080"); + Updates.setApiUrl("http://10.123.212.163:8080"); MobileCenter.start(getApplication(), getAppSecret(), Analytics.class, Crashes.class, Updates.class); Log.i(LOG_TAG, "Crashes.hasCrashedInLastSession=" + Crashes.hasCrashedInLastSession()); diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index c6c039b6d3..c1450ffc6d 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -82,14 +82,14 @@ public class Updates extends AbstractMobileCenterService { private static final String GOOGLE_CHROME_URL_SCHEME = "googlechrome://navigate?url="; /** - * Host (and possibly port), to open browser. + * Base URL used to open browser to login. */ - private static final String DEFAULT_LOGIN_HOST = "http://10.123.212.163:8080"; + private static final String DEFAULT_LOGIN_URL = "https://install.mobile.azure.com"; /** * Base URL to call server to check latest release. */ - private static final String DEFAULT_CHECK_UPDATE_SERVER_URL = "http://10.123.212.163:8080"; + private static final String DEFAULT_API_URL = "https://api.mobile.azure.com"; /** * Login URL path. Trailing slash matters to avoid redirection that loses query string. @@ -99,7 +99,7 @@ public class Updates extends AbstractMobileCenterService { /** * Check latest release API URL path. */ - private static final String CHECK_UPDATE_URL_PATH = "/apps/%s/releases/latest"; + private static final String CHECK_UPDATE_URL_PATH = "/sdk/apps/%s/releases/latest"; /** * API parameter for release hash. @@ -167,6 +167,16 @@ public class Updates extends AbstractMobileCenterService { @SuppressLint("StaticFieldLeak") private static Updates sInstance = null; + /** + * Current login base URL. + */ + private String mLoginUrl = DEFAULT_LOGIN_URL; + + /** + * Current API base URL. + */ + private String mApiUrl = DEFAULT_API_URL; + /** * Application context, if not null it means onStart was called. */ @@ -233,6 +243,7 @@ public class Updates extends AbstractMobileCenterService { * This can be reset to check update again when app restarts. */ private boolean mWorkflowCompleted; + /** * Cache launch intent not to resolve it every time from package manager in every onCreate call. */ @@ -274,6 +285,24 @@ public static void setEnabled(boolean enabled) { getInstance().setInstanceEnabled(enabled); } + /** + * Change the base URL opened in the browser to get update token from user login information. + * + * @param loginUrl login base URL. + */ + public static void setLoginUrl(String loginUrl) { + getInstance().setInstanceLoginUrl(loginUrl); + } + + /** + * Change the base URL used to make API calls. + * + * @param apiUrl API base URL. + */ + public static void setApiUrl(String apiUrl) { + getInstance().setInstanceApiUrl(apiUrl); + } + /** * Get the intent used to open installation U.I. * @@ -341,8 +370,9 @@ public synchronized void onActivityCreated(Activity activity, Bundle savedInstan /* Clear workflow finished state if launch recreated, to achieve check on "startup". */ if (activity.getClass().getName().equals(mLauncherActivityClassName)) { MobileCenterLog.info(LOG_TAG, "Launcher activity restarted."); - if (mWorkflowCompleted && StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI) == null) { + if (StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI) == null) { mWorkflowCompleted = false; + mBrowserOpened = false; } } } @@ -373,6 +403,20 @@ public synchronized void setInstanceEnabled(boolean enabled) { } } + /** + * Implements {@link #setLoginUrl(String)}. + */ + private synchronized void setInstanceLoginUrl(String loginUrl) { + mLoginUrl = loginUrl; + } + + /** + * Implements {@link #setApiUrl(String)}}. + */ + private synchronized void setInstanceApiUrl(String apiUrl) { + mApiUrl = apiUrl; + } + /** * Cancel everything. */ @@ -473,7 +517,7 @@ private synchronized void resumeUpdateWorkflow() { MobileCenterLog.error(LOG_TAG, "Could not get package info", e); return; } - String url = DEFAULT_LOGIN_HOST; + String url = mLoginUrl; url += String.format(LOGIN_PAGE_URL_PATH, mAppSecret); url += "?" + PARAMETER_RELEASE_HASH + "=" + releaseHash; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); @@ -614,7 +658,7 @@ private synchronized void getLatestReleaseDetails(@NonNull String updateToken) { HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(mContext); HttpClient httpClient = new HttpClientNetworkStateHandler(retryer, networkStateHelper); - String url = DEFAULT_CHECK_UPDATE_SERVER_URL + String.format(CHECK_UPDATE_URL_PATH, mAppSecret); + String url = mApiUrl + String.format(CHECK_UPDATE_URL_PATH, mAppSecret); Map headers = new HashMap<>(); headers.put(HEADER_API_TOKEN, updateToken); final Object releaseCallId = mCheckReleaseCallId = new Object(); @@ -641,6 +685,7 @@ private synchronized void handleApiCallFailure(Object releaseCallId, Exception e /* Check if state did not change. */ if (mCheckReleaseCallId == releaseCallId) { MobileCenterLog.error(LOG_TAG, "Failed to check latest release:", e); + completeWorkflow(); } } From e44e25e9f09ca498e95a4abad7e6e6217ac9d038 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 3 Feb 2017 18:43:47 -0800 Subject: [PATCH 028/142] Minor refactoring in updates --- .../microsoft/azure/mobile/updates/Updates.java | 17 ++++++++++------- .../azure/mobile/utils/AsyncTaskUtils.java | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index c1450ffc6d..002e5bad2b 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -680,6 +680,9 @@ public void onCallFailed(Exception e) { }); } + /** + * Handle API call failure. + */ private synchronized void handleApiCallFailure(Object releaseCallId, Exception e) { /* Check if state did not change. */ @@ -690,9 +693,9 @@ private synchronized void handleApiCallFailure(Object releaseCallId, Exception e } /** - * Query package manager and compute hash in background. + * Handle API call success. */ - private synchronized void handleApiCallSuccess(final Object releaseCallId, final ReleaseDetails releaseDetails) { + private synchronized void handleApiCallSuccess(Object releaseCallId, ReleaseDetails releaseDetails) { /* Check if state did not change. */ if (mCheckReleaseCallId == releaseCallId) { @@ -717,7 +720,7 @@ private synchronized void handleApiCallSuccess(final Object releaseCallId, final MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); } - /* If update dialog was not started, complete workflow. */ + /* If update dialog was not shown or scheduled, complete workflow. */ completeWorkflow(); } } @@ -787,7 +790,7 @@ public void onCancel(DialogInterface dialog) { */ private synchronized void scheduleDownload(ReleaseDetails releaseDetails) { if (releaseDetails == mReleaseDetails) { - MobileCenterLog.debug(LOG_TAG, "Schedule background version/hash check..."); + MobileCenterLog.debug(LOG_TAG, "Schedule download..."); mDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new DownloadTask(releaseDetails)); } } @@ -808,7 +811,7 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager /* Delete previous download. */ long previousDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); if (previousDownloadId > 0) { - MobileCenterLog.debug(LOG_TAG, "Delete previous download an notification id=" + previousDownloadId); + MobileCenterLog.debug(LOG_TAG, "Delete previous download and notification id=" + previousDownloadId); downloadManager.remove(previousDownloadId); NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(getNotificationId()); @@ -949,7 +952,7 @@ protected Void doInBackground(Void[] params) { /** * Inspect a completed download, this uses APIs that would trigger strict mode violation if used in U.I. thread. */ - private class ProcessDownloadCompletionTask extends AsyncTask { + private class ProcessDownloadCompletionTask extends AsyncTask { /** * Context. @@ -973,7 +976,7 @@ private class ProcessDownloadCompletionTask extends AsyncTask Date: Fri, 3 Feb 2017 19:07:36 -0800 Subject: [PATCH 029/142] Use a temporary mock server that is not local --- .../azure/mobile/sasquatch/activities/MainActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index 23c0c92e72..782625ca15 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -50,8 +50,8 @@ protected void onCreate(Bundle savedInstanceState) { } MobileCenter.setLogLevel(Log.VERBOSE); Crashes.setListener(getCrashesListener()); - Updates.setLoginUrl("http://10.123.212.163:8080"); - Updates.setApiUrl("http://10.123.212.163:8080"); + Updates.setLoginUrl("http://mockilecenterupdate.azurewebsites.net"); + Updates.setApiUrl("http://mockilecenterupdate.azurewebsites.net"); MobileCenter.start(getApplication(), getAppSecret(), Analytics.class, Crashes.class, Updates.class); Log.i(LOG_TAG, "Crashes.hasCrashedInLastSession=" + Crashes.hasCrashedInLastSession()); From aea44f560b0229f0ef551168add387eef0b6920f Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 3 Feb 2017 19:42:09 -0800 Subject: [PATCH 030/142] Fix installation on old devices --- .../azure/mobile/updates/Updates.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 002e5bad2b..e7b64484ef 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -17,6 +17,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -1000,6 +1001,23 @@ protected Void doInBackground(Void... params) { /* Build install intent. */ MobileCenterLog.debug(LOG_TAG, "Download was successful for id=" + mDownloadId + " uri=" + uriForDownloadedFile); Intent intent = getInstallIntent(uriForDownloadedFile); + if (intent.resolveActivity(mContext.getPackageManager()) == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + Cursor cursor = downloadManager.query(new DownloadManager.Query().setFilterById(mDownloadId)); + if (cursor != null && cursor.moveToNext()) { + //noinspection deprecation + uriForDownloadedFile = Uri.parse("file://" + cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME))); + intent = getInstallIntent(uriForDownloadedFile); + if (intent.resolveActivity(mContext.getPackageManager()) == null) { + MobileCenterLog.error(LOG_TAG, "Installer not found"); + return null; + } + } + } else { + MobileCenterLog.error(LOG_TAG, "Installer not found"); + return null; + } + } /* Exit check point. */ Activity activity; From 64bccd1217dd387f28a050dd5950fc71b0a8cd15 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 6 Feb 2017 14:51:24 -0800 Subject: [PATCH 031/142] Fix download completion state changes --- .../azure/mobile/updates/Updates.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index e7b64484ef..bc4ba2331c 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -611,10 +611,20 @@ private synchronized void completeWorkflow(ReleaseDetails releaseDetails) { } } + @WorkerThread + private synchronized void completeWorkflow(long downloadId) { + if (!"".equals(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI))) { + return; + } + if (StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID) == downloadId) { + completeWorkflow(); + } + } + /** * Reset all variables that matter to restart checking a new release on launcher activity restart. */ - private void completeWorkflow() { + private synchronized void completeWorkflow() { StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); mCheckReleaseApiCall = null; mCheckReleaseCallId = null; @@ -881,9 +891,11 @@ private synchronized Activity checkStateIsValidFor(ProcessDownloadCompletionTask * @param context context. * @param task task that prepared the notification to check state. * @param notification notification to post. + * @param uri uri to persist to remember download completed state. */ - private synchronized void notifyDownload(Context context, ProcessDownloadCompletionTask task, Notification notification) { + private synchronized void notifyDownload(Context context, ProcessDownloadCompletionTask task, Notification notification, String uri) { if (task == mProcessDownloadCompletionTask) { + StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, uri); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(getNotificationId(), notification); } @@ -1030,17 +1042,17 @@ protected Void doInBackground(Void... params) { } /* If foreground, execute now, otherwise post notification. */ + String uri = uriForDownloadedFile.toString(); if (activity != null) { /* This start call triggers strict mode violation in U.I. thread so it needs to be done here, and we can't synchronize anymore... */ MobileCenterLog.debug(LOG_TAG, "Application is in foreground, launch install UI now."); activity.startActivity(intent); - completeWorkflow(); + completeWorkflow(mDownloadId); } else { /* Remember we have a download ready. */ MobileCenterLog.debug(LOG_TAG, "Application is in background, post a notification."); - StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, uriForDownloadedFile.toString()); /* And notify. */ int icon; @@ -1065,10 +1077,11 @@ protected Void doInBackground(Void... params) { notification = builder.getNotification(); } notification.flags |= Notification.FLAG_AUTO_CANCEL; - notifyDownload(mContext, this, notification); + notifyDownload(mContext, this, notification, uri); } } else { MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + mDownloadId); + completeWorkflow(mDownloadId); } return null; } From 325c2b138ea00898fd2c3bb20d94547a27bdfcaf Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 6 Feb 2017 14:57:33 -0800 Subject: [PATCH 032/142] Code simplification --- .../microsoft/azure/mobile/updates/Updates.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index bc4ba2331c..4b64bbbc6b 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -611,16 +611,6 @@ private synchronized void completeWorkflow(ReleaseDetails releaseDetails) { } } - @WorkerThread - private synchronized void completeWorkflow(long downloadId) { - if (!"".equals(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI))) { - return; - } - if (StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID) == downloadId) { - completeWorkflow(); - } - } - /** * Reset all variables that matter to restart checking a new release on launcher activity restart. */ @@ -1048,7 +1038,7 @@ protected Void doInBackground(Void... params) { /* This start call triggers strict mode violation in U.I. thread so it needs to be done here, and we can't synchronize anymore... */ MobileCenterLog.debug(LOG_TAG, "Application is in foreground, launch install UI now."); activity.startActivity(intent); - completeWorkflow(mDownloadId); + completeWorkflow(mReleaseDetails); } else { /* Remember we have a download ready. */ @@ -1081,7 +1071,7 @@ protected Void doInBackground(Void... params) { } } else { MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + mDownloadId); - completeWorkflow(mDownloadId); + completeWorkflow(mReleaseDetails); } return null; } From 01666f97c951d9a32deafc3e83ebbb616dd92ce6 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 6 Feb 2017 17:05:15 -0800 Subject: [PATCH 033/142] Start adding coverage tests for updates --- .../updates/DownloadCompletionReceiver.java | 34 ++--- .../azure/mobile/updates/Updates.java | 3 +- .../mobile/updates/DeepLinkActivityTest.java | 134 ++++++++++++++++++ .../DownloadCompletionReceiverTest.java | 36 +++++ .../azure/mobile/updates/UpdatesTest.java | 109 +++++++++++++- 5 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverTest.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java index f7db06bb7e..766425f401 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java @@ -14,24 +14,26 @@ public class DownloadCompletionReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { /* Check intent action. */ - switch (intent.getAction()) { + if (intent.getAction() != null) { + switch (intent.getAction()) { - /* - * Just resume app if clicking on pending download notification as - * it's always weird to click on a notification and nothing happening. - * Another option would be to open download list. - */ - case DownloadManager.ACTION_NOTIFICATION_CLICKED: - Updates.getInstance().resumeApp(context); - break; + /* + * Just resume app if clicking on pending download notification as + * it's always weird to click on a notification and nothing happening. + * Another option would be to open download list. + */ + case DownloadManager.ACTION_NOTIFICATION_CLICKED: + Updates.getInstance().resumeApp(context); + break; - /* - * Forward the download identifier to Updates for inspection. - */ - case DownloadManager.ACTION_DOWNLOAD_COMPLETE: - long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); - Updates.getInstance().processCompletedDownload(context, downloadId); - break; + /* + * Forward the download identifier to Updates for inspection. + */ + case DownloadManager.ACTION_DOWNLOAD_COMPLETE: + long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); + Updates.getInstance().processCompletedDownload(context, downloadId); + break; + } } } } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 4b64bbbc6b..bda6a4d40e 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -140,7 +140,8 @@ public class Updates extends AbstractMobileCenterService { /** * Preference key to store token. */ - private static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; + @VisibleForTesting + static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; /** * Preference key for request identifier to validate deep link intent. diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java new file mode 100644 index 0000000000..409f8453c4 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java @@ -0,0 +1,134 @@ +package com.microsoft.azure.mobile.updates; + +import android.content.Intent; +import android.content.pm.PackageManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(Updates.class) +public class DeepLinkActivityTest { + + @Mock + private Updates mUpdates; + + @Before + public void setUp() { + mockStatic(Updates.class); + when(Updates.getInstance()).thenReturn(mUpdates); + } + + /** + * Common code to test invalid intent code path that will also test work around for restart. + */ + private void invalidIntent(Intent intent) { + + /* Test old browser restart workaround. */ + when(intent.cloneFilter()).thenReturn(intent); + when(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)).thenReturn(intent); + DeepLinkActivity activity = spy(new DeepLinkActivity()); + when(activity.getIntent()).thenReturn(intent); + activity.onCreate(null); + + /* Check interactions. */ + verify(activity).startActivity(intent); + verify(activity).finish(); + verifyStatic(never()); + Updates.getInstance(); + } + + @Test + public void missingParametersAndRestartWorkaround() { + Intent intent = mock(Intent.class); + invalidIntent(intent); + } + + @Test + public void missingRequestId() { + Intent intent = mock(Intent.class); + when(intent.getStringExtra(Updates.EXTRA_UPDATE_TOKEN)).thenReturn("mock"); + invalidIntent(intent); + } + + @Test + public void validAndNoTaskRoot() { + + /* Build valid intent. */ + Intent intent = mock(Intent.class); + when(intent.getStringExtra(Updates.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); + when(intent.getStringExtra(Updates.EXTRA_REQUEST_ID)).thenReturn("mock2"); + when(intent.getFlags()).thenReturn(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); + + /* Start activity. */ + DeepLinkActivity activity = spy(new DeepLinkActivity()); + when(activity.getIntent()).thenReturn(intent); + activity.onCreate(null); + + /* Verify interactions. */ + verify(activity, never()).startActivity(any(Intent.class)); + verify(activity).finish(); + verify(mUpdates).storeUpdateToken("mock1", "mock2"); + } + + @Test + public void validAndTaskRootNoLauncher() { + + /* Build valid intent. */ + Intent intent = mock(Intent.class); + when(intent.getStringExtra(Updates.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); + when(intent.getStringExtra(Updates.EXTRA_REQUEST_ID)).thenReturn("mock2"); + when(intent.getFlags()).thenReturn(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); + + /* Start activity. */ + DeepLinkActivity activity = spy(new DeepLinkActivity()); + when(activity.getIntent()).thenReturn(intent); + when(activity.isTaskRoot()).thenReturn(true); + when(activity.getPackageManager()).thenReturn(mock(PackageManager.class)); + activity.onCreate(null); + + /* Verify interactions. */ + verify(activity, never()).startActivity(any(Intent.class)); + verify(activity).finish(); + verify(mUpdates).storeUpdateToken("mock1", "mock2"); + } + + @Test + public void validAndTaskRootStartLauncher() { + + /* Build valid intent. */ + Intent intent = mock(Intent.class); + when(intent.getStringExtra(Updates.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); + when(intent.getStringExtra(Updates.EXTRA_REQUEST_ID)).thenReturn("mock2"); + when(intent.getFlags()).thenReturn(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); + + /* Start activity. */ + DeepLinkActivity activity = spy(new DeepLinkActivity()); + when(activity.getIntent()).thenReturn(intent); + when(activity.isTaskRoot()).thenReturn(true); + PackageManager packageManager = mock(PackageManager.class); + when(activity.getPackageName()).thenReturn("mock.package"); + Intent launcherIntent = mock(Intent.class); + when(packageManager.getLaunchIntentForPackage("mock.package")).thenReturn(launcherIntent); + when(activity.getPackageManager()).thenReturn(packageManager); + activity.onCreate(null); + + /* Verify interactions. */ + verify(activity).startActivity(launcherIntent); + verify(activity).finish(); + verify(mUpdates).storeUpdateToken("mock1", "mock2"); + } +} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverTest.java new file mode 100644 index 0000000000..489bf4b5f9 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverTest.java @@ -0,0 +1,36 @@ +package com.microsoft.azure.mobile.updates; + +import android.content.Context; +import android.content.Intent; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; + +/** + * Missing tests for this class not already covered in Updates. + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest(Updates.class) +public class DownloadCompletionReceiverTest { + + @Test + public void invalidIntent() { + mockStatic(Updates.class); + when(Updates.getInstance()).thenReturn(mock(Updates.class)); + Intent clickIntent = mock(Intent.class); + when(clickIntent.getAction()).thenReturn(Intent.ACTION_ANSWER); + new DownloadCompletionReceiver().onReceive(mock(Context.class), clickIntent); + when(clickIntent.getAction()).thenReturn(null); + new DownloadCompletionReceiver().onReceive(mock(Context.class), clickIntent); + verifyStatic(never()); + Updates.getInstance(); + } +} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java index 5d2eb28fc1..85df73de28 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java @@ -1,10 +1,117 @@ package com.microsoft.azure.mobile.updates; +import android.app.Activity; +import android.app.DownloadManager; +import android.content.Context; +import android.content.Intent; + +import com.microsoft.azure.mobile.MobileCenter; +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.storage.StorageHelper; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; +import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@SuppressWarnings("unused") +@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class}) public class UpdatesTest { + private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; + + @Rule + public PowerMockRule mPowerMockRule = new PowerMockRule(); + + @Before + public void setUp() { + Updates.unsetInstance(); + mockStatic(MobileCenterLog.class); + mockStatic(MobileCenter.class); + when(MobileCenter.isEnabled()).thenReturn(true); + + /* First call to com.microsoft.azure.mobile.MobileCenter.isEnabled shall return true, initial state. */ + mockStatic(StorageHelper.PreferencesStorage.class); + when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(true); + + /* Then simulate further changes to state. */ + PowerMockito.doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + + /* Whenever the new state is persisted, make further calls return the new state. */ + boolean enabled = (Boolean) invocation.getArguments()[1]; + when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(enabled); + return null; + } + }).when(StorageHelper.PreferencesStorage.class); + StorageHelper.PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); + } + @Test - public void todo() { + public void singleton() { + Assert.assertSame(Updates.getInstance(), Updates.getInstance()); + } + + @Test + public void resumeAppBeforeStart() throws Exception { + Intent clickIntent = mock(Intent.class); + when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); + Context context = mock(Context.class); + Intent startIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); + new DownloadCompletionReceiver().onReceive(context, clickIntent); + verify(context).startActivity(startIntent); + verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + @Test + public void resumeAfterBeforeStartButBackground() throws Exception { + Intent clickIntent = mock(Intent.class); + when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); + Context context = mock(Context.class); + Updates.getInstance().onStarted(context, "", mock(Channel.class)); + Intent startIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); + new DownloadCompletionReceiver().onReceive(context, clickIntent); + verify(context).startActivity(startIntent); + verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + @Test + public void resumeForegroundThenPause() throws Exception { + when(StorageHelper.PreferencesStorage.getString(eq(Updates.PREFERENCE_KEY_UPDATE_TOKEN))).thenReturn("mock"); + Intent clickIntent = mock(Intent.class); + when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); + Context context = mock(Context.class); + Updates.getInstance().onStarted(context, "", mock(Channel.class)); + Intent startIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + new DownloadCompletionReceiver().onReceive(context, clickIntent); + verify(context, never()).startActivity(startIntent); + + /* Then pause and test again. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + new DownloadCompletionReceiver().onReceive(context, clickIntent); + verify(context).startActivity(startIntent); + verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } } From d08a4403b46b4d9f05907c6a4d79b27049c30288 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 6 Feb 2017 21:59:33 -0800 Subject: [PATCH 034/142] Add test + refactoring --- .../LoginCallbackActivityAndroidTest.java | 10 -- .../mobile/updates/ReleaseDetailsTest.java | 151 ++++++++++++++++++ .../azure/mobile/updates/ReleaseDetails.java | 37 +---- .../azure/mobile/updates/Updates.java | 4 + .../mobile/updates/AbstractUpdatesTest.java | 55 +++++++ ...adCompletionReceiverIgnoreIntentTest.java} | 5 +- .../UpdatesPlusDownloadReceiverTest.java | 66 ++++++++ .../azure/mobile/updates/UpdatesTest.java | 106 +----------- 8 files changed, 285 insertions(+), 149 deletions(-) delete mode 100644 sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/LoginCallbackActivityAndroidTest.java create mode 100644 sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java rename sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/{DownloadCompletionReceiverTest.java => DownloadCompletionReceiverIgnoreIntentTest.java} (90%) create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/LoginCallbackActivityAndroidTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/LoginCallbackActivityAndroidTest.java deleted file mode 100644 index 8c29bae2ff..0000000000 --- a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/LoginCallbackActivityAndroidTest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.microsoft.azure.mobile.updates; - -import org.junit.Test; - -public class LoginCallbackActivityAndroidTest { - - @Test - public void todo() { - } -} diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java new file mode 100644 index 0000000000..8131532120 --- /dev/null +++ b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java @@ -0,0 +1,151 @@ +package com.microsoft.azure.mobile.updates; + +import android.net.Uri; + +import org.json.JSONException; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class ReleaseDetailsTest { + + @Test + public void parse() throws JSONException { + String json = "{" + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "min_os: ''," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails releaseDetails = ReleaseDetails.parse(json); + assertNotNull(releaseDetails); + assertEquals(14, releaseDetails.getVersion()); + assertEquals("2.1.5", releaseDetails.getShortVersion()); + assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); + assertEquals("", releaseDetails.getMinOs()); + assertEquals("b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c", releaseDetails.getFingerprint()); + assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + } + + @Test(expected = JSONException.class) + public void missingVersion() throws JSONException { + String json = "{" + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "min_os: ''," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + + @Test(expected = JSONException.class) + public void invalidVersion() throws JSONException { + String json = "{" + + "version: true," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "min_os: ''," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + + @Test(expected = JSONException.class) + public void missingShortVersion() throws JSONException { + String json = "{" + + "version: '14'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "min_os: ''," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + + @Test + public void missingReleaseNotes() throws JSONException { + String json = "{" + + "version: '14'," + + "short_version: '2.1.5'," + + "min_os: ''," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails releaseDetails = ReleaseDetails.parse(json); + assertNotNull(releaseDetails); + assertEquals(14, releaseDetails.getVersion()); + assertEquals("2.1.5", releaseDetails.getShortVersion()); + assertNull(releaseDetails.getReleaseNotes()); + assertEquals("", releaseDetails.getMinOs()); + assertEquals("b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c", releaseDetails.getFingerprint()); + assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + } + + @Test(expected = JSONException.class) + public void missingMinOs() throws JSONException { + String json = "{" + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + + @Test(expected = JSONException.class) + public void missingFingerprint() throws JSONException { + String json = "{" + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "min_os: ''," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + + @Test(expected = JSONException.class) + public void missingDownloadUrl() throws JSONException { + String json = "{" + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "min_os: ''," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "}"; + ReleaseDetails.parse(json); + } + + @Test(expected = JSONException.class) + public void missingDownloadUrlScheme() throws JSONException { + String json = "{" + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "min_os: ''," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "download_url: 'someFile'" + + "}"; + ReleaseDetails.parse(json); + } + + @Test(expected = JSONException.class) + public void invalidDownloadUrlScheme() throws JSONException { + String json = "{" + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "min_os: ''," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "download_url: 'ftp://someFile'" + + "}"; + ReleaseDetails.parse(json); + } +} diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java index c9926463c7..1ec2076fd0 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java @@ -5,6 +5,9 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * Release details JSON schema. + */ class ReleaseDetails { private static final String VERSION = "version"; @@ -73,6 +76,10 @@ static ReleaseDetails parse(String json) throws JSONException { releaseDetails.minOs = object.getString(MIN_OS); releaseDetails.fingerprint = object.getString(FINGERPRINT); releaseDetails.downloadUrl = Uri.parse(object.getString(DOWNLOAD_URL)); + String scheme = releaseDetails.downloadUrl.getScheme(); + if (scheme == null || !scheme.startsWith("http")) { + throw new JSONException("Invalid download_url scheme."); + } return releaseDetails; } @@ -129,34 +136,4 @@ String getFingerprint() { Uri getDownloadUrl() { return this.downloadUrl; } - - @Override - @SuppressWarnings("SimplifiableIfStatement") - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ReleaseDetails that = (ReleaseDetails) o; - - if (version != that.version) return false; - if (shortVersion != null ? !shortVersion.equals(that.shortVersion) : that.shortVersion != null) - return false; - if (releaseNotes != null ? !releaseNotes.equals(that.releaseNotes) : that.releaseNotes != null) - return false; - if (minOs != null ? !minOs.equals(that.minOs) : that.minOs != null) return false; - if (fingerprint != null ? !fingerprint.equals(that.fingerprint) : that.fingerprint != null) - return false; - return downloadUrl != null ? downloadUrl.equals(that.downloadUrl) : that.downloadUrl == null; - } - - @Override - public int hashCode() { - int result = version; - result = 31 * result + (shortVersion != null ? shortVersion.hashCode() : 0); - result = 31 * result + (releaseNotes != null ? releaseNotes.hashCode() : 0); - result = 31 * result + (minOs != null ? minOs.hashCode() : 0); - result = 31 * result + (fingerprint != null ? fingerprint.hashCode() : 0); - result = 31 * result + (downloadUrl != null ? downloadUrl.hashCode() : 0); - return result; - } } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index bda6a4d40e..f9e6b2abf6 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -465,10 +465,14 @@ private synchronized void resumeUpdateWorkflow() { /* If we have a download ready but we were in background, pop install UI now. */ String downloadUri = StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI); if ("".equals(downloadUri)) { + + /* TODO double check that with download manager. */ MobileCenterLog.verbose(LOG_TAG, "Download is still in progress..."); return; } else if (downloadUri != null) try { + + /* FIXME this can cause strict mode violation. */ Uri apkUri = Uri.parse(downloadUri); MobileCenterLog.debug(LOG_TAG, "Now in foreground, remove notification and start install for APK uri=" + apkUri); NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java new file mode 100644 index 0000000000..468bd859e3 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -0,0 +1,55 @@ +package com.microsoft.azure.mobile.updates; + +import com.microsoft.azure.mobile.MobileCenter; +import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.storage.StorageHelper; + +import org.junit.Before; +import org.junit.Rule; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; + +import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +@SuppressWarnings("WeakerAccess") +@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class}) +public class AbstractUpdatesTest { + + private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; + + @Rule + public PowerMockRule mPowerMockRule = new PowerMockRule(); + + @Before + public void setUp() { + Updates.unsetInstance(); + mockStatic(MobileCenterLog.class); + mockStatic(MobileCenter.class); + when(MobileCenter.isEnabled()).thenReturn(true); + + /* First call to com.microsoft.azure.mobile.MobileCenter.isEnabled shall return true, initial state. */ + mockStatic(StorageHelper.PreferencesStorage.class); + when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(true); + + /* Then simulate further changes to state. */ + PowerMockito.doAnswer(new Answer() { + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + + /* Whenever the new state is persisted, make further calls return the new state. */ + boolean enabled = (Boolean) invocation.getArguments()[1]; + when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(enabled); + return null; + } + }).when(StorageHelper.PreferencesStorage.class); + StorageHelper.PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); + } +} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverIgnoreIntentTest.java similarity index 90% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverTest.java rename to sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverIgnoreIntentTest.java index 489bf4b5f9..1947e0730a 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverIgnoreIntentTest.java @@ -14,12 +14,9 @@ import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyStatic; -/** - * Missing tests for this class not already covered in Updates. - */ @RunWith(PowerMockRunner.class) @PrepareForTest(Updates.class) -public class DownloadCompletionReceiverTest { +public class DownloadCompletionReceiverIgnoreIntentTest { @Test public void invalidIntent() { diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java new file mode 100644 index 0000000000..c4257ce12f --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java @@ -0,0 +1,66 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.Activity; +import android.app.DownloadManager; +import android.content.Context; +import android.content.Intent; + +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.utils.storage.StorageHelper; + +import org.junit.Test; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +public class UpdatesPlusDownloadReceiverTest extends AbstractUpdatesTest { + + @Test + public void resumeAppBeforeStart() throws Exception { + Intent clickIntent = mock(Intent.class); + when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); + Context context = mock(Context.class); + Intent startIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); + new DownloadCompletionReceiver().onReceive(context, clickIntent); + verify(context).startActivity(startIntent); + verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + @Test + public void resumeAfterBeforeStartButBackground() throws Exception { + Intent clickIntent = mock(Intent.class); + when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); + Context context = mock(Context.class); + Updates.getInstance().onStarted(context, "", mock(Channel.class)); + Intent startIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); + new DownloadCompletionReceiver().onReceive(context, clickIntent); + verify(context).startActivity(startIntent); + verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + @Test + public void resumeForegroundThenPause() throws Exception { + when(StorageHelper.PreferencesStorage.getString(eq(Updates.PREFERENCE_KEY_UPDATE_TOKEN))).thenReturn("mock"); + Intent clickIntent = mock(Intent.class); + when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); + Context context = mock(Context.class); + Updates.getInstance().onStarted(context, "", mock(Channel.class)); + Intent startIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + new DownloadCompletionReceiver().onReceive(context, clickIntent); + verify(context, never()).startActivity(startIntent); + + /* Then pause and test again. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + new DownloadCompletionReceiver().onReceive(context, clickIntent); + verify(context).startActivity(startIntent); + verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } +} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java index 85df73de28..c32963be96 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java @@ -1,117 +1,13 @@ package com.microsoft.azure.mobile.updates; -import android.app.Activity; -import android.app.DownloadManager; -import android.content.Context; -import android.content.Intent; - -import com.microsoft.azure.mobile.MobileCenter; -import com.microsoft.azure.mobile.channel.Channel; -import com.microsoft.azure.mobile.utils.MobileCenterLog; -import com.microsoft.azure.mobile.utils.storage.StorageHelper; - import junit.framework.Assert; -import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.rule.PowerMockRule; - -import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mockStatic; -import static org.powermock.api.mockito.PowerMockito.whenNew; - -@SuppressWarnings("unused") -@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class}) -public class UpdatesTest { - - private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; - @Rule - public PowerMockRule mPowerMockRule = new PowerMockRule(); - - @Before - public void setUp() { - Updates.unsetInstance(); - mockStatic(MobileCenterLog.class); - mockStatic(MobileCenter.class); - when(MobileCenter.isEnabled()).thenReturn(true); - - /* First call to com.microsoft.azure.mobile.MobileCenter.isEnabled shall return true, initial state. */ - mockStatic(StorageHelper.PreferencesStorage.class); - when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(true); - - /* Then simulate further changes to state. */ - PowerMockito.doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - - /* Whenever the new state is persisted, make further calls return the new state. */ - boolean enabled = (Boolean) invocation.getArguments()[1]; - when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(enabled); - return null; - } - }).when(StorageHelper.PreferencesStorage.class); - StorageHelper.PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); - } +public class UpdatesTest extends AbstractUpdatesTest { @Test public void singleton() { Assert.assertSame(Updates.getInstance(), Updates.getInstance()); } - - @Test - public void resumeAppBeforeStart() throws Exception { - Intent clickIntent = mock(Intent.class); - when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); - Context context = mock(Context.class); - Intent startIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); - new DownloadCompletionReceiver().onReceive(context, clickIntent); - verify(context).startActivity(startIntent); - verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - - @Test - public void resumeAfterBeforeStartButBackground() throws Exception { - Intent clickIntent = mock(Intent.class); - when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); - Context context = mock(Context.class); - Updates.getInstance().onStarted(context, "", mock(Channel.class)); - Intent startIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); - new DownloadCompletionReceiver().onReceive(context, clickIntent); - verify(context).startActivity(startIntent); - verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - - @Test - public void resumeForegroundThenPause() throws Exception { - when(StorageHelper.PreferencesStorage.getString(eq(Updates.PREFERENCE_KEY_UPDATE_TOKEN))).thenReturn("mock"); - Intent clickIntent = mock(Intent.class); - when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); - Context context = mock(Context.class); - Updates.getInstance().onStarted(context, "", mock(Channel.class)); - Intent startIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - new DownloadCompletionReceiver().onReceive(context, clickIntent); - verify(context, never()).startActivity(startIntent); - - /* Then pause and test again. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - new DownloadCompletionReceiver().onReceive(context, clickIntent); - verify(context).startActivity(startIntent); - verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } } From f4702d20a2e31dc5a1a28168360051e86ac01410 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 7 Feb 2017 16:27:14 -0800 Subject: [PATCH 035/142] Refactoring and test browser picking --- .../mobile/updates/BrowserUtilsTest.java | 328 ++++++++++++++++++ .../azure/mobile/updates/BrowserUtils.java | 102 ++++++ .../mobile/updates/DeepLinkActivity.java | 6 +- .../azure/mobile/updates/UpdateConstants.java | 32 ++ .../azure/mobile/updates/Updates.java | 96 +---- .../mobile/updates/DeepLinkActivityTest.java | 14 +- .../mobile/updates/UpdatesConstantsTest.java | 13 + 7 files changed, 493 insertions(+), 98 deletions(-) create mode 100644 sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java create mode 100644 sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java create mode 100644 sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesConstantsTest.java diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java new file mode 100644 index 0000000000..ca5ed84216 --- /dev/null +++ b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java @@ -0,0 +1,328 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; + +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InOrder; + +import java.util.Collections; + +import static com.microsoft.azure.mobile.updates.BrowserUtils.GOOGLE_CHROME_URL_SCHEME; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@SuppressWarnings("WrongConstant") +public class BrowserUtilsTest { + + @Test + public void coverage() { + assertNotNull(new BrowserUtils()); + } + + @Test + public void chrome() throws Exception { + Activity activity = mock(Activity.class); + final String url = "https://www.contoso.com?a=b"; + BrowserUtils.openBrowser(url, activity); + verify(activity).startActivity(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); + } + })); + verifyNoMoreInteractions(activity); + } + + @Test + public void noBrowser() throws Exception { + + /* Mock no browser. */ + Activity activity = mock(Activity.class); + PackageManager packageManager = mock(PackageManager.class); + doThrow(new ActivityNotFoundException()).when(activity).startActivity(any(Intent.class)); + when(activity.getPackageManager()).thenReturn(packageManager); + when(packageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(Collections.emptyList()); + + /* Open Chrome then abort. */ + final String url = "https://www.contoso.com?a=b"; + BrowserUtils.openBrowser(url, activity); + verify(activity).startActivity(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); + } + })); + + /* Verify no more call to startActivity. */ + verify(activity).startActivity(any(Intent.class)); + } + + @Test + public void onlySystemBrowserNoDefaultAsNull() throws Exception { + + /* Mock no browser. */ + final String url = "https://www.contoso.com?a=b"; + Activity activity = mock(Activity.class); + ArgumentMatcher chromeMatcher = new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); + } + }; + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + PackageManager packageManager = mock(PackageManager.class); + when(activity.getPackageManager()).thenReturn(packageManager); + when(packageManager.resolveActivity(any(Intent.class), eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(null); + { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = "system"; + activityInfo.name = "browser"; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + when(packageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(Collections.singletonList(resolveInfo)); + } + + /* Open Chrome then abort. */ + BrowserUtils.openBrowser(url, activity); + InOrder order = inOrder(activity); + order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); + } + })); + order.verifyNoMoreInteractions(); + } + + @Test + public void onlySystemBrowserNoDefaultAsPicker() throws Exception { + + /* Mock no browser. */ + final String url = "https://www.contoso.com?a=b"; + Activity activity = mock(Activity.class); + ArgumentMatcher chromeMatcher = new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); + } + }; + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + PackageManager packageManager = mock(PackageManager.class); + when(activity.getPackageManager()).thenReturn(packageManager); + { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = "system"; + activityInfo.name = "picker"; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + when(packageManager.resolveActivity(any(Intent.class), eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(resolveInfo); + } + { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = "system"; + activityInfo.name = "browser"; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + when(packageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(Collections.singletonList(resolveInfo)); + } + + /* Open Chrome then abort. */ + BrowserUtils.openBrowser(url, activity); + InOrder order = inOrder(activity); + order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); + } + })); + order.verifyNoMoreInteractions(); + } + + @Test + public void onlySystemBrowserAndIsDefault() throws Exception { + + /* Mock no browser. */ + final String url = "https://www.contoso.com?a=b"; + Activity activity = mock(Activity.class); + ArgumentMatcher chromeMatcher = new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); + } + }; + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + PackageManager packageManager = mock(PackageManager.class); + when(activity.getPackageManager()).thenReturn(packageManager); + { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = "system"; + activityInfo.name = "browser"; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + when(packageManager.resolveActivity(any(Intent.class), eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(resolveInfo); + } + { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = "system"; + activityInfo.name = "browser"; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + when(packageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(Collections.singletonList(resolveInfo)); + } + + /* Open Chrome then abort. */ + BrowserUtils.openBrowser(url, activity); + InOrder order = inOrder(activity); + order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); + } + })); + order.verifyNoMoreInteractions(); + } + + @Test + public void twoBrowsersAndNoDefault() throws Exception { + + /* Mock no browser. */ + final String url = "https://www.contoso.com?a=b"; + Activity activity = mock(Activity.class); + ArgumentMatcher chromeMatcher = new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); + } + }; + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + PackageManager packageManager = mock(PackageManager.class); + when(activity.getPackageManager()).thenReturn(packageManager); + { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = "system"; + activityInfo.name = "picker"; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + when(packageManager.resolveActivity(any(Intent.class), eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(resolveInfo); + } + { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = "system"; + activityInfo.name = "browser"; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + ActivityInfo activityInfo2 = new ActivityInfo(); + activityInfo2.packageName = "mozilla"; + activityInfo2.name = "firefox"; + ResolveInfo resolveInfo2 = new ResolveInfo(); + resolveInfo2.activityInfo = activityInfo; + when(packageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(asList(resolveInfo, resolveInfo2)); + } + + /* Open Chrome then abort. */ + BrowserUtils.openBrowser(url, activity); + InOrder order = inOrder(activity); + order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); + } + })); + order.verifyNoMoreInteractions(); + } + + @Test + public void secondBrowserIsDefault() throws Exception { + + /* Mock no browser. */ + final String url = "https://www.contoso.com?a=b"; + Activity activity = mock(Activity.class); + ArgumentMatcher chromeMatcher = new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); + } + }; + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + PackageManager packageManager = mock(PackageManager.class); + when(activity.getPackageManager()).thenReturn(packageManager); + { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = "mozilla"; + activityInfo.name = "firefox"; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + when(packageManager.resolveActivity(any(Intent.class), eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(resolveInfo); + } + { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = "system"; + activityInfo.name = "browser"; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + ActivityInfo activityInfo2 = new ActivityInfo(); + activityInfo2.packageName = "mozilla"; + activityInfo2.name = "firefox"; + ResolveInfo resolveInfo2 = new ResolveInfo(); + resolveInfo2.activityInfo = activityInfo2; + when(packageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(asList(resolveInfo, resolveInfo2)); + } + + /* Open Chrome then abort. */ + BrowserUtils.openBrowser(url, activity); + InOrder order = inOrder(activity); + order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("firefox"); + } + })); + order.verifyNoMoreInteractions(); + } +} diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java new file mode 100644 index 0000000000..cdc0da8d5c --- /dev/null +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java @@ -0,0 +1,102 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; + +import com.microsoft.azure.mobile.utils.MobileCenterLog; + +import java.util.List; + +import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; + +/** + * Browser utils. + */ +class BrowserUtils { + + /** + * Scheme used to open URLs in Google Chrome instead of any browser. + */ + @VisibleForTesting + static final String GOOGLE_CHROME_URL_SCHEME = "googlechrome://navigate?url="; + + @VisibleForTesting + BrowserUtils() { + } + + + /** + * Open a URL in the best browser available. + * + * @param url url to open. + * @param activity activity from which to open browser. + */ + static void openBrowser(@NonNull String url, @NonNull Activity activity) { + + /* Try to force using Chrome first, we want fall back url support for intent. */ + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(GOOGLE_CHROME_URL_SCHEME + url)); + try { + activity.startActivity(intent); + } catch (ActivityNotFoundException e) { + + /* Fall back using a browser but we don't want a chooser U.I. to pop. */ + MobileCenterLog.debug(LOG_TAG, "Google Chrome not found, pick another one."); + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + List browsers = activity.getPackageManager().queryIntentActivities(intent, 0); + if (browsers.isEmpty()) { + MobileCenterLog.error(LOG_TAG, "No browser found on device, abort login."); + } else { + + /* + * Check the default browser is not the picker, + * last thing we want is app to start and suddenly asks user to pick + * between 2 browsers without explaining why. + */ + String defaultBrowserPackageName = null; + String defaultBrowserClassName = null; + ResolveInfo defaultBrowser = activity.getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (defaultBrowser != null) { + ActivityInfo activityInfo = defaultBrowser.activityInfo; + defaultBrowserPackageName = activityInfo.packageName; + defaultBrowserClassName = activityInfo.name; + MobileCenterLog.debug(LOG_TAG, "Default browser seems to be " + defaultBrowserPackageName + "/" + defaultBrowserClassName); + } + String selectedPackageName = null; + String selectedClassName = null; + for (ResolveInfo browser : browsers) { + ActivityInfo activityInfo = browser.activityInfo; + if (activityInfo.packageName.equals(defaultBrowserPackageName) && activityInfo.name.equals(defaultBrowserClassName)) { + selectedPackageName = defaultBrowserPackageName; + selectedClassName = defaultBrowserClassName; + MobileCenterLog.debug(LOG_TAG, "And its not the picker."); + break; + } + } + if (defaultBrowser != null && selectedPackageName == null) { + MobileCenterLog.debug(LOG_TAG, "Default browser is actually a picker..."); + } + + /* If no default browser found, pick first one we can find. */ + if (selectedPackageName == null) { + MobileCenterLog.debug(LOG_TAG, "Picking first browser in list."); + ResolveInfo browser = browsers.iterator().next(); + ActivityInfo activityInfo = browser.activityInfo; + selectedPackageName = activityInfo.packageName; + selectedClassName = activityInfo.name; + } + + /* Launch generic browser. */ + MobileCenterLog.debug(LOG_TAG, "Launch browser=" + selectedPackageName + "/" + selectedClassName); + intent.setClassName(selectedPackageName, selectedClassName); + activity.startActivity(intent); + } + } + } +} diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java index 3d913d417c..42afb5417d 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java @@ -6,9 +6,9 @@ import com.microsoft.azure.mobile.utils.MobileCenterLog; -import static com.microsoft.azure.mobile.updates.Updates.EXTRA_REQUEST_ID; -import static com.microsoft.azure.mobile.updates.Updates.EXTRA_UPDATE_TOKEN; -import static com.microsoft.azure.mobile.updates.Updates.LOG_TAG; +import static com.microsoft.azure.mobile.updates.UpdateConstants.EXTRA_REQUEST_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.EXTRA_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; /** * Generic activity used for deep linking in updates. diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java new file mode 100644 index 0000000000..eae0107348 --- /dev/null +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -0,0 +1,32 @@ +package com.microsoft.azure.mobile.updates; + +import android.support.annotation.VisibleForTesting; + +import com.microsoft.azure.mobile.MobileCenter; + +final class UpdateConstants { + + /** + * Update service name. + */ + static final String SERVICE_NAME = "Updates"; + + /** + * Used for deep link intent from browser, string field for update token. + */ + static final String EXTRA_UPDATE_TOKEN = "update_token"; + + /** + * Used for deep link intent from browser, string field for request identifier. + */ + static final String EXTRA_REQUEST_ID = "request_id"; + + /** + * Log tag for this service. + */ + static final String LOG_TAG = MobileCenter.LOG_TAG + SERVICE_NAME; + + @VisibleForTesting + UpdateConstants() { + } +} diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index f9e6b2abf6..764ee7b5e2 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -7,16 +7,13 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; @@ -28,7 +25,6 @@ import android.text.TextUtils; import com.microsoft.azure.mobile.AbstractMobileCenterService; -import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.http.DefaultHttpClient; import com.microsoft.azure.mobile.http.HttpClient; @@ -46,42 +42,20 @@ import org.json.JSONException; import java.util.HashMap; -import java.util.List; import java.util.Map; import static android.content.Context.DOWNLOAD_SERVICE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; +import static com.microsoft.azure.mobile.updates.UpdateConstants.EXTRA_REQUEST_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.EXTRA_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; +import static com.microsoft.azure.mobile.updates.UpdateConstants.SERVICE_NAME; /** * Updates service. */ public class Updates extends AbstractMobileCenterService { - /** - * Used for deep link intent from browser, string field for update token. - */ - static final String EXTRA_UPDATE_TOKEN = "update_token"; - - /** - * Used for deep link intent from browser, string field for request identifier. - */ - static final String EXTRA_REQUEST_ID = "request_id"; - - /** - * Update service name. - */ - private static final String SERVICE_NAME = "Updates"; - - /** - * Log tag for this service. - */ - static final String LOG_TAG = MobileCenter.LOG_TAG + SERVICE_NAME; - - /** - * Scheme used to open URLs in Google Chrome instead of any browser. - */ - private static final String GOOGLE_CHROME_URL_SCHEME = "googlechrome://navigate?url="; - /** * Base URL used to open browser to login. */ @@ -523,6 +497,8 @@ private synchronized void resumeUpdateWorkflow() { MobileCenterLog.error(LOG_TAG, "Could not get package info", e); return; } + + /* Build URL. */ String url = mLoginUrl; url += String.format(LOGIN_PAGE_URL_PATH, mAppSecret); url += "?" + PARAMETER_RELEASE_HASH + "=" + releaseHash; @@ -531,64 +507,8 @@ private synchronized void resumeUpdateWorkflow() { url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to login url=" + url); - /* Try to force using Chrome first, we want fall back url support for intent. */ - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(GOOGLE_CHROME_URL_SCHEME + url)); - try { - mForegroundActivity.startActivity(intent); - } catch (ActivityNotFoundException e) { - - /* Fall back using a browser but we don't want a chooser U.I. to pop. */ - MobileCenterLog.debug(LOG_TAG, "Google Chrome not found, pick another one."); - intent.setData(Uri.parse(url)); - List browsers = mForegroundActivity.getPackageManager().queryIntentActivities(intent, 0); - if (browsers.isEmpty()) { - MobileCenterLog.error(LOG_TAG, "No browser found on device, abort login."); - } else { - - /* - * Check the default browser is not the picker, - * last thing we want is app to start and suddenly asks user to pick - * between 2 browsers without explaining why. - */ - String defaultBrowserPackageName = null; - String defaultBrowserClassName = null; - ResolveInfo defaultBrowser = mForegroundActivity.getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); - if (defaultBrowser != null) { - ActivityInfo activityInfo = defaultBrowser.activityInfo; - defaultBrowserPackageName = activityInfo.packageName; - defaultBrowserClassName = activityInfo.name; - MobileCenterLog.debug(LOG_TAG, "Default browser seems to be " + defaultBrowserPackageName + "/" + defaultBrowserClassName); - } - String selectedPackageName = null; - String selectedClassName = null; - for (ResolveInfo browser : browsers) { - ActivityInfo activityInfo = browser.activityInfo; - if (activityInfo.packageName.equals(defaultBrowserPackageName) && activityInfo.name.equals(defaultBrowserClassName)) { - selectedPackageName = defaultBrowserPackageName; - selectedClassName = defaultBrowserClassName; - MobileCenterLog.debug(LOG_TAG, "And its not the picker."); - break; - } - } - if (defaultBrowser != null && selectedPackageName == null) { - MobileCenterLog.debug(LOG_TAG, "Default browser is actually a picker..."); - } - - /* If no default browser found, pick first one we can find. */ - if (selectedPackageName == null) { - MobileCenterLog.debug(LOG_TAG, "Picking first browser in list."); - ResolveInfo browser = browsers.iterator().next(); - ActivityInfo activityInfo = browser.activityInfo; - selectedPackageName = activityInfo.packageName; - selectedClassName = activityInfo.name; - } - - /* Launch generic browser. */ - MobileCenterLog.debug(LOG_TAG, "Launch browser=" + selectedPackageName + "/" + selectedClassName); - intent.setClassName(selectedPackageName, selectedClassName); - mForegroundActivity.startActivity(intent); - } - } + /* Open browser, remember that whatever the outcome to avoid opening it twice. */ + BrowserUtils.openBrowser(url, mForegroundActivity); mBrowserOpened = true; } } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java index 409f8453c4..beea1f7983 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java @@ -60,7 +60,7 @@ public void missingParametersAndRestartWorkaround() { @Test public void missingRequestId() { Intent intent = mock(Intent.class); - when(intent.getStringExtra(Updates.EXTRA_UPDATE_TOKEN)).thenReturn("mock"); + when(intent.getStringExtra(UpdateConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock"); invalidIntent(intent); } @@ -69,8 +69,8 @@ public void validAndNoTaskRoot() { /* Build valid intent. */ Intent intent = mock(Intent.class); - when(intent.getStringExtra(Updates.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); - when(intent.getStringExtra(Updates.EXTRA_REQUEST_ID)).thenReturn("mock2"); + when(intent.getStringExtra(UpdateConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); + when(intent.getStringExtra(UpdateConstants.EXTRA_REQUEST_ID)).thenReturn("mock2"); when(intent.getFlags()).thenReturn(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); /* Start activity. */ @@ -89,8 +89,8 @@ public void validAndTaskRootNoLauncher() { /* Build valid intent. */ Intent intent = mock(Intent.class); - when(intent.getStringExtra(Updates.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); - when(intent.getStringExtra(Updates.EXTRA_REQUEST_ID)).thenReturn("mock2"); + when(intent.getStringExtra(UpdateConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); + when(intent.getStringExtra(UpdateConstants.EXTRA_REQUEST_ID)).thenReturn("mock2"); when(intent.getFlags()).thenReturn(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); /* Start activity. */ @@ -111,8 +111,8 @@ public void validAndTaskRootStartLauncher() { /* Build valid intent. */ Intent intent = mock(Intent.class); - when(intent.getStringExtra(Updates.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); - when(intent.getStringExtra(Updates.EXTRA_REQUEST_ID)).thenReturn("mock2"); + when(intent.getStringExtra(UpdateConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); + when(intent.getStringExtra(UpdateConstants.EXTRA_REQUEST_ID)).thenReturn("mock2"); when(intent.getFlags()).thenReturn(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); /* Start activity. */ diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesConstantsTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesConstantsTest.java new file mode 100644 index 0000000000..94f4d64d9e --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesConstantsTest.java @@ -0,0 +1,13 @@ +package com.microsoft.azure.mobile.updates; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; + +public class UpdatesConstantsTest { + + @Test + public void coverage() { + assertNotNull(new UpdateConstants()); + } +} From 25230036da782aa2e863e6c0570ab55483aa82ad Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 7 Feb 2017 16:59:39 -0800 Subject: [PATCH 036/142] Refactoring to start testing interactions with storage --- .../azure/mobile/updates/UpdateConstants.java | 34 ++++++++++++++++- .../azure/mobile/updates/Updates.java | 37 ++----------------- .../UpdatesPlusDownloadReceiverTest.java | 11 +++--- .../azure/mobile/updates/UpdatesTest.java | 16 ++++++++ 4 files changed, 58 insertions(+), 40 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java index eae0107348..961388d9da 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -11,6 +11,11 @@ final class UpdateConstants { */ static final String SERVICE_NAME = "Updates"; + /** + * Log tag for this service. + */ + static final String LOG_TAG = MobileCenter.LOG_TAG + SERVICE_NAME; + /** * Used for deep link intent from browser, string field for update token. */ @@ -22,9 +27,34 @@ final class UpdateConstants { static final String EXTRA_REQUEST_ID = "request_id"; /** - * Log tag for this service. + * Base key for stored preferences. */ - static final String LOG_TAG = MobileCenter.LOG_TAG + SERVICE_NAME; + private static final String PREFERENCE_PREFIX = SERVICE_NAME + "."; + + /** + * Preference key to store the last download file location on download manager if completed, + * empty string while download is in progress, null if we launched install U.I. + * If this is null and {@link #PREFERENCE_KEY_DOWNLOAD_ID} is not null, it's to remember we + * downloaded a file for later removal (when we disable SDK or prepare a new download). + *

+ * Rationale is that we keep the file in case the user chooses to install it from downloads U.I. + */ + static final String PREFERENCE_KEY_DOWNLOAD_URI = PREFERENCE_PREFIX + "download_uri"; + + /** + * Preference key to store the last download identifier. + */ + static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; + + /** + * Preference key for request identifier to validate deep link intent. + */ + static final String PREFERENCE_KEY_REQUEST_ID = PREFERENCE_PREFIX + EXTRA_REQUEST_ID; + + /** + * Preference key to store token. + */ + static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; @VisibleForTesting UpdateConstants() { diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 764ee7b5e2..2fab168a9b 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -46,9 +46,11 @@ import static android.content.Context.DOWNLOAD_SERVICE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; -import static com.microsoft.azure.mobile.updates.UpdateConstants.EXTRA_REQUEST_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.EXTRA_UPDATE_TOKEN; import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.updates.UpdateConstants.SERVICE_NAME; /** @@ -106,37 +108,6 @@ public class Updates extends AbstractMobileCenterService { */ private static final String HEADER_API_TOKEN = "x-api-token"; - /** - * Base key for stored preferences. - */ - private static final String PREFERENCE_PREFIX = SERVICE_NAME + "."; - - /** - * Preference key to store token. - */ - @VisibleForTesting - static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; - - /** - * Preference key for request identifier to validate deep link intent. - */ - private static final String PREFERENCE_KEY_REQUEST_ID = PREFERENCE_PREFIX + EXTRA_REQUEST_ID; - - /** - * Preference key to store the last download identifier. - */ - private static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; - - /** - * Preference key to store the last download file location on download manager if completed, - * empty string while download is in progress, null if we launched install U.I. - * If this is null and {@link #PREFERENCE_KEY_DOWNLOAD_ID} is not null, it's to remember we - * downloaded a file for later removal (when we disable SDK or prepare a new download). - *

- * Rationale is that we keep the file in case the user chooses to install it from downloads U.I. - */ - private static final String PREFERENCE_KEY_DOWNLOAD_URI = PREFERENCE_PREFIX + "download_uri"; - /** * Shared instance. */ diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java index c4257ce12f..03ccc1c2b4 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java @@ -1,7 +1,6 @@ package com.microsoft.azure.mobile.updates; import android.app.Activity; -import android.app.DownloadManager; import android.content.Context; import android.content.Intent; @@ -10,6 +9,8 @@ import org.junit.Test; +import static android.app.DownloadManager.ACTION_NOTIFICATION_CLICKED; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -22,7 +23,7 @@ public class UpdatesPlusDownloadReceiverTest extends AbstractUpdatesTest { @Test public void resumeAppBeforeStart() throws Exception { Intent clickIntent = mock(Intent.class); - when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); + when(clickIntent.getAction()).thenReturn(ACTION_NOTIFICATION_CLICKED); Context context = mock(Context.class); Intent startIntent = mock(Intent.class); whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); @@ -34,7 +35,7 @@ public void resumeAppBeforeStart() throws Exception { @Test public void resumeAfterBeforeStartButBackground() throws Exception { Intent clickIntent = mock(Intent.class); - when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); + when(clickIntent.getAction()).thenReturn(ACTION_NOTIFICATION_CLICKED); Context context = mock(Context.class); Updates.getInstance().onStarted(context, "", mock(Channel.class)); Intent startIntent = mock(Intent.class); @@ -46,9 +47,9 @@ public void resumeAfterBeforeStartButBackground() throws Exception { @Test public void resumeForegroundThenPause() throws Exception { - when(StorageHelper.PreferencesStorage.getString(eq(Updates.PREFERENCE_KEY_UPDATE_TOKEN))).thenReturn("mock"); + when(StorageHelper.PreferencesStorage.getString(eq(PREFERENCE_KEY_UPDATE_TOKEN))).thenReturn("mock"); Intent clickIntent = mock(Intent.class); - when(clickIntent.getAction()).thenReturn(DownloadManager.ACTION_NOTIFICATION_CLICKED); + when(clickIntent.getAction()).thenReturn(ACTION_NOTIFICATION_CLICKED); Context context = mock(Context.class); Updates.getInstance().onStarted(context, "", mock(Channel.class)); Intent startIntent = mock(Intent.class); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java index c32963be96..5a45caa446 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java @@ -1,13 +1,29 @@ package com.microsoft.azure.mobile.updates; +import com.microsoft.azure.mobile.utils.storage.StorageHelper; + import junit.framework.Assert; import org.junit.Test; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; + public class UpdatesTest extends AbstractUpdatesTest { @Test public void singleton() { Assert.assertSame(Updates.getInstance(), Updates.getInstance()); } + + @Test + public void storeTokenBeforeStart() { + when(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn("r"); + Updates.getInstance().storeUpdateToken("some token", "r"); + verifyStatic(never()); + StorageHelper.PreferencesStorage.putString(anyString(), anyString()); + } } From fe426cfbc83010b2414d3b18146b48a610adccb1 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 7 Feb 2017 18:39:57 -0800 Subject: [PATCH 037/142] More test and refactoring in updates --- .../azure/mobile/updates/UpdateConstants.java | 45 ++++++++- .../azure/mobile/updates/Updates.java | 60 ++---------- .../mobile/updates/AbstractUpdatesTest.java | 28 +++++- .../azure/mobile/updates/UpdatesTest.java | 97 ++++++++++++++++++- .../azure/mobile/utils/UUIDUtils.java | 2 +- 5 files changed, 170 insertions(+), 62 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java index 961388d9da..d9a1602043 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -25,12 +25,50 @@ final class UpdateConstants { * Used for deep link intent from browser, string field for request identifier. */ static final String EXTRA_REQUEST_ID = "request_id"; - + /** + * Base URL used to open browser to login. + */ + static final String DEFAULT_LOGIN_URL = "https://install.mobile.azure.com"; + /** + * Base URL to call server to check latest release. + */ + static final String DEFAULT_API_URL = "https://api.mobile.azure.com"; + /** + * Login URL path. Trailing slash matters to avoid redirection that loses query string. + */ + static final String LOGIN_PAGE_URL_PATH = "/apps/%s/update-setup/"; + /** + * Check latest release API URL path. + */ + static final String CHECK_UPDATE_URL_PATH = "/sdk/apps/%s/releases/latest"; + /** + * API parameter for release hash. + */ + static final String PARAMETER_RELEASE_HASH = "release_hash"; + /** + * API parameter for redirect URL. + */ + static final String PARAMETER_REDIRECT_ID = "redirect_id"; + /** + * API parameter for request identifier. + */ + static final String PARAMETER_REQUEST_ID = "request_id"; + /** + * API parameter for platform. + */ + static final String PARAMETER_PLATFORM = "platform"; + /** + * API parameter value for this platform. + */ + static final String PARAMETER_PLATFORM_VALUE = "Android"; + /** + * Header used to pass token when checking latest release. + */ + static final String HEADER_API_TOKEN = "x-api-token"; /** * Base key for stored preferences. */ private static final String PREFERENCE_PREFIX = SERVICE_NAME + "."; - /** * Preference key to store the last download file location on download manager if completed, * empty string while download is in progress, null if we launched install U.I. @@ -40,17 +78,14 @@ final class UpdateConstants { * Rationale is that we keep the file in case the user chooses to install it from downloads U.I. */ static final String PREFERENCE_KEY_DOWNLOAD_URI = PREFERENCE_PREFIX + "download_uri"; - /** * Preference key to store the last download identifier. */ static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; - /** * Preference key for request identifier to validate deep link intent. */ static final String PREFERENCE_KEY_REQUEST_ID = PREFERENCE_PREFIX + EXTRA_REQUEST_ID; - /** * Preference key to store token. */ diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 2fab168a9b..38ab1f6949 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -46,7 +46,17 @@ import static android.content.Context.DOWNLOAD_SERVICE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; +import static com.microsoft.azure.mobile.updates.UpdateConstants.CHECK_UPDATE_URL_PATH; +import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_API_URL; +import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_LOGIN_URL; +import static com.microsoft.azure.mobile.updates.UpdateConstants.HEADER_API_TOKEN; +import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH; import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REDIRECT_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_RELEASE_HASH; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REQUEST_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; @@ -58,56 +68,6 @@ */ public class Updates extends AbstractMobileCenterService { - /** - * Base URL used to open browser to login. - */ - private static final String DEFAULT_LOGIN_URL = "https://install.mobile.azure.com"; - - /** - * Base URL to call server to check latest release. - */ - private static final String DEFAULT_API_URL = "https://api.mobile.azure.com"; - - /** - * Login URL path. Trailing slash matters to avoid redirection that loses query string. - */ - private static final String LOGIN_PAGE_URL_PATH = "/apps/%s/update-setup/"; - - /** - * Check latest release API URL path. - */ - private static final String CHECK_UPDATE_URL_PATH = "/sdk/apps/%s/releases/latest"; - - /** - * API parameter for release hash. - */ - private static final String PARAMETER_RELEASE_HASH = "release_hash"; - - /** - * API parameter for redirect URL. - */ - private static final String PARAMETER_REDIRECT_ID = "redirect_id"; - - /** - * API parameter for request identifier. - */ - private static final String PARAMETER_REQUEST_ID = "request_id"; - - /** - * API parameter for platform. - */ - private static final String PARAMETER_PLATFORM = "platform"; - - /** - * API parameter value for this platform. - */ - private static final String PARAMETER_PLATFORM_VALUE = "Android"; - - /** - * Header used to pass token when checking latest release. - */ - private static final String HEADER_API_TOKEN = "x-api-token"; - /** * Shared instance. */ diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index 468bd859e3..0d253e663c 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -1,25 +1,33 @@ package com.microsoft.azure.mobile.updates; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.UUIDUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper; import org.junit.Before; import org.junit.Rule; +import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; +import org.powermock.reflect.Whitebox; import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mockStatic; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class}) +@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class}) public class AbstractUpdatesTest { private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; @@ -27,8 +35,11 @@ public class AbstractUpdatesTest { @Rule public PowerMockRule mPowerMockRule = new PowerMockRule(); + @Mock + Context mContext; + @Before - public void setUp() { + public void setUp() throws PackageManager.NameNotFoundException { Updates.unsetInstance(); mockStatic(MobileCenterLog.class); mockStatic(MobileCenter.class); @@ -51,5 +62,18 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } }).when(StorageHelper.PreferencesStorage.class); StorageHelper.PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); + + /* Mock package manager. */ + PackageManager packageManager = mock(PackageManager.class); + when(mContext.getPackageName()).thenReturn("com.contoso"); + when(mContext.getPackageManager()).thenReturn(packageManager); + PackageInfo packageInfo = mock(PackageInfo.class); + when(packageManager.getPackageInfo("com.contoso", 0)).thenReturn(packageInfo); + Whitebox.setInternalState(packageInfo, "versionName", "1.2.3"); + Whitebox.setInternalState(packageInfo, "versionCode", 6); + + /* Mock others. */ + mockStatic(BrowserUtils.class); + mockStatic(UUIDUtils.class); } } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java index 5a45caa446..f6e2d59829 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java @@ -1,16 +1,42 @@ package com.microsoft.azure.mobile.updates; -import com.microsoft.azure.mobile.utils.storage.StorageHelper; +import android.app.Activity; +import android.content.Context; + +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.utils.HashUtils; +import com.microsoft.azure.mobile.utils.UUIDUtils; import junit.framework.Assert; import org.junit.Test; +import java.util.HashMap; +import java.util.UUID; + +import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REDIRECT_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_RELEASE_HASH; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REQUEST_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; public class UpdatesTest extends AbstractUpdatesTest { @@ -20,10 +46,73 @@ public void singleton() { } @Test - public void storeTokenBeforeStart() { - when(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn("r"); + public void storeTokenBeforeStart() throws Exception { + + /* Setup mock. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn("r"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + + /* Store token before start, start in background, no storage access. */ Updates.getInstance().storeUpdateToken("some token", "r"); + Updates.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); + verifyStatic(never()); + PreferencesStorage.putString(anyString(), anyString()); verifyStatic(never()); - StorageHelper.PreferencesStorage.putString(anyString(), anyString()); + PreferencesStorage.remove(anyString()); + + /* Unlock the processing by going into foreground. */ + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void storeTokenFromBrowserSameProcess() throws Exception { + + /* Setup mock. */ + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + UUID requestId = UUID.randomUUID(); + when(UUIDUtils.randomUUID()).thenReturn(requestId); + when(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn(requestId.toString()); + + /* Start and resume: open browser. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Activity activity = mock(Activity.class); + Updates.getInstance().onActivityResumed(activity); + verifyStatic(); + String url = UpdateConstants.DEFAULT_LOGIN_URL; + url += String.format(LOGIN_PAGE_URL_PATH, "a"); + url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); + url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); + url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); + url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; + BrowserUtils.openBrowser(url, activity); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); + + /* Store token and verify. */ + Updates.getInstance().storeUpdateToken("some token", requestId.toString()); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/UUIDUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/UUIDUtils.java index 7459638a0d..9014db6de8 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/UUIDUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/UUIDUtils.java @@ -10,7 +10,7 @@ /** * UUID utils. */ -public final class UUIDUtils { +public class UUIDUtils { @VisibleForTesting static Implementation sImplementation = new Implementation() { From 044579d82c061cf0bfa0f218bb71c300fc52f84f Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 7 Feb 2017 18:46:57 -0800 Subject: [PATCH 038/142] Reformat constants file --- .../azure/mobile/updates/UpdateConstants.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java index d9a1602043..08e94366bb 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -25,50 +25,62 @@ final class UpdateConstants { * Used for deep link intent from browser, string field for request identifier. */ static final String EXTRA_REQUEST_ID = "request_id"; + /** * Base URL used to open browser to login. */ static final String DEFAULT_LOGIN_URL = "https://install.mobile.azure.com"; + /** * Base URL to call server to check latest release. */ static final String DEFAULT_API_URL = "https://api.mobile.azure.com"; + /** * Login URL path. Trailing slash matters to avoid redirection that loses query string. */ static final String LOGIN_PAGE_URL_PATH = "/apps/%s/update-setup/"; + /** * Check latest release API URL path. */ static final String CHECK_UPDATE_URL_PATH = "/sdk/apps/%s/releases/latest"; + /** * API parameter for release hash. */ static final String PARAMETER_RELEASE_HASH = "release_hash"; + /** * API parameter for redirect URL. */ static final String PARAMETER_REDIRECT_ID = "redirect_id"; + /** * API parameter for request identifier. */ static final String PARAMETER_REQUEST_ID = "request_id"; + /** * API parameter for platform. */ static final String PARAMETER_PLATFORM = "platform"; + /** * API parameter value for this platform. */ static final String PARAMETER_PLATFORM_VALUE = "Android"; + /** * Header used to pass token when checking latest release. */ static final String HEADER_API_TOKEN = "x-api-token"; + /** * Base key for stored preferences. */ private static final String PREFERENCE_PREFIX = SERVICE_NAME + "."; + /** * Preference key to store the last download file location on download manager if completed, * empty string while download is in progress, null if we launched install U.I. @@ -78,14 +90,17 @@ final class UpdateConstants { * Rationale is that we keep the file in case the user chooses to install it from downloads U.I. */ static final String PREFERENCE_KEY_DOWNLOAD_URI = PREFERENCE_PREFIX + "download_uri"; + /** * Preference key to store the last download identifier. */ static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; + /** * Preference key for request identifier to validate deep link intent. */ static final String PREFERENCE_KEY_REQUEST_ID = PREFERENCE_PREFIX + EXTRA_REQUEST_ID; + /** * Preference key to store token. */ From e9658677a9620b78ceedeb8f7225b4c7df8f4cca Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 8 Feb 2017 18:41:33 -0800 Subject: [PATCH 039/142] Add more tests, fix minor bugs and simplify code --- .../azure/mobile/updates/Updates.java | 18 +- .../mobile/updates/AbstractUpdatesTest.java | 12 +- .../updates/UpdatesBeforeApiSuccessTests.java | 512 ++++++++++++++++++ .../azure/mobile/updates/UpdatesTest.java | 118 ---- 4 files changed, 530 insertions(+), 130 deletions(-) create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java delete mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 38ab1f6949..74a0afea6f 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -307,6 +307,7 @@ public synchronized void setInstanceEnabled(boolean enabled) { mWorkflowCompleted = false; cancelPreviousTasks(); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); } } @@ -356,10 +357,10 @@ private synchronized void cancelPreviousTasks() { * Method that triggers the update workflow or proceed to the next step. */ private synchronized void resumeUpdateWorkflow() { - if (mForegroundActivity != null && !mWorkflowCompleted) { + if (mForegroundActivity != null && !mWorkflowCompleted && isInstanceEnabled()) { /* If we received the update token before Mobile Center was started/enabled, process it now. */ - if (mBeforeStartUpdateToken != null && mBeforeStartRequestId != null) { + if (mBeforeStartUpdateToken != null) { MobileCenterLog.debug(LOG_TAG, "Processing update token we kept in memory before onStarted"); storeUpdateToken(mBeforeStartUpdateToken, mBeforeStartRequestId); mBeforeStartUpdateToken = null; @@ -416,19 +417,19 @@ private synchronized void resumeUpdateWorkflow() { return; } - /* Generate request identifier and store it. */ - String requestId = UUIDUtils.randomUUID().toString(); - StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId); - /* Compute hash. */ String releaseHash; try { releaseHash = computeHash(mContext); } catch (PackageManager.NameNotFoundException e) { MobileCenterLog.error(LOG_TAG, "Could not get package info", e); + mBrowserOpened = true; return; } + /* Generate request identifier. */ + String requestId = UUIDUtils.randomUUID().toString(); + /* Build URL. */ String url = mLoginUrl; url += String.format(LOGIN_PAGE_URL_PATH, mAppSecret); @@ -438,6 +439,9 @@ private synchronized void resumeUpdateWorkflow() { url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to login url=" + url); + /* Store request id. */ + StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId); + /* Open browser, remember that whatever the outcome to avoid opening it twice. */ BrowserUtils.openBrowser(url, mForegroundActivity); mBrowserOpened = true; @@ -492,8 +496,6 @@ synchronized void storeUpdateToken(@NonNull String updateToken, @NonNull String MobileCenterLog.debug(LOG_TAG, "Update token received before onStart, keep it in memory."); mBeforeStartUpdateToken = updateToken; mBeforeStartRequestId = requestId; - } else if (!isInstanceEnabled()) { - MobileCenterLog.warn(LOG_TAG, "Ignoring update token as Updates are disabled."); } else if (requestId.equals(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID))) { StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index 0d253e663c..5d058f1745 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -27,7 +27,7 @@ import static org.powermock.api.mockito.PowerMockito.mockStatic; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class}) +@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class}) public class AbstractUpdatesTest { private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; @@ -38,6 +38,9 @@ public class AbstractUpdatesTest { @Mock Context mContext; + @Mock + PackageManager mPackageManager; + @Before public void setUp() throws PackageManager.NameNotFoundException { Updates.unsetInstance(); @@ -64,16 +67,17 @@ public Object answer(InvocationOnMock invocation) throws Throwable { StorageHelper.PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); /* Mock package manager. */ - PackageManager packageManager = mock(PackageManager.class); + mPackageManager = mock(PackageManager.class); when(mContext.getPackageName()).thenReturn("com.contoso"); - when(mContext.getPackageManager()).thenReturn(packageManager); + when(mContext.getPackageManager()).thenReturn(mPackageManager); PackageInfo packageInfo = mock(PackageInfo.class); - when(packageManager.getPackageInfo("com.contoso", 0)).thenReturn(packageInfo); + when(mPackageManager.getPackageInfo("com.contoso", 0)).thenReturn(packageInfo); Whitebox.setInternalState(packageInfo, "versionName", "1.2.3"); Whitebox.setInternalState(packageInfo, "versionCode", 6); /* Mock others. */ mockStatic(BrowserUtils.class); mockStatic(UUIDUtils.class); + mockStatic(ReleaseDetails.class); } } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java new file mode 100644 index 0000000000..d9fd457329 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java @@ -0,0 +1,512 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; + +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.HttpException; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.utils.HashUtils; +import com.microsoft.azure.mobile.utils.UUIDUtils; + +import org.json.JSONException; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.Semaphore; + +import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REDIRECT_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_RELEASE_HASH; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REQUEST_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +/** + * Cover scenarios that are happening before we see an API call success for latest release. + */ +public class UpdatesBeforeApiSuccessTests extends AbstractUpdatesTest { + + @Test + public void storeTokenBeforeStart() throws Exception { + + /* Setup mock. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn("r"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + + /* Store token before start, start in background, no storage access. */ + Updates.getInstance().storeUpdateToken("some token", "r"); + Updates.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); + verifyStatic(never()); + PreferencesStorage.putString(anyString(), anyString()); + verifyStatic(never()); + PreferencesStorage.remove(anyString()); + + /* Unlock the processing by going into foreground. */ + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void happyPathUntilHangingCall() throws Exception { + + /* Setup mock. */ + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + UUID requestId = UUID.randomUUID(); + when(UUIDUtils.randomUUID()).thenReturn(requestId); + when(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn(requestId.toString()); + + /* Start and resume: open browser. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Activity activity = mock(Activity.class); + Updates.getInstance().onActivityResumed(activity); + verifyStatic(); + String url = UpdateConstants.DEFAULT_LOGIN_URL; + url += String.format(LOGIN_PAGE_URL_PATH, "a"); + url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); + url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); + url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); + url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; + BrowserUtils.openBrowser(url, activity); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); + + /* If browser already opened, activity changed must not recall it. */ + Updates.getInstance().onActivityPaused(activity); + Updates.getInstance().onActivityResumed(activity); + verifyStatic(); + BrowserUtils.openBrowser(url, activity); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); + + /* Store token. */ + Updates.getInstance().storeUpdateToken("some token", requestId.toString()); + + /* Verify behavior. */ + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + verify(httpClient).callAsync(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument.toString().startsWith(UpdateConstants.DEFAULT_API_URL); + } + }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* If call already made, activity changed must not recall it. */ + Updates.getInstance().onActivityPaused(activity); + Updates.getInstance().onActivityResumed(activity); + + /* Verify behavior. */ + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(httpClient).callAsync(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument.toString().startsWith(UpdateConstants.DEFAULT_API_URL); + } + }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Call is still in progress. If we restart app, nothing happens we still wait. */ + Intent intent = mock(Intent.class); + when(activity.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.getLaunchIntentForPackage(anyString())).thenReturn(intent); + ComponentName componentName = mock(ComponentName.class); + when(intent.resolveActivity(mPackageManager)).thenReturn(componentName); + when(componentName.getClassName()).thenReturn(activity.getClass().getName()); + Updates.getInstance().onActivityPaused(activity); + Updates.getInstance().onActivityStopped(activity); + Updates.getInstance().onActivityDestroyed(activity); + Updates.getInstance().onActivityCreated(activity, mock(Bundle.class)); + Updates.getInstance().onActivityResumed(activity); + Updates.getInstance().onActivityPaused(activity); + Updates.getInstance().onActivityStopped(activity); + Updates.getInstance().onActivityDestroyed(activity); + Updates.getInstance().onActivityCreated(activity, mock(Bundle.class)); + Updates.getInstance().onActivityResumed(activity); + + /* Verify behavior not changed. */ + verifyStatic(); + BrowserUtils.openBrowser(url, activity); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(httpClient).callAsync(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument.toString().startsWith(UpdateConstants.DEFAULT_API_URL); + } + }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void setUrls() throws Exception { + + /* Setup mock. */ + Updates.setLoginUrl("http://mock"); + Updates.setApiUrl("https://mock2"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + UUID requestId = UUID.randomUUID(); + when(UUIDUtils.randomUUID()).thenReturn(requestId); + when(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn(requestId.toString()); + + /* Start and resume: open browser. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Activity activity = mock(Activity.class); + Updates.getInstance().onActivityResumed(activity); + verifyStatic(); + String url = "http://mock"; + url += String.format(LOGIN_PAGE_URL_PATH, "a"); + url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); + url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); + url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); + url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; + BrowserUtils.openBrowser(url, activity); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); + + /* Store token. */ + Updates.getInstance().storeUpdateToken("some token", requestId.toString()); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + verify(httpClient).callAsync(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument.toString().startsWith("https://mock2"); + } + }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void computeHashFailsWhenOpeningBrowser() throws Exception { + + /* Mock package manager. */ + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + when(context.getPackageName()).thenReturn("com.contoso"); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); + + /* Start and resume: open browser. */ + Updates.getInstance().onStarted(context, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify only tried once. */ + verify(packageManager).getPackageInfo("com.contoso", 0); + + /* And verify we didn't open browser. */ + verifyStatic(never()); + BrowserUtils.openBrowser(anyString(), any(Activity.class)); + verifyStatic(never()); + PreferencesStorage.putString(anyString(), anyString()); + } + + @Test + public void disableBeforeStoreToken() { + + /* Start and resume: open browser. */ + UUID requestId = UUID.randomUUID(); + when(UUIDUtils.randomUUID()).thenReturn(requestId); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Activity activity = mock(Activity.class); + Updates.getInstance().onActivityResumed(activity); + verifyStatic(); + String url = UpdateConstants.DEFAULT_LOGIN_URL; + url += String.format(LOGIN_PAGE_URL_PATH, "a"); + url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); + url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); + url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); + url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; + BrowserUtils.openBrowser(url, activity); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); + + /* Disable. */ + Updates.setEnabled(false); + assertFalse(Updates.isEnabled()); + + /* Store token. */ + Updates.getInstance().storeUpdateToken("some token", requestId.toString()); + + /* Verify behavior. */ + verifyStatic(never()); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Since after disabling once, the request id was deleted we can enable/disable it will also ignore the request. */ + Updates.setEnabled(true); + assertTrue(Updates.isEnabled()); + + /* Store token. */ + Updates.getInstance().storeUpdateToken("some token", requestId.toString()); + + /* Verify behavior. */ + verifyStatic(never()); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); + } + + @Test + public void disableWhileCheckingRelease() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + ServiceCall firstCall = mock(ServiceCall.class); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenReturn(firstCall).thenReturn(mock(ServiceCall.class)); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + + /* The call is only triggered when app is resumed. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + verify(httpClient, never()).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify cancel on disabling. */ + verify(firstCall, never()).cancel(); + Updates.setEnabled(false); + verify(firstCall).cancel(); + + /* No more call on that one. */ + Updates.setEnabled(true); + Updates.setEnabled(false); + verify(firstCall).cancel(); + } + + @Test + public void checkReleaseFails() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallFailed(new HttpException(403)); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on failure we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void checkReleaseFailsParsing() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + when(ReleaseDetails.parse(anyString())).thenThrow(new JSONException("mock")); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on failure we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void disableBeforeCheckReleaseFails() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + final Semaphore beforeSemaphore = new Semaphore(0); + final Semaphore afterSemaphore = new Semaphore(0); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(final InvocationOnMock invocation) throws Throwable { + new Thread() { + + @Override + public void run() { + beforeSemaphore.acquireUninterruptibly(); + ((ServiceCallback) invocation.getArguments()[4]).onCallFailed(new HttpException(403)); + afterSemaphore.release(); + } + }.start(); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Disable before it fails. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + beforeSemaphore.release(); + afterSemaphore.acquireUninterruptibly(); + + /* Verify complete workflow call ignored. i.e. no more call to delete the state. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void disableBeforeCheckReleaseSucceed() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + final Semaphore beforeSemaphore = new Semaphore(0); + final Semaphore afterSemaphore = new Semaphore(0); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(final InvocationOnMock invocation) throws Throwable { + new Thread() { + + @Override + public void run() { + beforeSemaphore.acquireUninterruptibly(); + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + afterSemaphore.release(); + } + }.start(); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Disable before it succeeds. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + beforeSemaphore.release(); + afterSemaphore.acquireUninterruptibly(); + + /* Verify complete workflow call skipped. i.e. no more call to delete the state. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } +} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java deleted file mode 100644 index f6e2d59829..0000000000 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.microsoft.azure.mobile.updates; - -import android.app.Activity; -import android.content.Context; - -import com.microsoft.azure.mobile.channel.Channel; -import com.microsoft.azure.mobile.http.HttpClient; -import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; -import com.microsoft.azure.mobile.http.ServiceCallback; -import com.microsoft.azure.mobile.utils.HashUtils; -import com.microsoft.azure.mobile.utils.UUIDUtils; - -import junit.framework.Assert; - -import org.junit.Test; - -import java.util.HashMap; -import java.util.UUID; - -import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REDIRECT_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_RELEASE_HASH; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REQUEST_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; -import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.verifyStatic; -import static org.powermock.api.mockito.PowerMockito.whenNew; - -public class UpdatesTest extends AbstractUpdatesTest { - - @Test - public void singleton() { - Assert.assertSame(Updates.getInstance(), Updates.getInstance()); - } - - @Test - public void storeTokenBeforeStart() throws Exception { - - /* Setup mock. */ - when(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn("r"); - HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); - whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - - /* Store token before start, start in background, no storage access. */ - Updates.getInstance().storeUpdateToken("some token", "r"); - Updates.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); - verifyStatic(never()); - PreferencesStorage.putString(anyString(), anyString()); - verifyStatic(never()); - PreferencesStorage.remove(anyString()); - - /* Unlock the processing by going into foreground. */ - Updates.getInstance().onActivityResumed(mock(Activity.class)); - verifyStatic(); - PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - } - - @Test - public void storeTokenFromBrowserSameProcess() throws Exception { - - /* Setup mock. */ - HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); - whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - UUID requestId = UUID.randomUUID(); - when(UUIDUtils.randomUUID()).thenReturn(requestId); - when(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn(requestId.toString()); - - /* Start and resume: open browser. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Activity activity = mock(Activity.class); - Updates.getInstance().onActivityResumed(activity); - verifyStatic(); - String url = UpdateConstants.DEFAULT_LOGIN_URL; - url += String.format(LOGIN_PAGE_URL_PATH, "a"); - url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); - url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); - url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); - url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; - BrowserUtils.openBrowser(url, activity); - verifyStatic(); - PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); - - /* Store token and verify. */ - Updates.getInstance().storeUpdateToken("some token", requestId.toString()); - verifyStatic(); - PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - } -} From 1620f8543b6b56fd686c18783bde8b594dfeed3b Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 8 Feb 2017 19:02:47 -0800 Subject: [PATCH 040/142] Fix hiding updates in jcenterDependency --- apps/sasquatch/build.gradle | 2 +- .../sasquatch/activities/MainActivity.java | 18 +++++--- .../activities/SettingsActivity.java | 44 +++++++++++++------ 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/apps/sasquatch/build.gradle b/apps/sasquatch/build.gradle index 8c4bb7e06c..32b1805ad0 100644 --- a/apps/sasquatch/build.gradle +++ b/apps/sasquatch/build.gradle @@ -26,7 +26,7 @@ dependencies { compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" projectDependencyCompile project(':sdk:mobile-center-analytics') projectDependencyCompile project(':sdk:mobile-center-crashes') + projectDependencyCompile project(':sdk:mobile-center-updates') jcenterDependencyCompile "com.microsoft.azure.mobile:mobile-center-analytics:${version}" jcenterDependencyCompile "com.microsoft.azure.mobile:mobile-center-crashes:${version}" - compile project(':sdk:mobile-center-updates') } \ No newline at end of file diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index 782625ca15..de8eac5375 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -17,6 +17,7 @@ import android.widget.Toast; import com.microsoft.azure.mobile.MobileCenter; +import com.microsoft.azure.mobile.MobileCenterService; import com.microsoft.azure.mobile.ResultCallback; import com.microsoft.azure.mobile.analytics.Analytics; import com.microsoft.azure.mobile.crashes.AbstractCrashesListener; @@ -25,8 +26,7 @@ import com.microsoft.azure.mobile.sasquatch.R; import com.microsoft.azure.mobile.sasquatch.features.TestFeatures; import com.microsoft.azure.mobile.sasquatch.features.TestFeaturesListAdapter; -import com.microsoft.azure.mobile.updates.Updates; - +import com.microsoft.azure.mobile.utils.MobileCenterLog; public class MainActivity extends AppCompatActivity { @@ -50,9 +50,17 @@ protected void onCreate(Bundle savedInstanceState) { } MobileCenter.setLogLevel(Log.VERBOSE); Crashes.setListener(getCrashesListener()); - Updates.setLoginUrl("http://mockilecenterupdate.azurewebsites.net"); - Updates.setApiUrl("http://mockilecenterupdate.azurewebsites.net"); - MobileCenter.start(getApplication(), getAppSecret(), Analytics.class, Crashes.class, Updates.class); + MobileCenter.start(getApplication(), getAppSecret(), Analytics.class, Crashes.class); + try { + + @SuppressWarnings("unchecked") + Class updates = (Class) Class.forName("com.microsoft.azure.mobile.updates.Updates"); + updates.getMethod("setLoginUrl", String.class).invoke(null, "http://mockilecenterupdate.azurewebsites.net"); + updates.getMethod("setApiUrl", String.class).invoke(null, "http://mockilecenterupdate.azurewebsites.net"); + MobileCenter.start(updates); + } catch (Exception e) { + MobileCenterLog.info(LOG_TAG, "Updates class not yet available in this flavor."); + } Log.i(LOG_TAG, "Crashes.hasCrashedInLastSession=" + Crashes.hasCrashedInLastSession()); Crashes.getLastSessionCrashReport(new ResultCallback() { diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java index a31c709844..dc2f2b868d 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java @@ -16,14 +16,15 @@ import android.widget.Toast; import com.microsoft.azure.mobile.MobileCenter; +import com.microsoft.azure.mobile.MobileCenterService; import com.microsoft.azure.mobile.analytics.Analytics; import com.microsoft.azure.mobile.analytics.AnalyticsPrivateHelper; import com.microsoft.azure.mobile.crashes.Crashes; import com.microsoft.azure.mobile.sasquatch.R; -import com.microsoft.azure.mobile.updates.Updates; import com.microsoft.azure.mobile.utils.PrefStorageConstants; import com.microsoft.azure.mobile.utils.storage.StorageHelper; +import java.lang.reflect.Method; import java.util.UUID; import static com.microsoft.azure.mobile.sasquatch.activities.MainActivity.APP_SECRET_KEY; @@ -90,19 +91,36 @@ public boolean isEnabled() { return Crashes.isEnabled(); } }); - initCheckBoxSetting(R.string.mobile_center_updates_state_key, Updates.isEnabled(), R.string.mobile_center_updates_state_summary_enabled, R.string.mobile_center_updates_state_summary_disabled, new HasEnabled() { - - @Override - public void setEnabled(boolean enabled) { - Updates.setEnabled(enabled); - updatesEnabledPreference.setChecked(Updates.isEnabled()); - } + try { + + @SuppressWarnings("unchecked") + Class updates = (Class) Class.forName("com.microsoft.azure.mobile.updates.Updates"); + final Method isEnabled = updates.getMethod("isEnabled"); + final Method setEnabled = updates.getMethod("setEnabled", boolean.class); + initCheckBoxSetting(R.string.mobile_center_updates_state_key, (boolean) isEnabled.invoke(null), R.string.mobile_center_updates_state_summary_enabled, R.string.mobile_center_updates_state_summary_disabled, new HasEnabled() { + + @Override + public void setEnabled(boolean enabled) { + try { + setEnabled.invoke(null, enabled); + updatesEnabledPreference.setChecked((boolean) isEnabled.invoke(null)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } - @Override - public boolean isEnabled() { - return Updates.isEnabled(); - } - }); + @Override + public boolean isEnabled() { + try { + return (boolean) isEnabled.invoke(null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } catch (Exception e) { + getPreferenceScreen().removePreference(findPreference(getString(R.string.updates_key))); + } initCheckBoxSetting(R.string.mobile_center_auto_page_tracking_key, AnalyticsPrivateHelper.isAutoPageTrackingEnabled(), R.string.mobile_center_auto_page_tracking_enabled, R.string.mobile_center_auto_page_tracking_disabled, new HasEnabled() { @Override From 568a80a55b79eaa39e023cdd62522434bd59d49e Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 8 Feb 2017 20:26:54 -0800 Subject: [PATCH 041/142] More tests and refactoring --- .../azure/mobile/updates/Updates.java | 4 + .../mobile/updates/AbstractUpdatesTest.java | 34 ++- .../updates/UpdatesAfterApiCallTests.java | 238 ++++++++++++++++++ .../updates/UpdatesBeforeApiSuccessTests.java | 8 +- 4 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesAfterApiCallTests.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 74a0afea6f..47884e506a 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -179,6 +179,7 @@ static synchronized void unsetInstance() { * * @return true if enabled, false otherwise. */ + @SuppressWarnings("WeakerAccess") public static boolean isEnabled() { return getInstance().isInstanceEnabled(); } @@ -188,6 +189,7 @@ public static boolean isEnabled() { * * @param enabled true to enable, false to disable. */ + @SuppressWarnings("WeakerAccess") public static void setEnabled(boolean enabled) { getInstance().setInstanceEnabled(enabled); } @@ -197,6 +199,7 @@ public static void setEnabled(boolean enabled) { * * @param loginUrl login base URL. */ + @SuppressWarnings("WeakerAccess") public static void setLoginUrl(String loginUrl) { getInstance().setInstanceLoginUrl(loginUrl); } @@ -206,6 +209,7 @@ public static void setLoginUrl(String loginUrl) { * * @param apiUrl API base URL. */ + @SuppressWarnings("WeakerAccess") public static void setApiUrl(String apiUrl) { getInstance().setInstanceApiUrl(apiUrl); } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index 5d058f1745..f48fe440bb 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -1,8 +1,10 @@ package com.microsoft.azure.mobile.updates; +import android.app.AlertDialog; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.text.TextUtils; import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.utils.MobileCenterLog; @@ -20,14 +22,16 @@ import org.powermock.reflect.Whitebox; import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class}) +@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class}) public class AbstractUpdatesTest { private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; @@ -39,10 +43,13 @@ public class AbstractUpdatesTest { Context mContext; @Mock - PackageManager mPackageManager; + AlertDialog.Builder mDialogBuilder; + + @Mock + AlertDialog mDialog; @Before - public void setUp() throws PackageManager.NameNotFoundException { + public void setUp() throws Exception { Updates.unsetInstance(); mockStatic(MobileCenterLog.class); mockStatic(MobileCenter.class); @@ -67,17 +74,30 @@ public Object answer(InvocationOnMock invocation) throws Throwable { StorageHelper.PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); /* Mock package manager. */ - mPackageManager = mock(PackageManager.class); + PackageManager packageManager = mock(PackageManager.class); when(mContext.getPackageName()).thenReturn("com.contoso"); - when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getPackageManager()).thenReturn(packageManager); PackageInfo packageInfo = mock(PackageInfo.class); - when(mPackageManager.getPackageInfo("com.contoso", 0)).thenReturn(packageInfo); + when(packageManager.getPackageInfo("com.contoso", 0)).thenReturn(packageInfo); Whitebox.setInternalState(packageInfo, "versionName", "1.2.3"); Whitebox.setInternalState(packageInfo, "versionCode", 6); - /* Mock others. */ + /* Mock some statics. */ mockStatic(BrowserUtils.class); mockStatic(UUIDUtils.class); mockStatic(ReleaseDetails.class); + mockStatic(TextUtils.class); + when(TextUtils.isEmpty(any(CharSequence.class))).thenAnswer(new Answer() { + + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + CharSequence str = (CharSequence) invocation.getArguments()[0]; + return str == null || str.length() == 0; + } + }); + + /* Dialog. */ + whenNew(AlertDialog.Builder.class).withAnyArguments().thenReturn(mDialogBuilder); + when(mDialogBuilder.create()).thenReturn(mDialog); } } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesAfterApiCallTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesAfterApiCallTests.java new file mode 100644 index 0000000000..ccb8cd2e9e --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesAfterApiCallTests.java @@ -0,0 +1,238 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; + +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.utils.HashUtils; + +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.HashMap; + +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +public class UpdatesAfterApiCallTests extends AbstractUpdatesTest { + + @Test + public void failsToCompareVersion() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + when(ReleaseDetails.parse(anyString())).thenReturn(mock(ReleaseDetails.class)); + Context context = mock(Context.class); + when(context.getPackageName()).thenReturn("com.contoso"); + PackageManager packageManager = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); + + /* Trigger call. */ + Updates.getInstance().onStarted(context, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on failure we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mDialog, never()).show(); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void olderVersionCode() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + when(ReleaseDetails.parse(anyString())).thenReturn(mock(ReleaseDetails.class)); // 0 vs 6 + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on failure we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mDialog, never()).show(); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void sameVersionCodeAndSameHash() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(6); + when(releaseDetails.getFingerprint()).thenReturn(HashUtils.sha256("com.contoso:1.2.3:6")); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on failure we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mDialog, never()).show(); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void moreRecentHashNoReleaseNotesDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(6); + when(releaseDetails.getFingerprint()).thenReturn("mock"); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify dialog. */ + verify(mDialogBuilder).setTitle(R.string.mobile_center_updates_update_dialog_title); + verify(mDialogBuilder).setMessage(R.string.mobile_center_updates_update_dialog_message); + verify(mDialogBuilder, never()).setMessage(any(CharSequence.class)); + verify(mDialog).show(); + + /* After that if we resume app we refresh dialog. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* No more http call. */ + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* But dialog refreshed. */ + InOrder order = inOrder(mDialog); + order.verify(mDialog).hide(); + order.verify(mDialog).show(); + order.verifyNoMoreInteractions(); + verify(mDialog, times(2)).show(); + verify(mDialogBuilder, times(2)).create(); + + /* Disable does not hide the dialog. */ + Updates.setEnabled(false); + verifyNoMoreInteractions(mDialog); + } + + @Test + public void moreRecentVersionWithReleaseNotesDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(7); + when(releaseDetails.getReleaseNotes()).thenReturn("mock"); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify dialog. */ + verify(mDialogBuilder).setTitle(R.string.mobile_center_updates_update_dialog_title); + verify(mDialogBuilder).setMessage("mock"); + verify(mDialog).show(); + } +} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java index d9fd457329..29dca4a970 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java @@ -165,10 +165,11 @@ public boolean matches(Object argument) { /* Call is still in progress. If we restart app, nothing happens we still wait. */ Intent intent = mock(Intent.class); - when(activity.getPackageManager()).thenReturn(mPackageManager); - when(mPackageManager.getLaunchIntentForPackage(anyString())).thenReturn(intent); + PackageManager packageManager = mock(PackageManager.class); + when(activity.getPackageManager()).thenReturn(packageManager); + when(packageManager.getLaunchIntentForPackage(anyString())).thenReturn(intent); ComponentName componentName = mock(ComponentName.class); - when(intent.resolveActivity(mPackageManager)).thenReturn(componentName); + when(intent.resolveActivity(packageManager)).thenReturn(componentName); when(componentName.getClassName()).thenReturn(activity.getClass().getName()); Updates.getInstance().onActivityPaused(activity); Updates.getInstance().onActivityStopped(activity); @@ -508,5 +509,6 @@ public void run() { Updates.getInstance().onActivityPaused(mock(Activity.class)); Updates.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + verify(mDialog, never()).show(); } } From b53a95cfd7adf4cd4b12746ce719a01929305a02 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 9 Feb 2017 11:59:43 -0800 Subject: [PATCH 042/142] Cover all tests with dialog that don't trigger a download --- .../updates/UpdatesAfterApiCallTests.java | 238 -------- .../updates/UpdatesBeforeDownloadTests.java | 530 ++++++++++++++++++ .../azure/mobile/utils/AsyncTaskUtils.java | 2 +- 3 files changed, 531 insertions(+), 239 deletions(-) delete mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesAfterApiCallTests.java create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesAfterApiCallTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesAfterApiCallTests.java deleted file mode 100644 index ccb8cd2e9e..0000000000 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesAfterApiCallTests.java +++ /dev/null @@ -1,238 +0,0 @@ -package com.microsoft.azure.mobile.updates; - -import android.app.Activity; -import android.content.Context; -import android.content.pm.PackageManager; - -import com.microsoft.azure.mobile.channel.Channel; -import com.microsoft.azure.mobile.http.HttpClient; -import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; -import com.microsoft.azure.mobile.http.ServiceCall; -import com.microsoft.azure.mobile.http.ServiceCallback; -import com.microsoft.azure.mobile.utils.HashUtils; - -import org.junit.Test; -import org.mockito.InOrder; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.util.HashMap; - -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; -import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyMapOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.internal.verification.VerificationModeFactory.times; -import static org.powermock.api.mockito.PowerMockito.verifyStatic; -import static org.powermock.api.mockito.PowerMockito.whenNew; - -public class UpdatesAfterApiCallTests extends AbstractUpdatesTest { - - @Test - public void failsToCompareVersion() throws Exception { - - /* Mock we already have token. */ - when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); - HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); - whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { - - @Override - public ServiceCall answer(InvocationOnMock invocation) throws Throwable { - ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); - return mock(ServiceCall.class); - } - }); - HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); - when(ReleaseDetails.parse(anyString())).thenReturn(mock(ReleaseDetails.class)); - Context context = mock(Context.class); - when(context.getPackageName()).thenReturn("com.contoso"); - PackageManager packageManager = mock(PackageManager.class); - when(context.getPackageManager()).thenReturn(packageManager); - when(packageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); - - /* Trigger call. */ - Updates.getInstance().onStarted(context, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - - /* Verify on failure we complete workflow. */ - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - verify(mDialog, never()).show(); - - /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - } - - @Test - public void olderVersionCode() throws Exception { - - /* Mock we already have token. */ - when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); - HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); - whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { - - @Override - public ServiceCall answer(InvocationOnMock invocation) throws Throwable { - ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); - return mock(ServiceCall.class); - } - }); - HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); - when(ReleaseDetails.parse(anyString())).thenReturn(mock(ReleaseDetails.class)); // 0 vs 6 - - /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - - /* Verify on failure we complete workflow. */ - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - verify(mDialog, never()).show(); - - /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - } - - @Test - public void sameVersionCodeAndSameHash() throws Exception { - - /* Mock we already have token. */ - when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); - HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); - whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { - - @Override - public ServiceCall answer(InvocationOnMock invocation) throws Throwable { - ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); - return mock(ServiceCall.class); - } - }); - HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); - ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getVersion()).thenReturn(6); - when(releaseDetails.getFingerprint()).thenReturn(HashUtils.sha256("com.contoso:1.2.3:6")); - when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); - - /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - - /* Verify on failure we complete workflow. */ - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - verify(mDialog, never()).show(); - - /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - } - - @Test - public void moreRecentHashNoReleaseNotesDialog() throws Exception { - - /* Mock we already have token. */ - when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); - HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); - whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { - - @Override - public ServiceCall answer(InvocationOnMock invocation) throws Throwable { - ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); - return mock(ServiceCall.class); - } - }); - HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); - ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getVersion()).thenReturn(6); - when(releaseDetails.getFingerprint()).thenReturn("mock"); - when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); - - /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - - /* Verify dialog. */ - verify(mDialogBuilder).setTitle(R.string.mobile_center_updates_update_dialog_title); - verify(mDialogBuilder).setMessage(R.string.mobile_center_updates_update_dialog_message); - verify(mDialogBuilder, never()).setMessage(any(CharSequence.class)); - verify(mDialog).show(); - - /* After that if we resume app we refresh dialog. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - - /* No more http call. */ - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - - /* But dialog refreshed. */ - InOrder order = inOrder(mDialog); - order.verify(mDialog).hide(); - order.verify(mDialog).show(); - order.verifyNoMoreInteractions(); - verify(mDialog, times(2)).show(); - verify(mDialogBuilder, times(2)).create(); - - /* Disable does not hide the dialog. */ - Updates.setEnabled(false); - verifyNoMoreInteractions(mDialog); - } - - @Test - public void moreRecentVersionWithReleaseNotesDialog() throws Exception { - - /* Mock we already have token. */ - when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); - HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); - whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { - - @Override - public ServiceCall answer(InvocationOnMock invocation) throws Throwable { - ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); - return mock(ServiceCall.class); - } - }); - HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); - ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getVersion()).thenReturn(7); - when(releaseDetails.getReleaseNotes()).thenReturn("mock"); - when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); - - /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - - /* Verify dialog. */ - verify(mDialogBuilder).setTitle(R.string.mobile_center_updates_update_dialog_title); - verify(mDialogBuilder).setMessage("mock"); - verify(mDialog).show(); - } -} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java new file mode 100644 index 0000000000..0e9603d016 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java @@ -0,0 +1,530 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.os.AsyncTask; + +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.utils.AsyncTaskUtils; +import com.microsoft.azure.mobile.utils.HashUtils; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import java.util.HashMap; +import java.util.concurrent.Semaphore; + +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.anyVararg; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +public class UpdatesBeforeDownloadTests extends AbstractUpdatesTest { + + @Test + public void failsToCompareVersion() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + when(ReleaseDetails.parse(anyString())).thenReturn(mock(ReleaseDetails.class)); + Context context = mock(Context.class); + when(context.getPackageName()).thenReturn("com.contoso"); + PackageManager packageManager = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); + + /* Trigger call. */ + Updates.getInstance().onStarted(context, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on failure we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mDialogBuilder, never()).create(); + verify(mDialog, never()).show(); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void olderVersionCode() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + when(ReleaseDetails.parse(anyString())).thenReturn(mock(ReleaseDetails.class)); // 0 vs 6 + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on failure we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mDialogBuilder, never()).create(); + verify(mDialog, never()).show(); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void sameVersionCodeAndSameHash() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(6); + when(releaseDetails.getFingerprint()).thenReturn(HashUtils.sha256("com.contoso:1.2.3:6")); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on failure we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mDialogBuilder, never()).create(); + verify(mDialog, never()).show(); + + /* After that if we resume app nothing happens. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void moreRecentHashNoReleaseNotesDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(6); + when(releaseDetails.getFingerprint()).thenReturn("mock"); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify dialog. */ + verify(mDialogBuilder).setTitle(R.string.mobile_center_updates_update_dialog_title); + verify(mDialogBuilder).setMessage(R.string.mobile_center_updates_update_dialog_message); + verify(mDialogBuilder, never()).setMessage(any(CharSequence.class)); + verify(mDialogBuilder).create(); + verify(mDialog).show(); + + /* After that if we resume app we refresh dialog. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* No more http call. */ + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* But dialog refreshed. */ + InOrder order = inOrder(mDialog); + order.verify(mDialog).hide(); + order.verify(mDialog).show(); + order.verifyNoMoreInteractions(); + verify(mDialog, times(2)).show(); + verify(mDialogBuilder, times(2)).create(); + + /* Disable does not hide the dialog. */ + Updates.setEnabled(false); + verifyNoMoreInteractions(mDialog); + } + + @Test + public void moreRecentVersionWithReleaseNotesDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(7); + when(releaseDetails.getReleaseNotes()).thenReturn("mock"); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify dialog. */ + verify(mDialogBuilder).setTitle(R.string.mobile_center_updates_update_dialog_title); + verify(mDialogBuilder).setMessage("mock"); + verify(mDialogBuilder).create(); + verify(mDialog).show(); + } + + @Test + public void dialogWaitWhileInBackground() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + final Semaphore beforeSemaphore = new Semaphore(0); + final Semaphore afterSemaphore = new Semaphore(0); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(final InvocationOnMock invocation) throws Throwable { + new Thread() { + + @Override + public void run() { + beforeSemaphore.acquireUninterruptibly(); + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + afterSemaphore.release(); + } + }.start(); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(7); + when(releaseDetails.getReleaseNotes()).thenReturn("mock"); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + Updates.getInstance().onActivityPaused(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Release call in background. */ + beforeSemaphore.release(); + afterSemaphore.acquireUninterruptibly(); + + /* Verify dialog not shown. */ + verify(mDialogBuilder, never()).create(); + verify(mDialog, never()).show(); + + /* Go foreground. */ + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify dialog now shown. */ + verify(mDialogBuilder).create(); + verify(mDialog).show(); + } + + @Test + public void cancelDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(7); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify dialog. */ + ArgumentCaptor cancelListener = ArgumentCaptor.forClass(DialogInterface.OnCancelListener.class); + verify(mDialogBuilder).setOnCancelListener(cancelListener.capture()); + verify(mDialog).show(); + + /* Cancel it. */ + cancelListener.getValue().onCancel(mDialog); + + /* Verify. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void ignoreDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(7); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify dialog. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setNegativeButton(eq(R.string.mobile_center_updates_update_dialog_ignore), clickListener.capture()); + verify(mDialog).show(); + + /* Ignore it. */ + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_NEGATIVE); + + /* Verify. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void disableBeforeCancelDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(7); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify dialog. */ + ArgumentCaptor cancelListener = ArgumentCaptor.forClass(DialogInterface.OnCancelListener.class); + verify(mDialogBuilder).setOnCancelListener(cancelListener.capture()); + verify(mDialog).show(); + + /* Disable. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Cancel it. */ + cancelListener.getValue().onCancel(mDialog); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + } + + @Test + public void disableBeforeIgnoreDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(7); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify dialog. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setNegativeButton(eq(R.string.mobile_center_updates_update_dialog_ignore), clickListener.capture()); + verify(mDialog).show(); + + /* Disable. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Ignore it. */ + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_NEGATIVE); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + } + + @Test + @PrepareForTest(AsyncTaskUtils.class) + public void disableBeforeDownload() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(7); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + mockStatic(AsyncTaskUtils.class); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify dialog. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); + verify(mDialog).show(); + + /* Disable. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Click on download does nothing for now in the design if we disabled. */ + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no download scheduled. */ + verifyStatic(never()); + AsyncTaskUtils.execute(anyString(), any(AsyncTask.class), anyVararg()); + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java index 7a240b59e1..2f540c96ae 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java @@ -8,7 +8,7 @@ /** * AsyncTask utilities. */ -public final class AsyncTaskUtils { +public class AsyncTaskUtils { @VisibleForTesting AsyncTaskUtils() { From 4f3362688c474db2c7e77abd1bde7448f1f97a4a Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 9 Feb 2017 17:14:40 -0800 Subject: [PATCH 043/142] Start covering download manager related code + refactoring --- .../azure/mobile/updates/Updates.java | 27 +- .../mobile/updates/AbstractUpdatesTest.java | 6 +- .../mobile/updates/UpdatesDownloadTests.java | 313 ++++++++++++++++++ .../azure/mobile/utils/AsyncTaskUtils.java | 20 +- 4 files changed, 340 insertions(+), 26 deletions(-) create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 47884e506a..ef97de5033 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -7,7 +7,6 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -138,12 +137,12 @@ public class Updates extends AbstractMobileCenterService { /** * Current task inspecting the latest release details that we fetched from server. */ - private AsyncTask mDownloadTask; + private DownloadTask mDownloadTask; /** * Current task to process download completion. */ - private AsyncTask mProcessDownloadCompletionTask; + private ProcessDownloadCompletionTask mProcessDownloadCompletionTask; /** * True when update workflow reached final state. @@ -235,7 +234,8 @@ private static Intent getInstallIntent(Uri fileUri) { * * @return notification identifier for downloads. */ - private static int getNotificationId() { + @VisibleForTesting + static int getNotificationId() { return Updates.class.getName().hashCode(); } @@ -271,10 +271,7 @@ public synchronized void onActivityCreated(Activity activity, Bundle savedInstan PackageManager packageManager = activity.getPackageManager(); Intent intent = packageManager.getLaunchIntentForPackage(activity.getPackageName()); if (intent != null) { - ComponentName component = intent.resolveActivity(packageManager); - if (component != null) { - mLauncherActivityClassName = component.getClassName(); - } + mLauncherActivityClassName = intent.resolveActivity(packageManager).getClassName(); } } @@ -766,7 +763,8 @@ private void removeDownload(long downloadId) { /** * Removing a download violates strict mode in U.I. thread. */ - private class RemoveDownloadTask extends AsyncTask { + @VisibleForTesting + class RemoveDownloadTask extends AsyncTask { @Override protected Void doInBackground(Long... params) { @@ -783,7 +781,8 @@ protected Void doInBackground(Long... params) { /** * The download manager API violates strict mode in U.I. thread. */ - private class DownloadTask extends AsyncTask { + @VisibleForTesting + class DownloadTask extends AsyncTask { /** * Release details to check. @@ -811,13 +810,13 @@ protected Void doInBackground(Void[] params) { storeDownloadRequestId(downloadManager, this, downloadRequestId); return null; } - } /** * Inspect a completed download, this uses APIs that would trigger strict mode violation if used in U.I. thread. */ - private class ProcessDownloadCompletionTask extends AsyncTask { + @VisibleForTesting + class ProcessDownloadCompletionTask extends AsyncTask { /** * Context. @@ -851,8 +850,8 @@ protected Void doInBackground(Void... params) { } /* Check intent data is what we expected. */ - long expectedDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, -1); - if (expectedDownloadId != mDownloadId) { + long expectedDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); + if (expectedDownloadId > 0 && expectedDownloadId != mDownloadId) { MobileCenterLog.warn(LOG_TAG, "Ignoring completion for a download we didn't expect, id=" + mDownloadId); return null; } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index f48fe440bb..7b73b21651 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -16,7 +16,6 @@ import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; import org.powermock.reflect.Whitebox; @@ -27,6 +26,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.doAnswer; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; @@ -60,10 +60,10 @@ public void setUp() throws Exception { when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(true); /* Then simulate further changes to state. */ - PowerMockito.doAnswer(new Answer() { + doAnswer(new Answer() { @Override - public Object answer(InvocationOnMock invocation) throws Throwable { + public Void answer(InvocationOnMock invocation) throws Throwable { /* Whenever the new state is persisted, make further calls return the new state. */ boolean enabled = (Boolean) invocation.getArguments()[1]; diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java new file mode 100644 index 0000000000..47def0c563 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java @@ -0,0 +1,313 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.Activity; +import android.app.DownloadManager; +import android.app.NotificationManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; + +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.utils.AsyncTaskUtils; +import com.microsoft.azure.mobile.utils.storage.StorageHelper; +import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + +import static android.app.DownloadManager.EXTRA_DOWNLOAD_ID; +import static android.content.Context.NOTIFICATION_SERVICE; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.doAnswer; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyNew; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@PrepareForTest(AsyncTaskUtils.class) +public class UpdatesDownloadTests extends AbstractUpdatesTest { + + private static final long DOWNLOAD_ID = 42; + + @Mock + private Uri mDownloadUrl; + + @Mock + private DownloadManager mDownloadManager; + + @Mock + private NotificationManager mNotificationManager; + + @Mock + private DownloadManager.Request mDownloadRequest; + + private Semaphore mDownloadBeforeSemaphore; + + private Semaphore mDownloadAfterSemaphore; + + private AtomicReference mDownloadTask; + + private Semaphore mCompletionBeforeSemaphore; + + private Semaphore mCompletionAfterSemaphore; + + private AtomicReference mCompletionTask; + + @Before + public void setUpDownload() throws Exception { + + /* Mock download manager. */ + when(mContext.getSystemService(Context.DOWNLOAD_SERVICE)).thenReturn(mDownloadManager); + whenNew(DownloadManager.Request.class).withAnyArguments().thenReturn(mDownloadRequest); + when(mDownloadManager.enqueue(mDownloadRequest)).thenReturn(DOWNLOAD_ID); + + /* Mock notification manager. */ + when(mContext.getSystemService(NOTIFICATION_SERVICE)).thenReturn(mNotificationManager); + + /* Mock updates to storage. */ + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(StorageHelper.PreferencesStorage.getLong(invocation.getArguments()[0].toString())).thenReturn((Long) invocation.getArguments()[1]); + return null; + } + }).when(StorageHelper.PreferencesStorage.class); + StorageHelper.PreferencesStorage.putLong(eq(PREFERENCE_KEY_DOWNLOAD_ID), anyLong()); + + /* Mock everything that triggers a download. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getVersion()).thenReturn(7); + when(releaseDetails.getDownloadUrl()).thenReturn(mDownloadUrl); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + mockStatic(AsyncTaskUtils.class); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Mock download asyncTask. */ + mDownloadBeforeSemaphore = new Semaphore(0); + mDownloadAfterSemaphore = new Semaphore(0); + mDownloadTask = new AtomicReference<>(); + when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument instanceof Updates.DownloadTask; + } + }), Mockito.anyVararg())).then(new Answer() { + + @Override + public Updates.DownloadTask answer(InvocationOnMock invocation) throws Throwable { + final Updates.DownloadTask task = spy((Updates.DownloadTask) invocation.getArguments()[1]); + mDownloadTask.set(task); + new Thread() { + + @Override + public void run() { + mDownloadBeforeSemaphore.acquireUninterruptibly(); + task.doInBackground(null); + mDownloadAfterSemaphore.release(); + } + }.start(); + return task; + } + }); + + /* Mock remove download async task. */ + when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument instanceof Updates.RemoveDownloadTask; + } + }), Mockito.anyVararg())).then(new Answer() { + + @Override + public Updates.RemoveDownloadTask answer(InvocationOnMock invocation) throws Throwable { + final Updates.RemoveDownloadTask task = (Updates.RemoveDownloadTask) invocation.getArguments()[1]; + task.doInBackground((Long) invocation.getArguments()[2]); + return task; + } + }); + + /* Mock download completion async task. */ + mCompletionBeforeSemaphore = new Semaphore(0); + mCompletionAfterSemaphore = new Semaphore(0); + mCompletionTask = new AtomicReference<>(); + when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument instanceof Updates.ProcessDownloadCompletionTask; + } + }), Mockito.anyVararg())).then(new Answer() { + + @Override + public Updates.ProcessDownloadCompletionTask answer(InvocationOnMock invocation) throws Throwable { + final Updates.ProcessDownloadCompletionTask task = spy((Updates.ProcessDownloadCompletionTask) invocation.getArguments()[1]); + mCompletionTask.set(task); + new Thread() { + + @Override + public void run() { + mCompletionBeforeSemaphore.acquireUninterruptibly(); + task.doInBackground(); + mCompletionAfterSemaphore.release(); + } + }.start(); + return task; + } + }); + + /* Click on dialog. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + } + + private void waitDownloadTask() { + mDownloadBeforeSemaphore.release(); + mDownloadAfterSemaphore.acquireUninterruptibly(); + } + + private void waitCompletionTask() { + mCompletionBeforeSemaphore.release(); + mCompletionAfterSemaphore.acquireUninterruptibly(); + } + + @Test + public void startDownloadThenDisable() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Verify. */ + verify(mDownloadManager).enqueue(mDownloadRequest); + verifyNew(DownloadManager.Request.class).withArguments(mDownloadUrl); + verifyStatic(); + PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, ""); + + /* Cancel download by disabling. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mDownloadTask.get()).cancel(true); + verify(mDownloadManager).remove(DOWNLOAD_ID); + verify(mNotificationManager).cancel(Updates.getNotificationId()); + } + + @Test + public void disableWhileStartingDownload() throws Exception { + + /* Cancel download before async task completes. */ + ArgumentCaptor removeTask = ArgumentCaptor.forClass(Updates.RemoveDownloadTask.class); + verifyStatic(); + AsyncTaskUtils.execute(anyString(), removeTask.capture(), Mockito.anyVararg()); + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mDownloadTask.get()).cancel(true); + + /* Simulate async task. */ + waitDownloadTask(); + + /* Verify mDownloadRequest enqueued then removed. */ + verify(mDownloadManager).enqueue(mDownloadRequest); + verifyNew(DownloadManager.Request.class).withArguments(mDownloadUrl); + verify(mDownloadManager).remove(DOWNLOAD_ID); + + /* And that we didn't persist the state. */ + verifyStatic(never()); + PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, DOWNLOAD_ID); + verifyStatic(never()); + PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, ""); + verifyZeroInteractions(mNotificationManager); + } + + @Test + public void failDownloadRestartNoLauncher() { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent intent = mock(Intent.class); + when(intent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(intent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, intent); + + /* Wait. Fails as we dont mock success uri. */ + waitCompletionTask(); + + /* Check failure processing. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Nothing should happen if just changing activities. */ + Activity activity = mock(Activity.class); + Updates.getInstance().onActivityPaused(activity); + Updates.getInstance().onActivityResumed(activity); + + /* Verify download happened only once. */ + verify(mDownloadManager).enqueue(mDownloadRequest); + + /* Exit app. */ + Updates.getInstance().onActivityPaused(activity); + Updates.getInstance().onActivityStopped(activity); + Updates.getInstance().onActivityDestroyed(activity); + + /* Recreate activity, we'll cache that there is no launcher since no mock intent. */ + when(activity.getPackageManager()).thenReturn(mock(PackageManager.class)); + Updates.getInstance().onActivityCreated(activity, null); + + /* So nothing happens since no launcher restart detected. */ + verify(mDownloadManager).enqueue(mDownloadRequest); + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java index 2f540c96ae..7c20aa3b16 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/AsyncTaskUtils.java @@ -1,6 +1,7 @@ package com.microsoft.azure.mobile.utils; import android.os.AsyncTask; +import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import java.util.concurrent.RejectedExecutionException; @@ -20,21 +21,22 @@ public class AsyncTaskUtils { * Execute a task using {@link AsyncTask#THREAD_POOL_EXECUTOR} and fall back * using {@link AsyncTask#SERIAL_EXECUTOR} in case of {@link RejectedExecutionException}. * - * @param logTag log tag to use for logging a warning about the fallback. - * @param asyncTask task to execute. - * @param params parameters. - * @param parameters type. - * @param progress type. - * @param result type. + * @param logTag log tag to use for logging a warning about the fallback. + * @param asyncTask task to execute. + * @param params parameters. + * @param parameters type. + * @param task type. * @return the task. */ + @NonNull @SafeVarargs - public static AsyncTask execute(String logTag, AsyncTask asyncTask, Params... params) { + @SuppressWarnings("unchecked") + public static > Type execute(String logTag, @NonNull Type asyncTask, Params... params) { try { - return asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); + return (Type) asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); } catch (RejectedExecutionException e) { MobileCenterLog.warn(logTag, "THREAD_POOL_EXECUTOR saturated, fall back on SERIAL_EXECUTOR which has an unbounded queue", e); - return asyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, params); + return (Type) asyncTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, params); } } } From f4bf8cc1bc7e0fed4a1b2394ae6c24b6d66d3a50 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 9 Feb 2017 17:52:31 -0800 Subject: [PATCH 044/142] Refactoring in updates and utils --- .../azure/mobile/updates/BrowserUtilsTest.java | 4 ++-- .../azure/mobile/updates/BrowserUtils.java | 2 ++ .../azure/mobile/updates/UpdateConstants.java | 14 ++++++++++---- .../microsoft/azure/mobile/updates/Updates.java | 8 ++++---- ...ConstantsTest.java => UpdateConstantsTest.java} | 4 ++-- .../updates/UpdatesBeforeApiSuccessTests.java | 8 ++++---- 6 files changed, 24 insertions(+), 16 deletions(-) rename sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/{UpdatesConstantsTest.java => UpdateConstantsTest.java} (73%) diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java index ca5ed84216..efd5633b0d 100644 --- a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java +++ b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java @@ -32,7 +32,7 @@ public class BrowserUtilsTest { @Test - public void coverage() { + public void init() { assertNotNull(new BrowserUtils()); } @@ -53,7 +53,7 @@ public boolean matches(Object o) { } @Test - public void noBrowser() throws Exception { + public void noBrowserFound() throws Exception { /* Mock no browser. */ Activity activity = mock(Activity.class); diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java index cdc0da8d5c..53ac5dfa5b 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java @@ -29,6 +29,8 @@ class BrowserUtils { @VisibleForTesting BrowserUtils() { + + /* Hide constructor in utils pattern. */ } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java index 08e94366bb..c36adeca9b 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -4,6 +4,9 @@ import com.microsoft.azure.mobile.MobileCenter; +/** + * Updates constants. + */ final class UpdateConstants { /** @@ -37,14 +40,15 @@ final class UpdateConstants { static final String DEFAULT_API_URL = "https://api.mobile.azure.com"; /** - * Login URL path. Trailing slash matters to avoid redirection that loses query string. + * Login URL path. Contains the app secret variable to replace. + * Trailing slash needed to avoid redirection that can lose the query string on some servers. */ - static final String LOGIN_PAGE_URL_PATH = "/apps/%s/update-setup/"; + static final String LOGIN_PAGE_URL_PATH_FORMAT = "/apps/%s/update-setup/"; /** - * Check latest release API URL path. + * Check latest release API URL path. Contains the app secret variable to replace. */ - static final String CHECK_UPDATE_URL_PATH = "/sdk/apps/%s/releases/latest"; + static final String CHECK_UPDATE_URL_PATH_FORMAT = "/sdk/apps/%s/releases/latest"; /** * API parameter for release hash. @@ -108,5 +112,7 @@ final class UpdateConstants { @VisibleForTesting UpdateConstants() { + + /* Hide constructor as it's just a constant class. */ } } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index ef97de5033..b744612f78 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -45,11 +45,11 @@ import static android.content.Context.DOWNLOAD_SERVICE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; -import static com.microsoft.azure.mobile.updates.UpdateConstants.CHECK_UPDATE_URL_PATH; +import static com.microsoft.azure.mobile.updates.UpdateConstants.CHECK_UPDATE_URL_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_API_URL; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_LOGIN_URL; import static com.microsoft.azure.mobile.updates.UpdateConstants.HEADER_API_TOKEN; -import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH; +import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; @@ -433,7 +433,7 @@ private synchronized void resumeUpdateWorkflow() { /* Build URL. */ String url = mLoginUrl; - url += String.format(LOGIN_PAGE_URL_PATH, mAppSecret); + url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, mAppSecret); url += "?" + PARAMETER_RELEASE_HASH + "=" + releaseHash; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId; @@ -518,7 +518,7 @@ private synchronized void getLatestReleaseDetails(@NonNull String updateToken) { HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(mContext); HttpClient httpClient = new HttpClientNetworkStateHandler(retryer, networkStateHelper); - String url = mApiUrl + String.format(CHECK_UPDATE_URL_PATH, mAppSecret); + String url = mApiUrl + String.format(CHECK_UPDATE_URL_PATH_FORMAT, mAppSecret); Map headers = new HashMap<>(); headers.put(HEADER_API_TOKEN, updateToken); final Object releaseCallId = mCheckReleaseCallId = new Object(); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesConstantsTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java similarity index 73% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesConstantsTest.java rename to sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java index 94f4d64d9e..2d3580f242 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesConstantsTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java @@ -4,10 +4,10 @@ import static org.junit.Assert.assertNotNull; -public class UpdatesConstantsTest { +public class UpdateConstantsTest { @Test - public void coverage() { + public void init() { assertNotNull(new UpdateConstants()); } } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java index 29dca4a970..0ac0cd5911 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java @@ -26,7 +26,7 @@ import java.util.UUID; import java.util.concurrent.Semaphore; -import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH; +import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REDIRECT_ID; @@ -103,7 +103,7 @@ public void happyPathUntilHangingCall() throws Exception { Updates.getInstance().onActivityResumed(activity); verifyStatic(); String url = UpdateConstants.DEFAULT_LOGIN_URL; - url += String.format(LOGIN_PAGE_URL_PATH, "a"); + url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, "a"); url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); @@ -220,7 +220,7 @@ public void setUrls() throws Exception { Updates.getInstance().onActivityResumed(activity); verifyStatic(); String url = "http://mock"; - url += String.format(LOGIN_PAGE_URL_PATH, "a"); + url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, "a"); url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); @@ -277,7 +277,7 @@ public void disableBeforeStoreToken() { Updates.getInstance().onActivityResumed(activity); verifyStatic(); String url = UpdateConstants.DEFAULT_LOGIN_URL; - url += String.format(LOGIN_PAGE_URL_PATH, "a"); + url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, "a"); url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); From dccaf11dd75a134a4a2c05d5070fa8853d3e0af7 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 9 Feb 2017 17:57:56 -0800 Subject: [PATCH 045/142] Improve some texts and comments --- sdk/mobile-center-updates/src/main/res/values/strings.xml | 4 ++-- .../main/java/com/microsoft/azure/mobile/MobileCenter.java | 1 + .../java/com/microsoft/azure/mobile/MobileCenterService.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/res/values/strings.xml b/sdk/mobile-center-updates/src/main/res/values/strings.xml index 3b8c14c83a..365d292282 100644 --- a/sdk/mobile-center-updates/src/main/res/values/strings.xml +++ b/sdk/mobile-center-updates/src/main/res/values/strings.xml @@ -1,9 +1,9 @@ Application update downloaded. - Tap to install it now. + Tap to install the application. Update Available - No release notes was provided for this build. + No release notes were provided for this build. Ignore Download \ No newline at end of file diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index e97f0a0b15..92505b800d 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -70,6 +70,7 @@ public class MobileCenter { * Log serializer. */ private LogSerializer mLogSerializer; + /** * Channel. */ diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java index 3e497010fb..01af2f1bc8 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java @@ -39,7 +39,7 @@ public interface MobileCenterService extends Application.ActivityLifecycleCallba Map getLogFactories(); /** - * Called when the service has been started (disregarding if enabled or disabled). + * Called when the service is started (disregarding if enabled or disabled). * * @param context application context. * @param appSecret application secret. From 84d8b9cef19d0f7f20e7230b21f8e024f775ab3d Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 9 Feb 2017 18:38:42 -0800 Subject: [PATCH 046/142] Fix a race condition in update completion task and test it --- .../azure/mobile/updates/Updates.java | 19 ++++++-- .../mobile/updates/UpdatesDownloadTests.java | 48 +++++++++++++++---- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index b744612f78..399651c045 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -472,6 +472,17 @@ private synchronized void completeWorkflow(ReleaseDetails releaseDetails) { } } + /** + * Reset all variables that matter to restart checking a new release on launcher activity restart. + * + * @param task to check if state changed and that the call should be ignored. + */ + private synchronized void completeWorkflow(ProcessDownloadCompletionTask task) { + if (task == mProcessDownloadCompletionTask) { + completeWorkflow(); + } + } + /** * Reset all variables that matter to restart checking a new release on launcher activity restart. */ @@ -727,7 +738,7 @@ synchronized void processCompletedDownload(@NonNull Context context, long downlo * @return foreground activity if any, if state is valid. * @throws IllegalStateException if state changed. */ - private synchronized Activity checkStateIsValidFor(ProcessDownloadCompletionTask task) throws IllegalStateException { + private synchronized Activity getForegroundActivityWithStateCheck(ProcessDownloadCompletionTask task) throws IllegalStateException { if (task == mProcessDownloadCompletionTask) { return mForegroundActivity; } @@ -885,7 +896,7 @@ protected Void doInBackground(Void... params) { /* Exit check point. */ Activity activity; try { - activity = checkStateIsValidFor(this); + activity = getForegroundActivityWithStateCheck(this); } catch (IllegalStateException e) { /* If we were canceled, exit now. */ @@ -899,7 +910,7 @@ protected Void doInBackground(Void... params) { /* This start call triggers strict mode violation in U.I. thread so it needs to be done here, and we can't synchronize anymore... */ MobileCenterLog.debug(LOG_TAG, "Application is in foreground, launch install UI now."); activity.startActivity(intent); - completeWorkflow(mReleaseDetails); + completeWorkflow(this); } else { /* Remember we have a download ready. */ @@ -932,7 +943,7 @@ protected Void doInBackground(Void... params) { } } else { MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + mDownloadId); - completeWorkflow(mReleaseDetails); + completeWorkflow(this); } return null; } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java index 47def0c563..3d9dcf331f 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java @@ -104,6 +104,15 @@ public Void answer(InvocationOnMock invocation) throws Throwable { } }).when(StorageHelper.PreferencesStorage.class); StorageHelper.PreferencesStorage.putLong(eq(PREFERENCE_KEY_DOWNLOAD_ID), anyLong()); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(StorageHelper.PreferencesStorage.getLong(invocation.getArguments()[0].toString())).thenReturn(0L); + return null; + } + }).when(StorageHelper.PreferencesStorage.class); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); /* Mock everything that triggers a download. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -245,20 +254,15 @@ public void startDownloadThenDisable() throws Exception { public void disableWhileStartingDownload() throws Exception { /* Cancel download before async task completes. */ - ArgumentCaptor removeTask = ArgumentCaptor.forClass(Updates.RemoveDownloadTask.class); - verifyStatic(); - AsyncTaskUtils.execute(anyString(), removeTask.capture(), Mockito.anyVararg()); Updates.setEnabled(false); + waitDownloadTask(); + + /* Verify. */ verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); verify(mDownloadTask.get()).cancel(true); - - /* Simulate async task. */ - waitDownloadTask(); - - /* Verify mDownloadRequest enqueued then removed. */ verify(mDownloadManager).enqueue(mDownloadRequest); verifyNew(DownloadManager.Request.class).withArguments(mDownloadUrl); verify(mDownloadManager).remove(DOWNLOAD_ID); @@ -271,6 +275,34 @@ public void disableWhileStartingDownload() throws Exception { verifyZeroInteractions(mNotificationManager); } + @Test + public void disableWhileProcessingCompletion() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent intent = mock(Intent.class); + when(intent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(intent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, intent); + + /* Disable before completion. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + waitCompletionTask(); + + /* Verify cancellation. */ + verify(mCompletionTask.get()).cancel(true); + verify(mDownloadManager).remove(DOWNLOAD_ID); + verify(mNotificationManager).cancel(Updates.getNotificationId()); + + /* Check cleaned state only once, the completeWorkflow on failed download has to be ignored. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + } + @Test public void failDownloadRestartNoLauncher() { From 33ce4270dd4301fd7110ebfbb450ebc894db847d Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 9 Feb 2017 18:56:43 -0800 Subject: [PATCH 047/142] Fix code coverage issue in DownloadCompletionReceiver By using if instead of switch, it also reduces file length. --- .../updates/DownloadCompletionReceiver.java | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java index 766425f401..2d86676488 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java @@ -13,27 +13,22 @@ public class DownloadCompletionReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - /* Check intent action. */ - if (intent.getAction() != null) { - switch (intent.getAction()) { - - /* - * Just resume app if clicking on pending download notification as - * it's always weird to click on a notification and nothing happening. - * Another option would be to open download list. - */ - case DownloadManager.ACTION_NOTIFICATION_CLICKED: - Updates.getInstance().resumeApp(context); - break; + /* + * Just resume app if clicking on pending download notification as + * it's awkward to click on a notification and nothing happening. + * Another option would be to open download list. + */ + String action = intent.getAction(); + if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)) { + Updates.getInstance().resumeApp(context); + } - /* - * Forward the download identifier to Updates for inspection. - */ - case DownloadManager.ACTION_DOWNLOAD_COMPLETE: - long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); - Updates.getInstance().processCompletedDownload(context, downloadId); - break; - } + /* + * Forward the download identifier to Updates for inspection when a download completes. + */ + else if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { + long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); + Updates.getInstance().processCompletedDownload(context, downloadId); } } } From 8a1dc0550427b1b9b8e429d9c9e12bd553d7ce39 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 10 Feb 2017 15:08:25 -0800 Subject: [PATCH 048/142] Change notification title --- sdk/mobile-center-updates/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/mobile-center-updates/src/main/res/values/strings.xml b/sdk/mobile-center-updates/src/main/res/values/strings.xml index 365d292282..8f4f903f85 100644 --- a/sdk/mobile-center-updates/src/main/res/values/strings.xml +++ b/sdk/mobile-center-updates/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - Application update downloaded. + New update is downloaded Tap to install the application. Update Available No release notes were provided for this build. From 2703a86018980116cc0e96da199034c98c22d55e Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 10 Feb 2017 18:31:04 -0800 Subject: [PATCH 049/142] Add more tests and fix a minor bug in download completion --- .../azure/mobile/updates/Updates.java | 34 ++- .../mobile/updates/UpdatesDownloadTests.java | 219 +++++++++++++++++- .../azure/mobile/test/TestUtils.java | 27 +++ 3 files changed, 268 insertions(+), 12 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 399651c045..5e758d7c94 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -853,7 +853,13 @@ class ProcessDownloadCompletionTask extends AsyncTask { @Override protected Void doInBackground(Void... params) { - /* Completion might be triggered before MobileCenter.start. */ + /* + * Completion might be triggered in background before MobileCenter.start + * if application was killed after starting download. + * + * We still want to generate the notification: if we can find the data in preferences + * that means they were not deleted, and thus that the sdk was not disabled. + */ MobileCenterLog.debug(LOG_TAG, "Process download completion id=" + mDownloadId); if (Updates.this.mContext == null) { MobileCenterLog.debug(LOG_TAG, "Called before onStart, init storage"); @@ -875,22 +881,27 @@ protected Void doInBackground(Void... params) { /* Build install intent. */ MobileCenterLog.debug(LOG_TAG, "Download was successful for id=" + mDownloadId + " uri=" + uriForDownloadedFile); Intent intent = getInstallIntent(uriForDownloadedFile); + boolean installerFound = false; if (intent.resolveActivity(mContext.getPackageManager()) == null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { Cursor cursor = downloadManager.query(new DownloadManager.Query().setFilterById(mDownloadId)); - if (cursor != null && cursor.moveToNext()) { - //noinspection deprecation - uriForDownloadedFile = Uri.parse("file://" + cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME))); - intent = getInstallIntent(uriForDownloadedFile); - if (intent.resolveActivity(mContext.getPackageManager()) == null) { - MobileCenterLog.error(LOG_TAG, "Installer not found"); - return null; + if (cursor != null) { + if (cursor.moveToNext()) { + //noinspection deprecation + uriForDownloadedFile = Uri.parse("file://" + cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME))); + intent = getInstallIntent(uriForDownloadedFile); + installerFound = intent.resolveActivity(mContext.getPackageManager()) != null; } + cursor.close(); } - } else { - MobileCenterLog.error(LOG_TAG, "Installer not found"); - return null; } + } else { + installerFound = true; + } + if (!installerFound) { + MobileCenterLog.error(LOG_TAG, "Installer not found"); + completeWorkflow(this); + return null; } /* Exit check point. */ @@ -923,6 +934,7 @@ protected Void doInBackground(Void... params) { icon = applicationInfo.icon; } catch (PackageManager.NameNotFoundException e) { MobileCenterLog.error(LOG_TAG, "Could not get application icon", e); + completeWorkflow(this); return null; } Notification.Builder builder = new Notification.Builder(mContext) diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java index 3d9dcf331f..4764a16279 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java @@ -3,21 +3,26 @@ import android.app.Activity; import android.app.DownloadManager; import android.app.NotificationManager; +import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; +import android.database.Cursor; import android.net.Uri; +import android.os.Build; import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.http.HttpClient; import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.test.TestUtils; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -46,6 +51,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.doAnswer; @@ -71,6 +77,9 @@ public class UpdatesDownloadTests extends AbstractUpdatesTest { @Mock private DownloadManager.Request mDownloadRequest; + @Mock + private Activity mFirstActivity; + private Semaphore mDownloadBeforeSemaphore; private Semaphore mDownloadAfterSemaphore; @@ -132,7 +141,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); mockStatic(AsyncTaskUtils.class); Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mFirstActivity); /* Mock download asyncTask. */ mDownloadBeforeSemaphore = new Semaphore(0); @@ -342,4 +351,212 @@ public void failDownloadRestartNoLauncher() { /* So nothing happens since no launcher restart detected. */ verify(mDownloadManager).enqueue(mDownloadRequest); } + + @Test + public void successDownloadInstallerNotFoundCursorIsNull() { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent intent = mock(Intent.class); + when(intent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(intent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, intent); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(null); + + /* Simulate task. */ + waitCompletionTask(); + + /* Check we completed workflow without starting activity because installer not found. */ + verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + } + + @Test + public void successDownloadInstallerNotFoundCursorEmpty() { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent intent = mock(Intent.class); + when(intent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(intent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, intent); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); + Cursor cursor = mock(Cursor.class); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); + + /* Simulate task. */ + waitCompletionTask(); + + /* Check we completed workflow without starting activity because installer not found. */ + verify(cursor).close(); + verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + } + + @Test + public void successDownloadInstallerNotFoundEvenWithLocalFile() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); + Cursor cursor = mock(Cursor.class); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); + when(cursor.moveToNext()).thenReturn(true).thenReturn(false); + + /* Simulate task. */ + waitCompletionTask(); + + /* Check we completed workflow without starting activity because installer not found. */ + verify(cursor).close(); + verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + } + + @Test + public void successDownloadInstallerNotFoundAfterNougat() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); + TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.N); + + /* Simulate task. */ + waitCompletionTask(); + + /* Check we completed workflow without starting activity because installer not found. */ + verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + } + + @Test + public void disableWhileCompletingBeforeNougat() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); + Cursor cursor = mock(Cursor.class); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); + when(cursor.moveToNext()).thenReturn(true).thenReturn(false); + Intent installIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); + when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(null).thenReturn(mock(ComponentName.class)); + + /* Disable before task run. */ + Updates.setEnabled(false); + + /* Simulate task. */ + waitCompletionTask(); + + /* Check we completed workflow without starting activity because disabled. */ + verify(cursor).close(); + verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mNotificationManager).cancel(Updates.getNotificationId()); + verifyNoMoreInteractions(mNotificationManager); + } + + @Test + public void successInForeground() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + Uri uri = mock(Uri.class); + when(uri.toString()).thenReturn("original"); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); + Intent installIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); + when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + + /* Simulate task. */ + waitCompletionTask(); + + /* Verify start activity and complete workflow. */ + verify(mFirstActivity).startActivity(installIntent); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verifyNoMoreInteractions(mNotificationManager); + } + + @Test + public void startActivityButDisabledAfterCheckpoint() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + Uri uri = mock(Uri.class); + when(uri.toString()).thenReturn("original"); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); + final Intent installIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); + when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + final Semaphore beforeStartingActivityLock = new Semaphore(0); + final Semaphore disabledLock = new Semaphore(0); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + beforeStartingActivityLock.release(); + disabledLock.acquireUninterruptibly(); + return null; + } + }).when(mFirstActivity).startActivity(installIntent); + + /* Disable while calling startActivity... */ + mCompletionBeforeSemaphore.release(); + beforeStartingActivityLock.acquireUninterruptibly(); + Updates.setEnabled(false); + disabledLock.release(); + mCompletionAfterSemaphore.acquireUninterruptibly(); + + /* Verify start activity and complete workflow skipped, e.g. clean behavior happened only once. */ + verify(mFirstActivity).startActivity(installIntent); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mNotificationManager).cancel(Updates.getNotificationId()); + verifyNoMoreInteractions(mNotificationManager); + } + + @After + public void tearDown() throws Exception { + TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", 0); + } } diff --git a/test/src/main/java/com/microsoft/azure/mobile/test/TestUtils.java b/test/src/main/java/com/microsoft/azure/mobile/test/TestUtils.java index aeccdd0d2e..ff0c881520 100644 --- a/test/src/main/java/com/microsoft/azure/mobile/test/TestUtils.java +++ b/test/src/main/java/com/microsoft/azure/mobile/test/TestUtils.java @@ -1,5 +1,10 @@ package com.microsoft.azure.mobile.test; +import org.junit.After; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -29,4 +34,26 @@ public static void compareSelfNullClass(Object o) { assertNotEquals(o, null); assertNotEquals(o, o.getClass()); } + + /** + * Use this method as a last resort alternative when Whitebox.setInternalState fails + * on static final variable and you are using the PowerMockRule (or just mockito). + * Please note that even this method will be useless if the constant is inlined by the compiler + * (e.g. the constant is directly a String or number or boolean, for that method to work, + * the static final variable must be the result of a method call). + * You need to reset the state on a {@link After} method to avoid side effects on other tests. + * + * @param clazz class containing the field. + * @param fieldName field name. + * @param value value to set. + * @throws Exception if an exception occurs. + */ + public static void setInternalState(Class clazz, String fieldName, Object value) throws Exception { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + Field modifiers = field.getClass().getDeclaredField("modifiers"); + modifiers.setAccessible(true); + modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, value); + } } From b117c5f611baef13f0e05e21227683078b06f8f6 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 10 Feb 2017 19:26:33 -0800 Subject: [PATCH 050/142] Add more download tests and fix minor bugs --- .../azure/mobile/updates/Updates.java | 9 +- .../mobile/updates/AbstractUpdatesTest.java | 8 +- .../mobile/updates/UpdatesDownloadTests.java | 245 ++++++++++++++++++ 3 files changed, 254 insertions(+), 8 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 5e758d7c94..0eb7b7388b 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -7,6 +7,7 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -382,12 +383,12 @@ private synchronized void resumeUpdateWorkflow() { /* FIXME this can cause strict mode violation. */ Uri apkUri = Uri.parse(downloadUri); MobileCenterLog.debug(LOG_TAG, "Now in foreground, remove notification and start install for APK uri=" + apkUri); + mForegroundActivity.startActivity(getInstallIntent(apkUri)); NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(getNotificationId()); - mForegroundActivity.startActivity(getInstallIntent(apkUri)); completeWorkflow(); return; - } catch (RuntimeException e) { + } catch (ActivityNotFoundException e) { /* Cleanup on exception and resume update workflow. */ MobileCenterLog.warn(LOG_TAG, "Download uri was invalid", e); @@ -682,10 +683,8 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager /* Delete previous download. */ long previousDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); if (previousDownloadId > 0) { - MobileCenterLog.debug(LOG_TAG, "Delete previous download and notification id=" + previousDownloadId); + MobileCenterLog.debug(LOG_TAG, "Delete previous download id=" + previousDownloadId); downloadManager.remove(previousDownloadId); - NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(getNotificationId()); } /* Store new download identifier. */ diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index 7b73b21651..a9cdb6ed00 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -42,6 +42,9 @@ public class AbstractUpdatesTest { @Mock Context mContext; + @Mock + PackageManager mPackageManager; + @Mock AlertDialog.Builder mDialogBuilder; @@ -74,11 +77,10 @@ public Void answer(InvocationOnMock invocation) throws Throwable { StorageHelper.PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); /* Mock package manager. */ - PackageManager packageManager = mock(PackageManager.class); when(mContext.getPackageName()).thenReturn("com.contoso"); - when(mContext.getPackageManager()).thenReturn(packageManager); + when(mContext.getPackageManager()).thenReturn(mPackageManager); PackageInfo packageInfo = mock(PackageInfo.class); - when(packageManager.getPackageInfo("com.contoso", 0)).thenReturn(packageInfo); + when(mPackageManager.getPackageInfo("com.contoso", 0)).thenReturn(packageInfo); Whitebox.setInternalState(packageInfo, "versionName", "1.2.3"); Whitebox.setInternalState(packageInfo, "versionCode", 6); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java index 4764a16279..6e49300ce7 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java @@ -1,12 +1,17 @@ package com.microsoft.azure.mobile.updates; +import android.annotation.TargetApi; import android.app.Activity; import android.app.DownloadManager; +import android.app.Notification; import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; @@ -42,14 +47,17 @@ import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @@ -122,6 +130,24 @@ public Void answer(InvocationOnMock invocation) throws Throwable { } }).when(StorageHelper.PreferencesStorage.class); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(StorageHelper.PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn((String) invocation.getArguments()[1]); + return null; + } + }).when(StorageHelper.PreferencesStorage.class); + StorageHelper.PreferencesStorage.putString(eq(PREFERENCE_KEY_DOWNLOAD_URI), anyString()); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(StorageHelper.PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn(null); + return null; + } + }).when(StorageHelper.PreferencesStorage.class); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); /* Mock everything that triggers a download. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -248,6 +274,12 @@ public void startDownloadThenDisable() throws Exception { verifyStatic(); PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, ""); + /* Pause/resume should do nothing excepting mentioning progress. */ + verify(mDialog).show(); + Updates.getInstance().onActivityPaused(mFirstActivity); + Updates.getInstance().onActivityResumed(mFirstActivity); + verify(mDialog).show(); + /* Cancel download by disabling. */ Updates.setEnabled(false); verifyStatic(); @@ -555,6 +587,219 @@ public Void answer(InvocationOnMock invocation) throws Throwable { verifyNoMoreInteractions(mNotificationManager); } + @Test + public void failsToGetNotificationIcon() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + Uri uri = mock(Uri.class); + when(uri.toString()).thenReturn("original"); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); + Intent installIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); + when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + + /* In background. */ + Updates.getInstance().onActivityPaused(mFirstActivity); + + /* And the icon will fail. */ + when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenThrow(new PackageManager.NameNotFoundException()); + + /* Simulate task. */ + waitCompletionTask(); + + /* Verify complete workflow with no notification. */ + verify(mFirstActivity, never()).startActivity(installIntent); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verifyNoMoreInteractions(mNotificationManager); + } + + @Test + @PrepareForTest(Uri.class) + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public void notifyThenRestartApp() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + Uri uri = mock(Uri.class); + when(uri.toString()).thenReturn("original"); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); + Intent installIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); + when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + + /* In background. */ + Updates.getInstance().onActivityPaused(mFirstActivity); + + /* Mock notification. */ + when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); + TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.JELLY_BEAN); + Notification.Builder notificationBuilder = mock(Notification.Builder.class); + whenNew(Notification.Builder.class).withAnyArguments().thenReturn(notificationBuilder); + when(notificationBuilder.build()).thenReturn(mock(Notification.class)); + when(notificationBuilder.setTicker(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentTitle(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentText(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setSmallIcon(anyInt())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentIntent(any(PendingIntent.class))).thenReturn(notificationBuilder); + + /* Simulate task. */ + waitCompletionTask(); + + /* Verify notification. */ + verify(mFirstActivity, never()).startActivity(installIntent); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, "original"); + verify(notificationBuilder).build(); + verify(notificationBuilder, never()).getNotification(); + verify(mNotificationManager).notify(eq(Updates.getNotificationId()), any(Notification.class)); + verifyNoMoreInteractions(mNotificationManager); + + /* Launch app should pop install U.I. and cancel notification. */ + when(mFirstActivity.getPackageManager()).thenReturn(mPackageManager); + Intent launcherIntent = mock(Intent.class); + when(mPackageManager.getLaunchIntentForPackage(anyString())).thenReturn(launcherIntent); + ComponentName launcher = mock(ComponentName.class); + when(launcherIntent.resolveActivity(mPackageManager)).thenReturn(launcher); + when(launcher.getClassName()).thenReturn(mFirstActivity.getClass().getName()); + mockStatic(Uri.class); + when(Uri.parse("original")).thenReturn(uri); + Updates.getInstance().onActivityStopped(mFirstActivity); + Updates.getInstance().onActivityDestroyed(mFirstActivity); + Updates.getInstance().onActivityCreated(mFirstActivity, null); + Updates.getInstance().onActivityResumed(mFirstActivity); + + /* Verify. */ + verify(mFirstActivity).startActivity(installIntent); + verify(mNotificationManager).cancel(Updates.getNotificationId()); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Keep download. */ + verifyStatic(never()); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verify(mDownloadManager, never()).remove(DOWNLOAD_ID); + + /* Verify second download (restart app) cleans first one. */ + when(mDownloadManager.enqueue(mDownloadRequest)).thenReturn(DOWNLOAD_ID + 1); + Updates.getInstance().onActivityStopped(mFirstActivity); + Updates.getInstance().onActivityDestroyed(mFirstActivity); + Updates.getInstance().onActivityCreated(mFirstActivity, null); + Updates.getInstance().onActivityResumed(mFirstActivity); + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder, times(2)).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + waitDownloadTask(); + + /* Verify new download id in storage. */ + verifyStatic(); + PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, DOWNLOAD_ID + 1); + + /* Verify previous download removed. */ + verify(mDownloadManager).remove(DOWNLOAD_ID); + + /* Notification already canceled so no more call, i.e. only once. */ + verify(mNotificationManager).cancel(Updates.getNotificationId()); + } + + @Test + @PrepareForTest(Uri.class) + @SuppressWarnings("deprecation") + public void notifyThenRestartThenInstallerFails() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Kill app, this has nothing to do with failure, but we need to test that too. */ + Updates.unsetInstance(); + + /* Process download completion. */ + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + Uri originalUri = mock(Uri.class); + when(originalUri.toString()).thenReturn("original"); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(originalUri); + Cursor cursor = mock(Cursor.class); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); + when(cursor.moveToNext()).thenReturn(true).thenReturn(false); + when(cursor.getString(anyInt())).thenReturn("localFile"); + mockStatic(Uri.class); + Uri localFileUri = mock(Uri.class); + when(Uri.parse("file://localFile")).thenReturn(localFileUri); + when(localFileUri.toString()).thenReturn("file://localFile"); + Intent installIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); + when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(null).thenReturn(mock(ComponentName.class)); + + /* Mock notification. */ + when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); + Notification.Builder notificationBuilder = mock(Notification.Builder.class); + whenNew(Notification.Builder.class).withAnyArguments().thenReturn(notificationBuilder); + when(notificationBuilder.getNotification()).thenReturn(mock(Notification.class)); + when(notificationBuilder.setTicker(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentTitle(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentText(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setSmallIcon(anyInt())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentIntent(any(PendingIntent.class))).thenReturn(notificationBuilder); + + /* Simulate task. */ + waitCompletionTask(); + + /* Verify notification. */ + verify(mFirstActivity, never()).startActivity(installIntent); + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, "file://localFile"); + verify(mNotificationManager).notify(eq(Updates.getNotificationId()), any(Notification.class)); + verifyNoMoreInteractions(mNotificationManager); + + /* Restart app should pop install U.I. and cancel notification and pop a new dialog then a new download. */ + doThrow(new ActivityNotFoundException()).when(mFirstActivity).startActivity(installIntent); + when(mDownloadManager.enqueue(mDownloadRequest)).thenReturn(DOWNLOAD_ID + 1); + Updates.getInstance().onStarted(mContext, "", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mFirstActivity); + + /* Verify. */ + verify(mFirstActivity).startActivity(installIntent); + verify(mNotificationManager).cancel(Updates.getNotificationId()); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verify(mDownloadManager).remove(DOWNLOAD_ID); + + /* Verify workflow restarted right after failure. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder, times(2)).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + waitDownloadTask(); + verifyStatic(); + PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, DOWNLOAD_ID + 1); + + /* Check no duplicate cleaning tasks, i.e. happened only once. */ + verify(mNotificationManager).cancel(Updates.getNotificationId()); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + verify(mDownloadManager).remove(DOWNLOAD_ID); + } + @After public void tearDown() throws Exception { TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", 0); From de79e6a2309660151a4d670a7e331b6313f5773b Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 10 Feb 2017 20:16:45 -0800 Subject: [PATCH 051/142] Add more download tests --- .../mobile/updates/UpdatesDownloadTests.java | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java index 6e49300ce7..d638433a7a 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java @@ -621,6 +621,64 @@ public void failsToGetNotificationIcon() throws Exception { verifyNoMoreInteractions(mNotificationManager); } + @Test + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public void disableRightBeforeNotifying() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + Uri uri = mock(Uri.class); + when(uri.toString()).thenReturn("original"); + when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); + Intent installIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); + when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + + /* In background. */ + Updates.getInstance().onActivityPaused(mFirstActivity); + + /* Mock notification. */ + when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); + TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.JELLY_BEAN); + Notification.Builder notificationBuilder = mock(Notification.Builder.class); + whenNew(Notification.Builder.class).withAnyArguments().thenReturn(notificationBuilder); + when(notificationBuilder.setTicker(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentTitle(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentText(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setSmallIcon(anyInt())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentIntent(any(PendingIntent.class))).thenReturn(notificationBuilder); + final Semaphore beforeNotifying = new Semaphore(0); + final Semaphore disabledLock = new Semaphore(0); + when(notificationBuilder.build()).thenAnswer(new Answer() { + + @Override + public Notification answer(InvocationOnMock invocation) throws Throwable { + beforeNotifying.release(); + disabledLock.acquireUninterruptibly(); + return mock(Notification.class); + } + }); + + /* Disable while preparing notification... */ + mCompletionBeforeSemaphore.release(); + beforeNotifying.acquireUninterruptibly(); + Updates.setEnabled(false); + disabledLock.release(); + mCompletionAfterSemaphore.acquireUninterruptibly(); + + /* Verify no notification and complete workflow skipped, e.g. clean behavior happened only once. */ + verify(mFirstActivity, never()).startActivity(installIntent); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verify(mNotificationManager, never()).notify(anyInt(), any(Notification.class)); + } + @Test @PrepareForTest(Uri.class) @SuppressWarnings("deprecation") @@ -630,7 +688,17 @@ public void notifyThenRestartApp() throws Exception { /* Simulate async task. */ waitDownloadTask(); - /* Process download completion. */ + /* Process fake download completion, should not interfere and will be ignored. */ + { + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(404L); + new DownloadCompletionReceiver().onReceive(mContext, completionIntent); + waitCompletionTask(); + verify(mDownloadManager, never()).getUriForDownloadedFile(anyLong()); + } + + /* Process download completion with the real download identifier. */ Intent completionIntent = mock(Intent.class); when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); From 5269e86b7475baa6432c6c598f7d5d0b8c6e4465 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 13 Feb 2017 16:10:55 -0800 Subject: [PATCH 052/142] Fixed null release notes parsing --- .../mobile/updates/ReleaseDetailsTest.java | 20 +++++++++++++++++++ .../azure/mobile/updates/ReleaseDetails.java | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java index 8131532120..eddd6febda 100644 --- a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java +++ b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java @@ -87,6 +87,26 @@ public void missingReleaseNotes() throws JSONException { assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); } + @Test + public void nullReleaseNotes() throws JSONException { + String json = "{" + + "version: '14'," + + "release_notes: null," + + "short_version: '2.1.5'," + + "min_os: ''," + + "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + + "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails releaseDetails = ReleaseDetails.parse(json); + assertNotNull(releaseDetails); + assertEquals(14, releaseDetails.getVersion()); + assertEquals("2.1.5", releaseDetails.getShortVersion()); + assertNull(releaseDetails.getReleaseNotes()); + assertEquals("", releaseDetails.getMinOs()); + assertEquals("b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c", releaseDetails.getFingerprint()); + assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + } + @Test(expected = JSONException.class) public void missingMinOs() throws JSONException { String json = "{" + diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java index 1ec2076fd0..e122f68939 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java @@ -72,7 +72,7 @@ static ReleaseDetails parse(String json) throws JSONException { throw new JSONException(e.getMessage()); } releaseDetails.shortVersion = object.getString(SHORT_VERSION); - releaseDetails.releaseNotes = object.optString(RELEASE_NOTES, null); + releaseDetails.releaseNotes = object.isNull(RELEASE_NOTES) ? null : object.getString(RELEASE_NOTES); releaseDetails.minOs = object.getString(MIN_OS); releaseDetails.fingerprint = object.getString(FINGERPRINT); releaseDetails.downloadUrl = Uri.parse(object.getString(DOWNLOAD_URL)); From fc526369b02af20242819d912bef6604ea6b8042 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 13 Feb 2017 16:47:55 -0800 Subject: [PATCH 053/142] Comment about fingerprint which is the wrong field to use for release_hash change --- .../java/com/microsoft/azure/mobile/updates/ReleaseDetails.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java index e122f68939..543ca0d289 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java @@ -48,6 +48,7 @@ class ReleaseDetails { /** * Checksum of the release binary. + * FIXME need to use android_release_hash instead but it's not yet available. fingerprint is APK hash, not release_hash. */ private String fingerprint; From 0b5994918fc5aa0cc5a25282ee25317f4ef8f10a Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 13 Feb 2017 18:37:21 -0800 Subject: [PATCH 054/142] Adapt updates prototype to current INT environment * Use different appSecret/serverUrl in build flavours * Use fake release_hash for now to match current backend * Remove 2 useless fields from ReleaseDetails --- .../sasquatch/activities/MainActivity.java | 23 +++------ .../activities/SettingsActivity.java | 17 ++++--- apps/sasquatch/src/main/res/values/env.xml | 5 ++ .../src/projectDependency/res/values/env.xml | 5 ++ .../projectDependency/res/values/strings.xml | 2 +- .../mobile/updates/ReleaseDetailsTest.java | 48 ------------------- .../azure/mobile/updates/ReleaseDetails.java | 35 -------------- .../azure/mobile/updates/Updates.java | 11 +++-- .../mobile/updates/AbstractUpdatesTest.java | 10 +++- .../updates/UpdatesBeforeApiSuccessTests.java | 7 ++- .../updates/UpdatesBeforeDownloadTests.java | 14 ++---- .../azure/mobile/http/ServiceCallback.java | 2 + 12 files changed, 52 insertions(+), 127 deletions(-) create mode 100644 apps/sasquatch/src/main/res/values/env.xml create mode 100644 apps/sasquatch/src/projectDependency/res/values/env.xml diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index de8eac5375..e5780f349e 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -9,6 +9,7 @@ import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -30,7 +31,6 @@ public class MainActivity extends AppCompatActivity { - static final String APP_SECRET = "45d1d9f6-2492-4e68-bd44-7190351eb5f3"; static final String APP_SECRET_KEY = "appSecret"; static final String SERVER_URL_KEY = "serverUrl"; private static final String LOG_TAG = "MobileCenterSasquatch"; @@ -44,19 +44,19 @@ protected void onCreate(Bundle savedInstanceState) { sSharedPreferences = getSharedPreferences("Sasquatch", Context.MODE_PRIVATE); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().build()); - String serverUrl = sSharedPreferences.getString(SERVER_URL_KEY, null); - if (serverUrl != null) { + String serverUrl = sSharedPreferences.getString(SERVER_URL_KEY, getString(R.string.server_url)); + if (!TextUtils.isEmpty(serverUrl)) { MobileCenter.setServerUrl(serverUrl); } MobileCenter.setLogLevel(Log.VERBOSE); Crashes.setListener(getCrashesListener()); - MobileCenter.start(getApplication(), getAppSecret(), Analytics.class, Crashes.class); + MobileCenter.start(getApplication(), sSharedPreferences.getString(APP_SECRET_KEY, getString(R.string.app_secret)), Analytics.class, Crashes.class); try { @SuppressWarnings("unchecked") Class updates = (Class) Class.forName("com.microsoft.azure.mobile.updates.Updates"); - updates.getMethod("setLoginUrl", String.class).invoke(null, "http://mockilecenterupdate.azurewebsites.net"); - updates.getMethod("setApiUrl", String.class).invoke(null, "http://mockilecenterupdate.azurewebsites.net"); + updates.getMethod("setLoginUrl", String.class).invoke(null, "http://install.asgard-int.trafficmanager.net"); + updates.getMethod("setApiUrl", String.class).invoke(null, "https://asgard-int.trafficmanager.net/api/v0.1"); MobileCenter.start(updates); } catch (Exception e) { MobileCenterLog.info(LOG_TAG, "Updates class not yet available in this flavor."); @@ -96,17 +96,6 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } - private String getAppSecret() { - String appSecret = sSharedPreferences.getString(APP_SECRET_KEY, null); - if (appSecret == null) { - SharedPreferences.Editor editor = sSharedPreferences.edit(); - editor.putString(APP_SECRET_KEY, APP_SECRET); - editor.apply(); - appSecret = sSharedPreferences.getString(APP_SECRET_KEY, null); - } - return appSecret; - } - private AbstractCrashesListener getCrashesListener() { return new AbstractCrashesListener() { @Override diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java index dc2f2b868d..45880bd6a8 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java @@ -160,13 +160,13 @@ public void onClick(DialogInterface dialog, int which) { return true; } }); - initClickableSetting(R.string.app_secret_key, MainActivity.sSharedPreferences.getString(APP_SECRET_KEY, null), new Preference.OnPreferenceClickListener() { + initClickableSetting(R.string.app_secret_key, MainActivity.sSharedPreferences.getString(APP_SECRET_KEY, getString(R.string.app_secret)), new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(final Preference preference) { final EditText input = new EditText(getActivity()); input.setInputType(InputType.TYPE_CLASS_TEXT); - input.setText(MainActivity.sSharedPreferences.getString(APP_SECRET_KEY, null)); + input.setText(MainActivity.sSharedPreferences.getString(APP_SECRET_KEY, getString(R.string.app_secret))); new AlertDialog.Builder(getActivity()).setTitle(R.string.app_secret_title).setView(input) .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() { @@ -185,8 +185,9 @@ public void onClick(DialogInterface dialog, int which) { .setNeutralButton(R.string.reset, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - setKeyValue(APP_SECRET_KEY, MainActivity.APP_SECRET); - Toast.makeText(getActivity(), String.format(getActivity().getString(R.string.app_secret_changed_format), MainActivity.APP_SECRET), Toast.LENGTH_SHORT).show(); + String defaultAppSecret = getString(R.string.app_secret); + setKeyValue(APP_SECRET_KEY, defaultAppSecret); + Toast.makeText(getActivity(), String.format(getActivity().getString(R.string.app_secret_changed_format), defaultAppSecret), Toast.LENGTH_SHORT).show(); preference.setSummary(MainActivity.sSharedPreferences.getString(APP_SECRET_KEY, null)); } }) @@ -204,7 +205,9 @@ public boolean onPreferenceClick(Preference preference) { return true; } }); - initClickableSetting(R.string.server_url_key, MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, getString(R.string.server_url_production)), new Preference.OnPreferenceClickListener() { + String defaultServerUrl = getString(R.string.server_url); + final String defaultServerUrlDisplay = TextUtils.isEmpty(defaultServerUrl) ? getString(R.string.server_url_production) : defaultServerUrl; + initClickableSetting(R.string.server_url_key, MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, defaultServerUrlDisplay), new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(final Preference preference) { @@ -226,14 +229,14 @@ public void onClick(DialogInterface dialog, int which) { } else { Toast.makeText(getActivity(), R.string.server_url_invalid, Toast.LENGTH_SHORT).show(); } - preference.setSummary(MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, getString(R.string.server_url_production))); + preference.setSummary(MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, defaultServerUrlDisplay)); } }) .setNeutralButton(R.string.reset, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { setProductionUrl(); - preference.setSummary(MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, getString(R.string.server_url_production))); + preference.setSummary(MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, defaultServerUrlDisplay)); } }) .setNegativeButton(R.string.cancel, null) diff --git a/apps/sasquatch/src/main/res/values/env.xml b/apps/sasquatch/src/main/res/values/env.xml new file mode 100644 index 0000000000..a013923179 --- /dev/null +++ b/apps/sasquatch/src/main/res/values/env.xml @@ -0,0 +1,5 @@ + + + 45d1d9f6-2492-4e68-bd44-7190351eb5f3 + + diff --git a/apps/sasquatch/src/projectDependency/res/values/env.xml b/apps/sasquatch/src/projectDependency/res/values/env.xml new file mode 100644 index 0000000000..34af1dc5c8 --- /dev/null +++ b/apps/sasquatch/src/projectDependency/res/values/env.xml @@ -0,0 +1,5 @@ + + + 9e0d97c1-7838-46d0-9dab-1a0ef66aec6e + https://in-integration.dev.avalanch.es + diff --git a/apps/sasquatch/src/projectDependency/res/values/strings.xml b/apps/sasquatch/src/projectDependency/res/values/strings.xml index 1b51eac07c..a19bbd352d 100644 --- a/apps/sasquatch/src/projectDependency/res/values/strings.xml +++ b/apps/sasquatch/src/projectDependency/res/values/strings.xml @@ -1,4 +1,4 @@ - Sasquatch Test + sasquatch-test diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java index eddd6febda..54a98c6cfb 100644 --- a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java +++ b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java @@ -17,8 +17,6 @@ public void parse() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "min_os: ''," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -26,8 +24,6 @@ public void parse() throws JSONException { assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); - assertEquals("", releaseDetails.getMinOs()); - assertEquals("b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c", releaseDetails.getFingerprint()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); } @@ -36,8 +32,6 @@ public void missingVersion() throws JSONException { String json = "{" + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "min_os: ''," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails.parse(json); @@ -49,8 +43,6 @@ public void invalidVersion() throws JSONException { "version: true," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "min_os: ''," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails.parse(json); @@ -61,8 +53,6 @@ public void missingShortVersion() throws JSONException { String json = "{" + "version: '14'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "min_os: ''," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails.parse(json); @@ -73,8 +63,6 @@ public void missingReleaseNotes() throws JSONException { String json = "{" + "version: '14'," + "short_version: '2.1.5'," + - "min_os: ''," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -82,8 +70,6 @@ public void missingReleaseNotes() throws JSONException { assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertNull(releaseDetails.getReleaseNotes()); - assertEquals("", releaseDetails.getMinOs()); - assertEquals("b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c", releaseDetails.getFingerprint()); assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); } @@ -93,8 +79,6 @@ public void nullReleaseNotes() throws JSONException { "version: '14'," + "release_notes: null," + "short_version: '2.1.5'," + - "min_os: ''," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -102,43 +86,15 @@ public void nullReleaseNotes() throws JSONException { assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertNull(releaseDetails.getReleaseNotes()); - assertEquals("", releaseDetails.getMinOs()); - assertEquals("b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c", releaseDetails.getFingerprint()); assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); } - @Test(expected = JSONException.class) - public void missingMinOs() throws JSONException { - String json = "{" + - "version: '14'," + - "short_version: '2.1.5'," + - "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + - "}"; - ReleaseDetails.parse(json); - } - - @Test(expected = JSONException.class) - public void missingFingerprint() throws JSONException { - String json = "{" + - "version: '14'," + - "short_version: '2.1.5'," + - "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "min_os: ''," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + - "}"; - ReleaseDetails.parse(json); - } - @Test(expected = JSONException.class) public void missingDownloadUrl() throws JSONException { String json = "{" + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "min_os: ''," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + "}"; ReleaseDetails.parse(json); } @@ -149,8 +105,6 @@ public void missingDownloadUrlScheme() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "min_os: ''," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + "download_url: 'someFile'" + "}"; ReleaseDetails.parse(json); @@ -162,8 +116,6 @@ public void invalidDownloadUrlScheme() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "min_os: ''," + - "fingerprint: 'b407a9acbbdf509de2af3676de8d8fa26a21e4293a393dcef7d902deaa9caa1c'," + "download_url: 'ftp://someFile'" + "}"; ReleaseDetails.parse(json); diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java index 543ca0d289..a3d1979123 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java @@ -16,10 +16,6 @@ class ReleaseDetails { private static final String RELEASE_NOTES = "release_notes"; - private static final String MIN_OS = "min_os"; - - private static final String FINGERPRINT = "fingerprint"; - private static final String DOWNLOAD_URL = "download_url"; /** @@ -41,17 +37,6 @@ class ReleaseDetails { */ private String releaseNotes; - /** - * The release's minimum required operating system. - */ - private String minOs; - - /** - * Checksum of the release binary. - * FIXME need to use android_release_hash instead but it's not yet available. fingerprint is APK hash, not release_hash. - */ - private String fingerprint; - /** * The URL that hosts the binary for this release. */ @@ -74,8 +59,6 @@ static ReleaseDetails parse(String json) throws JSONException { } releaseDetails.shortVersion = object.getString(SHORT_VERSION); releaseDetails.releaseNotes = object.isNull(RELEASE_NOTES) ? null : object.getString(RELEASE_NOTES); - releaseDetails.minOs = object.getString(MIN_OS); - releaseDetails.fingerprint = object.getString(FINGERPRINT); releaseDetails.downloadUrl = Uri.parse(object.getString(DOWNLOAD_URL)); String scheme = releaseDetails.downloadUrl.getScheme(); if (scheme == null || !scheme.startsWith("http")) { @@ -111,24 +94,6 @@ String getReleaseNotes() { return this.releaseNotes; } - /** - * Get the minOs value. - * - * @return the minOs value - */ - String getMinOs() { - return this.minOs; - } - - /** - * Get the fingerprint value. - * - * @return the fingerprint value - */ - String getFingerprint() { - return this.fingerprint; - } - /** * Get the downloadUrl value. * diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 0eb7b7388b..1ab0267f64 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -33,7 +33,6 @@ import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; -import com.microsoft.azure.mobile.utils.HashUtils; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.NetworkStateHelper; import com.microsoft.azure.mobile.utils.UUIDUtils; @@ -459,7 +458,8 @@ private String computeHash(@NonNull Context context) throws PackageManager.NameN @NonNull private String computeHash(@NonNull Context context, @NonNull PackageInfo packageInfo) { - return HashUtils.sha256(context.getPackageName() + ":" + packageInfo.versionName + ":" + packageInfo.versionCode); + // TODO switch to the following hash when backend supports it: HashUtils.sha256(context.getPackageName() + ":" + packageInfo.versionName + ":" + packageInfo.versionCode); + return context.getString(packageInfo.applicationInfo.labelRes); } /** @@ -605,9 +605,10 @@ private synchronized void handleApiCallSuccess(Object releaseCallId, ReleaseDeta * @return true if latest release on server should be used. */ private boolean isMoreRecent(PackageInfo packageInfo, ReleaseDetails releaseDetails) { - if (releaseDetails.getVersion() == packageInfo.versionCode) { - return !releaseDetails.getFingerprint().equals(computeHash(mContext, packageInfo)); - } +// TODO when releaseHash is exposed in JSON. +// if (releaseDetails.getVersion() == packageInfo.versionCode) { +// return !releaseDetails.getReleaseHash().equals(computeHash(mContext, packageInfo)); +// } return releaseDetails.getVersion() > packageInfo.versionCode; } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index a9cdb6ed00..fa6fc52714 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -2,6 +2,7 @@ import android.app.AlertDialog; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.text.TextUtils; @@ -34,8 +35,8 @@ @PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class}) public class AbstractUpdatesTest { + static final String TEST_HASH = "testapp"; // TODO HashUtils.sha256("com.contoso:1.2.3:6"); private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; - @Rule public PowerMockRule mPowerMockRule = new PowerMockRule(); @@ -84,6 +85,13 @@ public Void answer(InvocationOnMock invocation) throws Throwable { Whitebox.setInternalState(packageInfo, "versionName", "1.2.3"); Whitebox.setInternalState(packageInfo, "versionCode", 6); + /* TODO temporary fake hash based on app name */ + ApplicationInfo applicationInfo = mock(ApplicationInfo.class); + Whitebox.setInternalState(applicationInfo, "labelRes", 1337); + Whitebox.setInternalState(packageInfo, "applicationInfo", applicationInfo); + //noinspection ResourceType + when(mContext.getString(1337)).thenReturn(TEST_HASH); + /* Mock some statics. */ mockStatic(BrowserUtils.class); mockStatic(UUIDUtils.class); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java index 0ac0cd5911..c9ac80cac5 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java @@ -13,7 +13,6 @@ import com.microsoft.azure.mobile.http.HttpException; import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; -import com.microsoft.azure.mobile.utils.HashUtils; import com.microsoft.azure.mobile.utils.UUIDUtils; import org.json.JSONException; @@ -104,7 +103,7 @@ public void happyPathUntilHangingCall() throws Exception { verifyStatic(); String url = UpdateConstants.DEFAULT_LOGIN_URL; url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, "a"); - url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); + url += "?" + PARAMETER_RELEASE_HASH + "=" + TEST_HASH; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; @@ -221,7 +220,7 @@ public void setUrls() throws Exception { verifyStatic(); String url = "http://mock"; url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, "a"); - url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); + url += "?" + PARAMETER_RELEASE_HASH + "=" + TEST_HASH; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; @@ -278,7 +277,7 @@ public void disableBeforeStoreToken() { verifyStatic(); String url = UpdateConstants.DEFAULT_LOGIN_URL; url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, "a"); - url += "?" + PARAMETER_RELEASE_HASH + "=" + HashUtils.sha256("com.contoso:1.2.3:6"); + url += "?" + PARAMETER_RELEASE_HASH + "=" + TEST_HASH; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java index 0e9603d016..5f41c653f0 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java @@ -4,7 +4,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageManager; -import android.os.AsyncTask; import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.http.HttpClient; @@ -12,11 +11,11 @@ import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; -import com.microsoft.azure.mobile.utils.HashUtils; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; +import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -30,7 +29,6 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.anyVararg; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -123,7 +121,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } @Test - public void sameVersionCodeAndSameHash() throws Exception { + public void sameVersionCode() throws Exception { /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -141,7 +139,6 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getVersion()).thenReturn(6); - when(releaseDetails.getFingerprint()).thenReturn(HashUtils.sha256("com.contoso:1.2.3:6")); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ @@ -162,7 +159,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } @Test - public void moreRecentHashNoReleaseNotesDialog() throws Exception { + public void moreRecentVersionWithoutReleaseNotesDialog() throws Exception { /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -179,8 +176,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getVersion()).thenReturn(6); - when(releaseDetails.getFingerprint()).thenReturn("mock"); + when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ @@ -525,6 +521,6 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify no download scheduled. */ verifyStatic(never()); - AsyncTaskUtils.execute(anyString(), any(AsyncTask.class), anyVararg()); + AsyncTaskUtils.execute(anyString(), any(Updates.DownloadTask.class), Mockito.anyVararg()); } } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCallback.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCallback.java index 2f07bbb699..ee0b3a56d8 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCallback.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/ServiceCallback.java @@ -7,6 +7,8 @@ public interface ServiceCallback { /** * Implement this method to handle successful REST call results. + * + * @param payload HTTP payload. */ void onCallSucceeded(String payload); From 5fb02b42641df2d39d8d147a3fd29eb518336a30 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 13 Feb 2017 18:52:01 -0800 Subject: [PATCH 055/142] Fix warnings and test output based on last CI build output --- .../azure/mobile/updates/Updates.java | 28 ++++++++++++------- .../HttpClientNetworkStateHandlerTest.java | 3 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 1ab0267f64..22c90f25f9 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -198,7 +198,7 @@ public static void setEnabled(boolean enabled) { * * @param loginUrl login base URL. */ - @SuppressWarnings("WeakerAccess") + @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) public static void setLoginUrl(String loginUrl) { getInstance().setInstanceLoginUrl(loginUrl); } @@ -208,7 +208,7 @@ public static void setLoginUrl(String loginUrl) { * * @param apiUrl API base URL. */ - @SuppressWarnings("WeakerAccess") + @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) public static void setApiUrl(String apiUrl) { getInstance().setInstanceApiUrl(apiUrl); } @@ -239,6 +239,20 @@ static int getNotificationId() { return Updates.class.getName().hashCode(); } + @SuppressWarnings("deprecation") + private static Uri getFileUriOnOldDevices(Cursor cursor) { + return Uri.parse("file://" + cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME))); + } + + @SuppressWarnings("deprecation") + private static Notification buildNotification(Notification.Builder builder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return builder.build(); + } else { + return builder.getNotification(); + } + } + @Override protected String getGroupName() { return null; @@ -887,8 +901,7 @@ protected Void doInBackground(Void... params) { Cursor cursor = downloadManager.query(new DownloadManager.Query().setFilterById(mDownloadId)); if (cursor != null) { if (cursor.moveToNext()) { - //noinspection deprecation - uriForDownloadedFile = Uri.parse("file://" + cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME))); + uriForDownloadedFile = getFileUriOnOldDevices(cursor); intent = getInstallIntent(uriForDownloadedFile); installerFound = intent.resolveActivity(mContext.getPackageManager()) != null; } @@ -944,12 +957,7 @@ protected Void doInBackground(Void... params) { .setSmallIcon(icon) .setContentIntent(PendingIntent.getActivities(mContext, 0, new Intent[]{intent}, 0)); Notification notification; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - notification = builder.build(); - } else { - //noinspection deprecation - notification = builder.getNotification(); - } + notification = buildNotification(builder); notification.flags |= Notification.FLAG_AUTO_CANCEL; notifyDownload(mContext, this, notification, uri); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java index 87b707bc1d..f6290897a6 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java @@ -200,8 +200,7 @@ public void run() { try { sleep(200); ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); - } catch (InterruptedException e) { - e.printStackTrace(); + } catch (InterruptedException ignored) { } } }; From 18bf7e627f6666e14cabb9704dda4b43a09e4c3e Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 13 Feb 2017 19:27:11 -0800 Subject: [PATCH 056/142] Implement ignore behavior, add postpone button Ignore remembers last ignored release id and does not prompt for it. Postpone is like closing dialog: next check at app restart. --- .../mobile/updates/ReleaseDetailsTest.java | 23 ++++ .../azure/mobile/updates/ReleaseDetails.java | 29 +++- .../azure/mobile/updates/UpdateConstants.java | 5 + .../azure/mobile/updates/Updates.java | 97 +++++++++----- .../src/main/res/values/strings.xml | 1 + .../mobile/updates/AbstractUpdatesTest.java | 14 +- .../updates/UpdatesBeforeDownloadTests.java | 124 +++++++++++++++++- .../mobile/updates/UpdatesDownloadTests.java | 1 + 8 files changed, 249 insertions(+), 45 deletions(-) diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java index 54a98c6cfb..d498ac06fe 100644 --- a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java +++ b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java @@ -14,6 +14,7 @@ public class ReleaseDetailsTest { @Test public void parse() throws JSONException { String json = "{" + + "id: '42'," + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + @@ -21,15 +22,28 @@ public void parse() throws JSONException { "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); + assertEquals("42", releaseDetails.getId()); assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); } + @Test(expected = JSONException.class) + public void missingId() throws JSONException { + String json = "{" + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + @Test(expected = JSONException.class) public void missingVersion() throws JSONException { String json = "{" + + "id: '42'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + @@ -40,6 +54,7 @@ public void missingVersion() throws JSONException { @Test(expected = JSONException.class) public void invalidVersion() throws JSONException { String json = "{" + + "id: '42'," + "version: true," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + @@ -51,6 +66,7 @@ public void invalidVersion() throws JSONException { @Test(expected = JSONException.class) public void missingShortVersion() throws JSONException { String json = "{" + + "id: '42'," + "version: '14'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + @@ -61,12 +77,14 @@ public void missingShortVersion() throws JSONException { @Test public void missingReleaseNotes() throws JSONException { String json = "{" + + "id: '42'," + "version: '14'," + "short_version: '2.1.5'," + "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); + assertEquals("42", releaseDetails.getId()); assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertNull(releaseDetails.getReleaseNotes()); @@ -76,6 +94,7 @@ public void missingReleaseNotes() throws JSONException { @Test public void nullReleaseNotes() throws JSONException { String json = "{" + + "id: '42'," + "version: '14'," + "release_notes: null," + "short_version: '2.1.5'," + @@ -83,6 +102,7 @@ public void nullReleaseNotes() throws JSONException { "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); + assertEquals("42", releaseDetails.getId()); assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertNull(releaseDetails.getReleaseNotes()); @@ -92,6 +112,7 @@ public void nullReleaseNotes() throws JSONException { @Test(expected = JSONException.class) public void missingDownloadUrl() throws JSONException { String json = "{" + + "id: '42'," + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + @@ -102,6 +123,7 @@ public void missingDownloadUrl() throws JSONException { @Test(expected = JSONException.class) public void missingDownloadUrlScheme() throws JSONException { String json = "{" + + "id: '42'," + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + @@ -113,6 +135,7 @@ public void missingDownloadUrlScheme() throws JSONException { @Test(expected = JSONException.class) public void invalidDownloadUrlScheme() throws JSONException { String json = "{" + + "id: '42'," + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java index a3d1979123..b46ca73463 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java @@ -1,6 +1,8 @@ package com.microsoft.azure.mobile.updates; import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; @@ -10,6 +12,8 @@ */ class ReleaseDetails { + private static final String ID = "id"; + private static final String VERSION = "version"; private static final String SHORT_VERSION = "short_version"; @@ -18,6 +22,11 @@ class ReleaseDetails { private static final String DOWNLOAD_URL = "download_url"; + /** + * ID identifying this unique release. + */ + private String id; + /** * The release's version.
* For iOS: CFBundleVersion from info.plist. @@ -52,6 +61,7 @@ class ReleaseDetails { static ReleaseDetails parse(String json) throws JSONException { JSONObject object = new JSONObject(json); ReleaseDetails releaseDetails = new ReleaseDetails(); + releaseDetails.id = object.getString(ID); try { releaseDetails.version = Integer.parseInt(object.getString(VERSION)); } catch (NumberFormatException e) { @@ -67,13 +77,23 @@ static ReleaseDetails parse(String json) throws JSONException { return releaseDetails; } + /** + * Get the id value. + * + * @return the id value. + */ + @NonNull + String getId() { + return id; + } + /** * Get the version value. * * @return the version value */ int getVersion() { - return this.version; + return version; } /** @@ -81,6 +101,7 @@ int getVersion() { * * @return the shortVersion value */ + @NonNull String getShortVersion() { return shortVersion; } @@ -90,8 +111,9 @@ String getShortVersion() { * * @return the releaseNotes value */ + @Nullable String getReleaseNotes() { - return this.releaseNotes; + return releaseNotes; } /** @@ -99,7 +121,8 @@ String getReleaseNotes() { * * @return the downloadUrl value */ + @NonNull Uri getDownloadUrl() { - return this.downloadUrl; + return downloadUrl; } } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java index c36adeca9b..59696876ea 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -110,6 +110,11 @@ final class UpdateConstants { */ static final String PREFERENCE_KEY_UPDATE_TOKEN = PREFERENCE_PREFIX + EXTRA_UPDATE_TOKEN; + /** + * Preference key to store ignored release id. + */ + static final String PREFERENCE_KEY_IGNORED_RELEASE_ID = PREFERENCE_PREFIX + "ignored_release_id"; + @VisibleForTesting UpdateConstants() { diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 22c90f25f9..893505bfdb 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -37,6 +37,7 @@ import com.microsoft.azure.mobile.utils.NetworkStateHelper; import com.microsoft.azure.mobile.utils.UUIDUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper; +import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import org.json.JSONException; @@ -58,6 +59,7 @@ import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REQUEST_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.updates.UpdateConstants.SERVICE_NAME; @@ -292,7 +294,7 @@ public synchronized void onActivityCreated(Activity activity, Bundle savedInstan /* Clear workflow finished state if launch recreated, to achieve check on "startup". */ if (activity.getClass().getName().equals(mLauncherActivityClassName)) { MobileCenterLog.info(LOG_TAG, "Launcher activity restarted."); - if (StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI) == null) { + if (PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI) == null) { mWorkflowCompleted = false; mBrowserOpened = false; } @@ -321,8 +323,9 @@ public synchronized void setInstanceEnabled(boolean enabled) { mBrowserOpened = false; mWorkflowCompleted = false; cancelPreviousTasks(); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); + PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + PreferencesStorage.remove(PREFERENCE_KEY_IGNORED_RELEASE_ID); } } @@ -359,13 +362,13 @@ private synchronized void cancelPreviousTasks() { mProcessDownloadCompletionTask.cancel(true); mProcessDownloadCompletionTask = null; } - long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); + long downloadId = PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); if (downloadId > 0) { MobileCenterLog.debug(LOG_TAG, "Removing download and notification id=" + downloadId); removeDownload(downloadId); } - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); } /** @@ -384,7 +387,7 @@ private synchronized void resumeUpdateWorkflow() { } /* If we have a download ready but we were in background, pop install UI now. */ - String downloadUri = StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI); + String downloadUri = PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI); if ("".equals(downloadUri)) { /* TODO double check that with download manager. */ @@ -421,7 +424,7 @@ private synchronized void resumeUpdateWorkflow() { } /* Check if we have previous stored the update token. */ - String updateToken = StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN); + String updateToken = PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN); if (updateToken != null) { getLatestReleaseDetails(updateToken); return; @@ -455,7 +458,7 @@ private synchronized void resumeUpdateWorkflow() { MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to login url=" + url); /* Store request id. */ - StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId); + PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId); /* Open browser, remember that whatever the outcome to avoid opening it twice. */ BrowserUtils.openBrowser(url, mForegroundActivity); @@ -502,7 +505,7 @@ private synchronized void completeWorkflow(ProcessDownloadCompletionTask task) { * Reset all variables that matter to restart checking a new release on launcher activity restart. */ private synchronized void completeWorkflow() { - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); mCheckReleaseApiCall = null; mCheckReleaseCallId = null; mUpdateDialog = null; @@ -523,9 +526,9 @@ synchronized void storeUpdateToken(@NonNull String updateToken, @NonNull String MobileCenterLog.debug(LOG_TAG, "Update token received before onStart, keep it in memory."); mBeforeStartUpdateToken = updateToken; mBeforeStartRequestId = requestId; - } else if (requestId.equals(StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID))) { - StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); + } else if (requestId.equals(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID))) { + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); + PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); MobileCenterLog.debug(LOG_TAG, "Stored update token."); cancelPreviousTasks(); getLatestReleaseDetails(updateToken); @@ -586,24 +589,31 @@ private synchronized void handleApiCallSuccess(Object releaseCallId, ReleaseDeta /* Check if state did not change. */ if (mCheckReleaseCallId == releaseCallId) { - /* Check version code is equals or higher and hash is different. */ - MobileCenterLog.debug(LOG_TAG, "Check version code and hash."); - PackageManager packageManager = mContext.getPackageManager(); - try { - PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0); - if (isMoreRecent(packageInfo, releaseDetails)) { + /* Check ignored. */ + String releaseId = releaseDetails.getId(); + if (releaseId.equals(PreferencesStorage.getString(PREFERENCE_KEY_IGNORED_RELEASE_ID))) { + MobileCenterLog.debug(LOG_TAG, "This release is ignored id=" + releaseId); + } else { + + /* Check version code is equals or higher and hash is different. */ + MobileCenterLog.debug(LOG_TAG, "Check version code."); + PackageManager packageManager = mContext.getPackageManager(); + try { + PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0); + if (isMoreRecent(packageInfo, releaseDetails)) { - /* Show update dialog. */ - mReleaseDetails = releaseDetails; - if (mForegroundActivity != null) { - showUpdateDialog(); + /* Show update dialog. */ + mReleaseDetails = releaseDetails; + if (mForegroundActivity != null) { + showUpdateDialog(); + } + return; + } else { + MobileCenterLog.debug(LOG_TAG, "Latest server version is not more recent."); } - return; - } else { - MobileCenterLog.debug(LOG_TAG, "Latest server version is not more recent."); + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); } - } catch (PackageManager.NameNotFoundException e) { - MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); } /* If update dialog was not shown or scheduled, complete workflow. */ @@ -654,6 +664,13 @@ public void onClick(DialogInterface dialog, int which) { }); dialogBuilder.setNegativeButton(R.string.mobile_center_updates_update_dialog_ignore, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ignoreRelease(releaseDetails); + } + }); + dialogBuilder.setNeutralButton(R.string.mobile_center_updates_update_dialog_postpone, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { completeWorkflow(releaseDetails); @@ -670,6 +687,20 @@ public void onCancel(DialogInterface dialog) { mUpdateDialog.show(); } + /** + * Ignore the specified release. It won't be prompted anymore until another release is available. + * + * @param releaseDetails release details. + */ + private synchronized void ignoreRelease(ReleaseDetails releaseDetails) { + if (releaseDetails == mReleaseDetails) { + String id = releaseDetails.getId(); + MobileCenterLog.debug(LOG_TAG, "Ignore release id=" + id); + PreferencesStorage.putString(PREFERENCE_KEY_IGNORED_RELEASE_ID, id); + completeWorkflow(); + } + } + /** * Check state did not change and schedule download of the release. * @@ -696,15 +727,15 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager if (mDownloadTask == task) { /* Delete previous download. */ - long previousDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); + long previousDownloadId = PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); if (previousDownloadId > 0) { MobileCenterLog.debug(LOG_TAG, "Delete previous download id=" + previousDownloadId); downloadManager.remove(previousDownloadId); } /* Store new download identifier. */ - StorageHelper.PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, downloadRequestId); - StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, ""); + PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, downloadRequestId); + PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, ""); } else { /* State changed quickly, cancel download. */ @@ -769,7 +800,7 @@ private synchronized Activity getForegroundActivityWithStateCheck(ProcessDownloa */ private synchronized void notifyDownload(Context context, ProcessDownloadCompletionTask task, Notification notification, String uri) { if (task == mProcessDownloadCompletionTask) { - StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, uri); + PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, uri); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(getNotificationId(), notification); } @@ -881,7 +912,7 @@ protected Void doInBackground(Void... params) { } /* Check intent data is what we expected. */ - long expectedDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); + long expectedDownloadId = PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); if (expectedDownloadId > 0 && expectedDownloadId != mDownloadId) { MobileCenterLog.warn(LOG_TAG, "Ignoring completion for a download we didn't expect, id=" + mDownloadId); return null; diff --git a/sdk/mobile-center-updates/src/main/res/values/strings.xml b/sdk/mobile-center-updates/src/main/res/values/strings.xml index 8f4f903f85..e40ba3f01e 100644 --- a/sdk/mobile-center-updates/src/main/res/values/strings.xml +++ b/sdk/mobile-center-updates/src/main/res/values/strings.xml @@ -6,4 +6,5 @@ No release notes were provided for this build. Ignore Download + Postpone
\ No newline at end of file diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index fa6fc52714..052c474494 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -10,7 +10,7 @@ import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.UUIDUtils; -import com.microsoft.azure.mobile.utils.storage.StorageHelper; +import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import org.junit.Before; import org.junit.Rule; @@ -32,7 +32,7 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class}) +@PrepareForTest({Updates.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class}) public class AbstractUpdatesTest { static final String TEST_HASH = "testapp"; // TODO HashUtils.sha256("com.contoso:1.2.3:6"); @@ -60,8 +60,8 @@ public void setUp() throws Exception { when(MobileCenter.isEnabled()).thenReturn(true); /* First call to com.microsoft.azure.mobile.MobileCenter.isEnabled shall return true, initial state. */ - mockStatic(StorageHelper.PreferencesStorage.class); - when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(true); + mockStatic(PreferencesStorage.class); + when(PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(true); /* Then simulate further changes to state. */ doAnswer(new Answer() { @@ -71,11 +71,11 @@ public Void answer(InvocationOnMock invocation) throws Throwable { /* Whenever the new state is persisted, make further calls return the new state. */ boolean enabled = (Boolean) invocation.getArguments()[1]; - when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(enabled); + when(PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(enabled); return null; } - }).when(StorageHelper.PreferencesStorage.class); - StorageHelper.PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); + }).when(PreferencesStorage.class); + PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); /* Mock package manager. */ when(mContext.getPackageName()).thenReturn("com.contoso"); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java index 5f41c653f0..b7b424a8a5 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java @@ -24,6 +24,7 @@ import java.util.concurrent.Semaphore; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.mockito.Matchers.any; @@ -37,6 +38,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.powermock.api.mockito.PowerMockito.doAnswer; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; @@ -60,7 +62,9 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { }); HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); - when(ReleaseDetails.parse(anyString())).thenReturn(mock(ReleaseDetails.class)); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); Context context = mock(Context.class); when(context.getPackageName()).thenReturn("com.contoso"); PackageManager packageManager = mock(PackageManager.class); @@ -101,7 +105,10 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { }); HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); - when(ReleaseDetails.parse(anyString())).thenReturn(mock(ReleaseDetails.class)); // 0 vs 6 + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getVersion()).thenReturn(5); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); @@ -138,6 +145,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(6); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -176,6 +184,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -229,6 +238,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(releaseDetails.getReleaseNotes()).thenReturn("mock"); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -273,6 +283,7 @@ public void run() { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(releaseDetails.getReleaseNotes()).thenReturn("mock"); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -315,6 +326,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -341,9 +353,72 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } + @Test + public void postponeDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getVersion()).thenReturn(7); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify dialog. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setNeutralButton(eq(R.string.mobile_center_updates_update_dialog_postpone), clickListener.capture()); + verify(mDialog).show(); + + /* Postpone it. */ + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_NEUTRAL); + + /* Verify. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + @Test public void ignoreDialog() throws Exception { + /* Mock ignore storage calls. */ + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn((String) invocation.getArguments()[1]); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.putString(eq(PREFERENCE_KEY_IGNORED_RELEASE_ID), anyString()); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn(null); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.remove(PREFERENCE_KEY_IGNORED_RELEASE_ID); + /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); @@ -357,6 +432,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -381,6 +457,23 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { Updates.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Restart app to check ignore. */ + Updates.unsetInstance(); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify second http call was made but dialog was skipped (e.g. shown only once). */ + verify(httpClient, times(2)).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + verify(mDialog).show(); + verifyStatic(times(2)); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Disable: it will prompt again as we clear storage. */ + Updates.setEnabled(false); + Updates.setEnabled(true); + verify(httpClient, times(3)).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + verify(mDialog, times(2)).show(); } @Test @@ -399,6 +492,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -431,6 +525,26 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { @Test public void disableBeforeIgnoreDialog() throws Exception { + /* Mock ignore storage calls. */ + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn((String) invocation.getArguments()[1]); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.putString(eq(PREFERENCE_KEY_IGNORED_RELEASE_ID), anyString()); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn(null); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.remove(PREFERENCE_KEY_IGNORED_RELEASE_ID); + /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); @@ -444,6 +558,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -471,6 +586,10 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_IGNORED_RELEASE_ID); + verifyStatic(never()); + PreferencesStorage.putString(eq(PREFERENCE_KEY_IGNORED_RELEASE_ID), anyString()); } @Test @@ -490,6 +609,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); mockStatic(AsyncTaskUtils.class); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java index d638433a7a..81bd9641c6 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java @@ -162,6 +162,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(releaseDetails.getDownloadUrl()).thenReturn(mDownloadUrl); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); From a468435aca30e9921ce0d0afd2f6308772c17b1b Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 14 Feb 2017 12:14:05 -0800 Subject: [PATCH 057/142] Rename setLoginUrl to setInstallUrl and related code --- .../sasquatch/activities/MainActivity.java | 2 +- .../azure/mobile/updates/UpdateConstants.java | 10 +++--- .../azure/mobile/updates/Updates.java | 34 +++++++++---------- .../updates/UpdatesBeforeApiSuccessTests.java | 14 ++++---- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index e5780f349e..339aff9981 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -55,7 +55,7 @@ protected void onCreate(Bundle savedInstanceState) { @SuppressWarnings("unchecked") Class updates = (Class) Class.forName("com.microsoft.azure.mobile.updates.Updates"); - updates.getMethod("setLoginUrl", String.class).invoke(null, "http://install.asgard-int.trafficmanager.net"); + updates.getMethod("setInstallUrl", String.class).invoke(null, "http://install.asgard-int.trafficmanager.net"); updates.getMethod("setApiUrl", String.class).invoke(null, "https://asgard-int.trafficmanager.net/api/v0.1"); MobileCenter.start(updates); } catch (Exception e) { diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java index c36adeca9b..25b28ec1fd 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -30,9 +30,9 @@ final class UpdateConstants { static final String EXTRA_REQUEST_ID = "request_id"; /** - * Base URL used to open browser to login. + * Base URL used to open browser to check install and get API token to check latest release. */ - static final String DEFAULT_LOGIN_URL = "https://install.mobile.azure.com"; + static final String DEFAULT_INSTALL_URL = "https://install.mobile.azure.com"; /** * Base URL to call server to check latest release. @@ -40,15 +40,15 @@ final class UpdateConstants { static final String DEFAULT_API_URL = "https://api.mobile.azure.com"; /** - * Login URL path. Contains the app secret variable to replace. + * Update setup URL path. Contains the app secret variable to replace. * Trailing slash needed to avoid redirection that can lose the query string on some servers. */ - static final String LOGIN_PAGE_URL_PATH_FORMAT = "/apps/%s/update-setup/"; + static final String UPDATE_SETUP_PATH_FORMAT = "/apps/%s/update-setup/"; /** * Check latest release API URL path. Contains the app secret variable to replace. */ - static final String CHECK_UPDATE_URL_PATH_FORMAT = "/sdk/apps/%s/releases/latest"; + static final String GET_LATEST_RELEASE_PATH_FORMAT = "/sdk/apps/%s/releases/latest"; /** * API parameter for release hash. diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 22c90f25f9..acc4c6ff8a 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -45,11 +45,10 @@ import static android.content.Context.DOWNLOAD_SERVICE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; -import static com.microsoft.azure.mobile.updates.UpdateConstants.CHECK_UPDATE_URL_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_API_URL; -import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_LOGIN_URL; +import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_INSTALL_URL; +import static com.microsoft.azure.mobile.updates.UpdateConstants.GET_LATEST_RELEASE_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.HEADER_API_TOKEN; -import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; @@ -61,6 +60,7 @@ import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.updates.UpdateConstants.SERVICE_NAME; +import static com.microsoft.azure.mobile.updates.UpdateConstants.UPDATE_SETUP_PATH_FORMAT; /** * Updates service. @@ -74,9 +74,9 @@ public class Updates extends AbstractMobileCenterService { private static Updates sInstance = null; /** - * Current login base URL. + * Current install base URL. */ - private String mLoginUrl = DEFAULT_LOGIN_URL; + private String mInstallUrl = DEFAULT_INSTALL_URL; /** * Current API base URL. @@ -99,7 +99,7 @@ public class Updates extends AbstractMobileCenterService { private Activity mForegroundActivity; /** - * Remember if we already opened browser to login. + * Remember if we already opened browser to check install setup. */ private boolean mBrowserOpened; @@ -196,11 +196,11 @@ public static void setEnabled(boolean enabled) { /** * Change the base URL opened in the browser to get update token from user login information. * - * @param loginUrl login base URL. + * @param installUrl install base URL. */ @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) - public static void setLoginUrl(String loginUrl) { - getInstance().setInstanceLoginUrl(loginUrl); + public static void setInstallUrl(String installUrl) { + getInstance().setInstanceInstallUrl(installUrl); } /** @@ -327,10 +327,10 @@ public synchronized void setInstanceEnabled(boolean enabled) { } /** - * Implements {@link #setLoginUrl(String)}. + * Implements {@link #setInstallUrl(String)}. */ - private synchronized void setInstanceLoginUrl(String loginUrl) { - mLoginUrl = loginUrl; + private synchronized void setInstanceInstallUrl(String installUrl) { + mInstallUrl = installUrl; } /** @@ -427,7 +427,7 @@ private synchronized void resumeUpdateWorkflow() { return; } - /* If not, open browser to login. */ + /* If not, open browser to update setup. */ if (mBrowserOpened) { return; } @@ -446,13 +446,13 @@ private synchronized void resumeUpdateWorkflow() { String requestId = UUIDUtils.randomUUID().toString(); /* Build URL. */ - String url = mLoginUrl; - url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, mAppSecret); + String url = mInstallUrl; + url += String.format(UPDATE_SETUP_PATH_FORMAT, mAppSecret); url += "?" + PARAMETER_RELEASE_HASH + "=" + releaseHash; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId; url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; - MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to login url=" + url); + MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to url=" + url); /* Store request id. */ StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId); @@ -544,7 +544,7 @@ private synchronized void getLatestReleaseDetails(@NonNull String updateToken) { HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(mContext); HttpClient httpClient = new HttpClientNetworkStateHandler(retryer, networkStateHelper); - String url = mApiUrl + String.format(CHECK_UPDATE_URL_PATH_FORMAT, mAppSecret); + String url = mApiUrl + String.format(GET_LATEST_RELEASE_PATH_FORMAT, mAppSecret); Map headers = new HashMap<>(); headers.put(HEADER_API_TOKEN, updateToken); final Object releaseCallId = mCheckReleaseCallId = new Object(); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java index c9ac80cac5..b20a550e66 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java @@ -25,7 +25,6 @@ import java.util.UUID; import java.util.concurrent.Semaphore; -import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REDIRECT_ID; @@ -35,6 +34,7 @@ import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.updates.UpdateConstants.UPDATE_SETUP_PATH_FORMAT; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -101,8 +101,8 @@ public void happyPathUntilHangingCall() throws Exception { Activity activity = mock(Activity.class); Updates.getInstance().onActivityResumed(activity); verifyStatic(); - String url = UpdateConstants.DEFAULT_LOGIN_URL; - url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, "a"); + String url = UpdateConstants.DEFAULT_INSTALL_URL; + url += String.format(UPDATE_SETUP_PATH_FORMAT, "a"); url += "?" + PARAMETER_RELEASE_HASH + "=" + TEST_HASH; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); @@ -205,7 +205,7 @@ public boolean matches(Object argument) { public void setUrls() throws Exception { /* Setup mock. */ - Updates.setLoginUrl("http://mock"); + Updates.setInstallUrl("http://mock"); Updates.setApiUrl("https://mock2"); HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); @@ -219,7 +219,7 @@ public void setUrls() throws Exception { Updates.getInstance().onActivityResumed(activity); verifyStatic(); String url = "http://mock"; - url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, "a"); + url += String.format(UPDATE_SETUP_PATH_FORMAT, "a"); url += "?" + PARAMETER_RELEASE_HASH + "=" + TEST_HASH; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); @@ -275,8 +275,8 @@ public void disableBeforeStoreToken() { Activity activity = mock(Activity.class); Updates.getInstance().onActivityResumed(activity); verifyStatic(); - String url = UpdateConstants.DEFAULT_LOGIN_URL; - url += String.format(LOGIN_PAGE_URL_PATH_FORMAT, "a"); + String url = UpdateConstants.DEFAULT_INSTALL_URL; + url += String.format(UPDATE_SETUP_PATH_FORMAT, "a"); url += "?" + PARAMETER_RELEASE_HASH + "=" + TEST_HASH; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); From e45c729c7a60fc43f419de9766d02376203cf8cf Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 14 Feb 2017 18:33:16 -0800 Subject: [PATCH 058/142] Updates are now inactive if installed from Store --- .../azure/mobile/updates/InstallerUtils.java | 54 ++++++++++++ .../azure/mobile/updates/Updates.java | 7 ++ .../mobile/updates/AbstractUpdatesTest.java | 3 +- .../mobile/updates/InstallerUtilsTest.java | 86 +++++++++++++++++++ .../updates/UpdatesBeforeApiSuccessTests.java | 26 ++++++ 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/InstallerUtilsTest.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java new file mode 100644 index 0000000000..2e725e7508 --- /dev/null +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java @@ -0,0 +1,54 @@ +package com.microsoft.azure.mobile.updates; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; + +import com.microsoft.azure.mobile.utils.MobileCenterLog; + +import java.util.HashSet; +import java.util.Set; + +/** + * Installer utils. + */ +class InstallerUtils { + + /** + * Installer package names that are not app stores. + */ + private static final Set LOCAL_STORES = new HashSet<>(); + + /** + * Used to cache the result of {@link #isInstalledFromAppStore(String, Context)}, null until first call. + */ + private static Boolean sInstalledFromAppStore; + + /** Populate local stores. */ + static { + LOCAL_STORES.add("adb"); + LOCAL_STORES.add("com.google.android.packageinstaller"); + } + + @VisibleForTesting + InstallerUtils() { + + /* Hide constructor in utils pattern. */ + } + + /** + * Check if this installation was made via an application store. + * + * @param logTag log tag for debug. + * @param context any context. + * @return true if the application was installed from an app store, false if it was installed via adb or via unknown source. + */ + static synchronized boolean isInstalledFromAppStore(@NonNull String logTag, @NonNull Context context) { + if (sInstalledFromAppStore == null) { + String installer = context.getPackageManager().getInstallerPackageName(context.getPackageName()); + MobileCenterLog.debug(logTag, "InstallerPackageName=" + installer); + sInstalledFromAppStore = installer != null && !LOCAL_STORES.contains(installer); + } + return sInstalledFromAppStore; + } +} diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 22c90f25f9..7e70e1c165 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -374,6 +374,13 @@ private synchronized void cancelPreviousTasks() { private synchronized void resumeUpdateWorkflow() { if (mForegroundActivity != null && !mWorkflowCompleted && isInstanceEnabled()) { + /* Don't go any further if the app was installed from an app store. */ + if (InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)) { + MobileCenterLog.info(LOG_TAG, "Not checking in app updates as installed from a store."); + mWorkflowCompleted = true; + return; + } + /* If we received the update token before Mobile Center was started/enabled, process it now. */ if (mBeforeStartUpdateToken != null) { MobileCenterLog.debug(LOG_TAG, "Processing update token we kept in memory before onStarted"); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index fa6fc52714..e8a1c1ef40 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -32,7 +32,7 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class}) +@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, InstallerUtils.class}) public class AbstractUpdatesTest { static final String TEST_HASH = "testapp"; // TODO HashUtils.sha256("com.contoso:1.2.3:6"); @@ -97,6 +97,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { mockStatic(UUIDUtils.class); mockStatic(ReleaseDetails.class); mockStatic(TextUtils.class); + mockStatic(InstallerUtils.class); when(TextUtils.isEmpty(any(CharSequence.class))).thenAnswer(new Answer() { @Override diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/InstallerUtilsTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/InstallerUtilsTest.java new file mode 100644 index 0000000000..8a731d07e7 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/InstallerUtilsTest.java @@ -0,0 +1,86 @@ +package com.microsoft.azure.mobile.updates; + +import android.content.Context; +import android.content.pm.PackageManager; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +public class InstallerUtilsTest { + + @Mock + private Context mContext; + + @Mock + private PackageManager mPackageManager; + + @Before + public void setUp() { + when(mContext.getPackageManager()).thenReturn(mPackageManager); + } + + @After + public void tearDown() { + + /* Reset cache. */ + Whitebox.setInternalState(InstallerUtils.class, "sInstalledFromAppStore", (Boolean) null); + } + + @Test + public void init() { + assertNotNull(new InstallerUtils()); + } + + @Test + public void nullInstallerIsNotStore() { + assertFalse(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + + /* Check cache. */ + assertFalse(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + verify(mPackageManager).getInstallerPackageName(anyString()); + } + + @Test + public void appStore() { + when(mPackageManager.getInstallerPackageName(anyString())).thenReturn("com.android.vending"); + assertTrue(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + + /* Check cache. */ + assertTrue(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + verify(mPackageManager).getInstallerPackageName(anyString()); + } + + @Test + public void adbIsNotStore() { + when(mPackageManager.getInstallerPackageName(anyString())).thenReturn("adb"); + assertFalse(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + + /* Check cache. */ + assertFalse(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + verify(mPackageManager).getInstallerPackageName(anyString()); + } + + @Test + public void localInstallerIsNotStore() { + when(mPackageManager.getInstallerPackageName(anyString())).thenReturn("com.google.android.packageinstaller"); + assertFalse(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + + /* Check cache. */ + assertFalse(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + verify(mPackageManager).getInstallerPackageName(anyString()); + } +} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java index c9ac80cac5..10ccd3b58a 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java @@ -55,6 +55,32 @@ */ public class UpdatesBeforeApiSuccessTests extends AbstractUpdatesTest { + @Test + public void doNothingIfInstallComesFromStore() throws Exception { + + /* Mock from store. */ + when(InstallerUtils.isInstalledFromAppStore(anyString(), any(Context.class))).thenReturn(true); + + /* Check browser not opened. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verifyStatic(never()); + BrowserUtils.openBrowser(anyString(), any(Activity.class)); + + /* + * Even if we had a token on a previous install that was not from store + * (if package name and signature matches an APK on Google Play you can upgrade to + * Google Play version without losing data. + */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + Updates.unsetInstance(); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient, never()).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + @Test public void storeTokenBeforeStart() throws Exception { From 5f48e19a20bea3287aa8e4e04dc1add07f963fa1 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 15 Feb 2017 16:36:36 -0800 Subject: [PATCH 059/142] Refactoring based on pr comments --- .../azure/mobile/analytics/AnalyticsTest.java | 4 +- .../mobile/updates/BrowserUtilsTest.java | 120 +++++------------- .../azure/mobile/updates/Updates.java | 17 +-- .../mobile/updates/AbstractUpdatesTest.java | 4 +- ....java => UpdatesBeforeApiSuccessTest.java} | 2 +- ...ts.java => UpdatesBeforeDownloadTest.java} | 2 +- .../azure/mobile/ingestion/IngestionHttp.java | 18 +-- .../azure/mobile/MobileCenterTest.java | 36 +++--- .../HttpClientNetworkStateHandlerTest.java | 2 +- .../mobile/http/HttpClientRetryerTest.java | 4 +- .../mobile/ingestion/IngestionHttpTest.java | 9 +- 11 files changed, 86 insertions(+), 132 deletions(-) rename sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/{UpdatesBeforeApiSuccessTests.java => UpdatesBeforeApiSuccessTest.java} (99%) rename sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/{UpdatesBeforeDownloadTests.java => UpdatesBeforeDownloadTest.java} (99%) diff --git a/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java b/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java index d1f985da82..321dac6e43 100644 --- a/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java +++ b/sdk/mobile-center-analytics/src/test/java/com/microsoft/azure/mobile/analytics/AnalyticsTest.java @@ -16,6 +16,7 @@ import com.microsoft.azure.mobile.ingestion.models.Log; import com.microsoft.azure.mobile.ingestion.models.json.LogFactory; import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.PrefStorageConstants; import com.microsoft.azure.mobile.utils.storage.StorageHelper; import junit.framework.Assert; @@ -35,7 +36,6 @@ import java.util.Map; import java.util.UUID; -import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -60,7 +60,7 @@ @PrepareForTest({SystemClock.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class}) public class AnalyticsTest { - private static final String ANALYTICS_ENABLED_KEY = KEY_ENABLED + "_Analytics"; + private static final String ANALYTICS_ENABLED_KEY = PrefStorageConstants.KEY_ENABLED + "_" + Analytics.getInstance().getServiceName(); @Before public void setUp() { diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java index efd5633b0d..efa38b55b8 100644 --- a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java +++ b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java @@ -31,6 +31,17 @@ @SuppressWarnings("WrongConstant") public class BrowserUtilsTest { + private static final String TEST_URL = "https://www.contoso.com?a=b"; + + private static final ArgumentMatcher CHROME_MATCHER = new ArgumentMatcher() { + + @Override + public boolean matches(Object o) { + Intent intent = (Intent) o; + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + TEST_URL).equals(intent.getData()); + } + }; + @Test public void init() { assertNotNull(new BrowserUtils()); @@ -39,16 +50,8 @@ public void init() { @Test public void chrome() throws Exception { Activity activity = mock(Activity.class); - final String url = "https://www.contoso.com?a=b"; - BrowserUtils.openBrowser(url, activity); - verify(activity).startActivity(argThat(new ArgumentMatcher() { - - @Override - public boolean matches(Object o) { - Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); - } - })); + BrowserUtils.openBrowser(TEST_URL, activity); + verify(activity).startActivity(argThat(CHROME_MATCHER)); verifyNoMoreInteractions(activity); } @@ -63,16 +66,8 @@ public void noBrowserFound() throws Exception { when(packageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(Collections.emptyList()); /* Open Chrome then abort. */ - final String url = "https://www.contoso.com?a=b"; - BrowserUtils.openBrowser(url, activity); - verify(activity).startActivity(argThat(new ArgumentMatcher() { - - @Override - public boolean matches(Object o) { - Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); - } - })); + BrowserUtils.openBrowser(TEST_URL, activity); + verify(activity).startActivity(argThat(CHROME_MATCHER)); /* Verify no more call to startActivity. */ verify(activity).startActivity(any(Intent.class)); @@ -82,17 +77,8 @@ public boolean matches(Object o) { public void onlySystemBrowserNoDefaultAsNull() throws Exception { /* Mock no browser. */ - final String url = "https://www.contoso.com?a=b"; Activity activity = mock(Activity.class); - ArgumentMatcher chromeMatcher = new ArgumentMatcher() { - - @Override - public boolean matches(Object o) { - Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); - } - }; - doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(CHROME_MATCHER)); PackageManager packageManager = mock(PackageManager.class); when(activity.getPackageManager()).thenReturn(packageManager); when(packageManager.resolveActivity(any(Intent.class), eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(null); @@ -106,15 +92,15 @@ public boolean matches(Object o) { } /* Open Chrome then abort. */ - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(TEST_URL, activity); InOrder order = inOrder(activity); - order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(CHROME_MATCHER)); order.verify(activity).startActivity(argThat(new ArgumentMatcher() { @Override public boolean matches(Object o) { Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(TEST_URL).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); } })); order.verifyNoMoreInteractions(); @@ -124,17 +110,8 @@ public boolean matches(Object o) { public void onlySystemBrowserNoDefaultAsPicker() throws Exception { /* Mock no browser. */ - final String url = "https://www.contoso.com?a=b"; Activity activity = mock(Activity.class); - ArgumentMatcher chromeMatcher = new ArgumentMatcher() { - - @Override - public boolean matches(Object o) { - Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); - } - }; - doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(CHROME_MATCHER)); PackageManager packageManager = mock(PackageManager.class); when(activity.getPackageManager()).thenReturn(packageManager); { @@ -155,15 +132,15 @@ public boolean matches(Object o) { } /* Open Chrome then abort. */ - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(TEST_URL, activity); InOrder order = inOrder(activity); - order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(CHROME_MATCHER)); order.verify(activity).startActivity(argThat(new ArgumentMatcher() { @Override public boolean matches(Object o) { Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(TEST_URL).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); } })); order.verifyNoMoreInteractions(); @@ -173,17 +150,8 @@ public boolean matches(Object o) { public void onlySystemBrowserAndIsDefault() throws Exception { /* Mock no browser. */ - final String url = "https://www.contoso.com?a=b"; Activity activity = mock(Activity.class); - ArgumentMatcher chromeMatcher = new ArgumentMatcher() { - - @Override - public boolean matches(Object o) { - Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); - } - }; - doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(BrowserUtilsTest.CHROME_MATCHER)); PackageManager packageManager = mock(PackageManager.class); when(activity.getPackageManager()).thenReturn(packageManager); { @@ -204,15 +172,15 @@ public boolean matches(Object o) { } /* Open Chrome then abort. */ - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(TEST_URL, activity); InOrder order = inOrder(activity); - order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(CHROME_MATCHER)); order.verify(activity).startActivity(argThat(new ArgumentMatcher() { @Override public boolean matches(Object o) { Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(TEST_URL).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); } })); order.verifyNoMoreInteractions(); @@ -222,17 +190,8 @@ public boolean matches(Object o) { public void twoBrowsersAndNoDefault() throws Exception { /* Mock no browser. */ - final String url = "https://www.contoso.com?a=b"; Activity activity = mock(Activity.class); - ArgumentMatcher chromeMatcher = new ArgumentMatcher() { - - @Override - public boolean matches(Object o) { - Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); - } - }; - doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(CHROME_MATCHER)); PackageManager packageManager = mock(PackageManager.class); when(activity.getPackageManager()).thenReturn(packageManager); { @@ -258,15 +217,15 @@ public boolean matches(Object o) { } /* Open Chrome then abort. */ - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(TEST_URL, activity); InOrder order = inOrder(activity); - order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(CHROME_MATCHER)); order.verify(activity).startActivity(argThat(new ArgumentMatcher() { @Override public boolean matches(Object o) { Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(TEST_URL).equals(intent.getData()) && intent.getComponent().getClassName().equals("browser"); } })); order.verifyNoMoreInteractions(); @@ -276,17 +235,8 @@ public boolean matches(Object o) { public void secondBrowserIsDefault() throws Exception { /* Mock no browser. */ - final String url = "https://www.contoso.com?a=b"; Activity activity = mock(Activity.class); - ArgumentMatcher chromeMatcher = new ArgumentMatcher() { - - @Override - public boolean matches(Object o) { - Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(GOOGLE_CHROME_URL_SCHEME + url).equals(intent.getData()); - } - }; - doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(chromeMatcher)); + doThrow(new ActivityNotFoundException()).when(activity).startActivity(argThat(CHROME_MATCHER)); PackageManager packageManager = mock(PackageManager.class); when(activity.getPackageManager()).thenReturn(packageManager); { @@ -312,15 +262,15 @@ public boolean matches(Object o) { } /* Open Chrome then abort. */ - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(TEST_URL, activity); InOrder order = inOrder(activity); - order.verify(activity).startActivity(argThat(chromeMatcher)); + order.verify(activity).startActivity(argThat(CHROME_MATCHER)); order.verify(activity).startActivity(argThat(new ArgumentMatcher() { @Override public boolean matches(Object o) { Intent intent = (Intent) o; - return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(url).equals(intent.getData()) && intent.getComponent().getClassName().equals("firefox"); + return Intent.ACTION_VIEW.equals(intent.getAction()) && Uri.parse(TEST_URL).equals(intent.getData()) && intent.getComponent().getClassName().equals("firefox"); } })); order.verifyNoMoreInteractions(); diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 22c90f25f9..3e726cdfe0 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -99,9 +99,9 @@ public class Updates extends AbstractMobileCenterService { private Activity mForegroundActivity; /** - * Remember if we already opened browser to login. + * Remember if we already tried to open the browser to update setup. */ - private boolean mBrowserOpened; + private boolean mBrowserOpenedOrAborted; /** * In memory token if we receive deep link intent before onStart. @@ -294,7 +294,7 @@ public synchronized void onActivityCreated(Activity activity, Bundle savedInstan MobileCenterLog.info(LOG_TAG, "Launcher activity restarted."); if (StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI) == null) { mWorkflowCompleted = false; - mBrowserOpened = false; + mBrowserOpenedOrAborted = false; } } } @@ -318,7 +318,7 @@ public synchronized void setInstanceEnabled(boolean enabled) { } else { /* Clean all state on disabling, cancel everything. */ - mBrowserOpened = false; + mBrowserOpenedOrAborted = false; mWorkflowCompleted = false; cancelPreviousTasks(); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); @@ -390,7 +390,7 @@ private synchronized void resumeUpdateWorkflow() { /* TODO double check that with download manager. */ MobileCenterLog.verbose(LOG_TAG, "Download is still in progress..."); return; - } else if (downloadUri != null) + } else if (downloadUri != null) { try { /* FIXME this can cause strict mode violation. */ @@ -407,6 +407,7 @@ private synchronized void resumeUpdateWorkflow() { MobileCenterLog.warn(LOG_TAG, "Download uri was invalid", e); cancelPreviousTasks(); } + } /* If we were waiting after API call to resume app to show the dialog do it now. */ if (mReleaseDetails != null) { @@ -428,7 +429,7 @@ private synchronized void resumeUpdateWorkflow() { } /* If not, open browser to login. */ - if (mBrowserOpened) { + if (mBrowserOpenedOrAborted) { return; } @@ -438,7 +439,7 @@ private synchronized void resumeUpdateWorkflow() { releaseHash = computeHash(mContext); } catch (PackageManager.NameNotFoundException e) { MobileCenterLog.error(LOG_TAG, "Could not get package info", e); - mBrowserOpened = true; + mBrowserOpenedOrAborted = true; return; } @@ -459,7 +460,7 @@ private synchronized void resumeUpdateWorkflow() { /* Open browser, remember that whatever the outcome to avoid opening it twice. */ BrowserUtils.openBrowser(url, mForegroundActivity); - mBrowserOpened = true; + mBrowserOpenedOrAborted = true; } } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index fa6fc52714..b4b1b21dcb 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -36,7 +36,9 @@ public class AbstractUpdatesTest { static final String TEST_HASH = "testapp"; // TODO HashUtils.sha256("com.contoso:1.2.3:6"); + private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; + @Rule public PowerMockRule mPowerMockRule = new PowerMockRule(); @@ -53,6 +55,7 @@ public class AbstractUpdatesTest { AlertDialog mDialog; @Before + @SuppressWarnings("ResourceType") public void setUp() throws Exception { Updates.unsetInstance(); mockStatic(MobileCenterLog.class); @@ -89,7 +92,6 @@ public Void answer(InvocationOnMock invocation) throws Throwable { ApplicationInfo applicationInfo = mock(ApplicationInfo.class); Whitebox.setInternalState(applicationInfo, "labelRes", 1337); Whitebox.setInternalState(packageInfo, "applicationInfo", applicationInfo); - //noinspection ResourceType when(mContext.getString(1337)).thenReturn(TEST_HASH); /* Mock some statics. */ diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java similarity index 99% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java rename to sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java index c9ac80cac5..770e63bd4d 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java @@ -53,7 +53,7 @@ /** * Cover scenarios that are happening before we see an API call success for latest release. */ -public class UpdatesBeforeApiSuccessTests extends AbstractUpdatesTest { +public class UpdatesBeforeApiSuccessTest extends AbstractUpdatesTest { @Test public void storeTokenBeforeStart() throws Exception { diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java similarity index 99% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java rename to sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java index 5f41c653f0..74e3886991 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java @@ -41,7 +41,7 @@ import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; -public class UpdatesBeforeDownloadTests extends AbstractUpdatesTest { +public class UpdatesBeforeDownloadTest extends AbstractUpdatesTest { @Test public void failsToCompareVersion() throws Exception { diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java index 1c0d5e44c5..217a85a8ed 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java @@ -2,6 +2,7 @@ import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; import com.microsoft.azure.mobile.http.DefaultHttpClient; import com.microsoft.azure.mobile.http.HttpClient; @@ -27,21 +28,20 @@ public class IngestionHttp implements Ingestion { - /** - * Default base URL. - */ - private static final String DEFAULT_BASE_URL = "https://in.mobile.azure.com"; - /** * API Path. */ - private static final String API_PATH = "/logs?api_version=1.0.0-preview20160914"; - + @VisibleForTesting + static final String API_PATH = "/logs?api_version=1.0.0-preview20160914"; /** * Installation identifier HTTP Header. */ - private static final String INSTALL_ID = "Install-ID"; - + @VisibleForTesting + static final String INSTALL_ID = "Install-ID"; + /** + * Default base URL. + */ + private static final String DEFAULT_BASE_URL = "https://in.mobile.azure.com"; /** * Log serializer. */ diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index 265eceff43..67ba64ee7b 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -164,7 +164,7 @@ public void useDummyServiceTest() { DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @@ -180,7 +180,7 @@ public void useDummyServiceTestSplitCall() { DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @@ -194,7 +194,8 @@ public void configureAndStartTwiceTest() { DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); + verify(service, never()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET + "a"), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @@ -209,7 +210,8 @@ public void configureTwiceTest() { DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); + verify(service, never()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET + "a"), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @@ -223,13 +225,13 @@ public void startTwoServicesTest() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -244,13 +246,13 @@ public void startTwoServicesSplit() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -266,13 +268,13 @@ public void startTwoServicesSplitEvenMore() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -286,13 +288,13 @@ public void startTwoServicesWithSomeInvalidReferences() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -308,13 +310,13 @@ public void startTwoServicesWithSomeInvalidReferencesSplit() { { assertTrue(MobileCenter.getInstance().getServices().contains(DummyService.getInstance())); verify(DummyService.getInstance()).getLogFactories(); - verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(DummyService.getInstance()); } { assertTrue(MobileCenter.getInstance().getServices().contains(AnotherDummyService.getInstance())); verify(AnotherDummyService.getInstance()).getLogFactories(); - verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(AnotherDummyService.getInstance()).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } } @@ -331,7 +333,7 @@ public void startServiceTwice() { DummyService service = DummyService.getInstance(); assertTrue(MobileCenter.getInstance().getServices().contains(service)); verify(service).getLogFactories(); - verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); /* Start twice, this call is ignored. */ @@ -340,7 +342,7 @@ public void startServiceTwice() { /* Verify that single service has been loaded and configured (only once interaction). */ assertEquals(1, MobileCenter.getInstance().getServices().size()); verify(service).getLogFactories(); - verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(service).onStarted(any(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); } @@ -554,7 +556,7 @@ public void duplicateServiceTest() { MobileCenter.start(application, DUMMY_APP_SECRET, DummyService.class, DummyService.class); /* Verify that only one service has been loaded and configured */ - verify(DummyService.getInstance()).onStarted(notNull(Context.class), eq(DUMMY_APP_SECRET), notNull(Channel.class)); + verify(DummyService.getInstance()).onStarted(notNull(Context.class), eq(DUMMY_APP_SECRET), any(Channel.class)); assertEquals(1, MobileCenter.getInstance().getServices().size()); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java index f6290897a6..8635727e0f 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java @@ -199,7 +199,7 @@ public ServiceCall answer(final InvocationOnMock invocationOnMock) throws Throwa public void run() { try { sleep(200); - ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded("mockPayload"); } catch (InterruptedException ignored) { } } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java index caf4a0659d..d6cefd0f30 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java @@ -56,7 +56,7 @@ public void success() { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded("mockSuccessPayload"); return call; } }).when(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); @@ -82,7 +82,7 @@ public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { - ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded("mockSuccessPayload"); return mock(ServiceCall.class); } }).when(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java index e7b6024564..f74d879719 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java @@ -92,8 +92,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify call to http client. */ HashMap expectedHeaders = new HashMap<>(); expectedHeaders.put(DefaultHttpClient.APP_SECRET, appSecret); - expectedHeaders.put("Install-ID", installId.toString()); - verify(httpClient).callAsync(eq("http://mock/logs?api_version=1.0.0-preview20160914"), eq(METHOD_POST), eq(expectedHeaders), notNull(HttpClient.CallTemplate.class), eq(serviceCallback)); + expectedHeaders.put(IngestionHttp.INSTALL_ID, installId.toString()); + verify(httpClient).callAsync(eq("http://mock/" + IngestionHttp.API_PATH), eq(METHOD_POST), eq(expectedHeaders), notNull(HttpClient.CallTemplate.class), eq(serviceCallback)); assertNotNull(callTemplate.get()); assertEquals("mockPayload", callTemplate.get().buildRequestBody()); @@ -151,15 +151,14 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify call to http client. */ HashMap expectedHeaders = new HashMap<>(); expectedHeaders.put(DefaultHttpClient.APP_SECRET, appSecret); - expectedHeaders.put("Install-ID", installId.toString()); + expectedHeaders.put(IngestionHttp.INSTALL_ID, installId.toString()); verify(httpClient).callAsync(eq("http://mock/logs?api_version=1.0.0-preview20160914"), eq(METHOD_POST), eq(expectedHeaders), notNull(HttpClient.CallTemplate.class), eq(serviceCallback)); assertNotNull(callTemplate.get()); try { callTemplate.get().buildRequestBody(); Assert.fail("Expected json exception"); - } catch (JSONException e) { - e.printStackTrace(); + } catch (JSONException ignored) { } /* Verify toffset manipulation. */ From 3d6f5cebe6f6e129703617f5293c15112f289671 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 15 Feb 2017 17:00:43 -0800 Subject: [PATCH 060/142] Fix unit tests after last refactoring --- .../azure/mobile/http/HttpClientNetworkStateHandlerTest.java | 2 +- .../microsoft/azure/mobile/http/HttpClientRetryerTest.java | 4 ++-- .../microsoft/azure/mobile/ingestion/IngestionHttpTest.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java index 8635727e0f..0caed18fd6 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java @@ -263,7 +263,7 @@ public ServiceCall answer(final InvocationOnMock invocationOnMock) throws Throwa public void run() { try { sleep(200); - ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded("mockPayload"); } catch (InterruptedException e) { e.printStackTrace(); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java index d6cefd0f30..b76740d7b3 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientRetryerTest.java @@ -62,7 +62,7 @@ public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { }).when(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); HttpClientRetryer retryer = new HttpClientRetryer(httpClient); retryer.callAsync(null, null, null, null, callback); - verify(callback).onCallSucceeded(""); + verify(callback).onCallSucceeded("mockSuccessPayload"); verifyNoMoreInteractions(callback); verifyNoMoreInteractions(call); } @@ -92,7 +92,7 @@ public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { retryer.callAsync(null, null, null, null, callback); verifyDelay(handler, 0); verifyNoMoreInteractions(handler); - verify(callback).onCallSucceeded(""); + verify(callback).onCallSucceeded("mockSuccessPayload"); verifyNoMoreInteractions(callback); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java index f74d879719..5bdb49309e 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java @@ -93,7 +93,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap expectedHeaders = new HashMap<>(); expectedHeaders.put(DefaultHttpClient.APP_SECRET, appSecret); expectedHeaders.put(IngestionHttp.INSTALL_ID, installId.toString()); - verify(httpClient).callAsync(eq("http://mock/" + IngestionHttp.API_PATH), eq(METHOD_POST), eq(expectedHeaders), notNull(HttpClient.CallTemplate.class), eq(serviceCallback)); + verify(httpClient).callAsync(eq("http://mock" + IngestionHttp.API_PATH), eq(METHOD_POST), eq(expectedHeaders), notNull(HttpClient.CallTemplate.class), eq(serviceCallback)); assertNotNull(callTemplate.get()); assertEquals("mockPayload", callTemplate.get().buildRequestBody()); From 14b0259b6bb0e54b2de75dd362d64ce3a61102f9 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 15 Feb 2017 17:55:17 -0800 Subject: [PATCH 061/142] Allow downloadId = 0 from download manager + refactoring --- .../azure/mobile/updates/UpdateConstants.java | 5 +++++ .../azure/mobile/updates/Updates.java | 22 ++++++++++++++----- .../mobile/updates/AbstractUpdatesTest.java | 19 ++++++++++------ .../mobile/updates/UpdatesDownloadTests.java | 5 +++-- .../azure/mobile/ingestion/IngestionHttp.java | 3 +++ 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java index c36adeca9b..91323b3796 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -80,6 +80,11 @@ final class UpdateConstants { */ static final String HEADER_API_TOKEN = "x-api-token"; + /** + * Invalid download identifier. + */ + static final long INVALID_DOWNLOAD_IDENTIFIER = -1; + /** * Base key for stored preferences. */ diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 3e726cdfe0..268a5021c3 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -49,6 +49,7 @@ import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_API_URL; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_LOGIN_URL; import static com.microsoft.azure.mobile.updates.UpdateConstants.HEADER_API_TOKEN; +import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; import static com.microsoft.azure.mobile.updates.UpdateConstants.LOGIN_PAGE_URL_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; @@ -253,6 +254,15 @@ private static Notification buildNotification(Notification.Builder builder) { } } + /** + * Get download identifier from storage. + * + * @return download identifier or negative value if not found. + */ + private static long getStoredDownloadId() { + return StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); + } + @Override protected String getGroupName() { return null; @@ -359,8 +369,8 @@ private synchronized void cancelPreviousTasks() { mProcessDownloadCompletionTask.cancel(true); mProcessDownloadCompletionTask = null; } - long downloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); - if (downloadId > 0) { + long downloadId = getStoredDownloadId(); + if (downloadId >= 0) { MobileCenterLog.debug(LOG_TAG, "Removing download and notification id=" + downloadId); removeDownload(downloadId); } @@ -697,8 +707,8 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager if (mDownloadTask == task) { /* Delete previous download. */ - long previousDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); - if (previousDownloadId > 0) { + long previousDownloadId = getStoredDownloadId(); + if (previousDownloadId >= 0) { MobileCenterLog.debug(LOG_TAG, "Delete previous download id=" + previousDownloadId); downloadManager.remove(previousDownloadId); } @@ -882,8 +892,8 @@ protected Void doInBackground(Void... params) { } /* Check intent data is what we expected. */ - long expectedDownloadId = StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID); - if (expectedDownloadId > 0 && expectedDownloadId != mDownloadId) { + long expectedDownloadId = getStoredDownloadId(); + if (expectedDownloadId >= 0 && expectedDownloadId != mDownloadId) { MobileCenterLog.warn(LOG_TAG, "Ignoring completion for a download we didn't expect, id=" + mDownloadId); return null; } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index b4b1b21dcb..fa93cc169b 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -10,7 +10,7 @@ import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.UUIDUtils; -import com.microsoft.azure.mobile.utils.storage.StorageHelper; +import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import org.junit.Before; import org.junit.Rule; @@ -21,6 +21,8 @@ import org.powermock.modules.junit4.rule.PowerMockRule; import org.powermock.reflect.Whitebox; +import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; @@ -32,7 +34,7 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Updates.class, StorageHelper.PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class}) +@PrepareForTest({Updates.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class}) public class AbstractUpdatesTest { static final String TEST_HASH = "testapp"; // TODO HashUtils.sha256("com.contoso:1.2.3:6"); @@ -63,8 +65,8 @@ public void setUp() throws Exception { when(MobileCenter.isEnabled()).thenReturn(true); /* First call to com.microsoft.azure.mobile.MobileCenter.isEnabled shall return true, initial state. */ - mockStatic(StorageHelper.PreferencesStorage.class); - when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(true); + mockStatic(PreferencesStorage.class); + when(PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(true); /* Then simulate further changes to state. */ doAnswer(new Answer() { @@ -74,11 +76,14 @@ public Void answer(InvocationOnMock invocation) throws Throwable { /* Whenever the new state is persisted, make further calls return the new state. */ boolean enabled = (Boolean) invocation.getArguments()[1]; - when(StorageHelper.PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(enabled); + when(PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(enabled); return null; } - }).when(StorageHelper.PreferencesStorage.class); - StorageHelper.PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); + }).when(PreferencesStorage.class); + PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); + + /* Default download id when not found. */ + when(PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER)).thenReturn(INVALID_DOWNLOAD_IDENTIFIER); /* Mock package manager. */ when(mContext.getPackageName()).thenReturn("com.contoso"); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java index d638433a7a..75d820960c 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java @@ -43,6 +43,7 @@ import static android.app.DownloadManager.EXTRA_DOWNLOAD_ID; import static android.content.Context.NOTIFICATION_SERVICE; +import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; @@ -116,7 +117,7 @@ public void setUpDownload() throws Exception { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(StorageHelper.PreferencesStorage.getLong(invocation.getArguments()[0].toString())).thenReturn((Long) invocation.getArguments()[1]); + when(StorageHelper.PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn((Long) invocation.getArguments()[1]); return null; } }).when(StorageHelper.PreferencesStorage.class); @@ -125,7 +126,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(StorageHelper.PreferencesStorage.getLong(invocation.getArguments()[0].toString())).thenReturn(0L); + when(StorageHelper.PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn(INVALID_DOWNLOAD_IDENTIFIER); return null; } }).when(StorageHelper.PreferencesStorage.class); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java index 217a85a8ed..9978f7c45d 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java @@ -33,15 +33,18 @@ public class IngestionHttp implements Ingestion { */ @VisibleForTesting static final String API_PATH = "/logs?api_version=1.0.0-preview20160914"; + /** * Installation identifier HTTP Header. */ @VisibleForTesting static final String INSTALL_ID = "Install-ID"; + /** * Default base URL. */ private static final String DEFAULT_BASE_URL = "https://in.mobile.azure.com"; + /** * Log serializer. */ From 40b32a08c098648ae13862e3b9290cbd70454f16 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 15 Feb 2017 18:15:17 -0800 Subject: [PATCH 062/142] Rename a test class --- .../{UpdatesDownloadTests.java => UpdatesDownloadTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/{UpdatesDownloadTests.java => UpdatesDownloadTest.java} (99%) diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java similarity index 99% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java rename to sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java index 75d820960c..54b48598d5 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTests.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java @@ -70,7 +70,7 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @PrepareForTest(AsyncTaskUtils.class) -public class UpdatesDownloadTests extends AbstractUpdatesTest { +public class UpdatesDownloadTest extends AbstractUpdatesTest { private static final long DOWNLOAD_ID = 42; From 691dc5a68665e8bab8e0a5dc9d20cde67a409957 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 15 Feb 2017 18:19:31 -0800 Subject: [PATCH 063/142] Add some verifications for postpone/cancel tests --- .../mobile/updates/UpdatesBeforeDownloadTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java index 3c2ab152ae..41aac84dbe 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java @@ -351,6 +351,13 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { Updates.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Restart should check release and show dialog again. */ + Updates.unsetInstance(); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog, times(2)).show(); + verify(httpClient, times(2)).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @Test @@ -394,6 +401,13 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { Updates.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Restart should check release and show dialog again. */ + Updates.unsetInstance(); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog, times(2)).show(); + verify(httpClient, times(2)).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @Test From ba7d2603595c0fbdb522ca532140f1931549a92c Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 15 Feb 2017 19:34:27 -0800 Subject: [PATCH 064/142] Make ErrorLogHelper test more reliable --- .../azure/mobile/crashes/utils/ErrorLogHelperAndroidTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/mobile-center-crashes/src/androidTest/java/com/microsoft/azure/mobile/crashes/utils/ErrorLogHelperAndroidTest.java b/sdk/mobile-center-crashes/src/androidTest/java/com/microsoft/azure/mobile/crashes/utils/ErrorLogHelperAndroidTest.java index de91243913..0daa428baf 100644 --- a/sdk/mobile-center-crashes/src/androidTest/java/com/microsoft/azure/mobile/crashes/utils/ErrorLogHelperAndroidTest.java +++ b/sdk/mobile-center-crashes/src/androidTest/java/com/microsoft/azure/mobile/crashes/utils/ErrorLogHelperAndroidTest.java @@ -70,10 +70,11 @@ public void getStoredFile() throws IOException { assertEquals(0, files.length); /* Generate test files. */ + long date = System.currentTimeMillis(); for (int i = 0; i < 3; i++) { File file = new File(mErrorDirectory, new UUID(0, i).toString() + ErrorLogHelper.ERROR_LOG_FILE_EXTENSION); //noinspection ResultOfMethodCallIgnored - file.setLastModified(System.currentTimeMillis() - i * 3600); + file.setLastModified(date - i * 3600); StorageHelper.InternalStorage.write(file, "contents"); testFiles[i] = file; } From 00d0657536de5c584ed2699b9d755d9640938340 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 16 Feb 2017 11:16:22 -0800 Subject: [PATCH 065/142] Refactoring in a test class as suggested in PR comment --- .../HttpClientNetworkStateHandlerTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java index 0caed18fd6..b0eb09fd86 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpClientNetworkStateHandlerTest.java @@ -17,6 +17,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -40,8 +41,8 @@ public void success() throws IOException { @Override public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { ServiceCallback serviceCallback = (ServiceCallback) invocationOnMock.getArguments()[4]; - serviceCallback.onCallSucceeded(""); - serviceCallback.onCallSucceeded(""); + serviceCallback.onCallSucceeded("mockPayload"); + serviceCallback.onCallSucceeded("duplicateCallbackPayloadToIgnore"); return call; } }).when(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); @@ -54,7 +55,8 @@ public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { HttpClient decorator = new HttpClientNetworkStateHandler(httpClient, networkStateHelper); decorator.callAsync(url, METHOD_GET, headers, callTemplate, callback); verify(httpClient).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); - verify(callback).onCallSucceeded(""); + verify(callback).onCallSucceeded("mockPayload"); + verify(callback, never()).onCallSucceeded("duplicateCallbackPayloadToIgnore"); verifyNoMoreInteractions(callback); /* Close. */ @@ -264,8 +266,7 @@ public void run() { try { sleep(200); ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded("mockPayload"); - } catch (InterruptedException e) { - e.printStackTrace(); + } catch (InterruptedException ignore) { } } }; @@ -325,9 +326,8 @@ public ServiceCall answer(final InvocationOnMock invocationOnMock) throws Throwa public void run() { try { sleep(200); - ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded(""); - } catch (InterruptedException e) { - e.printStackTrace(); + ((ServiceCallback) invocationOnMock.getArguments()[4]).onCallSucceeded("mockPayload"); + } catch (InterruptedException ignore) { } } }; @@ -368,7 +368,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { decorator.onNetworkStateUpdated(true); verify(httpClient, times(2)).callAsync(eq(url), eq(METHOD_GET), eq(headers), eq(callTemplate), any(ServiceCallback.class)); Thread.sleep(300); - verify(callback).onCallSucceeded(""); + verify(callback).onCallSucceeded("mockPayload"); verifyNoMoreInteractions(callback); /* Close. */ From d524ac40884b18c185313467093883a1cfce2e16 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 16 Feb 2017 19:25:41 -0800 Subject: [PATCH 066/142] Show dialog when unknown sources are disabled * Fix flickering on dialogs. * Show toast when dialog click canceled by disabling module. --- .../azure/mobile/updates/InstallerUtils.java | 25 ++ .../azure/mobile/updates/Updates.java | 190 +++++++++- .../src/main/res/values/strings.xml | 3 + .../mobile/updates/AbstractUpdatesTest.java | 29 +- ...lsTest.java => AppStoreDetectionTest.java} | 2 +- .../updates/UnknownSourcesDetectionTest.java | 90 +++++ .../updates/UpdatesBeforeDownloadTest.java | 50 ++- .../mobile/updates/UpdatesDownloadTest.java | 3 + .../UpdatesWarnUnknownSourcesTest.java | 350 ++++++++++++++++++ 9 files changed, 720 insertions(+), 22 deletions(-) rename sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/{InstallerUtilsTest.java => AppStoreDetectionTest.java} (98%) create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java index f4ce8903da..27047220de 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java @@ -1,6 +1,9 @@ package com.microsoft.azure.mobile.updates; +import android.content.ContentResolver; import android.content.Context; +import android.os.Build; +import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; @@ -14,6 +17,12 @@ */ class InstallerUtils { + /** + * Value when {@link Settings.Secure#INSTALL_NON_MARKET_APPS} setting is enabled. + */ + @VisibleForTesting + static final String INSTALL_NON_MARKET_APPS_ENABLED = "1"; + /** * Installer package names that are not app stores. */ @@ -51,4 +60,20 @@ static synchronized boolean isInstalledFromAppStore(@NonNull String logTag, @Non } return sInstalledFromAppStore; } + + /** + * Check whether user enabled installation via unknown sources. + * + * @param context any context. + * @return true if installation via unknown sources is enabled, false otherwise. + */ + @SuppressWarnings("deprecation") + static boolean isUnknownSourcesEnabled(@NonNull Context context) { + ContentResolver contentResolver = context.getContentResolver(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return INSTALL_NON_MARKET_APPS_ENABLED.equals(Settings.Global.getString(contentResolver, Settings.Global.INSTALL_NON_MARKET_APPS)); + } else { + return INSTALL_NON_MARKET_APPS_ENABLED.equals(Settings.Secure.getString(contentResolver, Settings.Secure.INSTALL_NON_MARKET_APPS)); + } + } } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 911e0bbd9a..183b7104d8 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -19,10 +19,14 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.provider.Settings; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.text.TextUtils; +import android.widget.Toast; import com.microsoft.azure.mobile.AbstractMobileCenterService; import com.microsoft.azure.mobile.channel.Channel; @@ -41,6 +45,7 @@ import org.json.JSONException; +import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; @@ -137,6 +142,17 @@ public class Updates extends AbstractMobileCenterService { */ private AlertDialog mUpdateDialog; + /** + * Last unknown sources dialog that was shown. + */ + private AlertDialog mUnknownSourcesDialog; + + /** + * Last activity that did show a dialog. + * Used to avoid replacing a dialog in same screen as it causes flickering. + */ + private WeakReference mLastActivityWithDialog = new WeakReference<>(null); + /** * Current task inspecting the latest release details that we fetched from server. */ @@ -363,6 +379,7 @@ private synchronized void cancelPreviousTasks() { mCheckReleaseCallId = null; } mUpdateDialog = null; + mUnknownSourcesDialog = null; mReleaseDetails = null; if (mDownloadTask != null) { mDownloadTask.cancel(true); @@ -429,9 +446,24 @@ private synchronized void resumeUpdateWorkflow() { } } - /* If we were waiting after API call to resume app to show the dialog do it now. */ + /* If we were waiting after API call to resume app to show/resume the dialog do it now. */ if (mReleaseDetails != null) { - showUpdateDialog(); + + /* Restore the U.I. state after a rotation or if activity covered by another one. */ + if (mUnknownSourcesDialog != null) { + + /* + * Resume click download step if last time we were showing unknown source dialog. + * Note that we could be executed here after going to enable settings and being back in app. + * We can start download if the setting is now enabled, + * otherwise restore dialog if activity rotated or was covered. + */ + enqueueDownloadOrShowUnknownSourcesDialog(mReleaseDetails); + } else { + + /* Or restore update dialog if that's the last thing we did before being paused. */ + showUpdateDialog(); + } return; } @@ -527,6 +559,7 @@ private synchronized void completeWorkflow() { mCheckReleaseApiCall = null; mCheckReleaseCallId = null; mUpdateDialog = null; + mUnknownSourcesDialog = null; mReleaseDetails = null; mWorkflowCompleted = true; } @@ -654,17 +687,54 @@ private boolean isMoreRecent(PackageInfo packageInfo, ReleaseDetails releaseDeta return releaseDetails.getVersion() > packageInfo.versionCode; } + /** + * Check if dialog should be restored in the new activity. Hiding previous dialog version if any. + * + * @param alertDialog existing dialog if any, always returning true when null. + * @return true if a new dialog should be displayed, false otherwise. + */ + private boolean shouldRefreshDialog(@Nullable AlertDialog alertDialog) { + + /* We could be in another activity now, refresh dialog. */ + if (alertDialog != null) { + + /* Nothing to if resuming same activity with dialog already displayed. */ + if (alertDialog.isShowing()) { + if (mForegroundActivity == mLastActivityWithDialog.get()) { + MobileCenterLog.debug(LOG_TAG, "Previous dialog still shown in same activity."); + return false; + } + + /* Otherwise replace dialog. */ + alertDialog.hide(); + } + } + return true; + } + + /** + * Show dialog and remember which activity displayed it for later U.I. state change. + * + * @param dialogBuilder dialog builder that prepared the new dialog. + * @return the dialog that is shown. + */ + private AlertDialog showAndRememberDialogActivity(AlertDialog.Builder dialogBuilder) { + AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.show(); + mLastActivityWithDialog = new WeakReference<>(mForegroundActivity); + return alertDialog; + } + /** * Show update dialog. This can be called multiple times if clicking on HOME and app resumed * (it could be resumed in another activity covering the previous one). */ + @UiThread private synchronized void showUpdateDialog() { - - /* We could be in another activity now, refresh dialog. */ - MobileCenterLog.debug(LOG_TAG, "Show update dialog."); - if (mUpdateDialog != null) { - mUpdateDialog.hide(); + if (!shouldRefreshDialog(mUpdateDialog)) { + return; } + MobileCenterLog.debug(LOG_TAG, "Show new update dialog."); AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mForegroundActivity); dialogBuilder.setTitle(R.string.mobile_center_updates_update_dialog_title); final ReleaseDetails releaseDetails = mReleaseDetails; @@ -677,7 +747,7 @@ private synchronized void showUpdateDialog() { @Override public void onClick(DialogInterface dialog, int which) { - scheduleDownload(releaseDetails); + enqueueDownloadOrShowUnknownSourcesDialog(releaseDetails); } }); dialogBuilder.setNegativeButton(R.string.mobile_center_updates_update_dialog_ignore, new DialogInterface.OnClickListener() { @@ -701,8 +771,84 @@ public void onCancel(DialogInterface dialog) { completeWorkflow(releaseDetails); } }); - mUpdateDialog = dialogBuilder.create(); - mUpdateDialog.show(); + mUpdateDialog = showAndRememberDialogActivity(dialogBuilder); + } + + /** + * Show unknown sources dialog. This can be called multiple times if clicking on HOME and app resumed + * (it could be resumed in another activity covering the previous one). + */ + @UiThread + private synchronized void showUnknownSourcesDialog() { + + /* Check if we need to replace dialog. */ + if (!shouldRefreshDialog(mUnknownSourcesDialog)) { + return; + } + MobileCenterLog.debug(LOG_TAG, "Show new unknown sources dialog."); + + /* + * We invite user to go to setting and will navigate to setting upon clicking, + * but no monitoring is possible and application will be left. + * We want consistent behavior whether application is killed in the mean time or not. + * Not changing any state here provide that consistency as a new update dialog + * will be shown when coming back to application. + * + * Also for buttons and texts we try do to the same as the system dialog on standard devices. + */ + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mForegroundActivity); + dialogBuilder.setMessage(R.string.mobile_center_updates_unknown_sources_dialog_message); + final ReleaseDetails releaseDetails = mReleaseDetails; + dialogBuilder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + completeWorkflow(releaseDetails); + } + }); + dialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + + @Override + public void onCancel(DialogInterface dialog) { + completeWorkflow(releaseDetails); + } + }); + + /* We use generic OK button as we can't promise we can navigate to settings. */ + dialogBuilder.setPositiveButton(R.string.mobile_center_updates_unknown_sources_dialog_settings, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + goToSettings(releaseDetails); + } + }); + mUnknownSourcesDialog = showAndRememberDialogActivity(dialogBuilder); + } + + /** + * Navigate to secure settings. + * + * @param releaseDetails release details to check for state change. + */ + private synchronized void goToSettings(ReleaseDetails releaseDetails) { + try { + + /* + * We can't use startActivityForResult as we don't subclass activities. + * And a no U.I. activity of our own must finish in onCreate, + * so it cannot receive a result. + */ + mForegroundActivity.startActivity(new Intent(Settings.ACTION_SECURITY_SETTINGS)); + } catch (ActivityNotFoundException e) { + + /* On some devices, it's not possible, user will do it by himself. */ + MobileCenterLog.warn(LOG_TAG, "No way to navigate to secure settings on this device automatically"); + + /* Don't pop dialog until app restarted in that case. */ + if (releaseDetails == mReleaseDetails) { + completeWorkflow(); + } + } } /** @@ -716,6 +862,8 @@ private synchronized void ignoreRelease(ReleaseDetails releaseDetails) { MobileCenterLog.debug(LOG_TAG, "Ignore release id=" + id); PreferencesStorage.putString(PREFERENCE_KEY_IGNORED_RELEASE_ID, id); completeWorkflow(); + } else { + showDisabledToast(); } } @@ -724,13 +872,29 @@ private synchronized void ignoreRelease(ReleaseDetails releaseDetails) { * * @param releaseDetails release details. */ - private synchronized void scheduleDownload(ReleaseDetails releaseDetails) { + private synchronized void enqueueDownloadOrShowUnknownSourcesDialog(final ReleaseDetails releaseDetails) { if (releaseDetails == mReleaseDetails) { - MobileCenterLog.debug(LOG_TAG, "Schedule download..."); - mDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new DownloadTask(releaseDetails)); + if (InstallerUtils.isUnknownSourcesEnabled(mContext)) { + MobileCenterLog.debug(LOG_TAG, "Schedule download..."); + mDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new DownloadTask(releaseDetails)); + } else { + showUnknownSourcesDialog(); + } + } else { + showDisabledToast(); } } + /** + * Show disabled toast so that user is not surprised why the dialog action does not work. + * Calling setEnabled(false) before actioning dialog is a corner case + * (possible only if developer has code running the disable in background/or in the mean time) + * that will likely never happen but we guard for it. + */ + private void showDisabledToast() { + Toast.makeText(mContext, R.string.mobile_center_updates_dialog_actioned_on_disabled_toast, Toast.LENGTH_SHORT).show(); + } + /** * Persist download state. * diff --git a/sdk/mobile-center-updates/src/main/res/values/strings.xml b/sdk/mobile-center-updates/src/main/res/values/strings.xml index e40ba3f01e..ba1ad8d0e5 100644 --- a/sdk/mobile-center-updates/src/main/res/values/strings.xml +++ b/sdk/mobile-center-updates/src/main/res/values/strings.xml @@ -7,4 +7,7 @@ Ignore Download Postpone + Updates were disabled + For security, your device is set to block installation of apps obtained from unknown sources. + Settings \ No newline at end of file diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index 13b8a008e3..5eac75defd 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -1,11 +1,13 @@ package com.microsoft.azure.mobile.updates; +import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.text.TextUtils; +import android.widget.Toast; import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.utils.MobileCenterLog; @@ -26,6 +28,7 @@ import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -34,7 +37,7 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Updates.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, InstallerUtils.class}) +@PrepareForTest({Updates.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, InstallerUtils.class, Toast.class}) public class AbstractUpdatesTest { static final String TEST_HASH = "testapp"; // TODO HashUtils.sha256("com.contoso:1.2.3:6"); @@ -56,7 +59,11 @@ public class AbstractUpdatesTest { @Mock AlertDialog mDialog; + @Mock + Toast mToast; + @Before + @SuppressLint("ShowToast") @SuppressWarnings("ResourceType") public void setUp() throws Exception { Updates.unsetInstance(); @@ -117,5 +124,25 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { /* Dialog. */ whenNew(AlertDialog.Builder.class).withAnyArguments().thenReturn(mDialogBuilder); when(mDialogBuilder.create()).thenReturn(mDialog); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(mDialog.isShowing()).thenReturn(true); + return null; + } + }).when(mDialog).show(); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(mDialog.isShowing()).thenReturn(false); + return null; + } + }).when(mDialog).hide(); + + /* Toast. */ + mockStatic(Toast.class); + when(Toast.makeText(any(Context.class), anyInt(), anyInt())).thenReturn(mToast); } } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/InstallerUtilsTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java similarity index 98% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/InstallerUtilsTest.java rename to sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java index 8a731d07e7..0fb7cbd62d 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/InstallerUtilsTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java @@ -20,7 +20,7 @@ import static org.mockito.Mockito.when; @RunWith(PowerMockRunner.class) -public class InstallerUtilsTest { +public class AppStoreDetectionTest { @Mock private Context mContext; diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java new file mode 100644 index 0000000000..ab90f6b981 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java @@ -0,0 +1,90 @@ +package com.microsoft.azure.mobile.updates; + +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.Context; +import android.os.Build; +import android.provider.Settings; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import java.util.Arrays; + +import static com.microsoft.azure.mobile.updates.InstallerUtils.INSTALL_NON_MARKET_APPS_ENABLED; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +@SuppressLint("InlinedApi") +@SuppressWarnings("deprecation") +@RunWith(PowerMockRunner.class) +@PrepareForTest({Build.class, Settings.Global.class, Settings.Secure.class}) +public class UnknownSourcesDetectionTest { + + private static void mockApiLevel(int apiLevel) { + Whitebox.setInternalState(Build.VERSION.class, "SDK_INT", apiLevel); + } + + @Before + public void setUp() { + mockStatic(Settings.Global.class); + mockStatic(Settings.Secure.class); + } + + @Test + public void unknownSourcesEnabledViaSystemSecure() { + when(Settings.Secure.getString(any(ContentResolver.class), eq(Settings.Secure.INSTALL_NON_MARKET_APPS))).thenReturn(INSTALL_NON_MARKET_APPS_ENABLED); + when(Settings.Global.getString(any(ContentResolver.class), eq(Settings.Global.INSTALL_NON_MARKET_APPS))).thenReturn(null); + for (int apiLevel = Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1; apiLevel < Build.VERSION_CODES.JELLY_BEAN_MR1; apiLevel++) { + mockApiLevel(apiLevel); + assertTrue(InstallerUtils.isUnknownSourcesEnabled(mock(Context.class))); + } + for (int apiLevel = Build.VERSION_CODES.JELLY_BEAN_MR1; apiLevel < Build.VERSION_CODES.LOLLIPOP; apiLevel++) { + mockApiLevel(apiLevel); + assertFalse(InstallerUtils.isUnknownSourcesEnabled(mock(Context.class))); + } + for (int apiLevel = Build.VERSION_CODES.LOLLIPOP; apiLevel <= Build.VERSION_CODES.N_MR1; apiLevel++) { + mockApiLevel(apiLevel); + assertTrue(InstallerUtils.isUnknownSourcesEnabled(mock(Context.class))); + } + } + + @Test + public void unknownSourcesEnabledViaSystemGlobal() { + when(Settings.Global.getString(any(ContentResolver.class), eq(Settings.Global.INSTALL_NON_MARKET_APPS))).thenReturn(INSTALL_NON_MARKET_APPS_ENABLED); + when(Settings.Secure.getString(any(ContentResolver.class), eq(Settings.Secure.INSTALL_NON_MARKET_APPS))).thenReturn(null); + for (int apiLevel = Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1; apiLevel < Build.VERSION_CODES.JELLY_BEAN_MR1; apiLevel++) { + mockApiLevel(apiLevel); + assertFalse(InstallerUtils.isUnknownSourcesEnabled(mock(Context.class))); + } + for (int apiLevel = Build.VERSION_CODES.JELLY_BEAN_MR1; apiLevel < Build.VERSION_CODES.LOLLIPOP; apiLevel++) { + mockApiLevel(apiLevel); + assertTrue(InstallerUtils.isUnknownSourcesEnabled(mock(Context.class))); + } + for (int apiLevel = Build.VERSION_CODES.LOLLIPOP; apiLevel <= Build.VERSION_CODES.N_MR1; apiLevel++) { + mockApiLevel(apiLevel); + assertFalse(InstallerUtils.isUnknownSourcesEnabled(mock(Context.class))); + } + } + + @Test + public void disabledAndInvalidValues() { + for (String value : Arrays.asList(null, "", "0", "on", "true", "TRUE")) { + when(Settings.Global.getString(any(ContentResolver.class), eq(Settings.Global.INSTALL_NON_MARKET_APPS))).thenReturn(value); + when(Settings.Secure.getString(any(ContentResolver.class), eq(Settings.Secure.INSTALL_NON_MARKET_APPS))).thenReturn(value); + for (int apiLevel = Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1; apiLevel <= Build.VERSION_CODES.N_MR1; apiLevel++) { + mockApiLevel(apiLevel); + assertFalse(InstallerUtils.isUnknownSourcesEnabled(mock(Context.class))); + } + } + } +} diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java index 41aac84dbe..afe1d42af9 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java @@ -35,7 +35,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.internal.verification.VerificationModeFactory.times; import static org.powermock.api.mockito.PowerMockito.doAnswer; @@ -187,6 +186,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); /* Trigger call. */ Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); @@ -217,7 +217,12 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Disable does not hide the dialog. */ Updates.setEnabled(false); - verifyNoMoreInteractions(mDialog); + + /* We already called hide once, make sure its not called a second time. */ + verify(mDialog).hide(); + + /* Also no toast if we don't click on actionable button. */ + verify(mToast, never()).show(); } @Test @@ -256,7 +261,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } @Test - public void dialogWaitWhileInBackground() throws Exception { + public void dialogActivityStateChanges() throws Exception { /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -290,8 +295,9 @@ public void run() { /* Trigger call. */ Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - Updates.getInstance().onActivityPaused(mock(Activity.class)); + Activity activity = mock(Activity.class); + Updates.getInstance().onActivityResumed(activity); + Updates.getInstance().onActivityPaused(activity); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Release call in background. */ @@ -303,11 +309,27 @@ public void run() { verify(mDialog, never()).show(); /* Go foreground. */ - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Updates.getInstance().onActivityResumed(activity); /* Verify dialog now shown. */ verify(mDialogBuilder).create(); verify(mDialog).show(); + + /* Pause/resume should not alter dialog. */ + Updates.getInstance().onActivityPaused(activity); + Updates.getInstance().onActivityResumed(activity); + + /* Only once check, and no hiding. */ + verify(mDialogBuilder).create(); + verify(mDialog).show(); + verify(mDialog, never()).hide(); + + /* Cover activity. Dialog must be replaced. */ + Updates.getInstance().onActivityPaused(activity); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialogBuilder, times(2)).create(); + verify(mDialog, times(2)).show(); + verify(mDialog).hide(); } @Test @@ -341,6 +363,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Cancel it. */ cancelListener.getValue().onCancel(mDialog); + when(mDialog.isShowing()).thenReturn(false); /* Verify. */ verifyStatic(); @@ -391,6 +414,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Postpone it. */ clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_NEUTRAL); + when(mDialog.isShowing()).thenReturn(false); /* Verify. */ verifyStatic(); @@ -461,6 +485,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Ignore it. */ clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_NEGATIVE); + when(mDialog.isShowing()).thenReturn(false); /* Verify. */ verifyStatic(); @@ -526,6 +551,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Cancel it. */ cancelListener.getValue().onCancel(mDialog); + when(mDialog.isShowing()).thenReturn(false); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -575,6 +601,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(releaseDetails.getId()).thenReturn("someId"); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); /* Trigger call. */ Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); @@ -592,6 +619,10 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Ignore it. */ clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_NEGATIVE); + when(mDialog.isShowing()).thenReturn(false); + + /* Since we were disabled, no action but toast to explain what happened. */ + verify(mToast).show(); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -627,6 +658,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); mockStatic(AsyncTaskUtils.class); + when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); /* Trigger call. */ Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); @@ -642,8 +674,12 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - /* Click on download does nothing for now in the design if we disabled. */ + /* Click on download. */ clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + when(mDialog.isShowing()).thenReturn(false); + + /* Since we were disabled, no action but toast to explain what happened. */ + verify(mToast).show(); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java index 734db360b1..132ab48171 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java @@ -104,6 +104,9 @@ public class UpdatesDownloadTest extends AbstractUpdatesTest { @Before public void setUpDownload() throws Exception { + /* Allow unknown sources. */ + when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); + /* Mock download manager. */ when(mContext.getSystemService(Context.DOWNLOAD_SERVICE)).thenReturn(mDownloadManager); whenNew(DownloadManager.Request.class).withAnyArguments().thenReturn(mDownloadRequest); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java new file mode 100644 index 0000000000..be98aa8926 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java @@ -0,0 +1,350 @@ +package com.microsoft.azure.mobile.updates; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.provider.Settings; + +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.utils.AsyncTaskUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.anyVararg; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.powermock.api.mockito.PowerMockito.doAnswer; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +public class UpdatesWarnUnknownSourcesTest extends AbstractUpdatesTest { + + @Mock + private AlertDialog mUnknownSourcesDialog; + + @Mock + private Activity mFirstActivity; + + @Before + public void setUpDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getVersion()).thenReturn(7); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mFirstActivity); + + /* Mock second dialog. */ + when(mDialogBuilder.create()).thenReturn(mUnknownSourcesDialog); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(mUnknownSourcesDialog.isShowing()).thenReturn(true); + return null; + } + }).when(mUnknownSourcesDialog).show(); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(mUnknownSourcesDialog.isShowing()).thenReturn(false); + return null; + } + }).when(mUnknownSourcesDialog).hide(); + + /* Click on first dialog. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + when(mDialog.isShowing()).thenReturn(false); + + /* Second should show. */ + verify(mUnknownSourcesDialog).show(); + } + + @Test + public void cancelDialogWithBack() { + + /* Cancel. */ + ArgumentCaptor cancelListener = ArgumentCaptor.forClass(DialogInterface.OnCancelListener.class); + verify(mDialogBuilder, times(2)).setOnCancelListener(cancelListener.capture()); + cancelListener.getValue().onCancel(mUnknownSourcesDialog); + when(mUnknownSourcesDialog.isShowing()).thenReturn(false); + + /* Verify. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(mUnknownSourcesDialog).show(); + } + + @Test + public void cancelDialogWithButton() { + + /* Cancel. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setNegativeButton(eq(android.R.string.cancel), clickListener.capture()); + clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_NEGATIVE); + when(mUnknownSourcesDialog.isShowing()).thenReturn(false); + + /* Verify. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(mUnknownSourcesDialog).show(); + } + + @Test + public void disableBeforeCancelWithBack() { + + /* Disable. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Cancel. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setNegativeButton(eq(android.R.string.cancel), clickListener.capture()); + clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_NEGATIVE); + when(mUnknownSourcesDialog.isShowing()).thenReturn(false); + + /* Verify cancel did nothing more. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(mUnknownSourcesDialog).show(); + } + + @Test + public void disableBeforeCancelWithButton() { + + /* Disable. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Cancel. */ + ArgumentCaptor cancelListener = ArgumentCaptor.forClass(DialogInterface.OnCancelListener.class); + verify(mDialogBuilder, times(2)).setOnCancelListener(cancelListener.capture()); + cancelListener.getValue().onCancel(mUnknownSourcesDialog); + when(mUnknownSourcesDialog.isShowing()).thenReturn(false); + + /* Verify cancel did nothing more. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(mUnknownSourcesDialog).show(); + } + + @Test + public void coverActivity() { + + /* Pause/resume should not alter dialog. */ + Updates.getInstance().onActivityPaused(mFirstActivity); + Updates.getInstance().onActivityResumed(mFirstActivity); + + /* Only once check, and no hiding. */ + verify(mDialog).show(); + verify(mDialog, never()).hide(); + verify(mUnknownSourcesDialog).show(); + verify(mUnknownSourcesDialog, never()).hide(); + + /* Cover activity. Second dialog must be replaced. First one skipped. */ + Updates.getInstance().onActivityPaused(mFirstActivity); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(mDialog, never()).hide(); + verify(mUnknownSourcesDialog, times(2)).show(); + verify(mUnknownSourcesDialog).hide(); + } + + @Test + public void clickSettingsGoBackWithoutEnabling() throws Exception { + + /* Click settings. */ + Intent intent = mock(Intent.class); + whenNew(Intent.class).withArguments(Settings.ACTION_SECURITY_SETTINGS).thenReturn(intent); + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_unknown_sources_dialog_settings), clickListener.capture()); + clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_POSITIVE); + when(mUnknownSourcesDialog.isShowing()).thenReturn(false); + + /* Verify navigation. */ + verify(mFirstActivity).startActivity(intent); + + /* Simulate we go back and forth to settings without changing the value. */ + Updates.getInstance().onActivityPaused(mFirstActivity); + Updates.getInstance().onActivityResumed(mFirstActivity); + + /* Second dialog will be back directly, no update dialog again. */ + verify(mDialog).show(); + verify(mDialog, never()).hide(); + verify(mUnknownSourcesDialog, times(2)).show(); + verify(mUnknownSourcesDialog, never()).hide(); + } + + @Test + @PrepareForTest(AsyncTaskUtils.class) + public void clickSettingsThenEnableThenBack() throws Exception { + + /* Click settings. */ + Intent intent = mock(Intent.class); + whenNew(Intent.class).withArguments(Settings.ACTION_SECURITY_SETTINGS).thenReturn(intent); + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_unknown_sources_dialog_settings), clickListener.capture()); + clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_POSITIVE); + when(mUnknownSourcesDialog.isShowing()).thenReturn(false); + + /* Verify navigation. */ + verify(mFirstActivity).startActivity(intent); + + /* Simulate we go to settings, change value then go back. */ + mockStatic(AsyncTaskUtils.class); + Updates.getInstance().onActivityPaused(mFirstActivity); + when(InstallerUtils.isUnknownSourcesEnabled(mContext)).thenReturn(true); + Updates.getInstance().onActivityResumed(mFirstActivity); + + /* No more dialog, start download. */ + verify(mDialog).show(); + verify(mDialog, never()).hide(); + verify(mUnknownSourcesDialog).show(); + verify(mUnknownSourcesDialog, never()).hide(); + verifyStatic(); + AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher>() { + + @Override + public boolean matches(Object argument) { + return argument instanceof Updates.DownloadTask; + } + }), anyVararg()); + } + + @Test + public void clickSettingsFailsToNavigate() throws Exception { + + /* Click settings. */ + Intent intent = mock(Intent.class); + whenNew(Intent.class).withArguments(Settings.ACTION_SECURITY_SETTINGS).thenReturn(intent); + doThrow(new ActivityNotFoundException()).when(mFirstActivity).startActivity(intent); + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_unknown_sources_dialog_settings), clickListener.capture()); + clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_POSITIVE); + when(mUnknownSourcesDialog.isShowing()).thenReturn(false); + + /* Verify navigation attempted. */ + verify(mFirstActivity).startActivity(intent); + + /* Verify failure is treated as a cancel dialog. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(mUnknownSourcesDialog).show(); + } + + @Test + public void disableThenClickSettingsThenFailsToNavigate() throws Exception { + + /* Disable. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Click settings. */ + Intent intent = mock(Intent.class); + whenNew(Intent.class).withArguments(Settings.ACTION_SECURITY_SETTINGS).thenReturn(intent); + doThrow(new ActivityNotFoundException()).when(mFirstActivity).startActivity(intent); + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_unknown_sources_dialog_settings), clickListener.capture()); + clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_POSITIVE); + when(mUnknownSourcesDialog.isShowing()).thenReturn(false); + + /* Verify navigation attempted. */ + verify(mFirstActivity).startActivity(intent); + + /* Verify cleaning behavior happened only once, e.g. completeWorkflow skipped. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + + /* Verify no more calls, e.g. happened only once. */ + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verify(mDialog).show(); + verify(mUnknownSourcesDialog).show(); + } + + @After + public void restartShowDialog() { + + /* Restart should check release and show update dialog again. */ + when(mDialogBuilder.create()).thenReturn(mDialog); + Updates.unsetInstance(); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + Updates.setEnabled(true); + verify(mDialog, times(2)).show(); + } +} From 6a517a0a8119ae37393859315335018b003c3b22 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 17 Feb 2017 17:52:17 -0800 Subject: [PATCH 067/142] Restore actual release_hash code now that backend implements it --- .../com/microsoft/azure/mobile/updates/Updates.java | 4 ++-- .../azure/mobile/updates/AbstractUpdatesTest.java | 10 ++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 911e0bbd9a..55d67e4e85 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -33,6 +33,7 @@ import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; +import com.microsoft.azure.mobile.utils.HashUtils; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.NetworkStateHelper; import com.microsoft.azure.mobile.utils.UUIDUtils; @@ -493,8 +494,7 @@ private String computeHash(@NonNull Context context) throws PackageManager.NameN @NonNull private String computeHash(@NonNull Context context, @NonNull PackageInfo packageInfo) { - // TODO switch to the following hash when backend supports it: HashUtils.sha256(context.getPackageName() + ":" + packageInfo.versionName + ":" + packageInfo.versionCode); - return context.getString(packageInfo.applicationInfo.labelRes); + return HashUtils.sha256(context.getPackageName() + ":" + packageInfo.versionName + ":" + packageInfo.versionCode); } /** diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index 13b8a008e3..5ed875793b 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -2,12 +2,12 @@ import android.app.AlertDialog; import android.content.Context; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.text.TextUtils; import com.microsoft.azure.mobile.MobileCenter; +import com.microsoft.azure.mobile.utils.HashUtils; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.UUIDUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; @@ -37,7 +37,7 @@ @PrepareForTest({Updates.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, InstallerUtils.class}) public class AbstractUpdatesTest { - static final String TEST_HASH = "testapp"; // TODO HashUtils.sha256("com.contoso:1.2.3:6"); + static final String TEST_HASH = HashUtils.sha256("com.contoso:1.2.3:6"); private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; @@ -93,12 +93,6 @@ public Void answer(InvocationOnMock invocation) throws Throwable { Whitebox.setInternalState(packageInfo, "versionName", "1.2.3"); Whitebox.setInternalState(packageInfo, "versionCode", 6); - /* TODO temporary fake hash based on app name */ - ApplicationInfo applicationInfo = mock(ApplicationInfo.class); - Whitebox.setInternalState(applicationInfo, "labelRes", 1337); - Whitebox.setInternalState(packageInfo, "applicationInfo", applicationInfo); - when(mContext.getString(1337)).thenReturn(TEST_HASH); - /* Mock some statics. */ mockStatic(BrowserUtils.class); mockStatic(UUIDUtils.class); From bb3a0a94eda95d0090168db971e73db4a7aca711 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 17 Feb 2017 18:10:38 -0800 Subject: [PATCH 068/142] Refactoring based on #327 comments --- .../azure/mobile/updates/Updates.java | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 183b7104d8..fc3573e1e4 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -701,7 +701,7 @@ private boolean shouldRefreshDialog(@Nullable AlertDialog alertDialog) { /* Nothing to if resuming same activity with dialog already displayed. */ if (alertDialog.isShowing()) { if (mForegroundActivity == mLastActivityWithDialog.get()) { - MobileCenterLog.debug(LOG_TAG, "Previous dialog still shown in same activity."); + MobileCenterLog.debug(LOG_TAG, "Previous dialog is still being shown in the same activity."); return false; } @@ -764,13 +764,7 @@ public void onClick(DialogInterface dialog, int which) { completeWorkflow(releaseDetails); } }); - dialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { - - @Override - public void onCancel(DialogInterface dialog) { - completeWorkflow(releaseDetails); - } - }); + setOnCancelListener(dialogBuilder, releaseDetails); mUpdateDialog = showAndRememberDialogActivity(dialogBuilder); } @@ -806,13 +800,7 @@ public void onClick(DialogInterface dialog, int which) { completeWorkflow(releaseDetails); } }); - dialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { - - @Override - public void onCancel(DialogInterface dialog) { - completeWorkflow(releaseDetails); - } - }); + setOnCancelListener(dialogBuilder, releaseDetails); /* We use generic OK button as we can't promise we can navigate to settings. */ dialogBuilder.setPositiveButton(R.string.mobile_center_updates_unknown_sources_dialog_settings, new DialogInterface.OnClickListener() { @@ -825,6 +813,19 @@ public void onClick(DialogInterface dialog, int which) { mUnknownSourcesDialog = showAndRememberDialogActivity(dialogBuilder); } + /** + * Common code for dialogs cancel action (using BACK). + */ + private void setOnCancelListener(AlertDialog.Builder dialogBuilder, final ReleaseDetails releaseDetails) { + dialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + + @Override + public void onCancel(DialogInterface dialog) { + completeWorkflow(releaseDetails); + } + }); + } + /** * Navigate to secure settings. * From 5d3a778993559eba1798f2eeb8175b42e26fc433 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 21 Feb 2017 17:03:43 -0800 Subject: [PATCH 069/142] Fix a bug that was showing install U.I. after upgrade And refactor download manager interactions to fix strict mode and that bug. --- .../src/main/AndroidManifest.xml | 2 +- ...iver.java => DownloadManagerReceiver.java} | 4 +- .../azure/mobile/updates/UpdateConstants.java | 42 +- .../azure/mobile/updates/Updates.java | 337 +++++---- .../mobile/updates/AbstractUpdatesTest.java | 7 + ...nloadManagerReceiverIgnoreIntentTest.java} | 6 +- .../updates/UpdatesBeforeApiSuccessTest.java | 24 +- .../updates/UpdatesBeforeDownloadTest.java | 28 +- .../mobile/updates/UpdatesDownloadTest.java | 651 ++++++++++-------- .../UpdatesPlusDownloadReceiverTest.java | 8 +- .../UpdatesWarnUnknownSourcesTest.java | 20 +- 11 files changed, 665 insertions(+), 464 deletions(-) rename sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/{DownloadCompletionReceiver.java => DownloadManagerReceiver.java} (87%) rename sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/{DownloadCompletionReceiverIgnoreIntentTest.java => DownloadManagerReceiverIgnoreIntentTest.java} (81%) diff --git a/sdk/mobile-center-updates/src/main/AndroidManifest.xml b/sdk/mobile-center-updates/src/main/AndroidManifest.xml index 536ad10f49..bd2f2c85c7 100644 --- a/sdk/mobile-center-updates/src/main/AndroidManifest.xml +++ b/sdk/mobile-center-updates/src/main/AndroidManifest.xml @@ -21,7 +21,7 @@ - + diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java similarity index 87% rename from sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java rename to sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java index 2d86676488..904f56fd85 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiver.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java @@ -8,7 +8,7 @@ /** * Process download manager callbacks. */ -public class DownloadCompletionReceiver extends BroadcastReceiver { +public class DownloadManagerReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -28,7 +28,7 @@ public void onReceive(Context context, Intent intent) { */ else if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); - Updates.getInstance().processCompletedDownload(context, downloadId); + Updates.getInstance().checkDownload(context, downloadId); } } } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java index 2eaae6b619..edb859349c 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -1,5 +1,6 @@ package com.microsoft.azure.mobile.updates; +import android.app.DownloadManager; import android.support.annotation.VisibleForTesting; import com.microsoft.azure.mobile.MobileCenter; @@ -85,25 +86,38 @@ final class UpdateConstants { */ static final long INVALID_DOWNLOAD_IDENTIFIER = -1; + /** + * After we show install U.I, the download is mark completed but we keep the file. + * No download is also using this value. + */ + static final int DOWNLOAD_STATE_COMPLETED = 0; + + /** + * We are waiting to hear back from download manager, we may poll status on process restart. + */ + static final int DOWNLOAD_STATE_ENQUEUED = 1; + + /** + * Download is finished, notification was posted but user could ignore it, + * we use that state to show install U.I 1 time when application is resumed. + */ + static final int DOWNLOAD_STATE_NOTIFIED = 2; + /** * Base key for stored preferences. */ private static final String PREFERENCE_PREFIX = SERVICE_NAME + "."; /** - * Preference key to store the last download file location on download manager if completed, - * empty string while download is in progress, null if we launched install U.I. - * If this is null and {@link #PREFERENCE_KEY_DOWNLOAD_ID} is not null, it's to remember we - * downloaded a file for later removal (when we disable SDK or prepare a new download). - *

- * Rationale is that we keep the file in case the user chooses to install it from downloads U.I. + * Preference key to store the current/last download identifier (we keep download until a next + * one is scheduled as the file can be opened from device downloads U.I.). */ - static final String PREFERENCE_KEY_DOWNLOAD_URI = PREFERENCE_PREFIX + "download_uri"; + static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; /** - * Preference key to store the last download identifier. + * Preference key to store the SDK state related to {@link #PREFERENCE_KEY_DOWNLOAD_ID} when not null. */ - static final String PREFERENCE_KEY_DOWNLOAD_ID = PREFERENCE_PREFIX + "download_id"; + static final String PREFERENCE_KEY_DOWNLOAD_STATE = PREFERENCE_PREFIX + "download_state"; /** * Preference key for request identifier to validate deep link intent. @@ -120,6 +134,16 @@ final class UpdateConstants { */ static final String PREFERENCE_KEY_IGNORED_RELEASE_ID = PREFERENCE_PREFIX + "ignored_release_id"; + /** + * Preference key to store download start time. Used to avoid showing install U.I. of a completed + * download if we already updated (the download workflow can work across process restarts). + *

+ * We can't use {@link DownloadManager#COLUMN_LAST_MODIFIED_TIMESTAMP} as we could have a corner case + * where we install upgrade from email or another mean while waiting download triggered by SDK. + * So the time we store as a reference needs to be before download time. + */ + static final String PREFERENCE_KEY_DOWNLOAD_TIME = PREFERENCE_PREFIX + "download_time"; + @VisibleForTesting UpdateConstants() { diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index d234ea9429..ef2ba1e4ac 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -49,11 +49,15 @@ import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; +import java.util.NoSuchElementException; import static android.content.Context.DOWNLOAD_SERVICE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_API_URL; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_INSTALL_URL; +import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_COMPLETED; +import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_ENQUEUED; +import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_NOTIFIED; import static com.microsoft.azure.mobile.updates.UpdateConstants.GET_LATEST_RELEASE_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.HEADER_API_TOKEN; import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; @@ -64,7 +68,8 @@ import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_RELEASE_HASH; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REQUEST_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_TIME; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; @@ -160,9 +165,14 @@ public class Updates extends AbstractMobileCenterService { private DownloadTask mDownloadTask; /** - * Current task to process download completion. + * Current task to check download state and act on it. */ - private ProcessDownloadCompletionTask mProcessDownloadCompletionTask; + private CheckDownloadTask mCheckDownloadTask; + + /** + * Remember if we checked download since our own process restarted. + */ + private boolean mCheckedDownload; /** * True when update workflow reached final state. @@ -260,8 +270,8 @@ static int getNotificationId() { } @SuppressWarnings("deprecation") - private static Uri getFileUriOnOldDevices(Cursor cursor) { - return Uri.parse("file://" + cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME))); + private static Uri getFileUriOnOldDevices(Cursor cursor) throws IllegalArgumentException { + return Uri.parse("file://" + cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME))); } @SuppressWarnings("deprecation") @@ -282,6 +292,15 @@ private static long getStoredDownloadId() { return StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); } + /** + * Get download state from storage. + * + * @return download state (completed by default). + */ + private static int getStoredDownloadState() { + return PreferencesStorage.getInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_COMPLETED); + } + @Override protected String getGroupName() { return null; @@ -321,7 +340,7 @@ public synchronized void onActivityCreated(Activity activity, Bundle savedInstan /* Clear workflow finished state if launch recreated, to achieve check on "startup". */ if (activity.getClass().getName().equals(mLauncherActivityClassName)) { MobileCenterLog.info(LOG_TAG, "Launcher activity restarted."); - if (PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI) == null) { + if (getStoredDownloadState() == DOWNLOAD_STATE_COMPLETED) { mWorkflowCompleted = false; mBrowserOpenedOrAborted = false; } @@ -386,17 +405,19 @@ private synchronized void cancelPreviousTasks() { mDownloadTask.cancel(true); mDownloadTask = null; } - if (mProcessDownloadCompletionTask != null) { - mProcessDownloadCompletionTask.cancel(true); - mProcessDownloadCompletionTask = null; + if (mCheckDownloadTask != null) { + mCheckDownloadTask.cancel(true); + mCheckDownloadTask = null; } + mCheckedDownload = false; long downloadId = getStoredDownloadId(); if (downloadId >= 0) { MobileCenterLog.debug(LOG_TAG, "Removing download and notification id=" + downloadId); removeDownload(downloadId); } PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_TIME); } /** @@ -421,29 +442,32 @@ private synchronized void resumeUpdateWorkflow() { return; } - /* If we have a download ready but we were in background, pop install UI now. */ - String downloadUri = PreferencesStorage.getString(PREFERENCE_KEY_DOWNLOAD_URI); - if ("".equals(downloadUri)) { - - /* TODO double check that with download manager. */ - MobileCenterLog.verbose(LOG_TAG, "Download is still in progress..."); - return; - } else if (downloadUri != null) { - try { - - /* FIXME this can cause strict mode violation. */ - Uri apkUri = Uri.parse(downloadUri); - MobileCenterLog.debug(LOG_TAG, "Now in foreground, remove notification and start install for APK uri=" + apkUri); - mForegroundActivity.startActivity(getInstallIntent(apkUri)); - NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(getNotificationId()); - completeWorkflow(); + /* If we have a pending or notified download, check it. */ + if (getStoredDownloadState() != DOWNLOAD_STATE_COMPLETED) { + if (mCheckedDownload) { return; - } catch (ActivityNotFoundException e) { + } else { + + /* Discard download if application updated. Then immediately check release. */ + long lastUpdateTime; + try { + lastUpdateTime = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).lastUpdateTime; + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.debug(LOG_TAG, "Could not check last update time.", e); + completeWorkflow(); + return; + } + if (lastUpdateTime > StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_TIME)) { + MobileCenterLog.debug(LOG_TAG, "Discarding previous download as application updated."); + cancelPreviousTasks(); + } - /* Cleanup on exception and resume update workflow. */ - MobileCenterLog.warn(LOG_TAG, "Download uri was invalid", e); - cancelPreviousTasks(); + /* Otherwise check download. */ + else { + mCheckedDownload = true; + checkDownload(mContext, getStoredDownloadId()); + return; + } } } @@ -545,17 +569,28 @@ private synchronized void completeWorkflow(ReleaseDetails releaseDetails) { * * @param task to check if state changed and that the call should be ignored. */ - private synchronized void completeWorkflow(ProcessDownloadCompletionTask task) { - if (task == mProcessDownloadCompletionTask) { + private synchronized void completeWorkflow(CheckDownloadTask task) { + if (task == mCheckDownloadTask) { + cancelNotification(task.mContext); completeWorkflow(); } } + /** + * Cancel notification if needed. + */ + private synchronized void cancelNotification(Context context) { + if (getStoredDownloadState() == DOWNLOAD_STATE_NOTIFIED) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(getNotificationId()); + } + } + /** * Reset all variables that matter to restart checking a new release on launcher activity restart. */ private synchronized void completeWorkflow() { - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); mCheckReleaseApiCall = null; mCheckReleaseCallId = null; mUpdateDialog = null; @@ -877,6 +912,7 @@ private synchronized void enqueueDownloadOrShowUnknownSourcesDialog(final Releas if (releaseDetails == mReleaseDetails) { if (InstallerUtils.isUnknownSourcesEnabled(mContext)) { MobileCenterLog.debug(LOG_TAG, "Schedule download..."); + mCheckedDownload = true; mDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new DownloadTask(releaseDetails)); } else { showUnknownSourcesDialog(); @@ -900,11 +936,12 @@ private void showDisabledToast() { * Persist download state. * * @param downloadManager download manager. - * @param task current task to check race conditions. + * @param task current task to check state change. * @param downloadRequestId download identifier. + * @param enqueueTime time just before enqueuing download. */ @WorkerThread - private synchronized void storeDownloadRequestId(DownloadManager downloadManager, DownloadTask task, long downloadRequestId) { + private synchronized void storeDownloadRequestId(DownloadManager downloadManager, DownloadTask task, long downloadRequestId, long enqueueTime) { /* Check for if state changed and task not canceled in time. */ if (mDownloadTask == task) { @@ -918,7 +955,8 @@ private synchronized void storeDownloadRequestId(DownloadManager downloadManager /* Store new download identifier. */ PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, downloadRequestId); - PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, ""); + PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_ENQUEUED); + PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_TIME, enqueueTime); } else { /* State changed quickly, cancel download. */ @@ -948,59 +986,96 @@ synchronized void resumeApp(@NonNull Context context) { } /** - * Check a download that just completed. + * Check a download state and take action depending on that state. * * @param context any application context. * @param downloadId download identifier from DownloadManager. */ - synchronized void processCompletedDownload(@NonNull Context context, long downloadId) { + synchronized void checkDownload(@NonNull Context context, long downloadId) { - /* Querying download manager and even the start intent violate strict mode so do that in background. */ - mProcessDownloadCompletionTask = AsyncTaskUtils.execute(LOG_TAG, new ProcessDownloadCompletionTask(context, downloadId)); + /* Querying download manager and even the start intent are detected by strict mode so we do that in background. */ + mCheckDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new CheckDownloadTask(context, downloadId)); } /** - * Used by task processing the download completion in background prior to showing install U.I to check if request was canceled. + * Post notification about a completed download if we are in background when download completes. + * If this method is called on app process restart or if application is in foreground + * when download completes, it will not notify and return that the install U.I. should be shown now. * - * @param task task to check state for. - * @return foreground activity if any, if state is valid. - * @throws IllegalStateException if state changed. + * @param context context. + * @param task task that prepared the notification to check state. + * @param intent prepared install intent. + * @return false if install U.I should be shown now, true if a notification was posted or if the task was canceled. */ - private synchronized Activity getForegroundActivityWithStateCheck(ProcessDownloadCompletionTask task) throws IllegalStateException { - if (task == mProcessDownloadCompletionTask) { - return mForegroundActivity; + private synchronized boolean notifyDownload(Context context, CheckDownloadTask task, Intent intent) { + + /* Check state. */ + if (task != mCheckDownloadTask) { + return true; + } + + /* + * If we already notified, that means this check was triggered by application being resumed, + * thus in foreground at the moment the check download async task was started. + * + * We should not hold the install any longer now, even if the async task was long enough + * for app to be in background again, we should show install U.I. now. + */ + if (mForegroundActivity != null || getStoredDownloadState() == DOWNLOAD_STATE_NOTIFIED) { + return false; + } + + /* Post notification. */ + MobileCenterLog.debug(LOG_TAG, "Post a notification as the download finished in background."); + int icon; + try { + ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); + icon = applicationInfo.icon; + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.error(LOG_TAG, "Could not get application icon", e); + completeWorkflow(); + return true; } - throw new IllegalStateException(); + Notification.Builder builder = new Notification.Builder(context) + .setTicker(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) + .setContentTitle(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) + .setContentText(context.getString(R.string.mobile_center_updates_download_successful_notification_message)) + .setSmallIcon(icon) + .setContentIntent(PendingIntent.getActivities(context, 0, new Intent[]{intent}, 0)); + Notification notification = buildNotification(builder); + notification.flags |= Notification.FLAG_AUTO_CANCEL; + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(getNotificationId(), notification); + PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_NOTIFIED); + + /* Reset check download flag to show install U.I. on resume if notification ignored. */ + mCheckedDownload = false; + return true; } /** - * Post notification about a completed download if state did not change. + * Used to avoid querying download manager on every activity change. * - * @param context context. - * @param task task that prepared the notification to check state. - * @param notification notification to post. - * @param uri uri to persist to remember download completed state. + * @param task task to check for a state change. */ - private synchronized void notifyDownload(Context context, ProcessDownloadCompletionTask task, Notification notification, String uri) { - if (task == mProcessDownloadCompletionTask) { - PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, uri); - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(getNotificationId(), notification); + private synchronized void markDownloadStillInProgress(CheckDownloadTask task) { + if (task == mCheckDownloadTask) { + MobileCenterLog.verbose(LOG_TAG, "Download is still in progress..."); + mCheckedDownload = true; } } /** * Remove a previously downloaded file and any notification. */ - private void removeDownload(long downloadId) { + private synchronized void removeDownload(long downloadId) { MobileCenterLog.debug(LOG_TAG, "Delete previous notification downloadId=" + downloadId); - NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(getNotificationId()); + cancelNotification(mContext); AsyncTaskUtils.execute(LOG_TAG, new RemoveDownloadTask(), downloadId); } /** - * Removing a download violates strict mode in U.I. thread. + * Removing a download triggers strict mode exception in U.I. thread. */ @VisibleForTesting class RemoveDownloadTask extends AsyncTask { @@ -1018,7 +1093,7 @@ protected Void doInBackground(Long... params) { } /** - * The download manager API violates strict mode in U.I. thread. + * The download manager API triggers strict mode exception in U.I. thread. */ @VisibleForTesting class DownloadTask extends AsyncTask { @@ -1045,17 +1120,19 @@ protected Void doInBackground(Void[] params) { MobileCenterLog.debug(LOG_TAG, "Start downloading new release, url=" + downloadUrl); DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); DownloadManager.Request request = new DownloadManager.Request(downloadUrl); + long enqueueTime = System.currentTimeMillis(); long downloadRequestId = downloadManager.enqueue(request); - storeDownloadRequestId(downloadManager, this, downloadRequestId); + storeDownloadRequestId(downloadManager, this, downloadRequestId, enqueueTime); return null; } } /** - * Inspect a completed download, this uses APIs that would trigger strict mode violation if used in U.I. thread. + * Inspect a pending or completed download. + * This uses APIs that would trigger strict mode exception if used in U.I. thread. */ @VisibleForTesting - class ProcessDownloadCompletionTask extends AsyncTask { + class CheckDownloadTask extends AsyncTask { /** * Context. @@ -1073,7 +1150,7 @@ class ProcessDownloadCompletionTask extends AsyncTask { * @param context context. * @param downloadId download identifier. */ - ProcessDownloadCompletionTask(Context context, long downloadId) { + CheckDownloadTask(Context context, long downloadId) { mContext = context; mDownloadId = downloadId; } @@ -1088,8 +1165,8 @@ protected Void doInBackground(Void... params) { * We still want to generate the notification: if we can find the data in preferences * that means they were not deleted, and thus that the sdk was not disabled. */ - MobileCenterLog.debug(LOG_TAG, "Process download completion id=" + mDownloadId); - if (Updates.this.mContext == null) { + MobileCenterLog.debug(LOG_TAG, "Check download id=" + mDownloadId); + if (mAppSecret == null) { MobileCenterLog.debug(LOG_TAG, "Called before onStart, init storage"); StorageHelper.initialize(mContext); } @@ -1097,85 +1174,69 @@ protected Void doInBackground(Void... params) { /* Check intent data is what we expected. */ long expectedDownloadId = getStoredDownloadId(); if (expectedDownloadId >= 0 && expectedDownloadId != mDownloadId) { - MobileCenterLog.warn(LOG_TAG, "Ignoring completion for a download we didn't expect, id=" + mDownloadId); + MobileCenterLog.debug(LOG_TAG, "Ignoring download identifier we didn't expect, id=" + mDownloadId); return null; } - /* Check if download successful. */ + /* Query download manager. */ DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); - Uri uriForDownloadedFile = downloadManager.getUriForDownloadedFile(mDownloadId); - if (uriForDownloadedFile != null) { - - /* Build install intent. */ - MobileCenterLog.debug(LOG_TAG, "Download was successful for id=" + mDownloadId + " uri=" + uriForDownloadedFile); - Intent intent = getInstallIntent(uriForDownloadedFile); - boolean installerFound = false; - if (intent.resolveActivity(mContext.getPackageManager()) == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - Cursor cursor = downloadManager.query(new DownloadManager.Query().setFilterById(mDownloadId)); - if (cursor != null) { - if (cursor.moveToNext()) { - uriForDownloadedFile = getFileUriOnOldDevices(cursor); - intent = getInstallIntent(uriForDownloadedFile); - installerFound = intent.resolveActivity(mContext.getPackageManager()) != null; - } - cursor.close(); - } - } - } else { - installerFound = true; - } - if (!installerFound) { - MobileCenterLog.error(LOG_TAG, "Installer not found"); - completeWorkflow(this); - return null; + try { + Cursor cursor = downloadManager.query(new DownloadManager.Query().setFilterById(mDownloadId)); + if (cursor == null) { + throw new NoSuchElementException(); } - - /* Exit check point. */ - Activity activity; try { - activity = getForegroundActivityWithStateCheck(this); - } catch (IllegalStateException e) { - - /* If we were canceled, exit now. */ - return null; - } - - /* If foreground, execute now, otherwise post notification. */ - String uri = uriForDownloadedFile.toString(); - if (activity != null) { - - /* This start call triggers strict mode violation in U.I. thread so it needs to be done here, and we can't synchronize anymore... */ - MobileCenterLog.debug(LOG_TAG, "Application is in foreground, launch install UI now."); - activity.startActivity(intent); - completeWorkflow(this); - } else { - - /* Remember we have a download ready. */ - MobileCenterLog.debug(LOG_TAG, "Application is in background, post a notification."); + if (!cursor.moveToFirst()) { + throw new NoSuchElementException(); + } + int status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)); + if (status == DownloadManager.STATUS_FAILED) { + throw new IllegalStateException(); + } + if (status != DownloadManager.STATUS_SUCCESSFUL) { + markDownloadStillInProgress(this); + return null; + } - /* And notify. */ - int icon; - try { - ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(mContext.getPackageName(), 0); - icon = applicationInfo.icon; - } catch (PackageManager.NameNotFoundException e) { - MobileCenterLog.error(LOG_TAG, "Could not get application icon", e); + /* Build install intent. */ + String localUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)); + MobileCenterLog.debug(LOG_TAG, "Download was successful for id=" + mDownloadId + " uri=" + localUri); + Intent intent = getInstallIntent(Uri.parse(localUri)); + boolean installerFound = false; + if (intent.resolveActivity(mContext.getPackageManager()) == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + intent = getInstallIntent(getFileUriOnOldDevices(cursor)); + installerFound = intent.resolveActivity(mContext.getPackageManager()) != null; + } + } else { + installerFound = true; + } + if (!installerFound) { + MobileCenterLog.error(LOG_TAG, "Installer not found"); completeWorkflow(this); return null; } - Notification.Builder builder = new Notification.Builder(mContext) - .setTicker(mContext.getString(R.string.mobile_center_updates_download_successful_notification_title)) - .setContentTitle(mContext.getString(R.string.mobile_center_updates_download_successful_notification_title)) - .setContentText(mContext.getString(R.string.mobile_center_updates_download_successful_notification_message)) - .setSmallIcon(icon) - .setContentIntent(PendingIntent.getActivities(mContext, 0, new Intent[]{intent}, 0)); - Notification notification; - notification = buildNotification(builder); - notification.flags |= Notification.FLAG_AUTO_CANCEL; - notifyDownload(mContext, this, notification, uri); + + /* Check if a should install now. */ + if (!notifyDownload(mContext, this, intent)) { + + /* + * This start call triggers strict mode in U.I. thread so it + * needs to be done here without synchronizing + * (not to block methods waiting on synchronized on U.I. thread) + * so yes we could launch install and SDK being disabled... + * + * This corner case cannot be avoided without triggering + * strict mode exception. + */ + MobileCenterLog.info(LOG_TAG, "Show install UI now."); + mContext.startActivity(intent); + completeWorkflow(this); + } + } finally { + cursor.close(); } - } else { + } catch (RuntimeException e) { MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + mDownloadId); completeWorkflow(this); } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index e19111be9f..05f242eabb 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -16,6 +16,7 @@ import org.junit.Before; import org.junit.Rule; +import org.junit.rules.Timeout; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -47,6 +48,12 @@ public class AbstractUpdatesTest { @Rule public PowerMockRule mPowerMockRule = new PowerMockRule(); + /** + * Use a timeout to fail test if deadlocks happen due to a code change. + */ + @Rule + public Timeout mGlobalTimeout = Timeout.seconds(10); + @Mock Context mContext; diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverIgnoreIntentTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java similarity index 81% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverIgnoreIntentTest.java rename to sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java index 1947e0730a..aff1d5bcde 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadCompletionReceiverIgnoreIntentTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java @@ -16,7 +16,7 @@ @RunWith(PowerMockRunner.class) @PrepareForTest(Updates.class) -public class DownloadCompletionReceiverIgnoreIntentTest { +public class DownloadManagerReceiverIgnoreIntentTest { @Test public void invalidIntent() { @@ -24,9 +24,9 @@ public void invalidIntent() { when(Updates.getInstance()).thenReturn(mock(Updates.class)); Intent clickIntent = mock(Intent.class); when(clickIntent.getAction()).thenReturn(Intent.ACTION_ANSWER); - new DownloadCompletionReceiver().onReceive(mock(Context.class), clickIntent); + new DownloadManagerReceiver().onReceive(mock(Context.class), clickIntent); when(clickIntent.getAction()).thenReturn(null); - new DownloadCompletionReceiver().onReceive(mock(Context.class), clickIntent); + new DownloadManagerReceiver().onReceive(mock(Context.class), clickIntent); verifyStatic(never()); Updates.getInstance(); } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java index 34f68ef6a8..e340696246 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java @@ -31,7 +31,7 @@ import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_RELEASE_HASH; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REQUEST_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.updates.UpdateConstants.UPDATE_SETUP_PATH_FORMAT; @@ -106,7 +106,7 @@ public void storeTokenBeforeStart() throws Exception { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); @@ -156,7 +156,7 @@ public void happyPathUntilHangingCall() throws Exception { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); verify(httpClient).callAsync(argThat(new ArgumentMatcher() { @@ -179,7 +179,7 @@ public boolean matches(Object argument) { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verify(httpClient).callAsync(argThat(new ArgumentMatcher() { @Override @@ -217,7 +217,7 @@ public boolean matches(Object argument) { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verify(httpClient).callAsync(argThat(new ArgumentMatcher() { @Override @@ -326,7 +326,7 @@ public void disableBeforeStoreToken() { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Since after disabling once, the request id was deleted we can enable/disable it will also ignore the request. */ Updates.setEnabled(true); @@ -394,7 +394,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify on failure we complete workflow. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* After that if we resume app nothing happens. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -428,7 +428,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify on failure we complete workflow. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* After that if we resume app nothing happens. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -472,13 +472,13 @@ public void run() { /* Disable before it fails. */ Updates.setEnabled(false); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); beforeSemaphore.release(); afterSemaphore.acquireUninterruptibly(); /* Verify complete workflow call ignored. i.e. no more call to delete the state. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* After that if we resume app nothing happens. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -522,13 +522,13 @@ public void run() { /* Disable before it succeeds. */ Updates.setEnabled(false); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); beforeSemaphore.release(); afterSemaphore.acquireUninterruptibly(); /* Verify complete workflow call skipped. i.e. no more call to delete the state. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* After that if we resume app nothing happens. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java index afe1d42af9..3c0bd1b1cd 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java @@ -23,7 +23,7 @@ import java.util.HashMap; import java.util.concurrent.Semaphore; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; @@ -77,7 +77,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify on failure we complete workflow. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verify(mDialogBuilder, never()).create(); verify(mDialog, never()).show(); @@ -116,7 +116,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify on failure we complete workflow. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verify(mDialogBuilder, never()).create(); verify(mDialog, never()).show(); @@ -155,7 +155,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify on failure we complete workflow. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verify(mDialogBuilder, never()).create(); verify(mDialog, never()).show(); @@ -367,7 +367,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -418,7 +418,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -489,7 +489,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -506,7 +506,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(httpClient, times(2)).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verify(mDialog).show(); verifyStatic(times(2)); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Disable: it will prompt again as we clear storage. */ Updates.setEnabled(false); @@ -547,7 +547,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Disable. */ Updates.setEnabled(false); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Cancel it. */ cancelListener.getValue().onCancel(mDialog); @@ -559,7 +559,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); } @Test @@ -615,7 +615,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Disable. */ Updates.setEnabled(false); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Ignore it. */ clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_NEGATIVE); @@ -630,7 +630,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_IGNORED_RELEASE_ID); verifyStatic(never()); @@ -672,7 +672,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Disable. */ Updates.setEnabled(false); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Click on download. */ clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); @@ -687,7 +687,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no download scheduled. */ verifyStatic(never()); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java index 132ab48171..0ee8c916e4 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java @@ -12,10 +12,12 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Build; +import android.support.annotation.NonNull; import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.http.HttpClient; @@ -34,6 +36,7 @@ import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.internal.util.reflection.Whitebox; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -43,10 +46,15 @@ import static android.app.DownloadManager.EXTRA_DOWNLOAD_ID; import static android.content.Context.NOTIFICATION_SERVICE; +import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_COMPLETED; +import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_ENQUEUED; +import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_NOTIFIED; import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_TIME; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; @@ -95,11 +103,11 @@ public class UpdatesDownloadTest extends AbstractUpdatesTest { private AtomicReference mDownloadTask; - private Semaphore mCompletionBeforeSemaphore; + private Semaphore mCheckDownloadBeforeSemaphore; - private Semaphore mCompletionAfterSemaphore; + private Semaphore mCheckDownloadAfterSemaphore; - private AtomicReference mCompletionTask; + private AtomicReference mCompletionTask; @Before public void setUpDownload() throws Exception { @@ -138,20 +146,20 @@ public Void answer(InvocationOnMock invocation) throws Throwable { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(StorageHelper.PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn((String) invocation.getArguments()[1]); + when(StorageHelper.PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn((Integer) invocation.getArguments()[1]); return null; } }).when(StorageHelper.PreferencesStorage.class); - StorageHelper.PreferencesStorage.putString(eq(PREFERENCE_KEY_DOWNLOAD_URI), anyString()); + StorageHelper.PreferencesStorage.putInt(eq(PREFERENCE_KEY_DOWNLOAD_STATE), anyInt()); doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(StorageHelper.PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn(null); + when(StorageHelper.PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn(DOWNLOAD_STATE_COMPLETED); return null; } }).when(StorageHelper.PreferencesStorage.class); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Mock everything that triggers a download. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -221,28 +229,28 @@ public Updates.RemoveDownloadTask answer(InvocationOnMock invocation) throws Thr }); /* Mock download completion async task. */ - mCompletionBeforeSemaphore = new Semaphore(0); - mCompletionAfterSemaphore = new Semaphore(0); + mCheckDownloadBeforeSemaphore = new Semaphore(0); + mCheckDownloadAfterSemaphore = new Semaphore(0); mCompletionTask = new AtomicReference<>(); - when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { @Override public boolean matches(Object argument) { - return argument instanceof Updates.ProcessDownloadCompletionTask; + return argument instanceof Updates.CheckDownloadTask; } - }), Mockito.anyVararg())).then(new Answer() { + }), Mockito.anyVararg())).then(new Answer() { @Override - public Updates.ProcessDownloadCompletionTask answer(InvocationOnMock invocation) throws Throwable { - final Updates.ProcessDownloadCompletionTask task = spy((Updates.ProcessDownloadCompletionTask) invocation.getArguments()[1]); + public Updates.CheckDownloadTask answer(InvocationOnMock invocation) throws Throwable { + final Updates.CheckDownloadTask task = spy((Updates.CheckDownloadTask) invocation.getArguments()[1]); mCompletionTask.set(task); new Thread() { @Override public void run() { - mCompletionBeforeSemaphore.acquireUninterruptibly(); + mCheckDownloadBeforeSemaphore.acquireUninterruptibly(); task.doInBackground(); - mCompletionAfterSemaphore.release(); + mCheckDownloadAfterSemaphore.release(); } }.start(); return task; @@ -260,9 +268,60 @@ private void waitDownloadTask() { mDownloadAfterSemaphore.acquireUninterruptibly(); } - private void waitCompletionTask() { - mCompletionBeforeSemaphore.release(); - mCompletionAfterSemaphore.acquireUninterruptibly(); + private void waitCheckDownloadTask() { + mCheckDownloadBeforeSemaphore.release(); + mCheckDownloadAfterSemaphore.acquireUninterruptibly(); + } + + private void completeDownload() { + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadManagerReceiver().onReceive(mContext, completionIntent); + } + + @NonNull + private Cursor mockSuccessCursor() { + Cursor cursor = mock(Cursor.class); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); + when(cursor.moveToFirst()).thenReturn(true); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)).thenReturn(0); + when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_SUCCESSFUL); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)).thenReturn(1); + when(cursor.getString(1)).thenReturn("content://downloads/all_downloads/" + DOWNLOAD_ID); + return cursor; + } + + @NonNull + private Notification.Builder mockNotificationBuilderChain() throws Exception { + Notification.Builder notificationBuilder = mock(Notification.Builder.class); + whenNew(Notification.Builder.class).withAnyArguments().thenReturn(notificationBuilder); + when(notificationBuilder.setTicker(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentTitle(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentText(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setSmallIcon(anyInt())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentIntent(any(PendingIntent.class))).thenReturn(notificationBuilder); + return notificationBuilder; + } + + @NonNull + private Intent mockInstallIntent() throws Exception { + Intent installIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); + when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + return installIntent; + } + + private void restartActivity() { + Updates.getInstance().onActivityStopped(mFirstActivity); + Updates.getInstance().onActivityDestroyed(mFirstActivity); + Updates.getInstance().onActivityCreated(mFirstActivity, null); + Updates.getInstance().onActivityResumed(mFirstActivity); + } + + private void restartProcessAndSdk() { + Updates.unsetInstance(); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); } @Test @@ -277,7 +336,7 @@ public void startDownloadThenDisable() throws Exception { verifyStatic(); PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, DOWNLOAD_ID); verifyStatic(); - PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, ""); + PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_ENQUEUED); /* Pause/resume should do nothing excepting mentioning progress. */ verify(mDialog).show(); @@ -290,10 +349,10 @@ public void startDownloadThenDisable() throws Exception { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verify(mDownloadTask.get()).cancel(true); verify(mDownloadManager).remove(DOWNLOAD_ID); - verify(mNotificationManager).cancel(Updates.getNotificationId()); + verify(mNotificationManager, never()).notify(anyInt(), any(Notification.class)); } @Test @@ -307,7 +366,7 @@ public void disableWhileStartingDownload() throws Exception { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verify(mDownloadTask.get()).cancel(true); verify(mDownloadManager).enqueue(mDownloadRequest); verifyNew(DownloadManager.Request.class).withArguments(mDownloadUrl); @@ -317,7 +376,7 @@ public void disableWhileStartingDownload() throws Exception { verifyStatic(never()); PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, DOWNLOAD_ID); verifyStatic(never()); - PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, ""); + PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_STATE, ""); verifyZeroInteractions(mNotificationManager); } @@ -328,25 +387,22 @@ public void disableWhileProcessingCompletion() throws Exception { waitDownloadTask(); /* Process download completion. */ - Intent intent = mock(Intent.class); - when(intent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(intent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, intent); + completeDownload(); /* Disable before completion. */ Updates.setEnabled(false); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - waitCompletionTask(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + waitCheckDownloadTask(); /* Verify cancellation. */ verify(mCompletionTask.get()).cancel(true); verify(mDownloadManager).remove(DOWNLOAD_ID); - verify(mNotificationManager).cancel(Updates.getNotificationId()); + verifyZeroInteractions(mNotificationManager); /* Check cleaned state only once, the completeWorkflow on failed download has to be ignored. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); } @Test @@ -356,17 +412,14 @@ public void failDownloadRestartNoLauncher() { waitDownloadTask(); /* Process download completion. */ - Intent intent = mock(Intent.class); - when(intent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(intent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, intent); + completeDownload(); /* Wait. Fails as we dont mock success uri. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Check failure processing. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Nothing should happen if just changing activities. */ Activity activity = mock(Activity.class); @@ -390,51 +443,42 @@ public void failDownloadRestartNoLauncher() { } @Test - public void successDownloadInstallerNotFoundCursorIsNull() { + public void downloadCursorNull() { /* Simulate async task. */ waitDownloadTask(); /* Process download completion. */ - Intent intent = mock(Intent.class); - when(intent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(intent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, intent); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); - when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(null); + completeDownload(); /* Simulate task. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Check we completed workflow without starting activity because installer not found. */ - verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verify(mContext, never()).startActivity(any(Intent.class)); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); } @Test - public void successDownloadInstallerNotFoundCursorEmpty() { + public void downloadCursorEmpty() { /* Simulate async task. */ waitDownloadTask(); /* Process download completion. */ - Intent intent = mock(Intent.class); - when(intent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(intent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, intent); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); + completeDownload(); Cursor cursor = mock(Cursor.class); when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); /* Simulate task. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Check we completed workflow without starting activity because installer not found. */ verify(cursor).close(); - verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verify(mContext, never()).startActivity(any(Intent.class)); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); } @Test @@ -444,23 +488,18 @@ public void successDownloadInstallerNotFoundEvenWithLocalFile() throws Exception waitDownloadTask(); /* Process download completion. */ - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); - Cursor cursor = mock(Cursor.class); - when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); - when(cursor.moveToNext()).thenReturn(true).thenReturn(false); + completeDownload(); + Cursor cursor = mockSuccessCursor(); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(mock(Intent.class)); /* Simulate task. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Check we completed workflow without starting activity because installer not found. */ verify(cursor).close(); - verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verify(mContext, never()).startActivity(any(Intent.class)); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); } @Test @@ -470,20 +509,19 @@ public void successDownloadInstallerNotFoundAfterNougat() throws Exception { waitDownloadTask(); /* Process download completion. */ - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); + completeDownload(); + Cursor cursor = mockSuccessCursor(); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(mock(Intent.class)); TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.N); /* Simulate task. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Check we completed workflow without starting activity because installer not found. */ - verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verify(cursor).close(); + verify(mContext, never()).startActivity(any(Intent.class)); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); } @Test @@ -493,14 +531,8 @@ public void disableWhileCompletingBeforeNougat() throws Exception { waitDownloadTask(); /* Process download completion. */ - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(mock(Uri.class)); - Cursor cursor = mock(Cursor.class); - when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); - when(cursor.moveToNext()).thenReturn(true).thenReturn(false); + completeDownload(); + Cursor cursor = mockSuccessCursor(); Intent installIntent = mock(Intent.class); whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(null).thenReturn(mock(ComponentName.class)); @@ -509,15 +541,14 @@ public void disableWhileCompletingBeforeNougat() throws Exception { Updates.setEnabled(false); /* Simulate task. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Check we completed workflow without starting activity because disabled. */ verify(cursor).close(); - verify(mFirstActivity, never()).startActivity(any(Intent.class)); + verify(mContext, never()).startActivity(any(Intent.class)); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - verify(mNotificationManager).cancel(Updates.getNotificationId()); - verifyNoMoreInteractions(mNotificationManager); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + verifyZeroInteractions(mNotificationManager); } @Test @@ -527,25 +558,122 @@ public void successInForeground() throws Exception { waitDownloadTask(); /* Process download completion. */ - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - Uri uri = mock(Uri.class); - when(uri.toString()).thenReturn("original"); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); - Intent installIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); - when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + completeDownload(); + Cursor cursor = mockSuccessCursor(); + Intent installIntent = mockInstallIntent(); /* Simulate task. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Verify start activity and complete workflow. */ - verify(mFirstActivity).startActivity(installIntent); + verify(mContext).startActivity(installIntent); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verifyNoMoreInteractions(mNotificationManager); + verify(cursor).close(); + } + + @Test + public void longFailingDownload() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Mock running cursor. */ + Cursor cursor = mock(Cursor.class); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); + when(cursor.moveToFirst()).thenReturn(true); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)).thenReturn(0); + when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_RUNNING); + + /* Restart launcher, nothing happens. */ + when(mFirstActivity.getPackageManager()).thenReturn(mPackageManager); + Intent launcherIntent = mock(Intent.class); + when(mPackageManager.getLaunchIntentForPackage(anyString())).thenReturn(launcherIntent); + ComponentName launcher = mock(ComponentName.class); + when(launcherIntent.resolveActivity(mPackageManager)).thenReturn(launcher); + when(launcher.getClassName()).thenReturn(mFirstActivity.getClass().getName()); + restartActivity(); + + /* Restart app process. Still nothing as background. */ + restartProcessAndSdk(); + + /* No download check yet. */ + verifyStatic(never()); + AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument instanceof Updates.CheckDownloadTask; + } + }), Mockito.anyVararg()); + + /* Foreground: check still in progress. */ + Updates.getInstance().onActivityResumed(mFirstActivity); + waitCheckDownloadTask(); + verifyStatic(); + AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument instanceof Updates.CheckDownloadTask; + } + }), Mockito.anyVararg()); + verify(cursor).close(); + + /* Restart launcher. */ + Updates.getInstance().onActivityPaused(mFirstActivity); + restartActivity(); + + /* Verify we don't run the check again. (Only once). */ + verifyStatic(); + AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument instanceof Updates.CheckDownloadTask; + } + }), Mockito.anyVararg()); + + /* Download eventually fails. */ + when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_FAILED); + completeDownload(); + + /* Simulate task. */ + waitCheckDownloadTask(); + + /* Verify we complete workflow on failure. */ + verify(mContext, never()).startActivity(any(Intent.class)); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + verifyZeroInteractions(mNotificationManager); + } + + @Test + public void disabledWhileCheckingDownloadOnRestart() { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Mock running cursor. */ + Cursor cursor = mock(Cursor.class); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); + when(cursor.moveToFirst()).thenReturn(true); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)).thenReturn(0); + when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_RUNNING); + + /* Restart app process. Still nothing as background. */ + restartProcessAndSdk(); + Updates.getInstance().onActivityResumed(mFirstActivity); + + /* Disabled before async task runs. */ + Updates.setEnabled(false); + + /* Run async task. */ + waitCheckDownloadTask(); + + /* Verify we don't mark download checked as in progress. */ + assertEquals(false, Whitebox.getInternalState(Updates.getInstance(), "mCheckedDownload")); } @Test @@ -555,13 +683,7 @@ public void startActivityButDisabledAfterCheckpoint() throws Exception { waitDownloadTask(); /* Process download completion. */ - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - Uri uri = mock(Uri.class); - when(uri.toString()).thenReturn("original"); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); + Cursor cursor = mockSuccessCursor(); final Intent installIntent = mock(Intent.class); whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); @@ -575,21 +697,22 @@ public Void answer(InvocationOnMock invocation) throws Throwable { disabledLock.acquireUninterruptibly(); return null; } - }).when(mFirstActivity).startActivity(installIntent); + }).when(mContext).startActivity(installIntent); /* Disable while calling startActivity... */ - mCompletionBeforeSemaphore.release(); + completeDownload(); + mCheckDownloadBeforeSemaphore.release(); beforeStartingActivityLock.acquireUninterruptibly(); Updates.setEnabled(false); disabledLock.release(); - mCompletionAfterSemaphore.acquireUninterruptibly(); + mCheckDownloadAfterSemaphore.acquireUninterruptibly(); /* Verify start activity and complete workflow skipped, e.g. clean behavior happened only once. */ - verify(mFirstActivity).startActivity(installIntent); + verify(mContext).startActivity(installIntent); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - verify(mNotificationManager).cancel(Updates.getNotificationId()); - verifyNoMoreInteractions(mNotificationManager); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + verifyZeroInteractions(mNotificationManager); + verify(cursor).close(); } @Test @@ -599,16 +722,9 @@ public void failsToGetNotificationIcon() throws Exception { waitDownloadTask(); /* Process download completion. */ - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - Uri uri = mock(Uri.class); - when(uri.toString()).thenReturn("original"); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); - Intent installIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); - when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + completeDownload(); + Cursor cursor = mockSuccessCursor(); + Intent installIntent = mockInstallIntent(); /* In background. */ Updates.getInstance().onActivityPaused(mFirstActivity); @@ -617,78 +733,21 @@ public void failsToGetNotificationIcon() throws Exception { when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenThrow(new PackageManager.NameNotFoundException()); /* Simulate task. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Verify complete workflow with no notification. */ - verify(mFirstActivity, never()).startActivity(installIntent); + verify(mContext, never()).startActivity(installIntent); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verifyNoMoreInteractions(mNotificationManager); - } - - @Test - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - public void disableRightBeforeNotifying() throws Exception { - - /* Simulate async task. */ - waitDownloadTask(); - - /* Process download completion. */ - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - Uri uri = mock(Uri.class); - when(uri.toString()).thenReturn("original"); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); - Intent installIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); - when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); - - /* In background. */ - Updates.getInstance().onActivityPaused(mFirstActivity); - - /* Mock notification. */ - when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); - TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.JELLY_BEAN); - Notification.Builder notificationBuilder = mock(Notification.Builder.class); - whenNew(Notification.Builder.class).withAnyArguments().thenReturn(notificationBuilder); - when(notificationBuilder.setTicker(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentTitle(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentText(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setSmallIcon(anyInt())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentIntent(any(PendingIntent.class))).thenReturn(notificationBuilder); - final Semaphore beforeNotifying = new Semaphore(0); - final Semaphore disabledLock = new Semaphore(0); - when(notificationBuilder.build()).thenAnswer(new Answer() { - - @Override - public Notification answer(InvocationOnMock invocation) throws Throwable { - beforeNotifying.release(); - disabledLock.acquireUninterruptibly(); - return mock(Notification.class); - } - }); - - /* Disable while preparing notification... */ - mCompletionBeforeSemaphore.release(); - beforeNotifying.acquireUninterruptibly(); - Updates.setEnabled(false); - disabledLock.release(); - mCompletionAfterSemaphore.acquireUninterruptibly(); - - /* Verify no notification and complete workflow skipped, e.g. clean behavior happened only once. */ - verify(mFirstActivity, never()).startActivity(installIntent); - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - verify(mNotificationManager, never()).notify(anyInt(), any(Notification.class)); + verify(cursor).close(); } @Test @PrepareForTest(Uri.class) @SuppressWarnings("deprecation") @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - public void notifyThenRestartApp() throws Exception { + public void notifyThenRestartAppTwice() throws Exception { /* Simulate async task. */ waitDownloadTask(); @@ -698,22 +757,15 @@ public void notifyThenRestartApp() throws Exception { Intent completionIntent = mock(Intent.class); when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(404L); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - waitCompletionTask(); - verify(mDownloadManager, never()).getUriForDownloadedFile(anyLong()); + new DownloadManagerReceiver().onReceive(mContext, completionIntent); + waitCheckDownloadTask(); + verify(mDownloadManager, never()).query(any(DownloadManager.Query.class)); } /* Process download completion with the real download identifier. */ - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - Uri uri = mock(Uri.class); - when(uri.toString()).thenReturn("original"); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(uri); - Intent installIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); - when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + completeDownload(); + Cursor cursor = mockSuccessCursor(); + Intent installIntent = mockInstallIntent(); /* In background. */ Updates.getInstance().onActivityPaused(mFirstActivity); @@ -721,26 +773,21 @@ public void notifyThenRestartApp() throws Exception { /* Mock notification. */ when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.JELLY_BEAN); - Notification.Builder notificationBuilder = mock(Notification.Builder.class); - whenNew(Notification.Builder.class).withAnyArguments().thenReturn(notificationBuilder); + Notification.Builder notificationBuilder = mockNotificationBuilderChain(); when(notificationBuilder.build()).thenReturn(mock(Notification.class)); - when(notificationBuilder.setTicker(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentTitle(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentText(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setSmallIcon(anyInt())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentIntent(any(PendingIntent.class))).thenReturn(notificationBuilder); /* Simulate task. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Verify notification. */ - verify(mFirstActivity, never()).startActivity(installIntent); + verify(mContext, never()).startActivity(installIntent); verifyStatic(); - PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, "original"); + PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_NOTIFIED); verify(notificationBuilder).build(); verify(notificationBuilder, never()).getNotification(); verify(mNotificationManager).notify(eq(Updates.getNotificationId()), any(Notification.class)); verifyNoMoreInteractions(mNotificationManager); + verify(cursor).close(); /* Launch app should pop install U.I. and cancel notification. */ when(mFirstActivity.getPackageManager()).thenReturn(mPackageManager); @@ -749,30 +796,25 @@ public void notifyThenRestartApp() throws Exception { ComponentName launcher = mock(ComponentName.class); when(launcherIntent.resolveActivity(mPackageManager)).thenReturn(launcher); when(launcher.getClassName()).thenReturn(mFirstActivity.getClass().getName()); - mockStatic(Uri.class); - when(Uri.parse("original")).thenReturn(uri); - Updates.getInstance().onActivityStopped(mFirstActivity); - Updates.getInstance().onActivityDestroyed(mFirstActivity); - Updates.getInstance().onActivityCreated(mFirstActivity, null); - Updates.getInstance().onActivityResumed(mFirstActivity); + restartActivity(); - /* Verify. */ - verify(mFirstActivity).startActivity(installIntent); + /* Wait again. */ + waitCheckDownloadTask(); + + /* Verify U.I shown after restart and workflow completed. */ + verify(mContext).startActivity(installIntent); verify(mNotificationManager).cancel(Updates.getNotificationId()); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); - /* Keep download. */ + /* Verify however downloaded file was kept. */ verifyStatic(never()); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verify(mDownloadManager, never()).remove(DOWNLOAD_ID); - /* Verify second download (restart app) cleans first one. */ + /* Verify second download (restart app again) cleans first one. */ when(mDownloadManager.enqueue(mDownloadRequest)).thenReturn(DOWNLOAD_ID + 1); - Updates.getInstance().onActivityStopped(mFirstActivity); - Updates.getInstance().onActivityDestroyed(mFirstActivity); - Updates.getInstance().onActivityCreated(mFirstActivity, null); - Updates.getInstance().onActivityResumed(mFirstActivity); + restartActivity(); ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); verify(mDialogBuilder, times(2)).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); @@ -801,76 +843,143 @@ public void notifyThenRestartThenInstallerFails() throws Exception { Updates.unsetInstance(); /* Process download completion. */ - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadCompletionReceiver().onReceive(mContext, completionIntent); - Uri originalUri = mock(Uri.class); - when(originalUri.toString()).thenReturn("original"); - when(mDownloadManager.getUriForDownloadedFile(DOWNLOAD_ID)).thenReturn(originalUri); - Cursor cursor = mock(Cursor.class); - when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); - when(cursor.moveToNext()).thenReturn(true).thenReturn(false); - when(cursor.getString(anyInt())).thenReturn("localFile"); - mockStatic(Uri.class); - Uri localFileUri = mock(Uri.class); - when(Uri.parse("file://localFile")).thenReturn(localFileUri); - when(localFileUri.toString()).thenReturn("file://localFile"); + completeDownload(); + + /* Mock old device URI. */ + Cursor cursor = mockSuccessCursor(); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME)).thenReturn(2); Intent installIntent = mock(Intent.class); whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(null).thenReturn(mock(ComponentName.class)); /* Mock notification. */ when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); - Notification.Builder notificationBuilder = mock(Notification.Builder.class); - whenNew(Notification.Builder.class).withAnyArguments().thenReturn(notificationBuilder); + Notification.Builder notificationBuilder = mockNotificationBuilderChain(); when(notificationBuilder.getNotification()).thenReturn(mock(Notification.class)); - when(notificationBuilder.setTicker(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentTitle(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentText(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setSmallIcon(anyInt())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentIntent(any(PendingIntent.class))).thenReturn(notificationBuilder); /* Simulate task. */ - waitCompletionTask(); + waitCheckDownloadTask(); /* Verify notification. */ - verify(mFirstActivity, never()).startActivity(installIntent); + verify(mContext, never()).startActivity(installIntent); verifyStatic(); - PreferencesStorage.putString(PREFERENCE_KEY_DOWNLOAD_URI, "file://localFile"); + PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_NOTIFIED); verify(mNotificationManager).notify(eq(Updates.getNotificationId()), any(Notification.class)); verifyNoMoreInteractions(mNotificationManager); + verify(cursor).getString(2); + verify(cursor).close(); /* Restart app should pop install U.I. and cancel notification and pop a new dialog then a new download. */ - doThrow(new ActivityNotFoundException()).when(mFirstActivity).startActivity(installIntent); - when(mDownloadManager.enqueue(mDownloadRequest)).thenReturn(DOWNLOAD_ID + 1); - Updates.getInstance().onStarted(mContext, "", mock(Channel.class)); + doThrow(new ActivityNotFoundException()).when(mContext).startActivity(installIntent); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); Updates.getInstance().onActivityResumed(mFirstActivity); - /* Verify. */ - verify(mFirstActivity).startActivity(installIntent); + /* Wait download manager query. */ + waitCheckDownloadTask(); + + /* Verify workflow completed even on failure to show install U.I. */ + verify(mContext).startActivity(installIntent); verify(mNotificationManager).cancel(Updates.getNotificationId()); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); - verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + verifyStatic(never()); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); - verify(mDownloadManager).remove(DOWNLOAD_ID); + } - /* Verify workflow restarted right after failure. */ - ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder, times(2)).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); - clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + @Test + @SuppressWarnings("deprecation") + public void restartDownloadCheckIsLongEnoughToAppCanGoBackgroundAgain() throws Exception { + + /* Simulate async task. */ waitDownloadTask(); - verifyStatic(); - PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, DOWNLOAD_ID + 1); + Updates.getInstance().onActivityPaused(mFirstActivity); - /* Check no duplicate cleaning tasks, i.e. happened only once. */ - verify(mNotificationManager).cancel(Updates.getNotificationId()); - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + /* Process download completion to notify. */ + completeDownload(); + mockSuccessCursor(); + Intent installIntent = mockInstallIntent(); + when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); + Notification.Builder notificationBuilder = mockNotificationBuilderChain(); + when(notificationBuilder.getNotification()).thenReturn(mock(Notification.class)); + + /* Verify. */ + waitCheckDownloadTask(); + verify(mNotificationManager).notify(anyInt(), any(Notification.class)); + verify(mContext, never()).startActivity(installIntent); + + /* + * Restart app, even if app goes background while checking state, we must show U.I. as we + * already notified. + */ + restartProcessAndSdk(); + Updates.getInstance().onActivityResumed(mFirstActivity); + Updates.getInstance().onActivityPaused(mFirstActivity); + waitCheckDownloadTask(); + verify(mNotificationManager).notify(anyInt(), any(Notification.class)); + verify(mContext).startActivity(installIntent); + } + + @Test + @SuppressWarnings("deprecation") + public void dontShowInstallUiIfUpgradedAfterNotification() throws Exception { + + /* Mock download time storage. */ + doAnswer(new Answer() { + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_TIME)).thenReturn((Long) invocation.getArguments()[1]); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.putLong(eq(PREFERENCE_KEY_DOWNLOAD_TIME), anyLong()); + + /* Simulate async task. */ + waitDownloadTask(); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + PreferencesStorage.putLong(eq(PREFERENCE_KEY_DOWNLOAD_TIME), anyLong()); + + /* Mock download completion to notify. */ + mockSuccessCursor(); + Intent installIntent = mockInstallIntent(); + when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); + Notification.Builder notificationBuilder = mockNotificationBuilderChain(); + when(notificationBuilder.getNotification()).thenReturn(mock(Notification.class)); + + /* Make notification happen. */ + Updates.getInstance().onActivityPaused(mFirstActivity); + completeDownload(); + waitCheckDownloadTask(); + + /* Verify. */ + verify(mNotificationManager).notify(anyInt(), any(Notification.class)); + verify(mContext, never()).startActivity(installIntent); + + /* Restart app after upgrade, discard download and check update again. */ + PackageInfo packageInfo = mock(PackageInfo.class); + packageInfo.lastUpdateTime = Long.MAX_VALUE; + when(mPackageManager.getPackageInfo(mContext.getPackageName(), 0)).thenReturn(packageInfo); + restartProcessAndSdk(); + Updates.getInstance().onActivityResumed(mFirstActivity); verify(mDownloadManager).remove(DOWNLOAD_ID); + + /* Verify new release checked (for example what we installed was something else than the upgrade. */ + verify(mDialog, times(2)).show(); + } + + @Test + @SuppressWarnings("deprecation") + public void failToCheckLastUpdateTimeOnRestart() throws PackageManager.NameNotFoundException { + + /* Make the package manager fails on restart after download started. */ + waitDownloadTask(); + when(mPackageManager.getPackageInfo(mContext.getPackageName(), 0)).thenThrow(new PackageManager.NameNotFoundException()); + restartProcessAndSdk(); + Updates.getInstance().onActivityResumed(mFirstActivity); + + /* Verify workflow completed on failure. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); } @After diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java index 03ccc1c2b4..457af61274 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java @@ -27,7 +27,7 @@ public void resumeAppBeforeStart() throws Exception { Context context = mock(Context.class); Intent startIntent = mock(Intent.class); whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); - new DownloadCompletionReceiver().onReceive(context, clickIntent); + new DownloadManagerReceiver().onReceive(context, clickIntent); verify(context).startActivity(startIntent); verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } @@ -40,7 +40,7 @@ public void resumeAfterBeforeStartButBackground() throws Exception { Updates.getInstance().onStarted(context, "", mock(Channel.class)); Intent startIntent = mock(Intent.class); whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); - new DownloadCompletionReceiver().onReceive(context, clickIntent); + new DownloadManagerReceiver().onReceive(context, clickIntent); verify(context).startActivity(startIntent); verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } @@ -55,12 +55,12 @@ public void resumeForegroundThenPause() throws Exception { Intent startIntent = mock(Intent.class); whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); Updates.getInstance().onActivityResumed(mock(Activity.class)); - new DownloadCompletionReceiver().onReceive(context, clickIntent); + new DownloadManagerReceiver().onReceive(context, clickIntent); verify(context, never()).startActivity(startIntent); /* Then pause and test again. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); - new DownloadCompletionReceiver().onReceive(context, clickIntent); + new DownloadManagerReceiver().onReceive(context, clickIntent); verify(context).startActivity(startIntent); verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java index be98aa8926..f604d45d7a 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java @@ -25,7 +25,7 @@ import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_URI; +import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.mockito.Matchers.any; @@ -117,7 +117,7 @@ public void cancelDialogWithBack() { /* Verify. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -137,7 +137,7 @@ public void cancelDialogWithButton() { /* Verify. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -152,7 +152,7 @@ public void disableBeforeCancelWithBack() { /* Disable. */ Updates.setEnabled(false); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Cancel. */ ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); @@ -162,7 +162,7 @@ public void disableBeforeCancelWithBack() { /* Verify cancel did nothing more. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -177,7 +177,7 @@ public void disableBeforeCancelWithButton() { /* Disable. */ Updates.setEnabled(false); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Cancel. */ ArgumentCaptor cancelListener = ArgumentCaptor.forClass(DialogInterface.OnCancelListener.class); @@ -187,7 +187,7 @@ public void disableBeforeCancelWithButton() { /* Verify cancel did nothing more. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -296,7 +296,7 @@ public void clickSettingsFailsToNavigate() throws Exception { /* Verify failure is treated as a cancel dialog. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); @@ -311,7 +311,7 @@ public void disableThenClickSettingsThenFailsToNavigate() throws Exception { /* Disable. */ Updates.setEnabled(false); verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Click settings. */ Intent intent = mock(Intent.class); @@ -327,7 +327,7 @@ public void disableThenClickSettingsThenFailsToNavigate() throws Exception { /* Verify cleaning behavior happened only once, e.g. completeWorkflow skipped. */ verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_URI); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); From 22a69bcd9880ee980ccb9467ee1390f62b44e2c4 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 21 Feb 2017 19:34:29 -0800 Subject: [PATCH 070/142] Fix disabling before check download completes --- .../azure/mobile/updates/Updates.java | 8 +- .../mobile/updates/UpdatesDownloadTest.java | 145 ++++++++++++++---- 2 files changed, 119 insertions(+), 34 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index ef2ba1e4ac..2d0febe012 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -581,6 +581,7 @@ private synchronized void completeWorkflow(CheckDownloadTask task) { */ private synchronized void cancelNotification(Context context) { if (getStoredDownloadState() == DOWNLOAD_STATE_NOTIFIED) { + MobileCenterLog.debug(LOG_TAG, "Delete notification"); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(getNotificationId()); } @@ -1069,7 +1070,6 @@ private synchronized void markDownloadStillInProgress(CheckDownloadTask task) { * Remove a previously downloaded file and any notification. */ private synchronized void removeDownload(long downloadId) { - MobileCenterLog.debug(LOG_TAG, "Delete previous notification downloadId=" + downloadId); cancelNotification(mContext); AsyncTaskUtils.execute(LOG_TAG, new RemoveDownloadTask(), downloadId); } @@ -1084,10 +1084,8 @@ class RemoveDownloadTask extends AsyncTask { protected Void doInBackground(Long... params) { /* This special cleanup task does not require any cancellation on state change as a previous download will never be reused. */ - Long downloadId = params[0]; - MobileCenterLog.debug(LOG_TAG, "Delete previous download downloadId=" + downloadId); DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); - downloadManager.remove(downloadId); + downloadManager.remove(params[0]); return null; } } @@ -1173,7 +1171,7 @@ protected Void doInBackground(Void... params) { /* Check intent data is what we expected. */ long expectedDownloadId = getStoredDownloadId(); - if (expectedDownloadId >= 0 && expectedDownloadId != mDownloadId) { + if (expectedDownloadId == INVALID_DOWNLOAD_IDENTIFIER || expectedDownloadId != mDownloadId) { MobileCenterLog.debug(LOG_TAG, "Ignoring download identifier we didn't expect, id=" + mDownloadId); return null; } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java index 0ee8c916e4..1939c3fbac 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java @@ -26,7 +26,6 @@ import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.test.TestUtils; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; -import com.microsoft.azure.mobile.utils.storage.StorageHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import org.junit.After; @@ -41,6 +40,8 @@ import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicReference; @@ -70,11 +71,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.doAnswer; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyNew; import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.when; import static org.powermock.api.mockito.PowerMockito.whenNew; @PrepareForTest(AsyncTaskUtils.class) @@ -128,38 +129,38 @@ public void setUpDownload() throws Exception { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(StorageHelper.PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn((Long) invocation.getArguments()[1]); + when(PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn((Long) invocation.getArguments()[1]); return null; } - }).when(StorageHelper.PreferencesStorage.class); - StorageHelper.PreferencesStorage.putLong(eq(PREFERENCE_KEY_DOWNLOAD_ID), anyLong()); + }).when(PreferencesStorage.class); + PreferencesStorage.putLong(eq(PREFERENCE_KEY_DOWNLOAD_ID), anyLong()); doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(StorageHelper.PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn(INVALID_DOWNLOAD_IDENTIFIER); + when(PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn(INVALID_DOWNLOAD_IDENTIFIER); return null; } - }).when(StorageHelper.PreferencesStorage.class); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + }).when(PreferencesStorage.class); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(StorageHelper.PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn((Integer) invocation.getArguments()[1]); + when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn((Integer) invocation.getArguments()[1]); return null; } - }).when(StorageHelper.PreferencesStorage.class); - StorageHelper.PreferencesStorage.putInt(eq(PREFERENCE_KEY_DOWNLOAD_STATE), anyInt()); + }).when(PreferencesStorage.class); + PreferencesStorage.putInt(eq(PREFERENCE_KEY_DOWNLOAD_STATE), anyInt()); doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(StorageHelper.PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn(DOWNLOAD_STATE_COMPLETED); + when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn(DOWNLOAD_STATE_COMPLETED); return null; } - }).when(StorageHelper.PreferencesStorage.class); - StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + }).when(PreferencesStorage.class); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Mock everything that triggers a download. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -525,30 +526,35 @@ public void successDownloadInstallerNotFoundAfterNougat() throws Exception { } @Test - public void disableWhileCompletingBeforeNougat() throws Exception { + public void disableDuringDownload() throws Exception { /* Simulate async task. */ waitDownloadTask(); - /* Process download completion. */ - completeDownload(); - Cursor cursor = mockSuccessCursor(); - Intent installIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); - when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(null).thenReturn(mock(ComponentName.class)); - - /* Disable before task run. */ + /* Disable. */ Updates.setEnabled(false); + /* We receive intent from download manager when we remove download. */ + verify(mDownloadManager).remove(DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + completeDownload(); + /* Simulate task. */ waitCheckDownloadTask(); /* Check we completed workflow without starting activity because disabled. */ - verify(cursor).close(); verify(mContext, never()).startActivity(any(Intent.class)); + verifyZeroInteractions(mNotificationManager); + + /* Verify state deleted only at disable time. */ verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); - verifyZeroInteractions(mNotificationManager); + + /* Verify enabling triggers update dialog again. */ + verify(mDialog).show(); + Updates.setEnabled(true); + verify(mDialog, times(2)).show(); } @Test @@ -650,7 +656,7 @@ public boolean matches(Object argument) { } @Test - public void disabledWhileCheckingDownloadOnRestart() { + public void disabledWhileCheckingDownloadOnRestart() throws BrokenBarrierException, InterruptedException { /* Simulate async task. */ waitDownloadTask(); @@ -666,16 +672,97 @@ public void disabledWhileCheckingDownloadOnRestart() { restartProcessAndSdk(); Updates.getInstance().onActivityResumed(mFirstActivity); - /* Disabled before async task runs. */ + /* Change behavior of get download it to block to simulate the concurrency issue. */ + final CyclicBarrier barrier = new CyclicBarrier(2); + + /* Call get to execute last when so that we can override the answer for next calls. */ + final long downloadId = PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); + + /* Overwrite next answer. */ + final Thread testThread = Thread.currentThread(); + when(PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER)).then(new Answer() { + + @Override + public Long answer(InvocationOnMock invocation) throws Throwable { + + /* This is called by setEnabled too and we want to block only the async task. */ + if (testThread != Thread.currentThread()) { + barrier.await(); + } + return downloadId; + } + }); + + /* Make sure async task is getting storage. */ + mCheckDownloadBeforeSemaphore.release(); + + /* Disable now. */ Updates.setEnabled(false); - /* Run async task. */ - waitCheckDownloadTask(); + /* Release task. */ + barrier.await(); + + /* And wait for it to complete. */ + mCheckDownloadAfterSemaphore.acquireUninterruptibly(); /* Verify we don't mark download checked as in progress. */ assertEquals(false, Whitebox.getInternalState(Updates.getInstance(), "mCheckedDownload")); } + @Test + public void disabledBeforeNotifying() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Change behavior of get download it to block to simulate the concurrency issue. */ + final CyclicBarrier barrier = new CyclicBarrier(2); + + /* Call get to execute last when so that we can override the answer for next calls. */ + final long downloadId = PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); + + /* Overwrite next answer. */ + final Thread testThread = Thread.currentThread(); + when(PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER)).then(new Answer() { + + @Override + public Long answer(InvocationOnMock invocation) throws Throwable { + + /* This is called by setEnabled too and we want to block only the async task. */ + if (testThread != Thread.currentThread()) { + barrier.await(); + } + return downloadId; + } + }); + + /* Mock success in background. */ + Updates.getInstance().onActivityPaused(mFirstActivity); + mockSuccessCursor(); + mockInstallIntent(); + completeDownload(); + + /* Make sure async task is getting storage. */ + mCheckDownloadBeforeSemaphore.release(); + + /* Disable now. */ + Updates.setEnabled(false); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + + /* Release task. */ + barrier.await(); + + /* And wait for it to complete. */ + mCheckDownloadAfterSemaphore.acquireUninterruptibly(); + + /* Verify we skip notification and clean happens only in disable (only once). */ + verify(mContext, never()).startActivity(any(Intent.class)); + verifyZeroInteractions(mNotificationManager); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + } + @Test public void startActivityButDisabledAfterCheckpoint() throws Exception { From df94e03830d45b3191415d19a142ac27c4cf7129 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 21 Feb 2017 18:09:40 -0800 Subject: [PATCH 071/142] Keep token when disabling updates --- .../main/java/com/microsoft/azure/mobile/updates/Updates.java | 3 +-- .../azure/mobile/updates/UpdatesBeforeApiSuccessTest.java | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 2d0febe012..8d49bb5c8f 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -365,11 +365,10 @@ public synchronized void setInstanceEnabled(boolean enabled) { resumeUpdateWorkflow(); } else { - /* Clean all state on disabling, cancel everything. */ + /* Clean all state on disabling, cancel everything. Keep token though. */ mBrowserOpenedOrAborted = false; mWorkflowCompleted = false; cancelPreviousTasks(); - PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); PreferencesStorage.remove(PREFERENCE_KEY_IGNORED_RELEASE_ID); } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java index e340696246..a69b396725 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java @@ -473,6 +473,8 @@ public void run() { Updates.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + verifyStatic(never()); + PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); beforeSemaphore.release(); afterSemaphore.acquireUninterruptibly(); @@ -523,6 +525,8 @@ public void run() { Updates.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + verifyStatic(never()); + PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); beforeSemaphore.release(); afterSemaphore.acquireUninterruptibly(); From 96029c1799721165cba435e8477e6097bceea8ef Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 22 Feb 2017 11:09:54 -0800 Subject: [PATCH 072/142] Change lock logic in some of the tests to avoid random failures --- .../azure/mobile/updates/UpdatesDownloadTest.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java index 1939c3fbac..8de6751eb8 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java @@ -41,7 +41,6 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicReference; @@ -673,7 +672,7 @@ public void disabledWhileCheckingDownloadOnRestart() throws BrokenBarrierExcepti Updates.getInstance().onActivityResumed(mFirstActivity); /* Change behavior of get download it to block to simulate the concurrency issue. */ - final CyclicBarrier barrier = new CyclicBarrier(2); + final Semaphore waitDisabledSemaphore = new Semaphore(0); /* Call get to execute last when so that we can override the answer for next calls. */ final long downloadId = PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); @@ -687,7 +686,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { /* This is called by setEnabled too and we want to block only the async task. */ if (testThread != Thread.currentThread()) { - barrier.await(); + waitDisabledSemaphore.acquireUninterruptibly(); } return downloadId; } @@ -700,7 +699,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { Updates.setEnabled(false); /* Release task. */ - barrier.await(); + waitDisabledSemaphore.release(); /* And wait for it to complete. */ mCheckDownloadAfterSemaphore.acquireUninterruptibly(); @@ -716,7 +715,7 @@ public void disabledBeforeNotifying() throws Exception { waitDownloadTask(); /* Change behavior of get download it to block to simulate the concurrency issue. */ - final CyclicBarrier barrier = new CyclicBarrier(2); + final Semaphore waitDisabledSemaphore = new Semaphore(0); /* Call get to execute last when so that we can override the answer for next calls. */ final long downloadId = PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); @@ -730,7 +729,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { /* This is called by setEnabled too and we want to block only the async task. */ if (testThread != Thread.currentThread()) { - barrier.await(); + waitDisabledSemaphore.acquireUninterruptibly(); } return downloadId; } @@ -751,7 +750,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Release task. */ - barrier.await(); + waitDisabledSemaphore.release(); /* And wait for it to complete. */ mCheckDownloadAfterSemaphore.acquireUninterruptibly(); From 24a83e744b79e40d173fc820760010b1aa952016 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 22 Feb 2017 15:05:27 -0800 Subject: [PATCH 073/142] Fix crash check box showing analytics state instead --- .../azure/mobile/sasquatch/activities/SettingsActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java index 9db8d5c5d3..cf92acbd2f 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java @@ -80,7 +80,7 @@ public boolean isEnabled() { @Override public void setEnabled(boolean enabled) { Crashes.setEnabled(enabled); - crashesEnabledPreference.setChecked(Analytics.isEnabled()); + crashesEnabledPreference.setChecked(Crashes.isEnabled()); } @Override From 952e4d0718bfe3b99b5dc99fc66f81d41368eb85 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 22 Feb 2017 15:58:52 -0800 Subject: [PATCH 074/142] Change release identifier type to int --- .../mobile/updates/ReleaseDetailsTest.java | 54 ++++++++++++++----- .../azure/mobile/updates/ReleaseDetails.java | 7 ++- .../azure/mobile/updates/UpdateConstants.java | 5 ++ .../azure/mobile/updates/Updates.java | 9 ++-- .../updates/UpdatesBeforeDownloadTest.java | 40 +++++++------- .../mobile/updates/UpdatesDownloadTest.java | 2 +- .../UpdatesWarnUnknownSourcesTest.java | 2 +- 7 files changed, 78 insertions(+), 41 deletions(-) diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java index d498ac06fe..a5c6b5bb6d 100644 --- a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java +++ b/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java @@ -14,7 +14,7 @@ public class ReleaseDetailsTest { @Test public void parse() throws JSONException { String json = "{" + - "id: '42'," + + "id: 42," + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + @@ -22,7 +22,7 @@ public void parse() throws JSONException { "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); - assertEquals("42", releaseDetails.getId()); + assertEquals(42, releaseDetails.getId()); assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); @@ -41,9 +41,39 @@ public void missingId() throws JSONException { } @Test(expected = JSONException.class) - public void missingVersion() throws JSONException { + public void invalidId() throws JSONException { + String json = "{" + + "id: '42abc'," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + + @Test + public void acceptIdAsStringAsAndroidJsonDoesThat() throws JSONException { String json = "{" + "id: '42'," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails releaseDetails = ReleaseDetails.parse(json); + assertNotNull(releaseDetails); + assertEquals(42, releaseDetails.getId()); + assertEquals(14, releaseDetails.getVersion()); + assertEquals("2.1.5", releaseDetails.getShortVersion()); + assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); + assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + } + + @Test(expected = JSONException.class) + public void missingVersion() throws JSONException { + String json = "{" + + "id: 42," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + @@ -54,7 +84,7 @@ public void missingVersion() throws JSONException { @Test(expected = JSONException.class) public void invalidVersion() throws JSONException { String json = "{" + - "id: '42'," + + "id: 42," + "version: true," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + @@ -66,7 +96,7 @@ public void invalidVersion() throws JSONException { @Test(expected = JSONException.class) public void missingShortVersion() throws JSONException { String json = "{" + - "id: '42'," + + "id: 42," + "version: '14'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + @@ -77,14 +107,14 @@ public void missingShortVersion() throws JSONException { @Test public void missingReleaseNotes() throws JSONException { String json = "{" + - "id: '42'," + + "id: 42," + "version: '14'," + "short_version: '2.1.5'," + "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); - assertEquals("42", releaseDetails.getId()); + assertEquals(42, releaseDetails.getId()); assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertNull(releaseDetails.getReleaseNotes()); @@ -94,7 +124,7 @@ public void missingReleaseNotes() throws JSONException { @Test public void nullReleaseNotes() throws JSONException { String json = "{" + - "id: '42'," + + "id: 42," + "version: '14'," + "release_notes: null," + "short_version: '2.1.5'," + @@ -102,7 +132,7 @@ public void nullReleaseNotes() throws JSONException { "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); - assertEquals("42", releaseDetails.getId()); + assertEquals(42, releaseDetails.getId()); assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertNull(releaseDetails.getReleaseNotes()); @@ -112,7 +142,7 @@ public void nullReleaseNotes() throws JSONException { @Test(expected = JSONException.class) public void missingDownloadUrl() throws JSONException { String json = "{" + - "id: '42'," + + "id: 42," + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + @@ -123,7 +153,7 @@ public void missingDownloadUrl() throws JSONException { @Test(expected = JSONException.class) public void missingDownloadUrlScheme() throws JSONException { String json = "{" + - "id: '42'," + + "id: 42," + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + @@ -135,7 +165,7 @@ public void missingDownloadUrlScheme() throws JSONException { @Test(expected = JSONException.class) public void invalidDownloadUrlScheme() throws JSONException { String json = "{" + - "id: '42'," + + "id: 42," + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java index b46ca73463..1525c701f5 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java @@ -25,7 +25,7 @@ class ReleaseDetails { /** * ID identifying this unique release. */ - private String id; + private int id; /** * The release's version.
@@ -61,7 +61,7 @@ class ReleaseDetails { static ReleaseDetails parse(String json) throws JSONException { JSONObject object = new JSONObject(json); ReleaseDetails releaseDetails = new ReleaseDetails(); - releaseDetails.id = object.getString(ID); + releaseDetails.id = object.getInt(ID); try { releaseDetails.version = Integer.parseInt(object.getString(VERSION)); } catch (NumberFormatException e) { @@ -82,8 +82,7 @@ static ReleaseDetails parse(String json) throws JSONException { * * @return the id value. */ - @NonNull - String getId() { + int getId() { return id; } diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java index edb859349c..f866f149e1 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java @@ -81,6 +81,11 @@ final class UpdateConstants { */ static final String HEADER_API_TOKEN = "x-api-token"; + /** + * Invalid release identifier. + */ + static final int INVALID_RELEASE_IDENTIFIER = -1; + /** * Invalid download identifier. */ diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 8d49bb5c8f..2b29353aaa 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -61,6 +61,7 @@ import static com.microsoft.azure.mobile.updates.UpdateConstants.GET_LATEST_RELEASE_PATH_FORMAT; import static com.microsoft.azure.mobile.updates.UpdateConstants.HEADER_API_TOKEN; import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; +import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_RELEASE_IDENTIFIER; import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; @@ -676,8 +677,8 @@ private synchronized void handleApiCallSuccess(Object releaseCallId, ReleaseDeta if (mCheckReleaseCallId == releaseCallId) { /* Check ignored. */ - String releaseId = releaseDetails.getId(); - if (releaseId.equals(PreferencesStorage.getString(PREFERENCE_KEY_IGNORED_RELEASE_ID))) { + int releaseId = releaseDetails.getId(); + if (releaseId == PreferencesStorage.getInt(PREFERENCE_KEY_IGNORED_RELEASE_ID, INVALID_RELEASE_IDENTIFIER)) { MobileCenterLog.debug(LOG_TAG, "This release is ignored id=" + releaseId); } else { @@ -894,9 +895,9 @@ private synchronized void goToSettings(ReleaseDetails releaseDetails) { */ private synchronized void ignoreRelease(ReleaseDetails releaseDetails) { if (releaseDetails == mReleaseDetails) { - String id = releaseDetails.getId(); + int id = releaseDetails.getId(); MobileCenterLog.debug(LOG_TAG, "Ignore release id=" + id); - PreferencesStorage.putString(PREFERENCE_KEY_IGNORED_RELEASE_ID, id); + PreferencesStorage.putInt(PREFERENCE_KEY_IGNORED_RELEASE_ID, id); completeWorkflow(); } else { showDisabledToast(); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java index 3c0bd1b1cd..7d9cbb1aac 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java @@ -23,11 +23,13 @@ import java.util.HashMap; import java.util.concurrent.Semaphore; +import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_RELEASE_IDENTIFIER; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; @@ -62,7 +64,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); Context context = mock(Context.class); when(context.getPackageName()).thenReturn("com.contoso"); @@ -105,7 +107,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(5); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -144,7 +146,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(6); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -183,7 +185,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); @@ -243,7 +245,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(releaseDetails.getReleaseNotes()).thenReturn("mock"); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -288,7 +290,7 @@ public void run() { HashMap headers = new HashMap<>(); headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(releaseDetails.getReleaseNotes()).thenReturn("mock"); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -348,7 +350,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -399,7 +401,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -442,16 +444,16 @@ public void ignoreDialog() throws Exception { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn((String) invocation.getArguments()[1]); + when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), INVALID_RELEASE_IDENTIFIER)).thenReturn((int) invocation.getArguments()[1]); return null; } }).when(PreferencesStorage.class); - PreferencesStorage.putString(eq(PREFERENCE_KEY_IGNORED_RELEASE_ID), anyString()); + PreferencesStorage.putInt(eq(PREFERENCE_KEY_IGNORED_RELEASE_ID), anyInt()); doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn(null); + when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), INVALID_RELEASE_IDENTIFIER)).thenReturn(INVALID_RELEASE_IDENTIFIER); return null; } }).when(PreferencesStorage.class); @@ -470,7 +472,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -531,7 +533,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); @@ -570,16 +572,16 @@ public void disableBeforeIgnoreDialog() throws Exception { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn((String) invocation.getArguments()[1]); + when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), INVALID_RELEASE_IDENTIFIER)).thenReturn((int) invocation.getArguments()[1]); return null; } }).when(PreferencesStorage.class); - PreferencesStorage.putString(eq(PREFERENCE_KEY_IGNORED_RELEASE_ID), anyString()); + PreferencesStorage.putInt(eq(PREFERENCE_KEY_IGNORED_RELEASE_ID), anyInt()); doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn(null); + when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), INVALID_RELEASE_IDENTIFIER)).thenReturn(INVALID_RELEASE_IDENTIFIER); return null; } }).when(PreferencesStorage.class); @@ -598,7 +600,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); @@ -634,7 +636,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_IGNORED_RELEASE_ID); verifyStatic(never()); - PreferencesStorage.putString(eq(PREFERENCE_KEY_IGNORED_RELEASE_ID), anyString()); + PreferencesStorage.putInt(eq(PREFERENCE_KEY_IGNORED_RELEASE_ID), anyInt()); } @Test @@ -654,7 +656,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); mockStatic(AsyncTaskUtils.class); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java index 8de6751eb8..5c60ffbe2d 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java @@ -174,7 +174,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(releaseDetails.getDownloadUrl()).thenReturn(mDownloadUrl); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java index f604d45d7a..5f54bd0adc 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java @@ -69,7 +69,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn("someId"); + when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); From ba393f82a1ca733d763ee7b4979ca70bc1caea46 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 22 Feb 2017 16:57:27 -0800 Subject: [PATCH 075/142] Make in app updates inactive in debug --- .../azure/mobile/updates/Updates.java | 19 ++++++------ .../mobile/updates/AbstractUpdatesTest.java | 5 ++++ .../updates/UpdatesBeforeApiSuccessTest.java | 23 +++++++++++---- .../updates/UpdatesBeforeDownloadTest.java | 11 ++++--- .../mobile/updates/UpdatesDownloadTest.java | 28 ------------------ .../UpdatesPlusDownloadReceiverTest.java | 29 +++++++++---------- 6 files changed, 49 insertions(+), 66 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 8d49bb5c8f..8b882a7c83 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -52,6 +52,7 @@ import java.util.NoSuchElementException; import static android.content.Context.DOWNLOAD_SERVICE; +import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_API_URL; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_INSTALL_URL; @@ -425,6 +426,13 @@ private synchronized void cancelPreviousTasks() { private synchronized void resumeUpdateWorkflow() { if (mForegroundActivity != null && !mWorkflowCompleted && isInstanceEnabled()) { + /* Don't go any further it this is a debug app. */ + if ((mContext.getApplicationInfo().flags & FLAG_DEBUGGABLE) == FLAG_DEBUGGABLE) { + MobileCenterLog.info(LOG_TAG, "Not checking in app updates in debug."); + mWorkflowCompleted = true; + return; + } + /* Don't go any further if the app was installed from an app store. */ if (InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)) { MobileCenterLog.info(LOG_TAG, "Not checking in app updates as installed from a store."); @@ -1027,20 +1035,11 @@ private synchronized boolean notifyDownload(Context context, CheckDownloadTask t /* Post notification. */ MobileCenterLog.debug(LOG_TAG, "Post a notification as the download finished in background."); - int icon; - try { - ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); - icon = applicationInfo.icon; - } catch (PackageManager.NameNotFoundException e) { - MobileCenterLog.error(LOG_TAG, "Could not get application icon", e); - completeWorkflow(); - return true; - } Notification.Builder builder = new Notification.Builder(context) .setTicker(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) .setContentTitle(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) .setContentText(context.getString(R.string.mobile_center_updates_download_successful_notification_message)) - .setSmallIcon(icon) + .setSmallIcon(context.getApplicationInfo().icon) .setContentIntent(PendingIntent.getActivities(context, 0, new Intent[]{intent}, 0)); Notification notification = buildNotification(builder); notification.flags |= Notification.FLAG_AUTO_CANCEL; diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index 05f242eabb..dfc518f787 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -3,6 +3,7 @@ import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.text.TextUtils; @@ -60,6 +61,9 @@ public class AbstractUpdatesTest { @Mock PackageManager mPackageManager; + @Mock + ApplicationInfo mApplicationInfo; + @Mock AlertDialog.Builder mDialogBuilder; @@ -101,6 +105,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { /* Mock package manager. */ when(mContext.getPackageName()).thenReturn("com.contoso"); + when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo); when(mContext.getPackageManager()).thenReturn(mPackageManager); PackageInfo packageInfo = mock(PackageInfo.class); when(mPackageManager.getPackageInfo("com.contoso", 0)).thenReturn(packageInfo); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java index a69b396725..9a29e1fd02 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java @@ -4,6 +4,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Bundle; @@ -18,6 +19,7 @@ import org.json.JSONException; import org.junit.Test; import org.mockito.ArgumentMatcher; +import org.mockito.internal.util.reflection.Whitebox; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -55,11 +57,7 @@ */ public class UpdatesBeforeApiSuccessTest extends AbstractUpdatesTest { - @Test - public void doNothingIfInstallComesFromStore() throws Exception { - - /* Mock from store. */ - when(InstallerUtils.isInstalledFromAppStore(anyString(), any(Context.class))).thenReturn(true); + private void testInAppUpdatesInactive() throws Exception { /* Check browser not opened. */ Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); @@ -81,6 +79,18 @@ public void doNothingIfInstallComesFromStore() throws Exception { verify(httpClient, never()).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } + @Test + public void doNothingIfDebug() throws Exception { + Whitebox.setInternalState(mApplicationInfo, "flags", ApplicationInfo.FLAG_DEBUGGABLE); + testInAppUpdatesInactive(); + } + + @Test + public void doNothingIfInstallComesFromStore() throws Exception { + when(InstallerUtils.isInstalledFromAppStore(anyString(), any(Context.class))).thenReturn(true); + testInAppUpdatesInactive(); + } + @Test public void storeTokenBeforeStart() throws Exception { @@ -91,7 +101,7 @@ public void storeTokenBeforeStart() throws Exception { /* Store token before start, start in background, no storage access. */ Updates.getInstance().storeUpdateToken("some token", "r"); - Updates.getInstance().onStarted(mock(Context.class), "", mock(Channel.class)); + Updates.getInstance().onStarted(mContext, "", mock(Channel.class)); verifyStatic(never()); PreferencesStorage.putString(anyString(), anyString()); verifyStatic(never()); @@ -275,6 +285,7 @@ public void computeHashFailsWhenOpeningBrowser() throws Exception { PackageManager packageManager = mock(PackageManager.class); when(context.getPackageName()).thenReturn("com.contoso"); when(context.getPackageManager()).thenReturn(packageManager); + when(context.getApplicationInfo()).thenReturn(mApplicationInfo); when(packageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); /* Start and resume: open browser. */ diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java index 3c0bd1b1cd..a401bc15f6 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java @@ -64,14 +64,13 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn("someId"); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); - Context context = mock(Context.class); - when(context.getPackageName()).thenReturn("com.contoso"); - PackageManager packageManager = mock(PackageManager.class); - when(context.getPackageManager()).thenReturn(packageManager); - when(packageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); + + /* Override mock answer by calling it once then configuring new answer. */ + mPackageManager.getPackageInfo("com.contoso", 0); + when(mPackageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); /* Trigger call. */ - Updates.getInstance().onStarted(context, "a", mock(Channel.class)); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); Updates.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java index 8de6751eb8..10811d1f19 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java @@ -801,34 +801,6 @@ public Void answer(InvocationOnMock invocation) throws Throwable { verify(cursor).close(); } - @Test - public void failsToGetNotificationIcon() throws Exception { - - /* Simulate async task. */ - waitDownloadTask(); - - /* Process download completion. */ - completeDownload(); - Cursor cursor = mockSuccessCursor(); - Intent installIntent = mockInstallIntent(); - - /* In background. */ - Updates.getInstance().onActivityPaused(mFirstActivity); - - /* And the icon will fail. */ - when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenThrow(new PackageManager.NameNotFoundException()); - - /* Simulate task. */ - waitCheckDownloadTask(); - - /* Verify complete workflow with no notification. */ - verify(mContext, never()).startActivity(installIntent); - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); - verifyNoMoreInteractions(mNotificationManager); - verify(cursor).close(); - } - @Test @PrepareForTest(Uri.class) @SuppressWarnings("deprecation") diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java index 457af61274..1d6706036a 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java @@ -24,25 +24,23 @@ public class UpdatesPlusDownloadReceiverTest extends AbstractUpdatesTest { public void resumeAppBeforeStart() throws Exception { Intent clickIntent = mock(Intent.class); when(clickIntent.getAction()).thenReturn(ACTION_NOTIFICATION_CLICKED); - Context context = mock(Context.class); Intent startIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); - new DownloadManagerReceiver().onReceive(context, clickIntent); - verify(context).startActivity(startIntent); + whenNew(Intent.class).withArguments(mContext, DeepLinkActivity.class).thenReturn(startIntent); + new DownloadManagerReceiver().onReceive(mContext, clickIntent); verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + verify(mContext).startActivity(startIntent); } @Test public void resumeAfterBeforeStartButBackground() throws Exception { Intent clickIntent = mock(Intent.class); when(clickIntent.getAction()).thenReturn(ACTION_NOTIFICATION_CLICKED); - Context context = mock(Context.class); - Updates.getInstance().onStarted(context, "", mock(Channel.class)); + Updates.getInstance().onStarted(mContext, "", mock(Channel.class)); Intent startIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); - new DownloadManagerReceiver().onReceive(context, clickIntent); - verify(context).startActivity(startIntent); + whenNew(Intent.class).withArguments(mContext, DeepLinkActivity.class).thenReturn(startIntent); + new DownloadManagerReceiver().onReceive(mContext, clickIntent); verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + verify(mContext).startActivity(startIntent); } @Test @@ -50,18 +48,17 @@ public void resumeForegroundThenPause() throws Exception { when(StorageHelper.PreferencesStorage.getString(eq(PREFERENCE_KEY_UPDATE_TOKEN))).thenReturn("mock"); Intent clickIntent = mock(Intent.class); when(clickIntent.getAction()).thenReturn(ACTION_NOTIFICATION_CLICKED); - Context context = mock(Context.class); - Updates.getInstance().onStarted(context, "", mock(Channel.class)); + Updates.getInstance().onStarted(mContext, "", mock(Channel.class)); Intent startIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(context, DeepLinkActivity.class).thenReturn(startIntent); + whenNew(Intent.class).withArguments(mContext, DeepLinkActivity.class).thenReturn(startIntent); Updates.getInstance().onActivityResumed(mock(Activity.class)); - new DownloadManagerReceiver().onReceive(context, clickIntent); - verify(context, never()).startActivity(startIntent); + new DownloadManagerReceiver().onReceive(mContext, clickIntent); + verify(mContext, never()).startActivity(startIntent); /* Then pause and test again. */ Updates.getInstance().onActivityPaused(mock(Activity.class)); - new DownloadManagerReceiver().onReceive(context, clickIntent); - verify(context).startActivity(startIntent); + new DownloadManagerReceiver().onReceive(mContext, clickIntent); verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + verify(mContext).startActivity(startIntent); } } From 16fcd793c1cde0e1c5a453d80350f7548bf9b946 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 22 Feb 2017 17:09:31 -0800 Subject: [PATCH 076/142] Fix locking logic to make sure we cover all state checks --- .../mobile/updates/UpdatesDownloadTest.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java index 8de6751eb8..3f168636cd 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java @@ -672,7 +672,8 @@ public void disabledWhileCheckingDownloadOnRestart() throws BrokenBarrierExcepti Updates.getInstance().onActivityResumed(mFirstActivity); /* Change behavior of get download it to block to simulate the concurrency issue. */ - final Semaphore waitDisabledSemaphore = new Semaphore(0); + final Semaphore beforeDisabledSemaphore = new Semaphore(0); + final Semaphore afterDisabledSemaphore = new Semaphore(0); /* Call get to execute last when so that we can override the answer for next calls. */ final long downloadId = PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); @@ -686,7 +687,8 @@ public Long answer(InvocationOnMock invocation) throws Throwable { /* This is called by setEnabled too and we want to block only the async task. */ if (testThread != Thread.currentThread()) { - waitDisabledSemaphore.acquireUninterruptibly(); + beforeDisabledSemaphore.release(); + afterDisabledSemaphore.acquireUninterruptibly(); } return downloadId; } @@ -694,12 +696,13 @@ public Long answer(InvocationOnMock invocation) throws Throwable { /* Make sure async task is getting storage. */ mCheckDownloadBeforeSemaphore.release(); + beforeDisabledSemaphore.acquireUninterruptibly(); /* Disable now. */ Updates.setEnabled(false); /* Release task. */ - waitDisabledSemaphore.release(); + afterDisabledSemaphore.release(); /* And wait for it to complete. */ mCheckDownloadAfterSemaphore.acquireUninterruptibly(); @@ -715,7 +718,8 @@ public void disabledBeforeNotifying() throws Exception { waitDownloadTask(); /* Change behavior of get download it to block to simulate the concurrency issue. */ - final Semaphore waitDisabledSemaphore = new Semaphore(0); + final Semaphore beforeDisabledSemaphore = new Semaphore(0); + final Semaphore afterDisabledSemaphore = new Semaphore(0); /* Call get to execute last when so that we can override the answer for next calls. */ final long downloadId = PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); @@ -729,7 +733,8 @@ public Long answer(InvocationOnMock invocation) throws Throwable { /* This is called by setEnabled too and we want to block only the async task. */ if (testThread != Thread.currentThread()) { - waitDisabledSemaphore.acquireUninterruptibly(); + beforeDisabledSemaphore.release(); + afterDisabledSemaphore.acquireUninterruptibly(); } return downloadId; } @@ -743,6 +748,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { /* Make sure async task is getting storage. */ mCheckDownloadBeforeSemaphore.release(); + beforeDisabledSemaphore.acquireUninterruptibly(); /* Disable now. */ Updates.setEnabled(false); @@ -750,7 +756,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Release task. */ - waitDisabledSemaphore.release(); + afterDisabledSemaphore.release(); /* And wait for it to complete. */ mCheckDownloadAfterSemaphore.acquireUninterruptibly(); From 354363e123031e7cfbb5adac91a8b85e2bf215f4 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 22 Feb 2017 17:33:13 -0800 Subject: [PATCH 077/142] Fix code coverage when connected device does not have network --- .../microsoft/azure/mobile/utils/NetworkStateHelperTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/NetworkStateHelperTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/NetworkStateHelperTest.java index 11bd9ff35d..ea061f598a 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/NetworkStateHelperTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/NetworkStateHelperTest.java @@ -108,6 +108,10 @@ public Intent answer(InvocationOnMock invocation) throws Throwable { verify(listener2).onNetworkStateUpdated(false); verify(listener2).onNetworkStateUpdated(true); + /* Duplicate WIFI callback. */ + receiver.onReceive(context, mock(Intent.class)); + verifyNoMoreInteractions(listener2); + /* But then WIFI is disconnected. */ helper.removeListener(listener2); NetworkStateHelper.Listener listener3 = mock(NetworkStateHelper.Listener.class); From 31d7c8b2e652e9f763e0d3ac5654a99f142158b7 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 22 Feb 2017 18:47:28 -0800 Subject: [PATCH 078/142] Postpone browser launch when disconnected --- .../azure/mobile/updates/Updates.java | 11 ++++ .../mobile/updates/AbstractUpdatesTest.java | 11 +++- .../updates/UpdatesBeforeApiSuccessTest.java | 56 +++++++++++++++---- 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 8d49bb5c8f..d98924a359 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -509,6 +509,17 @@ private synchronized void resumeUpdateWorkflow() { return; } + /* + * If network is disconnected, browser will fail so wait. + * Also we can't just wait for network to be up and launch browser at that time + * as it's unpredictable and will interrupt the user, so just wait next relaunch. + */ + if (!NetworkStateHelper.getSharedInstance(mContext).isNetworkConnected()) { + MobileCenterLog.info(LOG_TAG, "Postpone enabling in app updates via browser as network is disconnected."); + completeWorkflow(); + return; + } + /* Compute hash. */ String releaseHash; try { diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java index 05f242eabb..e963b76aaa 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java @@ -11,6 +11,7 @@ import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.utils.HashUtils; import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.NetworkStateHelper; import com.microsoft.azure.mobile.utils.UUIDUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; @@ -18,6 +19,7 @@ import org.junit.Rule; import org.junit.rules.Timeout; import org.mockito.Mock; +import org.mockito.internal.stubbing.answers.Returns; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -38,7 +40,7 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Updates.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, InstallerUtils.class, Toast.class}) +@PrepareForTest({Updates.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, NetworkStateHelper.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, InstallerUtils.class, Toast.class}) public class AbstractUpdatesTest { static final String TEST_HASH = HashUtils.sha256("com.contoso:1.2.3:6"); @@ -69,6 +71,8 @@ public class AbstractUpdatesTest { @Mock Toast mToast; + NetworkStateHelper mNetworkStateHelper; + @Before @SuppressLint("ShowToast") @SuppressWarnings("ResourceType") @@ -107,6 +111,11 @@ public Void answer(InvocationOnMock invocation) throws Throwable { Whitebox.setInternalState(packageInfo, "versionName", "1.2.3"); Whitebox.setInternalState(packageInfo, "versionCode", 6); + /* Mock network. */ + mockStatic(NetworkStateHelper.class); + mNetworkStateHelper = mock(NetworkStateHelper.class, new Returns(true)); + when(NetworkStateHelper.getSharedInstance(any(Context.class))).thenReturn(mNetworkStateHelper); + /* Mock some statics. */ mockStatic(BrowserUtils.class); mockStatic(UUIDUtils.class); diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java index a69b396725..921fa8b87a 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java @@ -55,6 +55,24 @@ */ public class UpdatesBeforeApiSuccessTest extends AbstractUpdatesTest { + /** + * Shared code to mock a restart of an activity considered to be the launcher. + */ + private static void restartResumeLauncher(Activity activity) { + Intent intent = mock(Intent.class); + PackageManager packageManager = mock(PackageManager.class); + when(activity.getPackageManager()).thenReturn(packageManager); + when(packageManager.getLaunchIntentForPackage(anyString())).thenReturn(intent); + ComponentName componentName = mock(ComponentName.class); + when(intent.resolveActivity(packageManager)).thenReturn(componentName); + when(componentName.getClassName()).thenReturn(activity.getClass().getName()); + Updates.getInstance().onActivityPaused(activity); + Updates.getInstance().onActivityStopped(activity); + Updates.getInstance().onActivityDestroyed(activity); + Updates.getInstance().onActivityCreated(activity, mock(Bundle.class)); + Updates.getInstance().onActivityResumed(activity); + } + @Test public void doNothingIfInstallComesFromStore() throws Exception { @@ -112,6 +130,31 @@ public void storeTokenBeforeStart() throws Exception { verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } + @Test + public void postponeBrowserIfNoNetwork() throws Exception { + + /* Check browser not opened if no network. */ + when(mNetworkStateHelper.isNetworkConnected()).thenReturn(false); + Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verifyStatic(never()); + BrowserUtils.openBrowser(anyString(), any(Activity.class)); + + /* If network comes back, we don't open network unless we restart app. */ + when(mNetworkStateHelper.isNetworkConnected()).thenReturn(true); + Updates.getInstance().onActivityPaused(mock(Activity.class)); + Updates.getInstance().onActivityResumed(mock(Activity.class)); + verifyStatic(never()); + BrowserUtils.openBrowser(anyString(), any(Activity.class)); + + /* Restart should open browser if still have network. */ + when(UUIDUtils.randomUUID()).thenReturn(UUID.randomUUID()); + Activity activity = mock(Activity.class); + restartResumeLauncher(activity); + verifyStatic(); + BrowserUtils.openBrowser(anyString(), any(Activity.class)); + } + @Test public void happyPathUntilHangingCall() throws Exception { @@ -189,18 +232,7 @@ public boolean matches(Object argument) { }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Call is still in progress. If we restart app, nothing happens we still wait. */ - Intent intent = mock(Intent.class); - PackageManager packageManager = mock(PackageManager.class); - when(activity.getPackageManager()).thenReturn(packageManager); - when(packageManager.getLaunchIntentForPackage(anyString())).thenReturn(intent); - ComponentName componentName = mock(ComponentName.class); - when(intent.resolveActivity(packageManager)).thenReturn(componentName); - when(componentName.getClassName()).thenReturn(activity.getClass().getName()); - Updates.getInstance().onActivityPaused(activity); - Updates.getInstance().onActivityStopped(activity); - Updates.getInstance().onActivityDestroyed(activity); - Updates.getInstance().onActivityCreated(activity, mock(Bundle.class)); - Updates.getInstance().onActivityResumed(activity); + restartResumeLauncher(activity); Updates.getInstance().onActivityPaused(activity); Updates.getInstance().onActivityStopped(activity); Updates.getInstance().onActivityDestroyed(activity); From 3849adc8e4f048ac2f7e0a54a17a1df4551a7069 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 22 Feb 2017 19:32:23 -0800 Subject: [PATCH 079/142] Rename setServerUrl to setLogUrl --- .../sasquatch/activities/MainActivity.java | 22 ++++++++++++---- .../activities/SettingsActivity.java | 26 ++++++++++--------- .../src/main/res/values/settings.xml | 10 +++---- apps/sasquatch/src/main/res/xml/settings.xml | 4 +-- .../microsoft/azure/mobile/MobileCenter.java | 26 +++++++++---------- .../azure/mobile/channel/Channel.java | 6 ++--- .../azure/mobile/channel/DefaultChannel.java | 4 +-- .../azure/mobile/ingestion/Ingestion.java | 6 ++--- .../ingestion/http/IngestionDecorator.java | 4 +-- .../mobile/ingestion/http/IngestionHttp.java | 6 ++--- .../azure/mobile/MobileCenterTest.java | 20 +++++++------- .../mobile/channel/DefaultChannelTest.java | 8 +++--- .../ingestion/http/IngestionHttpTest.java | 2 +- .../IngestionNetworkStateHandlerTest.java | 8 +++--- .../ingestion/http/IngestionRetryerTest.java | 8 +++--- 15 files changed, 87 insertions(+), 73 deletions(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index f6e3ed1e91..7cfcb8bfe3 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -26,13 +26,15 @@ import com.microsoft.azure.mobile.sasquatch.features.TestFeatures; import com.microsoft.azure.mobile.sasquatch.features.TestFeaturesListAdapter; +import java.lang.reflect.Method; + public class MainActivity extends AppCompatActivity { - private static final String LOG_TAG = "MobileCenterSasquatch"; static final String APP_SECRET = "45d1d9f6-2492-4e68-bd44-7190351eb5f3"; static final String APP_SECRET_KEY = "appSecret"; - static final String SERVER_URL_KEY = "serverUrl"; + static final String LOG_URL_KEY = "logUrl"; + private static final String LOG_TAG = "MobileCenterSasquatch"; static SharedPreferences sSharedPreferences; @Override @@ -43,9 +45,19 @@ protected void onCreate(Bundle savedInstanceState) { sSharedPreferences = getSharedPreferences("Sasquatch", Context.MODE_PRIVATE); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().build()); - String serverUrl = sSharedPreferences.getString(SERVER_URL_KEY, null); - if (serverUrl != null) { - MobileCenter.setServerUrl(serverUrl); + String logUrl = sSharedPreferences.getString(LOG_URL_KEY, null); + if (logUrl != null) { + try { + Method setLogUrl; + try { + setLogUrl = MobileCenter.class.getMethod("setLogUrl", String.class); + } catch (NoSuchMethodException e) { + setLogUrl = MobileCenter.class.getMethod("setServerUrl", String.class); + } + setLogUrl.invoke(null, logUrl); + } catch (Exception e) { + throw new RuntimeException(e); + } } MobileCenter.setLogLevel(Log.VERBOSE); Crashes.setListener(getCrashesListener()); diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java index cf92acbd2f..8122481cff 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java @@ -26,7 +26,7 @@ import java.util.UUID; import static com.microsoft.azure.mobile.sasquatch.activities.MainActivity.APP_SECRET_KEY; -import static com.microsoft.azure.mobile.sasquatch.activities.MainActivity.SERVER_URL_KEY; +import static com.microsoft.azure.mobile.sasquatch.activities.MainActivity.LOG_URL_KEY; public class SettingsActivity extends AppCompatActivity { @@ -171,36 +171,38 @@ public boolean onPreferenceClick(Preference preference) { return true; } }); - initClickableSetting(R.string.server_url_key, MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, getString(R.string.server_url_production)), new Preference.OnPreferenceClickListener() { + initClickableSetting(R.string.log_url_key, MainActivity.sSharedPreferences.getString(LOG_URL_KEY, getString(R.string.log_url_production)), new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(final Preference preference) { final EditText input = new EditText(getActivity()); input.setInputType(InputType.TYPE_CLASS_TEXT); - input.setText(MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, null)); - input.setHint(R.string.server_url_production); + input.setText(MainActivity.sSharedPreferences.getString(LOG_URL_KEY, null)); + input.setHint(R.string.log_url_production); - new AlertDialog.Builder(getActivity()).setTitle(R.string.server_url_title).setView(input) + new AlertDialog.Builder(getActivity()).setTitle(R.string.log_url_title).setView(input) .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { if (Patterns.WEB_URL.matcher(input.getText().toString()).matches()) { String url = input.getText().toString(); - setKeyValue(SERVER_URL_KEY, url); - Toast.makeText(getActivity(), String.format(getActivity().getString(R.string.server_url_changed_format), url), Toast.LENGTH_SHORT).show(); + setKeyValue(LOG_URL_KEY, url); + Toast.makeText(getActivity(), String.format(getActivity().getString(R.string.log_url_changed_format), url), Toast.LENGTH_SHORT).show(); } else if (input.getText().toString().isEmpty()) { setProductionUrl(); } else { - Toast.makeText(getActivity(), R.string.server_url_invalid, Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), R.string.log_url_invalid, Toast.LENGTH_SHORT).show(); } - preference.setSummary(MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, getString(R.string.server_url_production))); + preference.setSummary(MainActivity.sSharedPreferences.getString(LOG_URL_KEY, getString(R.string.log_url_production))); } }) .setNeutralButton(R.string.reset, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { setProductionUrl(); - preference.setSummary(MainActivity.sSharedPreferences.getString(SERVER_URL_KEY, getString(R.string.server_url_production))); + preference.setSummary(MainActivity.sSharedPreferences.getString(LOG_URL_KEY, getString(R.string.log_url_production))); } }) .setNegativeButton(R.string.cancel, null) @@ -209,8 +211,8 @@ public void onClick(DialogInterface dialog, int which) { } private void setProductionUrl() { - setKeyValue(SERVER_URL_KEY, null); - Toast.makeText(getActivity(), R.string.server_url_production, Toast.LENGTH_SHORT).show(); + setKeyValue(LOG_URL_KEY, null); + Toast.makeText(getActivity(), R.string.log_url_production, Toast.LENGTH_SHORT).show(); } }); } diff --git a/apps/sasquatch/src/main/res/values/settings.xml b/apps/sasquatch/src/main/res/values/settings.xml index c9eed7ff6f..6cac39b799 100644 --- a/apps/sasquatch/src/main/res/values/settings.xml +++ b/apps/sasquatch/src/main/res/values/settings.xml @@ -47,11 +47,11 @@ Clear crash user confirmation Cleared - server_url_key - Server URL - Show Server URL has been changed to %s successfully. Please close the application completely and relaunch again. - Server URL is not valid. - Server URL currently set to the production. + log_url_key + Log URL + Show Log URL has been changed to %s successfully. Please close the application completely and relaunch again. + Log URL is not valid. + Log URL currently set to the production. Save Reset diff --git a/apps/sasquatch/src/main/res/xml/settings.xml b/apps/sasquatch/src/main/res/xml/settings.xml index 460eb49951..d7f459c5d9 100644 --- a/apps/sasquatch/src/main/res/xml/settings.xml +++ b/apps/sasquatch/src/main/res/xml/settings.xml @@ -41,7 +41,7 @@ android:key="@string/clear_crash_user_confirmation_key" android:title="@string/clear_crash_user_confirmation_title" /> + android:key="@string/log_url_key" + android:title="@string/log_url_title"/> \ No newline at end of file diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index 5f433e1d78..5e4a3991d6 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -47,9 +47,9 @@ public class MobileCenter { private boolean mLogLevelConfigured; /** - * Custom server Url if any. + * Custom log url if any. */ - private String mServerUrl; + private String mLogUrl; /** * Application context. @@ -120,12 +120,12 @@ public static void setLogLevel(@IntRange(from = VERBOSE, to = NONE) int logLevel } /** - * Change the base URL (scheme + authority + port only) used to communicate with the backend. + * Change the base URL (scheme + authority + port only) used to send logs. * - * @param serverUrl base URL to use for server communication. + * @param logUrl base log URL. */ - public static void setServerUrl(String serverUrl) { - getInstance().setInstanceServerUrl(serverUrl); + public static void setLogUrl(String logUrl) { + getInstance().setInstanceLogUrl(logUrl); } /** @@ -243,14 +243,14 @@ private synchronized void setInstanceLogLevel(int logLevel) { } /** - * {@link #setServerUrl(String)} implementation at instance level. + * {@link #setLogUrl(String)} implementation at instance level. * - * @param serverUrl server URL. + * @param logUrl log URL. */ - private synchronized void setInstanceServerUrl(String serverUrl) { - mServerUrl = serverUrl; + private synchronized void setInstanceLogUrl(String logUrl) { + mLogUrl = logUrl; if (mChannel != null) - mChannel.setServerUrl(serverUrl); + mChannel.setLogUrl(logUrl); } /** @@ -296,8 +296,8 @@ private synchronized boolean instanceConfigure(Application application, String a mLogSerializer = new DefaultLogSerializer(); mChannel = new DefaultChannel(application, appSecret, mLogSerializer); mChannel.setEnabled(isInstanceEnabled()); - if (mServerUrl != null) - mChannel.setServerUrl(mServerUrl); + if (mLogUrl != null) + mChannel.setLogUrl(mLogUrl); MobileCenterLog.logAssert(LOG_TAG, "Mobile Center SDK configured successfully."); return true; } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/Channel.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/Channel.java index 8685136f7f..9735b78893 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/Channel.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/Channel.java @@ -51,11 +51,11 @@ public interface Channel { void setEnabled(boolean enabled); /** - * Update server url. + * Update log url. * - * @param serverUrl server url. + * @param logUrl log url. */ - void setServerUrl(String serverUrl); + void setLogUrl(String logUrl); /** * Clear all persisted logs for the given group. diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/DefaultChannel.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/DefaultChannel.java index 4ff2655954..d115c6a8e6 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/DefaultChannel.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/DefaultChannel.java @@ -244,8 +244,8 @@ public synchronized void setEnabled(boolean enabled) { } @Override - public void setServerUrl(String serverUrl) { - mIngestion.setServerUrl(serverUrl); + public void setLogUrl(String logUrl) { + mIngestion.setLogUrl(logUrl); } /** diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java index 11e5ebc67c..00ee1ed103 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java @@ -23,9 +23,9 @@ public interface Ingestion extends Closeable { ServiceCall sendAsync(String appSecret, UUID installId, LogContainer logContainer, ServiceCallback serviceCallback) throws IllegalArgumentException; /** - * Update server url. + * Update log url. * - * @param serverUrl server url. + * @param logUrl log url. */ - void setServerUrl(String serverUrl); + void setLogUrl(String logUrl); } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionDecorator.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionDecorator.java index 51ff6d8401..44116253ed 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionDecorator.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionDecorator.java @@ -13,8 +13,8 @@ abstract class IngestionDecorator implements Ingestion { } @Override - public void setServerUrl(String serverUrl) { - mDecoratedApi.setServerUrl(serverUrl); + public void setLogUrl(String logUrl) { + mDecoratedApi.setLogUrl(logUrl); } @Override diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java index d54e75e284..94081c40e7 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java @@ -224,12 +224,12 @@ private static String dump(HttpURLConnection urlConnection) throws IOException { /** * Set the base url. * - * @param baseUrl the base url. + * @param logUrl the base url. */ @Override @SuppressWarnings("SameParameterValue") - public void setServerUrl(@NonNull String baseUrl) { - mBaseUrl = baseUrl; + public void setLogUrl(@NonNull String logUrl) { + mBaseUrl = logUrl; } @Override diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index fbc36ad5ca..093326df35 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -613,25 +613,25 @@ public void dontSetDefaultLogLevel() { } @Test - public void setServerUrl() throws Exception { + public void setLogUrl() throws Exception { - /* Change server URL before start. */ + /* Change log URL before start. */ DefaultChannel channel = mock(DefaultChannel.class); whenNew(DefaultChannel.class).withAnyArguments().thenReturn(channel); - String serverUrl = "http://mock"; - MobileCenter.setServerUrl(serverUrl); + String logUrl = "http://mock"; + MobileCenter.setLogUrl(logUrl); /* No effect for now. */ - verify(channel, never()).setServerUrl(serverUrl); + verify(channel, never()).setLogUrl(logUrl); - /* Start should propagate the server url. */ + /* Start should propagate the log url. */ MobileCenter.start(application, DUMMY_APP_SECRET, DummyService.class); - verify(channel).setServerUrl(serverUrl); + verify(channel).setLogUrl(logUrl); /* Change it after, should work immediately. */ - serverUrl = "http://mock2"; - MobileCenter.setServerUrl(serverUrl); - verify(channel).setServerUrl(serverUrl); + logUrl = "http://mock2"; + MobileCenter.setLogUrl(logUrl); + verify(channel).setLogUrl(logUrl); } private static class DummyService extends AbstractMobileCenterService { diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java index 288fcf81ba..760a8623c2 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java @@ -700,12 +700,12 @@ public Void answer(InvocationOnMock invocation) throws Throwable { @Test @SuppressWarnings("unchecked") - public void setServerUrl() { + public void setLogUrl() { Ingestion ingestion = mock(Ingestion.class); DefaultChannel channel = new DefaultChannel(mock(Context.class), UUIDUtils.randomUUID().toString(), mock(Persistence.class), ingestion); - String serverUrl = "http://mockUrl"; - channel.setServerUrl(serverUrl); - verify(ingestion).setServerUrl(serverUrl); + String logUrl = "http://mockUrl"; + channel.setLogUrl(logUrl); + verify(ingestion).setLogUrl(logUrl); } @Test diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttpTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttpTest.java index 3cea4ec85e..f000eed595 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttpTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttpTest.java @@ -112,7 +112,7 @@ public void success() throws Exception { LogSerializer serializer = mock(LogSerializer.class); when(serializer.serializeContainer(any(LogContainer.class))).thenReturn("mockPayload"); IngestionHttp httpClient = new IngestionHttp(serializer); - httpClient.setServerUrl("http://mock"); + httpClient.setLogUrl("http://mock"); /* Test calling code. Use shorter but valid app secret. */ String appSecret = "SHORT"; diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandlerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandlerTest.java index 14fe370550..d9766b3088 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandlerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionNetworkStateHandlerTest.java @@ -381,11 +381,11 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } @Test - public void setServerUrl() { + public void setLogUrl() { Ingestion ingestion = mock(Ingestion.class); Ingestion retryer = new IngestionNetworkStateHandler(ingestion, mock(NetworkStateHelper.class)); - String serverUrl = "http://someServerUrl"; - retryer.setServerUrl(serverUrl); - verify(ingestion).setServerUrl(serverUrl); + String logUrl = "http://someLogUrl"; + retryer.setLogUrl(logUrl); + verify(ingestion).setLogUrl(logUrl); } } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryerTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryerTest.java index 7d429c939e..999bcf7382 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryerTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/http/IngestionRetryerTest.java @@ -180,11 +180,11 @@ public ServiceCall answer(InvocationOnMock invocationOnMock) throws Throwable { } @Test - public void setServerUrl() { + public void setLogUrl() { Ingestion ingestion = mock(Ingestion.class); Ingestion retryer = new IngestionRetryer(ingestion, mock(Handler.class)); - String serverUrl = "http://someServerUrl"; - retryer.setServerUrl(serverUrl); - verify(ingestion).setServerUrl(serverUrl); + String logUrl = "http://someLogUrl"; + retryer.setLogUrl(logUrl); + verify(ingestion).setLogUrl(logUrl); } } From 27b439ee3159f0a7df02600d6cb04017a944943b Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Thu, 23 Feb 2017 11:06:40 -0800 Subject: [PATCH 080/142] Add onBeforeCalling callback to CallTemplate for logging --- .../azure/mobile/updates/Updates.java | 31 ++++++++++++++++++- .../azure/mobile/http/DefaultHttpClient.java | 27 +++------------- .../azure/mobile/http/HttpClient.java | 3 ++ .../azure/mobile/http/HttpUtils.java | 13 ++++++++ .../azure/mobile/ingestion/IngestionHttp.java | 22 +++++++++++++ 5 files changed, 72 insertions(+), 24 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index d234ea9429..56ab90c428 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -34,6 +34,7 @@ import com.microsoft.azure.mobile.http.HttpClient; import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; import com.microsoft.azure.mobile.http.HttpClientRetryer; +import com.microsoft.azure.mobile.http.HttpUtils; import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; @@ -47,10 +48,12 @@ import org.json.JSONException; import java.lang.ref.WeakReference; +import java.net.URL; import java.util.HashMap; import java.util.Map; import static android.content.Context.DOWNLOAD_SERVICE; +import static android.util.Log.VERBOSE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_API_URL; import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_INSTALL_URL; @@ -602,7 +605,33 @@ private synchronized void getLatestReleaseDetails(@NonNull String updateToken) { Map headers = new HashMap<>(); headers.put(HEADER_API_TOKEN, updateToken); final Object releaseCallId = mCheckReleaseCallId = new Object(); - mCheckReleaseApiCall = httpClient.callAsync(url, METHOD_GET, headers, null, new ServiceCallback() { + mCheckReleaseApiCall = httpClient.callAsync(url, METHOD_GET, headers, new HttpClient.CallTemplate() { + + @Override + public String buildRequestBody() throws JSONException { + + /* This method is getting called. */ + return null; + } + + @Override + public void onBeforeCalling(URL url, Map headers) { + + /* Log url. */ + String urlString = url.toString().replaceAll(mAppSecret, HttpUtils.hideSecret(mAppSecret)); + MobileCenterLog.verbose(LOG_TAG, "Calling " + urlString + "..."); + + /* Log headers. */ + if (MobileCenterLog.getLogLevel() <= VERBOSE) { + Map logHeaders = new HashMap<>(headers); + String apiToken = logHeaders.get(HEADER_API_TOKEN); + if (apiToken != null) { + logHeaders.put(HEADER_API_TOKEN, HttpUtils.hideSecret(apiToken)); + } + MobileCenterLog.verbose(LOG_TAG, "Headers: " + logHeaders); + } + } + }, new ServiceCallback() { @Override public void onCallSucceeded(String payload) { diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/DefaultHttpClient.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/DefaultHttpClient.java index 9c6df0ce3e..e7822117e5 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/DefaultHttpClient.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/DefaultHttpClient.java @@ -12,12 +12,9 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; -import java.util.Arrays; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.RejectedExecutionException; -import static android.util.Log.VERBOSE; import static com.microsoft.azure.mobile.MobileCenter.LOG_TAG; import static java.lang.Math.max; @@ -67,11 +64,6 @@ public class DefaultHttpClient implements HttpClient { */ private static final int READ_TIMEOUT = 20000; - /** - * Maximum characters to be displayed in a log for application secret. - */ - private static final int MAX_CHARACTERS_DISPLAYED_FOR_APP_SECRET = 8; - /** * Dump stream to string. * @@ -104,7 +96,6 @@ private static String doCall(String urlString, String method, Map logHeaders = new HashMap<>(headers); - String appSecret = logHeaders.get(APP_SECRET); - if (appSecret != null) { - int hidingEndIndex = appSecret.length() - (appSecret.length() >= MAX_CHARACTERS_DISPLAYED_FOR_APP_SECRET ? MAX_CHARACTERS_DISPLAYED_FOR_APP_SECRET : 0); - char[] fill = new char[hidingEndIndex]; - Arrays.fill(fill, '*'); - appSecret = new String(fill) + appSecret.substring(hidingEndIndex); - logHeaders.put(APP_SECRET, appSecret); - } - MobileCenterLog.verbose(LOG_TAG, "Headers: " + logHeaders); - } + /* Before send. */ + if (callTemplate != null) + callTemplate.onBeforeCalling(url, headers); /* Build payload. */ - if (method.equals(METHOD_POST)) { + if (method.equals(METHOD_POST) && callTemplate != null) { String payload = callTemplate.buildRequestBody(); MobileCenterLog.verbose(LOG_TAG, payload); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClient.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClient.java index d33a206a16..c1a7488391 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClient.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpClient.java @@ -3,6 +3,7 @@ import org.json.JSONException; import java.io.Closeable; +import java.net.URL; import java.util.Map; public interface HttpClient extends Closeable { @@ -11,5 +12,7 @@ public interface HttpClient extends Closeable { interface CallTemplate { String buildRequestBody() throws JSONException; + + void onBeforeCalling(URL url, Map headers); } } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java index 6a18bf833f..13ad67351d 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java @@ -6,6 +6,7 @@ import java.io.InterruptedIOException; import java.net.SocketException; import java.net.UnknownHostException; +import java.util.Arrays; import java.util.Locale; import java.util.concurrent.RejectedExecutionException; import java.util.regex.Pattern; @@ -33,6 +34,11 @@ public final class HttpUtils { */ private static final Pattern CONNECTION_ISSUE_PATTERN = Pattern.compile("connection (time|reset)|failure in ssl library, usually a protocol error"); + /** + * Maximum characters to be displayed in a log for application secret. + */ + private static final int MAX_CHARACTERS_DISPLAYED_FOR_SECRET = 8; + @VisibleForTesting HttpUtils() { } @@ -65,4 +71,11 @@ public static boolean isRecoverableError(Throwable t) { } return false; } + + public static String hideSecret(String secret) { + int hidingEndIndex = secret.length() - (secret.length() >= MAX_CHARACTERS_DISPLAYED_FOR_SECRET ? MAX_CHARACTERS_DISPLAYED_FOR_SECRET : 0); + char[] fill = new char[hidingEndIndex]; + Arrays.fill(fill, '*'); + return new String(fill) + secret.substring(hidingEndIndex); + } } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java index 9978f7c45d..2892afb457 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java @@ -8,21 +8,26 @@ import com.microsoft.azure.mobile.http.HttpClient; import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; import com.microsoft.azure.mobile.http.HttpClientRetryer; +import com.microsoft.azure.mobile.http.HttpUtils; import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.ingestion.models.Log; import com.microsoft.azure.mobile.ingestion.models.LogContainer; import com.microsoft.azure.mobile.ingestion.models.json.LogSerializer; +import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.NetworkStateHelper; import org.json.JSONException; import java.io.IOException; +import java.net.URL; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import static android.util.Log.VERBOSE; +import static com.microsoft.azure.mobile.MobileCenter.LOG_TAG; import static com.microsoft.azure.mobile.http.DefaultHttpClient.APP_SECRET; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_POST; @@ -139,5 +144,22 @@ public String buildRequestBody() throws JSONException { } return payload; } + + @Override + public void onBeforeCalling(URL url, Map headers) { + + /* Log url. */ + MobileCenterLog.verbose(LOG_TAG, "Calling " + url + "..."); + + /* Log headers. */ + if (MobileCenterLog.getLogLevel() <= VERBOSE) { + Map logHeaders = new HashMap<>(headers); + String appSecret = logHeaders.get(APP_SECRET); + if (appSecret != null) { + logHeaders.put(APP_SECRET, HttpUtils.hideSecret(appSecret)); + } + MobileCenterLog.verbose(LOG_TAG, "Headers: " + logHeaders); + } + } } } From 74016b3a0af17d0b140ac5511fb49718b659c41d Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Thu, 23 Feb 2017 15:07:00 -0800 Subject: [PATCH 081/142] Unit test for hiding app secret --- .../azure/mobile/updates/Updates.java | 13 +- .../azure/mobile/updates/UpdatesTest.java | 140 ++++++++++++++++++ .../azure/mobile/http/HttpUtils.java | 21 ++- .../azure/mobile/ingestion/IngestionHttp.java | 8 +- .../mobile/http/DefaultHttpClientTest.java | 91 ++++++++++++ .../azure/mobile/http/HttpUtilsTest.java | 37 +++++ .../mobile/ingestion/IngestionHttpTest.java | 90 ++++++++++- 7 files changed, 379 insertions(+), 21 deletions(-) create mode 100644 sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java create mode 100644 sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpUtilsTest.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 56ab90c428..0ec4beeaa7 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -596,7 +596,8 @@ synchronized void storeUpdateToken(@NonNull String updateToken, @NonNull String * * @param updateToken token to secure API call. */ - private synchronized void getLatestReleaseDetails(@NonNull String updateToken) { + @VisibleForTesting + synchronized void getLatestReleaseDetails(@NonNull String updateToken) { MobileCenterLog.debug(LOG_TAG, "Get latest release details..."); HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(mContext); @@ -616,13 +617,13 @@ public String buildRequestBody() throws JSONException { @Override public void onBeforeCalling(URL url, Map headers) { + if (MobileCenterLog.getLogLevel() <= VERBOSE) { - /* Log url. */ - String urlString = url.toString().replaceAll(mAppSecret, HttpUtils.hideSecret(mAppSecret)); - MobileCenterLog.verbose(LOG_TAG, "Calling " + urlString + "..."); + /* Log url. */ + String urlString = url.toString().replaceAll(mAppSecret, HttpUtils.hideSecret(mAppSecret)); + MobileCenterLog.verbose(LOG_TAG, "Calling " + urlString + "..."); - /* Log headers. */ - if (MobileCenterLog.getLogLevel() <= VERBOSE) { + /* Log headers. */ Map logHeaders = new HashMap<>(headers); String apiToken = logHeaders.get(HEADER_API_TOKEN); if (apiToken != null) { diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java new file mode 100644 index 0000000000..4a2b344c75 --- /dev/null +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java @@ -0,0 +1,140 @@ +package com.microsoft.azure.mobile.updates; + +import android.content.Context; + +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.HttpUtils; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.NetworkStateHelper; +import com.microsoft.azure.mobile.utils.UUIDUtils; + +import junit.framework.Assert; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static com.microsoft.azure.mobile.updates.UpdateConstants.HEADER_API_TOKEN; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.contains; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@SuppressWarnings("unused") +@PrepareForTest({NetworkStateHelper.class, MobileCenterLog.class, Updates.class}) +public class UpdatesTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Test + public void onBeforeCalling() throws Exception { + + /* Mock instances. */ + String urlFormat = "http://mock/path/%s/path/file"; + String appSecret = UUIDUtils.randomUUID().toString(); + String obfuscatedSecret = HttpUtils.hideSecret(appSecret); + String apiToken = UUIDUtils.randomUUID().toString(); + String obfuscatedToken = HttpUtils.hideSecret(apiToken); + URL url = new URL(String.format(urlFormat, appSecret)); + String obfuscatedUrlString = String.format(urlFormat, obfuscatedSecret); + Map headers = new HashMap<>(); + headers.put("Another-Header", "Another-Value"); + HttpClient.CallTemplate callTemplate = getCallTemplate(appSecret, apiToken); + MobileCenterLog.setLogLevel(android.util.Log.VERBOSE); + mockStatic(MobileCenterLog.class); + + /* Call onBeforeCalling with parameters. */ + callTemplate.onBeforeCalling(url, headers); + + /* Verify url log. */ + verifyStatic(); + MobileCenterLog.verbose(anyString(), contains(obfuscatedUrlString)); + + /* Verify header log. */ + for (Map.Entry header : headers.entrySet()) { + verifyStatic(); + MobileCenterLog.verbose(anyString(), contains(header.getValue())); + } + + /* Put api token to header. */ + headers.put(HEADER_API_TOKEN, apiToken); + callTemplate.onBeforeCalling(url, headers); + + /* Verify app secret is in log. */ + verifyStatic(); + MobileCenterLog.verbose(anyString(), contains(obfuscatedToken)); + } + + @Test + @SuppressWarnings("unchecked") + public void onBeforeCallingWithAnotherLogLevel() throws Exception { + + /* Mock instances. */ + String appSecret = UUIDUtils.randomUUID().toString(); + String apiToken = UUIDUtils.randomUUID().toString(); + HttpClient.CallTemplate callTemplate = getCallTemplate(appSecret, apiToken); + + /* Change log level. */ + MobileCenterLog.setLogLevel(android.util.Log.WARN); + + /* Call onBeforeCalling with parameters. */ + callTemplate.onBeforeCalling(mock(URL.class), mock(Map.class)); + + /* Verify. */ + verifyStatic(never()); + MobileCenterLog.verbose(anyString(), anyString()); + } + + @Test + public void buildRequestBody() throws Exception { + + /* Mock instances. */ + String appSecret = UUIDUtils.randomUUID().toString(); + String apiToken = UUIDUtils.randomUUID().toString(); + HttpClient.CallTemplate callTemplate = getCallTemplate(appSecret, apiToken); + + /* Updates don't have request body. Verify it. */ + Assert.assertNull(callTemplate.buildRequestBody()); + } + + private HttpClient.CallTemplate getCallTemplate(String appSecret, String apiToken) throws Exception { + + /* Configure mock HTTP to get an instance of IngestionCallTemplate. */ + final ServiceCall call = mock(ServiceCall.class); + final AtomicReference callTemplate = new AtomicReference<>(); + mockStatic(NetworkStateHelper.class); + when(NetworkStateHelper.getSharedInstance(any(Context.class))).thenReturn(mock(NetworkStateHelper.class)); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).then(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + callTemplate.set((HttpClient.CallTemplate) invocation.getArguments()[3]); + return call; + } + }); + Updates.getInstance().getLatestReleaseDetails(apiToken); + Updates.getInstance().onStarted(mock(Context.class), appSecret, mock(Channel.class)); + return callTemplate.get(); + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java index 13ad67351d..fa1971fafd 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java @@ -16,7 +16,13 @@ /** * HTTP utilities. */ -public final class HttpUtils { +public class HttpUtils { + + /** + * Maximum characters to be displayed in a log for application secret. + */ + @VisibleForTesting + static final int MAX_CHARACTERS_DISPLAYED_FOR_SECRET = 8; /** * Types of exception that can be retried, no matter what the details are. Sub-classes are included. @@ -28,17 +34,11 @@ public final class HttpUtils { UnknownHostException.class, RejectedExecutionException.class }; - /** * Some transient exceptions can only be detected by interpreting the message... */ private static final Pattern CONNECTION_ISSUE_PATTERN = Pattern.compile("connection (time|reset)|failure in ssl library, usually a protocol error"); - /** - * Maximum characters to be displayed in a log for application secret. - */ - private static final int MAX_CHARACTERS_DISPLAYED_FOR_SECRET = 8; - @VisibleForTesting HttpUtils() { } @@ -73,6 +73,13 @@ public static boolean isRecoverableError(Throwable t) { } public static String hideSecret(String secret) { + + /* Cannot hide null or empty string. */ + if (secret == null || secret.length() <= 0) { + return secret; + } + + /* Hide secret if string is neither null nor empty string. */ int hidingEndIndex = secret.length() - (secret.length() >= MAX_CHARACTERS_DISPLAYED_FOR_SECRET ? MAX_CHARACTERS_DISPLAYED_FOR_SECRET : 0); char[] fill = new char[hidingEndIndex]; Arrays.fill(fill, '*'); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java index 2892afb457..c21be4327c 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java @@ -147,12 +147,12 @@ public String buildRequestBody() throws JSONException { @Override public void onBeforeCalling(URL url, Map headers) { + if (MobileCenterLog.getLogLevel() <= VERBOSE) { - /* Log url. */ - MobileCenterLog.verbose(LOG_TAG, "Calling " + url + "..."); + /* Log url. */ + MobileCenterLog.verbose(LOG_TAG, "Calling " + url + "..."); - /* Log headers. */ - if (MobileCenterLog.getLogLevel() <= VERBOSE) { + /* Log headers. */ Map logHeaders = new HashMap<>(headers); String appSecret = logHeaders.get(APP_SECRET); if (appSecret != null) { diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/DefaultHttpClientTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/DefaultHttpClientTest.java index e766b9db77..25eb51267d 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/DefaultHttpClientTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/DefaultHttpClientTest.java @@ -29,6 +29,7 @@ import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_POST; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.eq; import static org.mockito.Matchers.notNull; import static org.mockito.Mockito.any; import static org.mockito.Mockito.never; @@ -78,6 +79,7 @@ public DefaultHttpClient.Call answer(InvocationOnMock invocation) throws Throwab } @Test + @SuppressWarnings("unchecked") public void post200() throws Exception { /* Set log level to verbose to test shorter app secret as well. */ @@ -113,7 +115,10 @@ public void post200() throws Exception { verify(urlConnection).setRequestProperty("Content-Type", "application/json"); verify(urlConnection).setRequestProperty("App-Secret", appSecret); verify(urlConnection).setRequestProperty("Install-ID", installId.toString()); + verify(urlConnection).setDoOutput(true); verify(urlConnection).disconnect(); + verify(callTemplate).onBeforeCalling(eq(url), any(Map.class)); + verify(callTemplate).buildRequestBody(); httpClient.close(); /* Verify payload. */ @@ -122,6 +127,51 @@ public void post200() throws Exception { } @Test + @SuppressWarnings("unchecked") + public void post200WithoutCallTemplate() throws Exception { + + /* Set log level to verbose to test shorter app secret as well. */ + MobileCenter.setLogLevel(VERBOSE); + + /* Configure mock HTTP. */ + String urlString = "http://mock/logs?api_version=1.0.0-preview20160914"; + URL url = mock(URL.class); + whenNew(URL.class).withArguments(urlString).thenReturn(url); + HttpURLConnection urlConnection = mock(HttpURLConnection.class); + when(url.openConnection()).thenReturn(urlConnection); + when(urlConnection.getResponseCode()).thenReturn(200); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + when(urlConnection.getOutputStream()).thenReturn(buffer); + when(urlConnection.getInputStream()).thenReturn(new ByteArrayInputStream("OK".getBytes())); + + /* Configure API client. */ + DefaultHttpClient httpClient = new DefaultHttpClient(); + + /* Test calling code. Use shorter but valid app secret. */ + String appSecret = "SHORT"; + UUID installId = UUIDUtils.randomUUID(); + Map headers = new HashMap<>(); + headers.put("App-Secret", appSecret); + headers.put("Install-ID", installId.toString()); + ServiceCallback serviceCallback = mock(ServiceCallback.class); + mockCall(); + httpClient.callAsync(urlString, METHOD_POST, headers, null, serviceCallback); + verify(serviceCallback).onCallSucceeded("OK"); + verifyNoMoreInteractions(serviceCallback); + verify(urlConnection).setRequestProperty("Content-Type", "application/json"); + verify(urlConnection).setRequestProperty("App-Secret", appSecret); + verify(urlConnection).setRequestProperty("Install-ID", installId.toString()); + verify(urlConnection, never()).setDoOutput(true); + verify(urlConnection).disconnect(); + httpClient.close(); + + /* Verify payload. */ + String sentPayload = buffer.toString("UTF-8"); + assertEquals("", sentPayload); + } + + @Test + @SuppressWarnings("unchecked") public void get200() throws Exception { /* Set log level to verbose to test shorter app secret as well. */ @@ -156,11 +206,52 @@ public void get200() throws Exception { verify(urlConnection).setRequestProperty("Content-Type", "application/json"); verify(urlConnection).setRequestProperty("App-Secret", appSecret); verify(urlConnection).setRequestProperty("Install-ID", installId.toString()); + verify(urlConnection, never()).setDoOutput(true); verify(urlConnection).disconnect(); + verify(callTemplate).onBeforeCalling(eq(url), any(Map.class)); verify(callTemplate, never()).buildRequestBody(); httpClient.close(); } + @Test + public void get200WithoutCallTemplate() throws Exception { + + /* Set log level to verbose to test shorter app secret as well. */ + MobileCenter.setLogLevel(VERBOSE); + + /* Configure mock HTTP. */ + String urlString = "http://mock/get"; + URL url = mock(URL.class); + whenNew(URL.class).withArguments(urlString).thenReturn(url); + HttpURLConnection urlConnection = mock(HttpURLConnection.class); + when(url.openConnection()).thenReturn(urlConnection); + when(urlConnection.getResponseCode()).thenReturn(200); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + when(urlConnection.getOutputStream()).thenReturn(buffer); + when(urlConnection.getInputStream()).thenReturn(new ByteArrayInputStream("OK".getBytes())); + + /* Configure API client. */ + DefaultHttpClient httpClient = new DefaultHttpClient(); + + /* Test calling code. */ + String appSecret = UUIDUtils.randomUUID().toString(); + UUID installId = UUIDUtils.randomUUID(); + Map headers = new HashMap<>(); + headers.put("App-Secret", appSecret); + headers.put("Install-ID", installId.toString()); + ServiceCallback serviceCallback = mock(ServiceCallback.class); + mockCall(); + httpClient.callAsync(urlString, METHOD_GET, headers, null, serviceCallback); + verify(serviceCallback).onCallSucceeded("OK"); + verifyNoMoreInteractions(serviceCallback); + verify(urlConnection).setRequestProperty("Content-Type", "application/json"); + verify(urlConnection).setRequestProperty("App-Secret", appSecret); + verify(urlConnection).setRequestProperty("Install-ID", installId.toString()); + verify(urlConnection, never()).setDoOutput(true); + verify(urlConnection).disconnect(); + httpClient.close(); + } + @Test public void error503() throws Exception { diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpUtilsTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpUtilsTest.java new file mode 100644 index 0000000000..f2380b80c2 --- /dev/null +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/http/HttpUtilsTest.java @@ -0,0 +1,37 @@ +package com.microsoft.azure.mobile.http; + +import junit.framework.Assert; + +import org.junit.Test; + +import static com.microsoft.azure.mobile.http.HttpUtils.MAX_CHARACTERS_DISPLAYED_FOR_SECRET; + +@SuppressWarnings("unused") +public class HttpUtilsTest { + + @Test + public void hideNullSecret() { + Assert.assertNull(HttpUtils.hideSecret(null)); + } + + @Test + public void hideEmptySecret() { + Assert.assertEquals("", HttpUtils.hideSecret("")); + } + + @Test + public void hideShortSecret() { + String secret = "Short"; + + //noinspection ReplaceAllDot + Assert.assertEquals(secret.replaceAll(".", "*"), HttpUtils.hideSecret(secret)); + } + + @Test + public void hideLongSecret() { + String secret = "This_is_very_long_secret_for_unit_test_in_http_utils"; + String obfuscatedSecret = HttpUtils.hideSecret(secret); + Assert.assertEquals(secret.length(), obfuscatedSecret.length()); + Assert.assertTrue(obfuscatedSecret.endsWith("*" + secret.substring(secret.length() - MAX_CHARACTERS_DISPLAYED_FOR_SECRET))); + } +} \ No newline at end of file diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java index 5bdb49309e..f5105cd31e 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java @@ -2,14 +2,15 @@ import android.content.Context; -import com.microsoft.azure.mobile.http.DefaultHttpClient; import com.microsoft.azure.mobile.http.HttpClient; import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.HttpUtils; import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.ingestion.models.Log; import com.microsoft.azure.mobile.ingestion.models.LogContainer; import com.microsoft.azure.mobile.ingestion.models.json.LogSerializer; +import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.UUIDUtils; import org.json.JSONException; @@ -21,28 +22,34 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; +import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; +import static com.microsoft.azure.mobile.http.DefaultHttpClient.APP_SECRET; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_POST; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.contains; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.notNull; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("unused") -@PrepareForTest(IngestionHttp.class) +@PrepareForTest({IngestionHttp.class, MobileCenterLog.class}) public class IngestionHttpTest { @Rule @@ -91,7 +98,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify call to http client. */ HashMap expectedHeaders = new HashMap<>(); - expectedHeaders.put(DefaultHttpClient.APP_SECRET, appSecret); + expectedHeaders.put(APP_SECRET, appSecret); expectedHeaders.put(IngestionHttp.INSTALL_ID, installId.toString()); verify(httpClient).callAsync(eq("http://mock" + IngestionHttp.API_PATH), eq(METHOD_POST), eq(expectedHeaders), notNull(HttpClient.CallTemplate.class), eq(serviceCallback)); assertNotNull(callTemplate.get()); @@ -150,7 +157,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify call to http client. */ HashMap expectedHeaders = new HashMap<>(); - expectedHeaders.put(DefaultHttpClient.APP_SECRET, appSecret); + expectedHeaders.put(APP_SECRET, appSecret); expectedHeaders.put(IngestionHttp.INSTALL_ID, installId.toString()); verify(httpClient).callAsync(eq("http://mock/logs?api_version=1.0.0-preview20160914"), eq(METHOD_POST), eq(expectedHeaders), notNull(HttpClient.CallTemplate.class), eq(serviceCallback)); assertNotNull(callTemplate.get()); @@ -169,4 +176,79 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { ingestionHttp.close(); verify(httpClient).close(); } + + @Test + public void onBeforeCalling() throws Exception { + + /* Mock instances. */ + URL url = new URL("http://mock/path/file"); + String appSecret = UUIDUtils.randomUUID().toString(); + String obfuscatedSecret = HttpUtils.hideSecret(appSecret); + Map headers = new HashMap<>(); + headers.put("Another-Header", "Another-Value"); + HttpClient.CallTemplate callTemplate = getCallTemplate(appSecret); + MobileCenterLog.setLogLevel(android.util.Log.VERBOSE); + mockStatic(MobileCenterLog.class); + + /* Call onBeforeCalling with parameters. */ + callTemplate.onBeforeCalling(url, headers); + + /* Verify url log. */ + verifyStatic(); + MobileCenterLog.verbose(anyString(), contains(url.toString())); + + /* Verify header log. */ + for (Map.Entry header : headers.entrySet()) { + verifyStatic(); + MobileCenterLog.verbose(anyString(), contains(header.getValue())); + } + + /* Put app secret to header. */ + headers.put(APP_SECRET, appSecret); + callTemplate.onBeforeCalling(url, headers); + + /* Verify app secret is in log. */ + verifyStatic(); + MobileCenterLog.verbose(anyString(), contains(obfuscatedSecret)); + } + + @Test + @SuppressWarnings("unchecked") + public void onBeforeCallingWithAnotherLogLevel() throws Exception { + + /* Mock instances. */ + String appSecret = UUIDUtils.randomUUID().toString(); + HttpClient.CallTemplate callTemplate = getCallTemplate(appSecret); + + /* Change log level. */ + MobileCenterLog.setLogLevel(android.util.Log.WARN); + + /* Call onBeforeCalling with parameters. */ + callTemplate.onBeforeCalling(mock(URL.class), mock(Map.class)); + + /* Verify. */ + verifyStatic(never()); + MobileCenterLog.verbose(anyString(), anyString()); + } + + private HttpClient.CallTemplate getCallTemplate(String appSecret) throws Exception { + + /* Configure mock HTTP to get an instance of IngestionCallTemplate. */ + final ServiceCall call = mock(ServiceCall.class); + final AtomicReference callTemplate = new AtomicReference<>(); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).then(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + callTemplate.set((HttpClient.CallTemplate) invocation.getArguments()[3]); + return call; + } + }); + IngestionHttp ingestionHttp = new IngestionHttp(mock(Context.class), mock(LogSerializer.class)); + ingestionHttp.setServerUrl("http://mock"); + assertEquals(call, ingestionHttp.sendAsync(appSecret, UUIDUtils.randomUUID(), mock(LogContainer.class), mock(ServiceCallback.class))); + return callTemplate.get(); + } } \ No newline at end of file From 96994ca42b2343a0c048eb7b7a6d841cdc71e14d Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 23 Feb 2017 15:57:43 -0800 Subject: [PATCH 082/142] Improve comments about log URL * Use URL in upper case. * Comment reflection usage in sasquatch. --- .../azure/mobile/sasquatch/activities/MainActivity.java | 3 +++ .../main/java/com/microsoft/azure/mobile/MobileCenter.java | 2 +- .../main/java/com/microsoft/azure/mobile/channel/Channel.java | 4 ++-- .../java/com/microsoft/azure/mobile/ingestion/Ingestion.java | 4 ++-- .../microsoft/azure/mobile/ingestion/http/IngestionHttp.java | 4 ++-- .../java/com/microsoft/azure/mobile/MobileCenterTest.java | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index 7cfcb8bfe3..f9431f6c1e 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -45,9 +45,12 @@ protected void onCreate(Bundle savedInstanceState) { sSharedPreferences = getSharedPreferences("Sasquatch", Context.MODE_PRIVATE); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().build()); + /* Set custom log URL if one was configured in settings. */ String logUrl = sSharedPreferences.getString(LOG_URL_KEY, null); if (logUrl != null) { try { + + /* Method name changed and jCenter not yet updated so need to use reflection. */ Method setLogUrl; try { setLogUrl = MobileCenter.class.getMethod("setLogUrl", String.class); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index 5e4a3991d6..872d0aa23d 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -47,7 +47,7 @@ public class MobileCenter { private boolean mLogLevelConfigured; /** - * Custom log url if any. + * Custom log URL if any. */ private String mLogUrl; diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/Channel.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/Channel.java index 9735b78893..f6a317e914 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/Channel.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/channel/Channel.java @@ -51,9 +51,9 @@ public interface Channel { void setEnabled(boolean enabled); /** - * Update log url. + * Update log URL. * - * @param logUrl log url. + * @param logUrl log URL. */ void setLogUrl(String logUrl); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java index 00ee1ed103..de69aac3b2 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/Ingestion.java @@ -23,9 +23,9 @@ public interface Ingestion extends Closeable { ServiceCall sendAsync(String appSecret, UUID installId, LogContainer logContainer, ServiceCallback serviceCallback) throws IllegalArgumentException; /** - * Update log url. + * Update log URL. * - * @param logUrl log url. + * @param logUrl log URL. */ void setLogUrl(String logUrl); } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java index 94081c40e7..0887f2da29 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/IngestionHttp.java @@ -222,9 +222,9 @@ private static String dump(HttpURLConnection urlConnection) throws IOException { } /** - * Set the base url. + * Set the base URL. * - * @param logUrl the base url. + * @param logUrl the base URL. */ @Override @SuppressWarnings("SameParameterValue") diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index 093326df35..0d9a020348 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -624,7 +624,7 @@ public void setLogUrl() throws Exception { /* No effect for now. */ verify(channel, never()).setLogUrl(logUrl); - /* Start should propagate the log url. */ + /* Start should propagate the log URL. */ MobileCenter.start(application, DUMMY_APP_SECRET, DummyService.class); verify(channel).setLogUrl(logUrl); From a2ffaca2a6d811613df8d615cd8a03ec7f855d22 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 23 Feb 2017 16:18:41 -0800 Subject: [PATCH 083/142] Simplify a test and remove related invalid comment --- .../azure/mobile/updates/UpdatesBeforeDownloadTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java index a401bc15f6..577696dc6d 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java @@ -64,9 +64,6 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn("someId"); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); - - /* Override mock answer by calling it once then configuring new answer. */ - mPackageManager.getPackageInfo("com.contoso", 0); when(mPackageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); /* Trigger call. */ From a5df4c58832066a3504be3c5062e543ea1f8335a Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Thu, 23 Feb 2017 16:28:28 -0800 Subject: [PATCH 084/142] Addressing PR feedbacks --- .../main/java/com/microsoft/azure/mobile/updates/Updates.java | 2 +- .../main/java/com/microsoft/azure/mobile/http/HttpUtils.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index 0ec4beeaa7..a61252a2d6 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -611,7 +611,7 @@ synchronized void getLatestReleaseDetails(@NonNull String updateToken) { @Override public String buildRequestBody() throws JSONException { - /* This method is getting called. */ + /* GET is only used for Updates service. This method is never getting called. */ return null; } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java index fa1971fafd..bef4b740b4 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/http/HttpUtils.java @@ -75,7 +75,7 @@ public static boolean isRecoverableError(Throwable t) { public static String hideSecret(String secret) { /* Cannot hide null or empty string. */ - if (secret == null || secret.length() <= 0) { + if (secret == null || secret.isEmpty()) { return secret; } From a6fd95208de8ee70029c05a22bdaff9b35f00964 Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Thu, 23 Feb 2017 16:31:35 -0800 Subject: [PATCH 085/142] Revise comment --- .../main/java/com/microsoft/azure/mobile/updates/Updates.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index a61252a2d6..10d06cd706 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -611,7 +611,7 @@ synchronized void getLatestReleaseDetails(@NonNull String updateToken) { @Override public String buildRequestBody() throws JSONException { - /* GET is only used for Updates service. This method is never getting called. */ + /* Only GET is used by Updates service. This method is never getting called. */ return null; } From f725189542455f599654721b70f40be79e7378a2 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 23 Feb 2017 17:52:54 -0800 Subject: [PATCH 086/142] Fix message when changing url to production (that also needs a restart) To prepare a simplification of all this code, make ingestion url public field. Will simplify everything after this gets published to jCenter... --- .../activities/SettingsActivity.java | 21 ++++++++++++------- .../src/main/res/values/settings.xml | 3 ++- .../azure/mobile/ingestion/IngestionHttp.java | 13 ++++++------ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java index 2819b50cb6..a08316fe6a 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java @@ -206,7 +206,7 @@ public boolean onPreferenceClick(Preference preference) { } }); String defaultServerUrl = getString(R.string.log_url); - final String defaultServerUrlDisplay = TextUtils.isEmpty(defaultServerUrl) ? getString(R.string.log_url_production) : defaultServerUrl; + final String defaultServerUrlDisplay = TextUtils.isEmpty(defaultServerUrl) ? getString(R.string.log_url_set_to_production) : defaultServerUrl; initClickableSetting(R.string.log_url_key, MainActivity.sSharedPreferences.getString(LOG_URL_KEY, defaultServerUrlDisplay), new Preference.OnPreferenceClickListener() { @Override @@ -214,7 +214,7 @@ public boolean onPreferenceClick(final Preference preference) { final EditText input = new EditText(getActivity()); input.setInputType(InputType.TYPE_CLASS_TEXT); input.setText(MainActivity.sSharedPreferences.getString(LOG_URL_KEY, null)); - input.setHint(R.string.log_url_production); + input.setHint(R.string.log_url_set_to_production); new AlertDialog.Builder(getActivity()).setTitle(R.string.log_url_title).setView(input) .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() { @@ -224,9 +224,9 @@ public void onClick(DialogInterface dialog, int which) { if (Patterns.WEB_URL.matcher(input.getText().toString()).matches()) { String url = input.getText().toString(); setKeyValue(LOG_URL_KEY, url); - Toast.makeText(getActivity(), String.format(getActivity().getString(R.string.log_url_changed_format), url), Toast.LENGTH_SHORT).show(); + toastUrlChange(url); } else if (input.getText().toString().isEmpty()) { - setProductionUrl(); + setDefaultUrl(); } else { Toast.makeText(getActivity(), R.string.log_url_invalid, Toast.LENGTH_SHORT).show(); } @@ -237,7 +237,7 @@ public void onClick(DialogInterface dialog, int which) { @Override public void onClick(DialogInterface dialog, int which) { - setProductionUrl(); + setDefaultUrl(); preference.setSummary(MainActivity.sSharedPreferences.getString(LOG_URL_KEY, defaultServerUrlDisplay)); } }) @@ -246,9 +246,16 @@ public void onClick(DialogInterface dialog, int which) { return true; } - private void setProductionUrl() { + private void setDefaultUrl() { setKeyValue(LOG_URL_KEY, null); - Toast.makeText(getActivity(), R.string.log_url_production, Toast.LENGTH_SHORT).show(); + toastUrlChange(getString(R.string.log_url)); + } + + private void toastUrlChange(String url) { + if (TextUtils.isEmpty(url)) { + url = getString(R.string.log_url_production); + } + Toast.makeText(getActivity(), String.format(getActivity().getString(R.string.log_url_changed_format), url), Toast.LENGTH_SHORT).show(); } }); } diff --git a/apps/sasquatch/src/main/res/values/settings.xml b/apps/sasquatch/src/main/res/values/settings.xml index 79d87487e1..8c00cb59df 100644 --- a/apps/sasquatch/src/main/res/values/settings.xml +++ b/apps/sasquatch/src/main/res/values/settings.xml @@ -59,7 +59,8 @@ Log URL Show Log URL has been changed to %s successfully. Please close the application completely and relaunch again. Log URL is not valid. - Log URL currently set to the production. + Log URL currently set to the production. + production Save Reset diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java index 0c370c1966..b47f5895a3 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/IngestionHttp.java @@ -33,6 +33,12 @@ public class IngestionHttp implements Ingestion { + /** + * Default log URL. + */ + @SuppressWarnings("WeakerAccess") + public static final String DEFAULT_LOG_URL = "https://in.mobile.azure.com"; + /** * API Path. */ @@ -45,11 +51,6 @@ public class IngestionHttp implements Ingestion { @VisibleForTesting static final String INSTALL_ID = "Install-ID"; - /** - * Default base URL. - */ - private static final String DEFAULT_BASE_URL = "https://in.mobile.azure.com"; - /** * Log serializer. */ @@ -76,7 +77,7 @@ public IngestionHttp(@NonNull Context context, @NonNull LogSerializer logSeriali HttpClientRetryer retryer = new HttpClientRetryer(new DefaultHttpClient()); NetworkStateHelper networkStateHelper = NetworkStateHelper.getSharedInstance(context); mHttpClient = new HttpClientNetworkStateHandler(retryer, networkStateHelper); - mLogUrl = DEFAULT_BASE_URL; + mLogUrl = DEFAULT_LOG_URL; } /** From fd79dcf7b15d8252bbe395735941d83f8dd31336 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 23 Feb 2017 17:57:57 -0800 Subject: [PATCH 087/142] Fix renaming serverUrl to logUrl after merging to updates --- .../mobile/sasquatch/activities/SettingsActivity.java | 10 +++++----- .../azure/mobile/ingestion/IngestionHttpTest.java | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java index a08316fe6a..09a6b404a2 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java @@ -205,9 +205,9 @@ public boolean onPreferenceClick(Preference preference) { return true; } }); - String defaultServerUrl = getString(R.string.log_url); - final String defaultServerUrlDisplay = TextUtils.isEmpty(defaultServerUrl) ? getString(R.string.log_url_set_to_production) : defaultServerUrl; - initClickableSetting(R.string.log_url_key, MainActivity.sSharedPreferences.getString(LOG_URL_KEY, defaultServerUrlDisplay), new Preference.OnPreferenceClickListener() { + String defaultLogUrl = getString(R.string.log_url); + final String defaultLogUrlDisplay = TextUtils.isEmpty(defaultLogUrl) ? getString(R.string.log_url_set_to_production) : defaultLogUrl; + initClickableSetting(R.string.log_url_key, MainActivity.sSharedPreferences.getString(LOG_URL_KEY, defaultLogUrlDisplay), new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(final Preference preference) { @@ -230,7 +230,7 @@ public void onClick(DialogInterface dialog, int which) { } else { Toast.makeText(getActivity(), R.string.log_url_invalid, Toast.LENGTH_SHORT).show(); } - preference.setSummary(MainActivity.sSharedPreferences.getString(LOG_URL_KEY, defaultServerUrlDisplay)); + preference.setSummary(MainActivity.sSharedPreferences.getString(LOG_URL_KEY, defaultLogUrlDisplay)); } }) .setNeutralButton(R.string.reset, new DialogInterface.OnClickListener() { @@ -238,7 +238,7 @@ public void onClick(DialogInterface dialog, int which) { @Override public void onClick(DialogInterface dialog, int which) { setDefaultUrl(); - preference.setSummary(MainActivity.sSharedPreferences.getString(LOG_URL_KEY, defaultServerUrlDisplay)); + preference.setSummary(MainActivity.sSharedPreferences.getString(LOG_URL_KEY, defaultLogUrlDisplay)); } }) .setNegativeButton(R.string.cancel, null) diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java index f5105cd31e..0433114d2d 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/IngestionHttpTest.java @@ -90,7 +90,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Test calling code. */ IngestionHttp ingestionHttp = new IngestionHttp(mock(Context.class), serializer); - ingestionHttp.setServerUrl("http://mock"); + ingestionHttp.setLogUrl("http://mock"); String appSecret = UUIDUtils.randomUUID().toString(); UUID installId = UUIDUtils.randomUUID(); ServiceCallback serviceCallback = mock(ServiceCallback.class); @@ -149,7 +149,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Test calling code. */ IngestionHttp ingestionHttp = new IngestionHttp(mock(Context.class), serializer); - ingestionHttp.setServerUrl("http://mock"); + ingestionHttp.setLogUrl("http://mock"); String appSecret = UUIDUtils.randomUUID().toString(); UUID installId = UUIDUtils.randomUUID(); ServiceCallback serviceCallback = mock(ServiceCallback.class); @@ -247,7 +247,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); IngestionHttp ingestionHttp = new IngestionHttp(mock(Context.class), mock(LogSerializer.class)); - ingestionHttp.setServerUrl("http://mock"); + ingestionHttp.setLogUrl("http://mock"); assertEquals(call, ingestionHttp.sendAsync(appSecret, UUIDUtils.randomUUID(), mock(LogContainer.class), mock(ServiceCallback.class))); return callTemplate.get(); } From 3cc523f97a94d2fef8fc614d17aadca198948806 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 23 Feb 2017 18:04:26 -0800 Subject: [PATCH 088/142] Suppress 2 false positive warnings --- .../main/java/com/microsoft/azure/mobile/updates/Updates.java | 1 + .../src/main/java/com/microsoft/azure/mobile/test/TestUtils.java | 1 + 2 files changed, 2 insertions(+) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java index a384b50a10..2f9c000083 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java @@ -770,6 +770,7 @@ private boolean isMoreRecent(PackageInfo packageInfo, ReleaseDetails releaseDeta * @param alertDialog existing dialog if any, always returning true when null. * @return true if a new dialog should be displayed, false otherwise. */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean shouldRefreshDialog(@Nullable AlertDialog alertDialog) { /* We could be in another activity now, refresh dialog. */ diff --git a/test/src/main/java/com/microsoft/azure/mobile/test/TestUtils.java b/test/src/main/java/com/microsoft/azure/mobile/test/TestUtils.java index ff0c881520..46528af23b 100644 --- a/test/src/main/java/com/microsoft/azure/mobile/test/TestUtils.java +++ b/test/src/main/java/com/microsoft/azure/mobile/test/TestUtils.java @@ -48,6 +48,7 @@ public static void compareSelfNullClass(Object o) { * @param value value to set. * @throws Exception if an exception occurs. */ + @SuppressWarnings("SameParameterValue") public static void setInternalState(Class clazz, String fieldName, Object value) throws Exception { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); From e4c51086d23371323dac7a4867a2c6d0e1263309 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Mon, 27 Feb 2017 16:51:31 +0300 Subject: [PATCH 089/142] Add uncaught exception handler in core module --- .../azure/mobile/crashes/Crashes.java | 4 -- .../crashes/UncaughtExceptionHandler.java | 14 ++--- .../crashes/UncaughtExceptionHandlerTest.java | 5 +- .../microsoft/azure/mobile/MobileCenter.java | 54 ++++++++++++++++++- .../azure/mobile/utils/ShutdownHelper.java | 18 +++++++ 5 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java diff --git a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java index 8183802c9c..64ae27aae2 100644 --- a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java +++ b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java @@ -794,10 +794,6 @@ void saveUncaughtException(Thread thread, Throwable exception) { } catch (IOException e) { MobileCenterLog.error(Crashes.LOG_TAG, "Error writing error log to file", e); } - - /* Wait channel to finish saving other logs in background. */ - if (mChannel != null) - mChannel.shutdown(); } /** diff --git a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandler.java b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandler.java index 38d4b6cd1f..4bdafe3a8a 100644 --- a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandler.java +++ b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandler.java @@ -1,8 +1,9 @@ package com.microsoft.azure.mobile.crashes; -import android.os.Process; import android.support.annotation.VisibleForTesting; +import com.microsoft.azure.mobile.utils.ShutdownHelper; + class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { private boolean mIgnoreDefaultExceptionHandler = false; @@ -20,7 +21,7 @@ public void uncaughtException(Thread thread, Throwable exception) { if (mDefaultUncaughtExceptionHandler != null) { mDefaultUncaughtExceptionHandler.uncaughtException(thread, exception); } else { - ShutdownHelper.shutdown(); + ShutdownHelper.shutdown(10); } } } @@ -50,13 +51,4 @@ void register() { void unregister() { Thread.setDefaultUncaughtExceptionHandler(mDefaultUncaughtExceptionHandler); } - - @VisibleForTesting - static class ShutdownHelper { - - static void shutdown() { - Process.killProcess(Process.myPid()); - System.exit(10); - } - } } diff --git a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java index 4294006365..38323a1bc0 100644 --- a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java +++ b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java @@ -11,6 +11,7 @@ import com.microsoft.azure.mobile.utils.DeviceInfoHelper; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.PrefStorageConstants; +import com.microsoft.azure.mobile.utils.ShutdownHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper; import org.json.JSONException; @@ -47,7 +48,7 @@ import static org.powermock.api.mockito.PowerMockito.when; @SuppressWarnings("unused") -@PrepareForTest({SystemClock.class, StorageHelper.PreferencesStorage.class, StorageHelper.InternalStorage.class, Crashes.class, ErrorLogHelper.class, DeviceInfoHelper.class, UncaughtExceptionHandler.ShutdownHelper.class, MobileCenterLog.class, Process.class}) +@PrepareForTest({SystemClock.class, StorageHelper.PreferencesStorage.class, StorageHelper.InternalStorage.class, Crashes.class, ErrorLogHelper.class, DeviceInfoHelper.class, ShutdownHelper.class, MobileCenterLog.class, Process.class}) public class UncaughtExceptionHandlerTest { private static final String CRASHES_ENABLED_KEY = PrefStorageConstants.KEY_ENABLED + "_" + Crashes.getInstance().getGroupName(); @@ -134,8 +135,6 @@ public void handleExceptionAndPassOn() { @Test public void handleExceptionAndIgnoreDefaultHandler() { - // dummy coverage - new UncaughtExceptionHandler.ShutdownHelper(); // mock process id when(Process.myPid()).thenReturn(123); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index 872d0aa23d..1e41377578 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -17,6 +17,7 @@ import com.microsoft.azure.mobile.utils.IdHelper; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.PrefStorageConstants; +import com.microsoft.azure.mobile.utils.ShutdownHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper; import java.util.HashSet; @@ -56,6 +57,8 @@ public class MobileCenter { */ private Application mApplication; + private UncaughtExceptionHandler mUncaughtExceptionHandler; + /** * Configured services. */ @@ -290,12 +293,20 @@ private synchronized boolean instanceConfigure(Application application, String a /* If parameters are valid, init context related resources. */ StorageHelper.initialize(application); + + /* For don't call PreferencesStorage twice. */ + boolean enabled = isInstanceEnabled(); + + /* Init uncaught exception handler. */ + mUncaughtExceptionHandler = new UncaughtExceptionHandler(); + if (enabled) + mUncaughtExceptionHandler.register(); mServices = new HashSet<>(); /* Init channel. */ mLogSerializer = new DefaultLogSerializer(); mChannel = new DefaultChannel(application, appSecret, mLogSerializer); - mChannel.setEnabled(isInstanceEnabled()); + mChannel.setEnabled(enabled); if (mLogUrl != null) mChannel.setLogUrl(mLogUrl); MobileCenterLog.logAssert(LOG_TAG, "Mobile Center SDK configured successfully."); @@ -383,6 +394,13 @@ private synchronized void setInstanceEnabled(boolean enabled) { boolean switchToDisabled = previouslyEnabled && !enabled; boolean switchToEnabled = !previouslyEnabled && enabled; + /* Update uncaught exception subscription. */ + if (switchToEnabled) { + mUncaughtExceptionHandler.register(); + } else if (switchToDisabled) { + mUncaughtExceptionHandler.unregister(); + } + /* Update state. */ StorageHelper.PreferencesStorage.putBoolean(PrefStorageConstants.KEY_ENABLED, enabled); @@ -424,4 +442,38 @@ Application getApplication() { void setChannel(Channel channel) { mChannel = channel; } + + @VisibleForTesting + class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { + + private Thread.UncaughtExceptionHandler mDefaultUncaughtExceptionHandler; + + @Override + public void uncaughtException(Thread thread, Throwable exception) { + if (isEnabled()) { + /* Wait channel to finish saving other logs in background. */ + if (mChannel != null) + mChannel.shutdown(); + } + if (mDefaultUncaughtExceptionHandler != null) { + mDefaultUncaughtExceptionHandler.uncaughtException(thread, exception); + } else { + ShutdownHelper.shutdown(10); + } + } + + @VisibleForTesting + Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() { + return mDefaultUncaughtExceptionHandler; + } + + void register() { + mDefaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(this); + } + + void unregister() { + Thread.setDefaultUncaughtExceptionHandler(mDefaultUncaughtExceptionHandler); + } + } } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java new file mode 100644 index 0000000000..86023c9461 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java @@ -0,0 +1,18 @@ +package com.microsoft.azure.mobile.utils; + +import android.os.Process; + +/** + * Shutdown helper. + */ +public class ShutdownHelper { + + public static void shutdown() { + shutdown(1); + } + + public static void shutdown(int status) { + Process.killProcess(Process.myPid()); + System.exit(status); + } +} From 3091e0eeb3ccda069aa72b4644fcbcce38305bb3 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Mon, 27 Feb 2017 16:53:16 +0300 Subject: [PATCH 090/142] Fix comments style --- .../crashes/UncaughtExceptionHandlerTest.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java index 38323a1bc0..25861fba0e 100644 --- a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java +++ b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java @@ -102,10 +102,12 @@ public Object answer(InvocationOnMock invocation) throws Throwable { @Test public void registerWorks() { - // Verify that exception handler is default + + /* Verify that exception handler is default */ assertEquals(mDefaultExceptionHandler, Thread.getDefaultUncaughtExceptionHandler()); mExceptionHandler.register(); - // Verify that creation registers handler and previously defined handler is correctly saved + + /* Verify that creation registers handler and previously defined handler is correctly saved */ assertEquals(mExceptionHandler, Thread.getDefaultUncaughtExceptionHandler()); assertEquals(mDefaultExceptionHandler, mExceptionHandler.getDefaultUncaughtExceptionHandler()); @@ -135,14 +137,13 @@ public void handleExceptionAndPassOn() { @Test public void handleExceptionAndIgnoreDefaultHandler() { - - // mock process id + /* Mock process id */ when(Process.myPid()).thenReturn(123); - // Register crash handler + /* Register crash handler */ mExceptionHandler.register(); - // Verify that the exception is handled and not being passed on to the previous default UncaughtExceptionHandler + /* Verify that the exception is handled and not being passed on to the previous default UncaughtExceptionHandler */ Thread thread = Thread.currentThread(); RuntimeException exception = new RuntimeException(); mExceptionHandler.setIgnoreDefaultExceptionHandler(true); @@ -159,7 +160,7 @@ public void handleExceptionAndIgnoreDefaultHandler() { @Test public void passDefaultHandler() { - // Verify that when crashes is disabled, an exception is instantly passed on + /* Verify that when crashes is disabled, an exception is instantly passed on */ when(Crashes.isEnabled()).thenReturn(false); mExceptionHandler.register(); @@ -175,7 +176,7 @@ public void passDefaultHandler() { @Test public void crashesDisabledNoDefaultHandler() { - // Verify that when crashes is disabled, an exception is instantly passed on + /* Verify that when crashes is disabled, an exception is instantly passed on */ when(Crashes.isEnabled()).thenReturn(false); mExceptionHandler.register(); From d0f5ee9a3bc3dabfac6d668d3b5bf15e070f8919 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Mon, 27 Feb 2017 18:07:17 +0300 Subject: [PATCH 091/142] Add uncaught exception handler tests --- .../crashes/UncaughtExceptionHandlerTest.java | 8 +-- .../microsoft/azure/mobile/MobileCenter.java | 5 ++ .../azure/mobile/utils/ShutdownHelper.java | 6 +++ .../azure/mobile/MobileCenterTest.java | 51 ++++++++++++++++++- .../mobile/utils/ShutdownHelperTest.java | 50 ++++++++++++++++++ 5 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java diff --git a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java index 25861fba0e..c29ad08cfb 100644 --- a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java +++ b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java @@ -48,7 +48,7 @@ import static org.powermock.api.mockito.PowerMockito.when; @SuppressWarnings("unused") -@PrepareForTest({SystemClock.class, StorageHelper.PreferencesStorage.class, StorageHelper.InternalStorage.class, Crashes.class, ErrorLogHelper.class, DeviceInfoHelper.class, ShutdownHelper.class, MobileCenterLog.class, Process.class}) +@PrepareForTest({SystemClock.class, StorageHelper.PreferencesStorage.class, StorageHelper.InternalStorage.class, Crashes.class, ErrorLogHelper.class, DeviceInfoHelper.class, ShutdownHelper.class, MobileCenterLog.class}) public class UncaughtExceptionHandlerTest { private static final String CRASHES_ENABLED_KEY = PrefStorageConstants.KEY_ENABLED + "_" + Crashes.getInstance().getGroupName(); @@ -69,7 +69,6 @@ public void setUp() { mockStatic(StorageHelper.InternalStorage.class); mockStatic(ErrorLogHelper.class); mockStatic(DeviceInfoHelper.class); - mockStatic(Process.class); mockStatic(System.class); when(StorageHelper.PreferencesStorage.getBoolean(CRASHES_ENABLED_KEY, true)).thenReturn(true); @@ -137,9 +136,6 @@ public void handleExceptionAndPassOn() { @Test public void handleExceptionAndIgnoreDefaultHandler() { - /* Mock process id */ - when(Process.myPid()).thenReturn(123); - /* Register crash handler */ mExceptionHandler.register(); @@ -153,8 +149,6 @@ public void handleExceptionAndIgnoreDefaultHandler() { verifyStatic(); ErrorLogHelper.createErrorLog(any(Context.class), any(Thread.class), any(Throwable.class), Matchers.>any(), anyLong(), anyBoolean()); verifyStatic(); - Process.killProcess(123); - verifyStatic(); System.exit(10); } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index 1e41377578..530bbe9efa 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -438,6 +438,11 @@ Application getApplication() { return mApplication; } + @VisibleForTesting + UncaughtExceptionHandler getUncaughtExceptionHandler() { + return mUncaughtExceptionHandler; + } + @VisibleForTesting void setChannel(Channel channel) { mChannel = channel; diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java index 86023c9461..fee8b12214 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java @@ -1,12 +1,18 @@ package com.microsoft.azure.mobile.utils; import android.os.Process; +import android.support.annotation.VisibleForTesting; /** * Shutdown helper. */ public class ShutdownHelper { + @VisibleForTesting + ShutdownHelper() { + /* Hide constructor. */ + } + public static void shutdown() { shutdown(1); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index 0d9a020348..81c4cf6b5a 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -14,6 +14,7 @@ import com.microsoft.azure.mobile.utils.DeviceInfoHelper; import com.microsoft.azure.mobile.utils.IdHelper; import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.ShutdownHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper; import org.junit.After; @@ -58,7 +59,20 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("unused") -@PrepareForTest({MobileCenter.class, Channel.class, Constants.class, MobileCenterLog.class, StorageHelper.class, StorageHelper.PreferencesStorage.class, IdHelper.class, StorageHelper.DatabaseStorage.class, DeviceInfoHelper.class}) +@PrepareForTest({ + MobileCenter.class, + MobileCenter.UncaughtExceptionHandler.class, + Channel.class, + Constants.class, + MobileCenterLog.class, + StorageHelper.class, + StorageHelper.PreferencesStorage.class, + IdHelper.class, + StorageHelper.DatabaseStorage.class, + DeviceInfoHelper.class, + Thread.class, + ShutdownHelper.class +}) public class MobileCenterTest { private static final String DUMMY_APP_SECRET = "123e4567-e89b-12d3-a456-426655440000"; @@ -86,6 +100,8 @@ public void setUp() { mockStatic(StorageHelper.PreferencesStorage.class); mockStatic(IdHelper.class); mockStatic(StorageHelper.DatabaseStorage.class); + mockStatic(Thread.class); + mockStatic(ShutdownHelper.class); /* First call to com.microsoft.azure.mobile.MobileCenter.isEnabled shall return true, initial state. */ when(StorageHelper.PreferencesStorage.getBoolean(anyString(), eq(true))).thenReturn(true); @@ -634,6 +650,39 @@ public void setLogUrl() throws Exception { verify(channel).setLogUrl(logUrl); } + @Test + public void uncaughtExceptionHandler() { + Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = mock(Thread.UncaughtExceptionHandler.class); + when(Thread.getDefaultUncaughtExceptionHandler()).thenReturn(defaultUncaughtExceptionHandler); + MobileCenter.configure(application, DUMMY_APP_SECRET); + MobileCenter.UncaughtExceptionHandler handler = MobileCenter.getInstance().getUncaughtExceptionHandler(); + assertNotNull(handler); + assertEquals(defaultUncaughtExceptionHandler, handler.getDefaultUncaughtExceptionHandler()); + verifyStatic(); + Thread.setDefaultUncaughtExceptionHandler(eq(handler)); + + Channel channel = mock(Channel.class); + Thread thread = mock(Thread.class); + Throwable exception = mock(Throwable.class); + MobileCenter.getInstance().setChannel(channel); + handler.uncaughtException(thread, exception); + verify(channel).shutdown(); + verify(defaultUncaughtExceptionHandler).uncaughtException(eq(thread), eq(exception)); + + MobileCenter.setEnabled(false); + verifyStatic(); + Thread.setDefaultUncaughtExceptionHandler(eq(defaultUncaughtExceptionHandler)); + handler.uncaughtException(thread, exception); + verify(channel, times(1)).shutdown(); + + when(Thread.getDefaultUncaughtExceptionHandler()).thenReturn(null); + MobileCenter.setEnabled(true); + assertNull(handler.getDefaultUncaughtExceptionHandler()); + handler.uncaughtException(thread, exception); + verifyStatic(); + ShutdownHelper.shutdown(10); + } + private static class DummyService extends AbstractMobileCenterService { private static DummyService sharedInstance; diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java new file mode 100644 index 0000000000..06008e672a --- /dev/null +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java @@ -0,0 +1,50 @@ +package com.microsoft.azure.mobile.utils; + +import android.os.Process; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; + +import static org.junit.Assert.assertNotNull; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +@SuppressWarnings("unused") +@PrepareForTest({ShutdownHelper.class, Process.class}) +public class ShutdownHelperTest { + + @Rule + public PowerMockRule mPowerMockRule = new PowerMockRule(); + + @Before + public void setUp() { + mockStatic(Process.class); + mockStatic(System.class); + } + + @Test + public void shutdown() throws Exception { + + /* Dummy coverage */ + assertNotNull(new ShutdownHelper()); + + /* Mock process id */ + when(Process.myPid()).thenReturn(123); + + ShutdownHelper.shutdown(); + verifyStatic(); + Process.killProcess(123); + verifyStatic(); + System.exit(1); + + ShutdownHelper.shutdown(999); + verifyStatic(); + System.exit(999); + } + + +} \ No newline at end of file From af213484ec158ac319c903d479c0b34d635134b8 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Mon, 27 Feb 2017 18:54:03 +0300 Subject: [PATCH 092/142] Fix test for complete code coverage --- .../test/java/com/microsoft/azure/mobile/MobileCenterTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index 81c4cf6b5a..ac77020bae 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -677,6 +677,7 @@ public void uncaughtExceptionHandler() { when(Thread.getDefaultUncaughtExceptionHandler()).thenReturn(null); MobileCenter.setEnabled(true); + MobileCenter.getInstance().setChannel(null); assertNull(handler.getDefaultUncaughtExceptionHandler()); handler.uncaughtException(thread, exception); verifyStatic(); From 6a23dd4d49710563e7730f713b368bf64f25141a Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Mon, 27 Feb 2017 11:25:11 -0800 Subject: [PATCH 093/142] Change directory from updates to distribute --- .../build.gradle | 0 .../proguard-rules.pro | 0 .../java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java | 0 .../com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java | 0 .../src/main/AndroidManifest.xml | 0 .../java/com/microsoft/azure/mobile/updates/BrowserUtils.java | 0 .../java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java | 0 .../microsoft/azure/mobile/updates/DownloadManagerReceiver.java | 0 .../java/com/microsoft/azure/mobile/updates/InstallerUtils.java | 0 .../java/com/microsoft/azure/mobile/updates/ReleaseDetails.java | 0 .../java/com/microsoft/azure/mobile/updates/UpdateConstants.java | 0 .../src/main/java/com/microsoft/azure/mobile/updates/Updates.java | 0 .../src/main/res/values/strings.xml | 0 .../com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java | 0 .../com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java | 0 .../com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java | 0 .../mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java | 0 .../azure/mobile/updates/UnknownSourcesDetectionTest.java | 0 .../com/microsoft/azure/mobile/updates/UpdateConstantsTest.java | 0 .../azure/mobile/updates/UpdatesBeforeApiSuccessTest.java | 0 .../microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java | 0 .../com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java | 0 .../azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java | 0 .../test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java | 0 .../azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java | 0 25 files changed, 0 insertions(+), 0 deletions(-) rename sdk/{mobile-center-updates => mobile-center-distribute}/build.gradle (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/proguard-rules.pro (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/main/AndroidManifest.xml (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/main/java/com/microsoft/azure/mobile/updates/Updates.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/main/res/values/strings.xml (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java (100%) rename sdk/{mobile-center-updates => mobile-center-distribute}/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java (100%) diff --git a/sdk/mobile-center-updates/build.gradle b/sdk/mobile-center-distribute/build.gradle similarity index 100% rename from sdk/mobile-center-updates/build.gradle rename to sdk/mobile-center-distribute/build.gradle diff --git a/sdk/mobile-center-updates/proguard-rules.pro b/sdk/mobile-center-distribute/proguard-rules.pro similarity index 100% rename from sdk/mobile-center-updates/proguard-rules.pro rename to sdk/mobile-center-distribute/proguard-rules.pro diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java similarity index 100% rename from sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java rename to sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java diff --git a/sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java similarity index 100% rename from sdk/mobile-center-updates/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java rename to sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java diff --git a/sdk/mobile-center-updates/src/main/AndroidManifest.xml b/sdk/mobile-center-distribute/src/main/AndroidManifest.xml similarity index 100% rename from sdk/mobile-center-updates/src/main/AndroidManifest.xml rename to sdk/mobile-center-distribute/src/main/AndroidManifest.xml diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java similarity index 100% rename from sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java similarity index 100% rename from sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java similarity index 100% rename from sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java similarity index 100% rename from sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java similarity index 100% rename from sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java similarity index 100% rename from sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/Updates.java similarity index 100% rename from sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/Updates.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/Updates.java diff --git a/sdk/mobile-center-updates/src/main/res/values/strings.xml b/sdk/mobile-center-distribute/src/main/res/values/strings.xml similarity index 100% rename from sdk/mobile-center-updates/src/main/res/values/strings.xml rename to sdk/mobile-center-distribute/src/main/res/values/strings.xml diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java similarity index 100% rename from sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java From 7b08a6d909e8a81562eda177c45be606b480b0a2 Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Mon, 27 Feb 2017 11:28:27 -0800 Subject: [PATCH 094/142] Refactor package name to distribute --- apps/sasquatch/build.gradle | 2 +- .../BrowserUtilsTest.java | 4 +- .../ReleaseDetailsTest.java | 2 +- .../src/main/AndroidManifest.xml | 6 +-- .../{updates => distribute}/BrowserUtils.java | 4 +- .../DeepLinkActivity.java | 8 +-- .../DownloadManagerReceiver.java | 2 +- .../InstallerUtils.java | 2 +- .../ReleaseDetails.java | 2 +- .../UpdateConstants.java | 2 +- .../{updates => distribute}/Updates.java | 49 +++++++++---------- .../AbstractUpdatesTest.java | 6 +-- .../AppStoreDetectionTest.java | 4 +- .../DeepLinkActivityTest.java | 2 +- ...wnloadManagerReceiverIgnoreIntentTest.java | 2 +- .../UnknownSourcesDetectionTest.java | 4 +- .../UpdateConstantsTest.java | 2 +- .../UpdatesBeforeApiSuccessTest.java | 22 ++++----- .../UpdatesBeforeDownloadTest.java | 10 ++-- .../UpdatesDownloadTest.java | 18 +++---- .../UpdatesPlusDownloadReceiverTest.java | 5 +- .../{updates => distribute}/UpdatesTest.java | 4 +- .../UpdatesWarnUnknownSourcesTest.java | 6 +-- settings.gradle | 2 +- 24 files changed, 84 insertions(+), 86 deletions(-) rename sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/{updates => distribute}/BrowserUtilsTest.java (98%) rename sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/{updates => distribute}/ReleaseDetailsTest.java (99%) rename sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/{updates => distribute}/BrowserUtils.java (97%) rename sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/{updates => distribute}/DeepLinkActivity.java (88%) rename sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/{updates => distribute}/DownloadManagerReceiver.java (95%) rename sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/{updates => distribute}/InstallerUtils.java (98%) rename sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/{updates => distribute}/ReleaseDetails.java (98%) rename sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/{updates => distribute}/UpdateConstants.java (99%) rename sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/{updates => distribute}/Updates.java (95%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/AbstractUpdatesTest.java (96%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/AppStoreDetectionTest.java (95%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/DeepLinkActivityTest.java (99%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/DownloadManagerReceiverIgnoreIntentTest.java (96%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/UnknownSourcesDetectionTest.java (96%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/UpdateConstantsTest.java (80%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/UpdatesBeforeApiSuccessTest.java (96%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/UpdatesBeforeDownloadTest.java (98%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/UpdatesDownloadTest.java (98%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/UpdatesPlusDownloadReceiverTest.java (94%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/UpdatesTest.java (97%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/{updates => distribute}/UpdatesWarnUnknownSourcesTest.java (98%) diff --git a/apps/sasquatch/build.gradle b/apps/sasquatch/build.gradle index 32b1805ad0..b3c1c7e0f0 100644 --- a/apps/sasquatch/build.gradle +++ b/apps/sasquatch/build.gradle @@ -26,7 +26,7 @@ dependencies { compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" projectDependencyCompile project(':sdk:mobile-center-analytics') projectDependencyCompile project(':sdk:mobile-center-crashes') - projectDependencyCompile project(':sdk:mobile-center-updates') + projectDependencyCompile project(':sdk:mobile-center-distribute') jcenterDependencyCompile "com.microsoft.azure.mobile:mobile-center-analytics:${version}" jcenterDependencyCompile "com.microsoft.azure.mobile:mobile-center-crashes:${version}" } \ No newline at end of file diff --git a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/BrowserUtilsTest.java similarity index 98% rename from sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java rename to sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/BrowserUtilsTest.java index efa38b55b8..9af125a698 100644 --- a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/BrowserUtilsTest.java +++ b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/BrowserUtilsTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.app.Activity; import android.content.ActivityNotFoundException; @@ -14,7 +14,7 @@ import java.util.Collections; -import static com.microsoft.azure.mobile.updates.BrowserUtils.GOOGLE_CHROME_URL_SCHEME; +import static com.microsoft.azure.mobile.distribute.BrowserUtils.GOOGLE_CHROME_URL_SCHEME; import static java.util.Arrays.asList; import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.any; diff --git a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java similarity index 99% rename from sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java rename to sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java index a5c6b5bb6d..3c08a12cc3 100644 --- a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/updates/ReleaseDetailsTest.java +++ b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.net.Uri; diff --git a/sdk/mobile-center-distribute/src/main/AndroidManifest.xml b/sdk/mobile-center-distribute/src/main/AndroidManifest.xml index bd2f2c85c7..801862eb68 100644 --- a/sdk/mobile-center-distribute/src/main/AndroidManifest.xml +++ b/sdk/mobile-center-distribute/src/main/AndroidManifest.xml @@ -1,12 +1,12 @@ + package="com.microsoft.azure.mobile.distribute"> @@ -21,7 +21,7 @@ - + diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/BrowserUtils.java similarity index 97% rename from sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/BrowserUtils.java index 53ac5dfa5b..0b3fd02de7 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/BrowserUtils.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/BrowserUtils.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.app.Activity; import android.content.ActivityNotFoundException; @@ -14,7 +14,7 @@ import java.util.List; -import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.LOG_TAG; /** * Browser utils. diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DeepLinkActivity.java similarity index 88% rename from sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DeepLinkActivity.java index 42afb5417d..651b4d8d72 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DeepLinkActivity.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DeepLinkActivity.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.app.Activity; import android.content.Intent; @@ -6,9 +6,9 @@ import com.microsoft.azure.mobile.utils.MobileCenterLog; -import static com.microsoft.azure.mobile.updates.UpdateConstants.EXTRA_REQUEST_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.EXTRA_UPDATE_TOKEN; -import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.EXTRA_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.EXTRA_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.LOG_TAG; /** * Generic activity used for deep linking in updates. diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java similarity index 95% rename from sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java index 904f56fd85..20dcd33e4a 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiver.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.app.DownloadManager; import android.content.BroadcastReceiver; diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/InstallerUtils.java similarity index 98% rename from sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/InstallerUtils.java index 27047220de..9b85415f43 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/InstallerUtils.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.content.ContentResolver; import android.content.Context; diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java similarity index 98% rename from sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java index 1525c701f5..19135a91bf 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/ReleaseDetails.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.net.Uri; import android.support.annotation.NonNull; diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/UpdateConstants.java similarity index 99% rename from sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/UpdateConstants.java index f866f149e1..ef41fc003c 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/UpdateConstants.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/UpdateConstants.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.app.DownloadManager; import android.support.annotation.VisibleForTesting; diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/Updates.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Updates.java similarity index 95% rename from sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/Updates.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Updates.java index c4989d5cc1..0f359f5dbf 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/updates/Updates.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Updates.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.annotation.SuppressLint; import android.app.Activity; @@ -11,7 +11,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.Cursor; @@ -57,29 +56,29 @@ import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE; import static android.util.Log.VERBOSE; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; -import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_API_URL; -import static com.microsoft.azure.mobile.updates.UpdateConstants.DEFAULT_INSTALL_URL; -import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_COMPLETED; -import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_ENQUEUED; -import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_NOTIFIED; -import static com.microsoft.azure.mobile.updates.UpdateConstants.GET_LATEST_RELEASE_PATH_FORMAT; -import static com.microsoft.azure.mobile.updates.UpdateConstants.HEADER_API_TOKEN; -import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; -import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_RELEASE_IDENTIFIER; -import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REDIRECT_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_RELEASE_HASH; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REQUEST_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_TIME; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; -import static com.microsoft.azure.mobile.updates.UpdateConstants.SERVICE_NAME; -import static com.microsoft.azure.mobile.updates.UpdateConstants.UPDATE_SETUP_PATH_FORMAT; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.DEFAULT_API_URL; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.DEFAULT_INSTALL_URL; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.DOWNLOAD_STATE_COMPLETED; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.DOWNLOAD_STATE_ENQUEUED; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.DOWNLOAD_STATE_NOTIFIED; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.GET_LATEST_RELEASE_PATH_FORMAT; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.HEADER_API_TOKEN; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.INVALID_RELEASE_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.LOG_TAG; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_PLATFORM; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_PLATFORM_VALUE; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_REDIRECT_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_RELEASE_HASH; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_TIME; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.SERVICE_NAME; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.UPDATE_SETUP_PATH_FORMAT; /** * Updates service. diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractUpdatesTest.java similarity index 96% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractUpdatesTest.java index 0a4d06dbf2..e843136c4d 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AbstractUpdatesTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractUpdatesTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.annotation.SuppressLint; import android.app.AlertDialog; @@ -27,8 +27,8 @@ import org.powermock.modules.junit4.rule.PowerMockRule; import org.powermock.reflect.Whitebox; -import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java similarity index 95% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java index 0fb7cbd62d..da29014d47 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.content.Context; import android.content.pm.PackageManager; @@ -11,7 +11,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; -import static com.microsoft.azure.mobile.updates.UpdateConstants.LOG_TAG; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.LOG_TAG; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java similarity index 99% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java index beea1f7983..c6f17241ad 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DeepLinkActivityTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.content.Intent; import android.content.pm.PackageManager; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiverIgnoreIntentTest.java similarity index 96% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiverIgnoreIntentTest.java index aff1d5bcde..4550f64882 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/DownloadManagerReceiverIgnoreIntentTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiverIgnoreIntentTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.content.Context; import android.content.Intent; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UnknownSourcesDetectionTest.java similarity index 96% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UnknownSourcesDetectionTest.java index ab90f6b981..0efa8faf00 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UnknownSourcesDetectionTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UnknownSourcesDetectionTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.annotation.SuppressLint; import android.content.ContentResolver; @@ -15,7 +15,7 @@ import java.util.Arrays; -import static com.microsoft.azure.mobile.updates.InstallerUtils.INSTALL_NON_MARKET_APPS_ENABLED; +import static com.microsoft.azure.mobile.distribute.InstallerUtils.INSTALL_NON_MARKET_APPS_ENABLED; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdateConstantsTest.java similarity index 80% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdateConstantsTest.java index 2d3580f242..bdcd18343f 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdateConstantsTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdateConstantsTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import org.junit.Test; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeApiSuccessTest.java similarity index 96% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeApiSuccessTest.java index c004e222cc..fa45dc3c35 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeApiSuccessTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeApiSuccessTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.app.Activity; import android.content.ComponentName; @@ -27,16 +27,16 @@ import java.util.UUID; import java.util.concurrent.Semaphore; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_PLATFORM_VALUE; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REDIRECT_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_RELEASE_HASH; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PARAMETER_REQUEST_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; -import static com.microsoft.azure.mobile.updates.UpdateConstants.UPDATE_SETUP_PATH_FORMAT; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_PLATFORM; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_PLATFORM_VALUE; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_REDIRECT_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_RELEASE_HASH; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.UPDATE_SETUP_PATH_FORMAT; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeDownloadTest.java similarity index 98% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeDownloadTest.java index 82c6b4a76a..5495a32d9e 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesBeforeDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeDownloadTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.app.Activity; import android.content.Context; @@ -23,10 +23,10 @@ import java.util.HashMap; import java.util.concurrent.Semaphore; -import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_RELEASE_IDENTIFIER; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.INVALID_RELEASE_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesDownloadTest.java similarity index 98% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesDownloadTest.java index 0184a11e22..1e08624c31 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesDownloadTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.annotation.TargetApi; import android.app.Activity; @@ -46,14 +46,14 @@ import static android.app.DownloadManager.EXTRA_DOWNLOAD_ID; import static android.content.Context.NOTIFICATION_SERVICE; -import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_COMPLETED; -import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_ENQUEUED; -import static com.microsoft.azure.mobile.updates.UpdateConstants.DOWNLOAD_STATE_NOTIFIED; -import static com.microsoft.azure.mobile.updates.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_TIME; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.DOWNLOAD_STATE_COMPLETED; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.DOWNLOAD_STATE_ENQUEUED; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.DOWNLOAD_STATE_NOTIFIED; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_TIME; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesPlusDownloadReceiverTest.java similarity index 94% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesPlusDownloadReceiverTest.java index 1d6706036a..f7b07aaa21 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesPlusDownloadReceiverTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesPlusDownloadReceiverTest.java @@ -1,7 +1,6 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.app.Activity; -import android.content.Context; import android.content.Intent; import com.microsoft.azure.mobile.channel.Channel; @@ -10,7 +9,7 @@ import org.junit.Test; import static android.app.DownloadManager.ACTION_NOTIFICATION_CLICKED; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesTest.java similarity index 97% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesTest.java index 4a2b344c75..aee2ef8ce7 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.content.Context; @@ -26,7 +26,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import static com.microsoft.azure.mobile.updates.UpdateConstants.HEADER_API_TOKEN; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.HEADER_API_TOKEN; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesWarnUnknownSourcesTest.java similarity index 98% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesWarnUnknownSourcesTest.java index 5f54bd0adc..8bca692773 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/updates/UpdatesWarnUnknownSourcesTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesWarnUnknownSourcesTest.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.mobile.updates; +package com.microsoft.azure.mobile.distribute; import android.app.Activity; import android.app.AlertDialog; @@ -25,8 +25,8 @@ import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; -import static com.microsoft.azure.mobile.updates.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyMapOf; diff --git a/settings.gradle b/settings.gradle index ae23b1fc29..e1ef1efb93 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,7 @@ include ':sdk' include ':sdk:mobile-center' include ':sdk:mobile-center-crashes' include ':sdk:mobile-center-analytics' -include ':sdk:mobile-center-updates' +include ':sdk:mobile-center-distribute' // common test code include ':test' From deb009baaf03342e35f37e492307029b77c72187 Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Mon, 27 Feb 2017 12:34:58 -0800 Subject: [PATCH 095/142] Rename updates to distribute --- .../sasquatch/activities/MainActivity.java | 10 +- .../activities/SettingsActivity.java | 14 +- .../src/main/res/values/settings.xml | 12 +- apps/sasquatch/src/main/res/xml/settings.xml | 8 +- .../azure/mobile/distribute/BrowserUtils.java | 2 +- .../mobile/distribute/DeepLinkActivity.java | 10 +- .../{Updates.java => Distribute.java} | 98 ++++----- ...onstants.java => DistributeConstants.java} | 10 +- .../distribute/DownloadManagerReceiver.java | 6 +- .../src/main/res/values/strings.xml | 20 +- ...sTest.java => AbstractDistributeTest.java} | 18 +- .../distribute/AppStoreDetectionTest.java | 2 +- .../distribute/DeepLinkActivityTest.java | 30 +-- ...va => DistributeBeforeApiSuccessTest.java} | 188 +++++++++--------- ...java => DistributeBeforeDownloadTest.java} | 168 ++++++++-------- ...Test.java => DistributeConstantsTest.java} | 4 +- ...dTest.java => DistributeDownloadTest.java} | 156 +++++++-------- ...> DistributePlusDownloadReceiverTest.java} | 12 +- .../{UpdatesTest.java => DistributeTest.java} | 12 +- ... => DistributeWarnUnknownSourcesTest.java} | 76 +++---- ...wnloadManagerReceiverIgnoreIntentTest.java | 8 +- 21 files changed, 432 insertions(+), 432 deletions(-) rename sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/{Updates.java => Distribute.java} (91%) rename sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/{UpdateConstants.java => DistributeConstants.java} (96%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{AbstractUpdatesTest.java => AbstractDistributeTest.java} (86%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{UpdatesBeforeApiSuccessTest.java => DistributeBeforeApiSuccessTest.java} (77%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{UpdatesBeforeDownloadTest.java => DistributeBeforeDownloadTest.java} (83%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{UpdateConstantsTest.java => DistributeConstantsTest.java} (64%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{UpdatesDownloadTest.java => DistributeDownloadTest.java} (87%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{UpdatesPlusDownloadReceiverTest.java => DistributePlusDownloadReceiverTest.java} (83%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{UpdatesTest.java => DistributeTest.java} (93%) rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{UpdatesWarnUnknownSourcesTest.java => DistributeWarnUnknownSourcesTest.java} (83%) diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index c52b1bab6e..785b8c29c0 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -69,12 +69,12 @@ protected void onCreate(Bundle savedInstanceState) { try { @SuppressWarnings("unchecked") - Class updates = (Class) Class.forName("com.microsoft.azure.mobile.updates.Updates"); - updates.getMethod("setInstallUrl", String.class).invoke(null, "http://install.asgard-int.trafficmanager.net"); - updates.getMethod("setApiUrl", String.class).invoke(null, "https://asgard-int.trafficmanager.net/api/v0.1"); - MobileCenter.start(updates); + Class distribute = (Class) Class.forName("com.microsoft.azure.mobile.distribute.Distribute"); + distribute.getMethod("setInstallUrl", String.class).invoke(null, "http://install.asgard-int.trafficmanager.net"); + distribute.getMethod("setApiUrl", String.class).invoke(null, "https://asgard-int.trafficmanager.net/api/v0.1"); + MobileCenter.start(distribute); } catch (Exception e) { - MobileCenterLog.info(LOG_TAG, "Updates class not yet available in this flavor."); + MobileCenterLog.info(LOG_TAG, "Distribute class not yet available in this flavor."); } Log.i(LOG_TAG, "Crashes.hasCrashedInLastSession=" + Crashes.hasCrashedInLastSession()); diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java index 09a6b404a2..41899dd6e6 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java @@ -50,7 +50,7 @@ public void onCreate(Bundle savedInstanceState) { addPreferencesFromResource(R.xml.settings); final CheckBoxPreference analyticsEnabledPreference = (CheckBoxPreference) getPreferenceManager().findPreference(getString(R.string.mobile_center_analytics_state_key)); final CheckBoxPreference crashesEnabledPreference = (CheckBoxPreference) getPreferenceManager().findPreference(getString(R.string.mobile_center_crashes_state_key)); - final CheckBoxPreference updatesEnabledPreference = (CheckBoxPreference) getPreferenceManager().findPreference(getString(R.string.mobile_center_updates_state_key)); + final CheckBoxPreference distributeEnabledPreference = (CheckBoxPreference) getPreferenceManager().findPreference(getString(R.string.mobile_center_distribute_state_key)); initCheckBoxSetting(R.string.mobile_center_state_key, MobileCenter.isEnabled(), R.string.mobile_center_state_summary_enabled, R.string.mobile_center_state_summary_disabled, new HasEnabled() { @Override @@ -94,16 +94,16 @@ public boolean isEnabled() { try { @SuppressWarnings("unchecked") - Class updates = (Class) Class.forName("com.microsoft.azure.mobile.updates.Updates"); - final Method isEnabled = updates.getMethod("isEnabled"); - final Method setEnabled = updates.getMethod("setEnabled", boolean.class); - initCheckBoxSetting(R.string.mobile_center_updates_state_key, (boolean) isEnabled.invoke(null), R.string.mobile_center_updates_state_summary_enabled, R.string.mobile_center_updates_state_summary_disabled, new HasEnabled() { + Class distribute = (Class) Class.forName("com.microsoft.azure.mobile.distribute.Distribute"); + final Method isEnabled = distribute.getMethod("isEnabled"); + final Method setEnabled = distribute.getMethod("setEnabled", boolean.class); + initCheckBoxSetting(R.string.mobile_center_distribute_state_key, (boolean) isEnabled.invoke(null), R.string.mobile_center_distribute_state_summary_enabled, R.string.mobile_center_distribute_state_summary_disabled, new HasEnabled() { @Override public void setEnabled(boolean enabled) { try { setEnabled.invoke(null, enabled); - updatesEnabledPreference.setChecked((boolean) isEnabled.invoke(null)); + distributeEnabledPreference.setChecked((boolean) isEnabled.invoke(null)); } catch (Exception e) { throw new RuntimeException(e); } @@ -119,7 +119,7 @@ public boolean isEnabled() { } }); } catch (Exception e) { - getPreferenceScreen().removePreference(findPreference(getString(R.string.updates_key))); + getPreferenceScreen().removePreference(findPreference(getString(R.string.distribute_key))); } initCheckBoxSetting(R.string.mobile_center_auto_page_tracking_key, AnalyticsPrivateHelper.isAutoPageTrackingEnabled(), R.string.mobile_center_auto_page_tracking_enabled, R.string.mobile_center_auto_page_tracking_disabled, new HasEnabled() { diff --git a/apps/sasquatch/src/main/res/values/settings.xml b/apps/sasquatch/src/main/res/values/settings.xml index 8c00cb59df..9b4e4f0fcd 100644 --- a/apps/sasquatch/src/main/res/values/settings.xml +++ b/apps/sasquatch/src/main/res/values/settings.xml @@ -27,13 +27,13 @@ Crashes are enabled Crashes are disabled - mobile_center_updates - Updates + mobile_center_distribute + Distribute - mobile_center_updates_state_key - Updates state - Updates are enabled - Updates are disabled + mobile_center_distribute_state_key + Distribute state + Distribute is enabled + Distribute is disabled application_info Application Information diff --git a/apps/sasquatch/src/main/res/xml/settings.xml b/apps/sasquatch/src/main/res/xml/settings.xml index f3907c843c..83083e2c81 100644 --- a/apps/sasquatch/src/main/res/xml/settings.xml +++ b/apps/sasquatch/src/main/res/xml/settings.xml @@ -24,11 +24,11 @@ + android:key="@string/distribute_key" + android:title="@string/distribute_title"> + android:key="@string/mobile_center_distribute_state_key" + android:title="@string/mobile_center_distribute_state_title"/> true if enabled, false otherwise. */ @@ -218,7 +218,7 @@ public static boolean isEnabled() { } /** - * Enable or disable Updates service. + * Enable or disable Distribute service. * * @param enabled true to enable, false to disable. */ @@ -270,7 +270,7 @@ private static Intent getInstallIntent(Uri fileUri) { */ @VisibleForTesting static int getNotificationId() { - return Updates.class.getName().hashCode(); + return Distribute.class.getName().hashCode(); } @SuppressWarnings("deprecation") @@ -325,7 +325,7 @@ public synchronized void onStarted(@NonNull Context context, @NonNull String app super.onStarted(context, appSecret, channel); mContext = context; mAppSecret = appSecret; - resumeUpdateWorkflow(); + resumeDistributeWorkflow(); } @Override @@ -354,7 +354,7 @@ public synchronized void onActivityCreated(Activity activity, Bundle savedInstan @Override public synchronized void onActivityResumed(Activity activity) { mForegroundActivity = activity; - resumeUpdateWorkflow(); + resumeDistributeWorkflow(); } @Override @@ -366,7 +366,7 @@ public synchronized void onActivityPaused(Activity activity) { public synchronized void setInstanceEnabled(boolean enabled) { super.setInstanceEnabled(enabled); if (enabled) { - resumeUpdateWorkflow(); + resumeDistributeWorkflow(); } else { /* Clean all state on disabling, cancel everything. Keep token though. */ @@ -424,9 +424,9 @@ private synchronized void cancelPreviousTasks() { } /** - * Method that triggers the update workflow or proceed to the next step. + * Method that triggers the distribute workflow or proceed to the next step. */ - private synchronized void resumeUpdateWorkflow() { + private synchronized void resumeDistributeWorkflow() { if (mForegroundActivity != null && !mWorkflowCompleted && isInstanceEnabled()) { /* Don't go any further it this is a debug app. */ @@ -665,7 +665,7 @@ synchronized void getLatestReleaseDetails(@NonNull String updateToken) { @Override public String buildRequestBody() throws JSONException { - /* Only GET is used by Updates service. This method is never getting called. */ + /* Only GET is used by Distribute service. This method is never getting called. */ return null; } @@ -821,28 +821,28 @@ private synchronized void showUpdateDialog() { } MobileCenterLog.debug(LOG_TAG, "Show new update dialog."); AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mForegroundActivity); - dialogBuilder.setTitle(R.string.mobile_center_updates_update_dialog_title); + dialogBuilder.setTitle(R.string.mobile_center_distribute_update_dialog_title); final ReleaseDetails releaseDetails = mReleaseDetails; String releaseNotes = releaseDetails.getReleaseNotes(); if (TextUtils.isEmpty(releaseNotes)) - dialogBuilder.setMessage(R.string.mobile_center_updates_update_dialog_message); + dialogBuilder.setMessage(R.string.mobile_center_distribute_update_dialog_message); else dialogBuilder.setMessage(releaseNotes); - dialogBuilder.setPositiveButton(R.string.mobile_center_updates_update_dialog_download, new DialogInterface.OnClickListener() { + dialogBuilder.setPositiveButton(R.string.mobile_center_distribute_update_dialog_download, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { enqueueDownloadOrShowUnknownSourcesDialog(releaseDetails); } }); - dialogBuilder.setNegativeButton(R.string.mobile_center_updates_update_dialog_ignore, new DialogInterface.OnClickListener() { + dialogBuilder.setNegativeButton(R.string.mobile_center_distribute_update_dialog_ignore, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ignoreRelease(releaseDetails); } }); - dialogBuilder.setNeutralButton(R.string.mobile_center_updates_update_dialog_postpone, new DialogInterface.OnClickListener() { + dialogBuilder.setNeutralButton(R.string.mobile_center_distribute_update_dialog_postpone, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -876,7 +876,7 @@ private synchronized void showUnknownSourcesDialog() { * Also for buttons and texts we try do to the same as the system dialog on standard devices. */ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mForegroundActivity); - dialogBuilder.setMessage(R.string.mobile_center_updates_unknown_sources_dialog_message); + dialogBuilder.setMessage(R.string.mobile_center_distribute_unknown_sources_dialog_message); final ReleaseDetails releaseDetails = mReleaseDetails; dialogBuilder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @@ -888,7 +888,7 @@ public void onClick(DialogInterface dialog, int which) { setOnCancelListener(dialogBuilder, releaseDetails); /* We use generic OK button as we can't promise we can navigate to settings. */ - dialogBuilder.setPositiveButton(R.string.mobile_center_updates_unknown_sources_dialog_settings, new DialogInterface.OnClickListener() { + dialogBuilder.setPositiveButton(R.string.mobile_center_distribute_unknown_sources_dialog_settings, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -979,7 +979,7 @@ private synchronized void enqueueDownloadOrShowUnknownSourcesDialog(final Releas * that will likely never happen but we guard for it. */ private void showDisabledToast() { - Toast.makeText(mContext, R.string.mobile_center_updates_dialog_actioned_on_disabled_toast, Toast.LENGTH_SHORT).show(); + Toast.makeText(mContext, R.string.mobile_center_distribute_dialog_actioned_on_disabled_toast, Toast.LENGTH_SHORT).show(); } /** @@ -1078,9 +1078,9 @@ private synchronized boolean notifyDownload(Context context, CheckDownloadTask t /* Post notification. */ MobileCenterLog.debug(LOG_TAG, "Post a notification as the download finished in background."); Notification.Builder builder = new Notification.Builder(context) - .setTicker(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) - .setContentTitle(context.getString(R.string.mobile_center_updates_download_successful_notification_title)) - .setContentText(context.getString(R.string.mobile_center_updates_download_successful_notification_message)) + .setTicker(context.getString(R.string.mobile_center_distribute_download_successful_notification_title)) + .setContentTitle(context.getString(R.string.mobile_center_distribute_download_successful_notification_title)) + .setContentText(context.getString(R.string.mobile_center_distribute_download_successful_notification_message)) .setSmallIcon(context.getApplicationInfo().icon) .setContentIntent(PendingIntent.getActivities(context, 0, new Intent[]{intent}, 0)); Notification notification = buildNotification(builder); diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/UpdateConstants.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java similarity index 96% rename from sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/UpdateConstants.java rename to sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java index ef41fc003c..a3cea76288 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/UpdateConstants.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java @@ -6,14 +6,14 @@ import com.microsoft.azure.mobile.MobileCenter; /** - * Updates constants. + * Distribute constants. */ -final class UpdateConstants { +final class DistributeConstants { /** - * Update service name. + * Distribute service name. */ - static final String SERVICE_NAME = "Updates"; + static final String SERVICE_NAME = "Distribute"; /** * Log tag for this service. @@ -150,7 +150,7 @@ final class UpdateConstants { static final String PREFERENCE_KEY_DOWNLOAD_TIME = PREFERENCE_PREFIX + "download_time"; @VisibleForTesting - UpdateConstants() { + DistributeConstants() { /* Hide constructor as it's just a constant class. */ } diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java index 20dcd33e4a..38f1933472 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java @@ -20,15 +20,15 @@ public void onReceive(Context context, Intent intent) { */ String action = intent.getAction(); if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)) { - Updates.getInstance().resumeApp(context); + Distribute.getInstance().resumeApp(context); } /* - * Forward the download identifier to Updates for inspection when a download completes. + * Forward the download identifier to Distribute for inspection when a download completes. */ else if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); - Updates.getInstance().checkDownload(context, downloadId); + Distribute.getInstance().checkDownload(context, downloadId); } } } diff --git a/sdk/mobile-center-distribute/src/main/res/values/strings.xml b/sdk/mobile-center-distribute/src/main/res/values/strings.xml index ba1ad8d0e5..94d7e1ae8a 100644 --- a/sdk/mobile-center-distribute/src/main/res/values/strings.xml +++ b/sdk/mobile-center-distribute/src/main/res/values/strings.xml @@ -1,13 +1,13 @@ - New update is downloaded - Tap to install the application. - Update Available - No release notes were provided for this build. - Ignore - Download - Postpone - Updates were disabled - For security, your device is set to block installation of apps obtained from unknown sources. - Settings + New update is downloaded + Tap to install the application. + Update Available + No release notes were provided for this build. + Ignore + Download + Postpone + Distribute was disabled + For security, your device is set to block installation of apps obtained from unknown sources. + Settings \ No newline at end of file diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractUpdatesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java similarity index 86% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractUpdatesTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java index e843136c4d..924c8b03e9 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractUpdatesTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java @@ -27,8 +27,8 @@ import org.powermock.modules.junit4.rule.PowerMockRule; import org.powermock.reflect.Whitebox; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.INVALID_DOWNLOAD_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_ID; import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; @@ -41,12 +41,12 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Updates.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, NetworkStateHelper.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, InstallerUtils.class, Toast.class}) -public class AbstractUpdatesTest { +@PrepareForTest({Distribute.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, NetworkStateHelper.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, InstallerUtils.class, Toast.class}) +public class AbstractDistributeTest { static final String TEST_HASH = HashUtils.sha256("com.contoso:1.2.3:6"); - private static final String UPDATES_ENABLED_KEY = KEY_ENABLED + "_Updates"; + private static final String DISTRIBUTE_ENABLED_KEY = KEY_ENABLED + "_Distribute"; @Rule public PowerMockRule mPowerMockRule = new PowerMockRule(); @@ -81,14 +81,14 @@ public class AbstractUpdatesTest { @SuppressLint("ShowToast") @SuppressWarnings("ResourceType") public void setUp() throws Exception { - Updates.unsetInstance(); + Distribute.unsetInstance(); mockStatic(MobileCenterLog.class); mockStatic(MobileCenter.class); when(MobileCenter.isEnabled()).thenReturn(true); /* First call to com.microsoft.azure.mobile.MobileCenter.isEnabled shall return true, initial state. */ mockStatic(PreferencesStorage.class); - when(PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(true); + when(PreferencesStorage.getBoolean(DISTRIBUTE_ENABLED_KEY, true)).thenReturn(true); /* Then simulate further changes to state. */ doAnswer(new Answer() { @@ -98,11 +98,11 @@ public Void answer(InvocationOnMock invocation) throws Throwable { /* Whenever the new state is persisted, make further calls return the new state. */ boolean enabled = (Boolean) invocation.getArguments()[1]; - when(PreferencesStorage.getBoolean(UPDATES_ENABLED_KEY, true)).thenReturn(enabled); + when(PreferencesStorage.getBoolean(DISTRIBUTE_ENABLED_KEY, true)).thenReturn(enabled); return null; } }).when(PreferencesStorage.class); - PreferencesStorage.putBoolean(eq(UPDATES_ENABLED_KEY), anyBoolean()); + PreferencesStorage.putBoolean(eq(DISTRIBUTE_ENABLED_KEY), anyBoolean()); /* Default download id when not found. */ when(PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER)).thenReturn(INVALID_DOWNLOAD_IDENTIFIER); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java index da29014d47..356fda5c96 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java @@ -11,7 +11,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.LOG_TAG; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.LOG_TAG; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java index c6f17241ad..9d1ce00612 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java @@ -20,16 +20,16 @@ import static org.powermock.api.mockito.PowerMockito.verifyStatic; @RunWith(PowerMockRunner.class) -@PrepareForTest(Updates.class) +@PrepareForTest(Distribute.class) public class DeepLinkActivityTest { @Mock - private Updates mUpdates; + private Distribute mDistribute; @Before public void setUp() { - mockStatic(Updates.class); - when(Updates.getInstance()).thenReturn(mUpdates); + mockStatic(Distribute.class); + when(Distribute.getInstance()).thenReturn(mDistribute); } /** @@ -48,7 +48,7 @@ private void invalidIntent(Intent intent) { verify(activity).startActivity(intent); verify(activity).finish(); verifyStatic(never()); - Updates.getInstance(); + Distribute.getInstance(); } @Test @@ -60,7 +60,7 @@ public void missingParametersAndRestartWorkaround() { @Test public void missingRequestId() { Intent intent = mock(Intent.class); - when(intent.getStringExtra(UpdateConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock"); + when(intent.getStringExtra(DistributeConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock"); invalidIntent(intent); } @@ -69,8 +69,8 @@ public void validAndNoTaskRoot() { /* Build valid intent. */ Intent intent = mock(Intent.class); - when(intent.getStringExtra(UpdateConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); - when(intent.getStringExtra(UpdateConstants.EXTRA_REQUEST_ID)).thenReturn("mock2"); + when(intent.getStringExtra(DistributeConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); + when(intent.getStringExtra(DistributeConstants.EXTRA_REQUEST_ID)).thenReturn("mock2"); when(intent.getFlags()).thenReturn(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); /* Start activity. */ @@ -81,7 +81,7 @@ public void validAndNoTaskRoot() { /* Verify interactions. */ verify(activity, never()).startActivity(any(Intent.class)); verify(activity).finish(); - verify(mUpdates).storeUpdateToken("mock1", "mock2"); + verify(mDistribute).storeUpdateToken("mock1", "mock2"); } @Test @@ -89,8 +89,8 @@ public void validAndTaskRootNoLauncher() { /* Build valid intent. */ Intent intent = mock(Intent.class); - when(intent.getStringExtra(UpdateConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); - when(intent.getStringExtra(UpdateConstants.EXTRA_REQUEST_ID)).thenReturn("mock2"); + when(intent.getStringExtra(DistributeConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); + when(intent.getStringExtra(DistributeConstants.EXTRA_REQUEST_ID)).thenReturn("mock2"); when(intent.getFlags()).thenReturn(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); /* Start activity. */ @@ -103,7 +103,7 @@ public void validAndTaskRootNoLauncher() { /* Verify interactions. */ verify(activity, never()).startActivity(any(Intent.class)); verify(activity).finish(); - verify(mUpdates).storeUpdateToken("mock1", "mock2"); + verify(mDistribute).storeUpdateToken("mock1", "mock2"); } @Test @@ -111,8 +111,8 @@ public void validAndTaskRootStartLauncher() { /* Build valid intent. */ Intent intent = mock(Intent.class); - when(intent.getStringExtra(UpdateConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); - when(intent.getStringExtra(UpdateConstants.EXTRA_REQUEST_ID)).thenReturn("mock2"); + when(intent.getStringExtra(DistributeConstants.EXTRA_UPDATE_TOKEN)).thenReturn("mock1"); + when(intent.getStringExtra(DistributeConstants.EXTRA_REQUEST_ID)).thenReturn("mock2"); when(intent.getFlags()).thenReturn(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); /* Start activity. */ @@ -129,6 +129,6 @@ public void validAndTaskRootStartLauncher() { /* Verify interactions. */ verify(activity).startActivity(launcherIntent); verify(activity).finish(); - verify(mUpdates).storeUpdateToken("mock1", "mock2"); + verify(mDistribute).storeUpdateToken("mock1", "mock2"); } } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeApiSuccessTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java similarity index 77% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeApiSuccessTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java index fa45dc3c35..8af35dd6f8 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeApiSuccessTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java @@ -27,16 +27,16 @@ import java.util.UUID; import java.util.concurrent.Semaphore; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_PLATFORM; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_PLATFORM_VALUE; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_REDIRECT_ID; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_RELEASE_HASH; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PARAMETER_REQUEST_ID; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_REQUEST_ID; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.UPDATE_SETUP_PATH_FORMAT; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_PLATFORM; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_PLATFORM_VALUE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_REDIRECT_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_RELEASE_HASH; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.UPDATE_SETUP_PATH_FORMAT; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -55,7 +55,7 @@ /** * Cover scenarios that are happening before we see an API call success for latest release. */ -public class UpdatesBeforeApiSuccessTest extends AbstractUpdatesTest { +public class DistributeBeforeApiSuccessTest extends AbstractDistributeTest { /** * Shared code to mock a restart of an activity considered to be the launcher. @@ -68,18 +68,18 @@ private static void restartResumeLauncher(Activity activity) { ComponentName componentName = mock(ComponentName.class); when(intent.resolveActivity(packageManager)).thenReturn(componentName); when(componentName.getClassName()).thenReturn(activity.getClass().getName()); - Updates.getInstance().onActivityPaused(activity); - Updates.getInstance().onActivityStopped(activity); - Updates.getInstance().onActivityDestroyed(activity); - Updates.getInstance().onActivityCreated(activity, mock(Bundle.class)); - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityPaused(activity); + Distribute.getInstance().onActivityStopped(activity); + Distribute.getInstance().onActivityDestroyed(activity); + Distribute.getInstance().onActivityCreated(activity, mock(Bundle.class)); + Distribute.getInstance().onActivityResumed(activity); } - private void testInAppUpdatesInactive() throws Exception { + private void testDistributeInactive() throws Exception { /* Check browser not opened. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verifyStatic(never()); BrowserUtils.openBrowser(anyString(), any(Activity.class)); @@ -91,22 +91,22 @@ private void testInAppUpdatesInactive() throws Exception { when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - Updates.unsetInstance(); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.unsetInstance(); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient, never()).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @Test public void doNothingIfDebug() throws Exception { Whitebox.setInternalState(mApplicationInfo, "flags", ApplicationInfo.FLAG_DEBUGGABLE); - testInAppUpdatesInactive(); + testDistributeInactive(); } @Test public void doNothingIfInstallComesFromStore() throws Exception { when(InstallerUtils.isInstalledFromAppStore(anyString(), any(Context.class))).thenReturn(true); - testInAppUpdatesInactive(); + testDistributeInactive(); } @Test @@ -118,15 +118,15 @@ public void storeTokenBeforeStart() throws Exception { whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); /* Store token before start, start in background, no storage access. */ - Updates.getInstance().storeUpdateToken("some token", "r"); - Updates.getInstance().onStarted(mContext, "", mock(Channel.class)); + Distribute.getInstance().storeUpdateToken("some token", "r"); + Distribute.getInstance().onStarted(mContext, "", mock(Channel.class)); verifyStatic(never()); PreferencesStorage.putString(anyString(), anyString()); verifyStatic(never()); PreferencesStorage.remove(anyString()); /* Unlock the processing by going into foreground. */ - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verifyStatic(); PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); verifyStatic(); @@ -136,7 +136,7 @@ public void storeTokenBeforeStart() throws Exception { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -145,15 +145,15 @@ public void postponeBrowserIfNoNetwork() throws Exception { /* Check browser not opened if no network. */ when(mNetworkStateHelper.isNetworkConnected()).thenReturn(false); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verifyStatic(never()); BrowserUtils.openBrowser(anyString(), any(Activity.class)); /* If network comes back, we don't open network unless we restart app. */ when(mNetworkStateHelper.isNetworkConnected()).thenReturn(true); - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verifyStatic(never()); BrowserUtils.openBrowser(anyString(), any(Activity.class)); @@ -176,11 +176,11 @@ public void happyPathUntilHangingCall() throws Exception { when(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn(requestId.toString()); /* Start and resume: open browser. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); Activity activity = mock(Activity.class); - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityResumed(activity); verifyStatic(); - String url = UpdateConstants.DEFAULT_INSTALL_URL; + String url = DistributeConstants.DEFAULT_INSTALL_URL; url += String.format(UPDATE_SETUP_PATH_FORMAT, "a"); url += "?" + PARAMETER_RELEASE_HASH + "=" + TEST_HASH; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); @@ -191,15 +191,15 @@ public void happyPathUntilHangingCall() throws Exception { PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); /* If browser already opened, activity changed must not recall it. */ - Updates.getInstance().onActivityPaused(activity); - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityPaused(activity); + Distribute.getInstance().onActivityResumed(activity); verifyStatic(); BrowserUtils.openBrowser(url, activity); verifyStatic(); PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); /* Store token. */ - Updates.getInstance().storeUpdateToken("some token", requestId.toString()); + Distribute.getInstance().storeUpdateToken("some token", requestId.toString()); /* Verify behavior. */ verifyStatic(); @@ -211,18 +211,18 @@ public void happyPathUntilHangingCall() throws Exception { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); verify(httpClient).callAsync(argThat(new ArgumentMatcher() { @Override public boolean matches(Object argument) { - return argument.toString().startsWith(UpdateConstants.DEFAULT_API_URL); + return argument.toString().startsWith(DistributeConstants.DEFAULT_API_URL); } }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* If call already made, activity changed must not recall it. */ - Updates.getInstance().onActivityPaused(activity); - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityPaused(activity); + Distribute.getInstance().onActivityResumed(activity); /* Verify behavior. */ verifyStatic(); @@ -237,17 +237,17 @@ public boolean matches(Object argument) { @Override public boolean matches(Object argument) { - return argument.toString().startsWith(UpdateConstants.DEFAULT_API_URL); + return argument.toString().startsWith(DistributeConstants.DEFAULT_API_URL); } }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Call is still in progress. If we restart app, nothing happens we still wait. */ restartResumeLauncher(activity); - Updates.getInstance().onActivityPaused(activity); - Updates.getInstance().onActivityStopped(activity); - Updates.getInstance().onActivityDestroyed(activity); - Updates.getInstance().onActivityCreated(activity, mock(Bundle.class)); - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityPaused(activity); + Distribute.getInstance().onActivityStopped(activity); + Distribute.getInstance().onActivityDestroyed(activity); + Distribute.getInstance().onActivityCreated(activity, mock(Bundle.class)); + Distribute.getInstance().onActivityResumed(activity); /* Verify behavior not changed. */ verifyStatic(); @@ -264,7 +264,7 @@ public boolean matches(Object argument) { @Override public boolean matches(Object argument) { - return argument.toString().startsWith(UpdateConstants.DEFAULT_API_URL); + return argument.toString().startsWith(DistributeConstants.DEFAULT_API_URL); } }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -273,8 +273,8 @@ public boolean matches(Object argument) { public void setUrls() throws Exception { /* Setup mock. */ - Updates.setInstallUrl("http://mock"); - Updates.setApiUrl("https://mock2"); + Distribute.setInstallUrl("http://mock"); + Distribute.setApiUrl("https://mock2"); HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); UUID requestId = UUID.randomUUID(); @@ -282,9 +282,9 @@ public void setUrls() throws Exception { when(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID)).thenReturn(requestId.toString()); /* Start and resume: open browser. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); Activity activity = mock(Activity.class); - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityResumed(activity); verifyStatic(); String url = "http://mock"; url += String.format(UPDATE_SETUP_PATH_FORMAT, "a"); @@ -297,9 +297,9 @@ public void setUrls() throws Exception { PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); /* Store token. */ - Updates.getInstance().storeUpdateToken("some token", requestId.toString()); + Distribute.getInstance().storeUpdateToken("some token", requestId.toString()); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); verify(httpClient).callAsync(argThat(new ArgumentMatcher() { @Override @@ -321,8 +321,8 @@ public void computeHashFailsWhenOpeningBrowser() throws Exception { when(packageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); /* Start and resume: open browser. */ - Updates.getInstance().onStarted(context, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(context, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); /* Verify only tried once. */ verify(packageManager).getPackageInfo("com.contoso", 0); @@ -340,11 +340,11 @@ public void disableBeforeStoreToken() { /* Start and resume: open browser. */ UUID requestId = UUID.randomUUID(); when(UUIDUtils.randomUUID()).thenReturn(requestId); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); Activity activity = mock(Activity.class); - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityResumed(activity); verifyStatic(); - String url = UpdateConstants.DEFAULT_INSTALL_URL; + String url = DistributeConstants.DEFAULT_INSTALL_URL; url += String.format(UPDATE_SETUP_PATH_FORMAT, "a"); url += "?" + PARAMETER_RELEASE_HASH + "=" + TEST_HASH; url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); @@ -355,11 +355,11 @@ public void disableBeforeStoreToken() { PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); /* Disable. */ - Updates.setEnabled(false); - assertFalse(Updates.isEnabled()); + Distribute.setEnabled(false); + assertFalse(Distribute.isEnabled()); /* Store token. */ - Updates.getInstance().storeUpdateToken("some token", requestId.toString()); + Distribute.getInstance().storeUpdateToken("some token", requestId.toString()); /* Verify behavior. */ verifyStatic(never()); @@ -372,11 +372,11 @@ public void disableBeforeStoreToken() { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Since after disabling once, the request id was deleted we can enable/disable it will also ignore the request. */ - Updates.setEnabled(true); - assertTrue(Updates.isEnabled()); + Distribute.setEnabled(true); + assertTrue(Distribute.isEnabled()); /* Store token. */ - Updates.getInstance().storeUpdateToken("some token", requestId.toString()); + Distribute.getInstance().storeUpdateToken("some token", requestId.toString()); /* Verify behavior. */ verifyStatic(never()); @@ -393,22 +393,22 @@ public void disableWhileCheckingRelease() throws Exception { ServiceCall firstCall = mock(ServiceCall.class); when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenReturn(firstCall).thenReturn(mock(ServiceCall.class)); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); /* The call is only triggered when app is resumed. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); verify(httpClient, never()).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Verify cancel on disabling. */ verify(firstCall, never()).cancel(); - Updates.setEnabled(false); + Distribute.setEnabled(false); verify(firstCall).cancel(); /* No more call on that one. */ - Updates.setEnabled(true); - Updates.setEnabled(false); + Distribute.setEnabled(true); + Distribute.setEnabled(false); verify(firstCall).cancel(); } @@ -428,11 +428,11 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Verify on failure we complete workflow. */ @@ -440,8 +440,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -461,12 +461,12 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); when(ReleaseDetails.parse(anyString())).thenThrow(new JSONException("mock")); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Verify on failure we complete workflow. */ @@ -474,8 +474,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -505,15 +505,15 @@ public void run() { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Disable before it fails. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verifyStatic(never()); @@ -526,8 +526,8 @@ public void run() { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -557,15 +557,15 @@ public void run() { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Disable before it succeeds. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verifyStatic(never()); @@ -578,8 +578,8 @@ public void run() { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verify(mDialog, never()).show(); } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java similarity index 83% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeDownloadTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java index 5495a32d9e..a36060eba7 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesBeforeDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java @@ -23,10 +23,10 @@ import java.util.HashMap; import java.util.concurrent.Semaphore; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.INVALID_RELEASE_IDENTIFIER; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.INVALID_RELEASE_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; @@ -44,7 +44,7 @@ import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; -public class UpdatesBeforeDownloadTest extends AbstractUpdatesTest { +public class DistributeBeforeDownloadTest extends AbstractDistributeTest { @Test public void failsToCompareVersion() throws Exception { @@ -62,15 +62,15 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); when(mPackageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Verify on failure we complete workflow. */ @@ -80,8 +80,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mDialog, never()).show(); /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -101,15 +101,15 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(5); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Verify on failure we complete workflow. */ @@ -119,8 +119,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mDialog, never()).show(); /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -140,15 +140,15 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(6); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Verify on failure we complete workflow. */ @@ -158,8 +158,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mDialog, never()).show(); /* After that if we resume app nothing happens. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -179,7 +179,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); @@ -187,20 +187,20 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Verify dialog. */ - verify(mDialogBuilder).setTitle(R.string.mobile_center_updates_update_dialog_title); - verify(mDialogBuilder).setMessage(R.string.mobile_center_updates_update_dialog_message); + verify(mDialogBuilder).setTitle(R.string.mobile_center_distribute_update_dialog_title); + verify(mDialogBuilder).setMessage(R.string.mobile_center_distribute_update_dialog_message); verify(mDialogBuilder, never()).setMessage(any(CharSequence.class)); verify(mDialogBuilder).create(); verify(mDialog).show(); /* After that if we resume app we refresh dialog. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); /* No more http call. */ verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); @@ -214,7 +214,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mDialogBuilder, times(2)).create(); /* Disable does not hide the dialog. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); /* We already called hide once, make sure its not called a second time. */ verify(mDialog).hide(); @@ -239,7 +239,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); @@ -247,12 +247,12 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Verify dialog. */ - verify(mDialogBuilder).setTitle(R.string.mobile_center_updates_update_dialog_title); + verify(mDialogBuilder).setTitle(R.string.mobile_center_distribute_update_dialog_title); verify(mDialogBuilder).setMessage("mock"); verify(mDialogBuilder).create(); verify(mDialog).show(); @@ -284,7 +284,7 @@ public void run() { } }); HashMap headers = new HashMap<>(); - headers.put(UpdateConstants.HEADER_API_TOKEN, "some token"); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); @@ -292,10 +292,10 @@ public void run() { when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); Activity activity = mock(Activity.class); - Updates.getInstance().onActivityResumed(activity); - Updates.getInstance().onActivityPaused(activity); + Distribute.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityPaused(activity); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Release call in background. */ @@ -307,15 +307,15 @@ public void run() { verify(mDialog, never()).show(); /* Go foreground. */ - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityResumed(activity); /* Verify dialog now shown. */ verify(mDialogBuilder).create(); verify(mDialog).show(); /* Pause/resume should not alter dialog. */ - Updates.getInstance().onActivityPaused(activity); - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityPaused(activity); + Distribute.getInstance().onActivityResumed(activity); /* Only once check, and no hiding. */ verify(mDialogBuilder).create(); @@ -323,8 +323,8 @@ public void run() { verify(mDialog, never()).hide(); /* Cover activity. Dialog must be replaced. */ - Updates.getInstance().onActivityPaused(activity); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(activity); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialogBuilder, times(2)).create(); verify(mDialog, times(2)).show(); verify(mDialog).hide(); @@ -351,8 +351,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); /* Verify dialog. */ ArgumentCaptor cancelListener = ArgumentCaptor.forClass(DialogInterface.OnCancelListener.class); @@ -368,15 +368,15 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Restart should check release and show dialog again. */ - Updates.unsetInstance(); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.unsetInstance(); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog, times(2)).show(); verify(httpClient, times(2)).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -402,12 +402,12 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); /* Verify dialog. */ ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setNeutralButton(eq(R.string.mobile_center_updates_update_dialog_postpone), clickListener.capture()); + verify(mDialogBuilder).setNeutralButton(eq(R.string.mobile_center_distribute_update_dialog_postpone), clickListener.capture()); verify(mDialog).show(); /* Postpone it. */ @@ -419,15 +419,15 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Restart should check release and show dialog again. */ - Updates.unsetInstance(); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.unsetInstance(); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog, times(2)).show(); verify(httpClient, times(2)).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } @@ -473,12 +473,12 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); /* Verify dialog. */ ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setNegativeButton(eq(R.string.mobile_center_updates_update_dialog_ignore), clickListener.capture()); + verify(mDialogBuilder).setNegativeButton(eq(R.string.mobile_center_distribute_update_dialog_ignore), clickListener.capture()); verify(mDialog).show(); /* Ignore it. */ @@ -490,15 +490,15 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Restart app to check ignore. */ - Updates.unsetInstance(); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.unsetInstance(); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); /* Verify second http call was made but dialog was skipped (e.g. shown only once). */ verify(httpClient, times(2)).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); @@ -507,8 +507,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Disable: it will prompt again as we clear storage. */ - Updates.setEnabled(false); - Updates.setEnabled(true); + Distribute.setEnabled(false); + Distribute.setEnabled(true); verify(httpClient, times(3)).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verify(mDialog, times(2)).show(); } @@ -534,8 +534,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); /* Verify dialog. */ ArgumentCaptor cancelListener = ArgumentCaptor.forClass(DialogInterface.OnCancelListener.class); @@ -543,7 +543,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mDialog).show(); /* Disable. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); @@ -552,8 +552,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(mDialog.isShowing()).thenReturn(false); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verifyStatic(); @@ -602,16 +602,16 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); /* Verify dialog. */ ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setNegativeButton(eq(R.string.mobile_center_updates_update_dialog_ignore), clickListener.capture()); + verify(mDialogBuilder).setNegativeButton(eq(R.string.mobile_center_distribute_update_dialog_ignore), clickListener.capture()); verify(mDialog).show(); /* Disable. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); @@ -623,8 +623,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mToast).show(); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verifyStatic(); @@ -659,16 +659,16 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); /* Verify dialog. */ ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_update_dialog_download), clickListener.capture()); verify(mDialog).show(); /* Disable. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); @@ -680,8 +680,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mToast).show(); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(httpClient).callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verifyStatic(); @@ -689,6 +689,6 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify no download scheduled. */ verifyStatic(never()); - AsyncTaskUtils.execute(anyString(), any(Updates.DownloadTask.class), Mockito.anyVararg()); + AsyncTaskUtils.execute(anyString(), any(Distribute.DownloadTask.class), Mockito.anyVararg()); } } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdateConstantsTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeConstantsTest.java similarity index 64% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdateConstantsTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeConstantsTest.java index bdcd18343f..00090fe062 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdateConstantsTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeConstantsTest.java @@ -4,10 +4,10 @@ import static org.junit.Assert.assertNotNull; -public class UpdateConstantsTest { +public class DistributeConstantsTest { @Test public void init() { - assertNotNull(new UpdateConstants()); + assertNotNull(new DistributeConstants()); } } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java similarity index 87% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesDownloadTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java index 1e08624c31..f7d32e2542 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java @@ -46,14 +46,14 @@ import static android.app.DownloadManager.EXTRA_DOWNLOAD_ID; import static android.content.Context.NOTIFICATION_SERVICE; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.DOWNLOAD_STATE_COMPLETED; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.DOWNLOAD_STATE_ENQUEUED; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.DOWNLOAD_STATE_NOTIFIED; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.INVALID_DOWNLOAD_IDENTIFIER; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_ID; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_TIME; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_COMPLETED; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_ENQUEUED; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_NOTIFIED; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.INVALID_DOWNLOAD_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_TIME; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; @@ -78,7 +78,7 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @PrepareForTest(AsyncTaskUtils.class) -public class UpdatesDownloadTest extends AbstractUpdatesTest { +public class DistributeDownloadTest extends AbstractDistributeTest { private static final long DOWNLOAD_ID = 42; @@ -101,13 +101,13 @@ public class UpdatesDownloadTest extends AbstractUpdatesTest { private Semaphore mDownloadAfterSemaphore; - private AtomicReference mDownloadTask; + private AtomicReference mDownloadTask; private Semaphore mCheckDownloadBeforeSemaphore; private Semaphore mCheckDownloadAfterSemaphore; - private AtomicReference mCompletionTask; + private AtomicReference mCompletionTask; @Before public void setUpDownload() throws Exception { @@ -179,24 +179,24 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(releaseDetails.getDownloadUrl()).thenReturn(mDownloadUrl); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); mockStatic(AsyncTaskUtils.class); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mFirstActivity); /* Mock download asyncTask. */ mDownloadBeforeSemaphore = new Semaphore(0); mDownloadAfterSemaphore = new Semaphore(0); mDownloadTask = new AtomicReference<>(); - when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { @Override public boolean matches(Object argument) { - return argument instanceof Updates.DownloadTask; + return argument instanceof Distribute.DownloadTask; } - }), Mockito.anyVararg())).then(new Answer() { + }), Mockito.anyVararg())).then(new Answer() { @Override - public Updates.DownloadTask answer(InvocationOnMock invocation) throws Throwable { - final Updates.DownloadTask task = spy((Updates.DownloadTask) invocation.getArguments()[1]); + public Distribute.DownloadTask answer(InvocationOnMock invocation) throws Throwable { + final Distribute.DownloadTask task = spy((Distribute.DownloadTask) invocation.getArguments()[1]); mDownloadTask.set(task); new Thread() { @@ -212,17 +212,17 @@ public void run() { }); /* Mock remove download async task. */ - when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { @Override public boolean matches(Object argument) { - return argument instanceof Updates.RemoveDownloadTask; + return argument instanceof Distribute.RemoveDownloadTask; } - }), Mockito.anyVararg())).then(new Answer() { + }), Mockito.anyVararg())).then(new Answer() { @Override - public Updates.RemoveDownloadTask answer(InvocationOnMock invocation) throws Throwable { - final Updates.RemoveDownloadTask task = (Updates.RemoveDownloadTask) invocation.getArguments()[1]; + public Distribute.RemoveDownloadTask answer(InvocationOnMock invocation) throws Throwable { + final Distribute.RemoveDownloadTask task = (Distribute.RemoveDownloadTask) invocation.getArguments()[1]; task.doInBackground((Long) invocation.getArguments()[2]); return task; } @@ -232,17 +232,17 @@ public Updates.RemoveDownloadTask answer(InvocationOnMock invocation) throws Thr mCheckDownloadBeforeSemaphore = new Semaphore(0); mCheckDownloadAfterSemaphore = new Semaphore(0); mCompletionTask = new AtomicReference<>(); - when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { @Override public boolean matches(Object argument) { - return argument instanceof Updates.CheckDownloadTask; + return argument instanceof Distribute.CheckDownloadTask; } - }), Mockito.anyVararg())).then(new Answer() { + }), Mockito.anyVararg())).then(new Answer() { @Override - public Updates.CheckDownloadTask answer(InvocationOnMock invocation) throws Throwable { - final Updates.CheckDownloadTask task = spy((Updates.CheckDownloadTask) invocation.getArguments()[1]); + public Distribute.CheckDownloadTask answer(InvocationOnMock invocation) throws Throwable { + final Distribute.CheckDownloadTask task = spy((Distribute.CheckDownloadTask) invocation.getArguments()[1]); mCompletionTask.set(task); new Thread() { @@ -259,7 +259,7 @@ public void run() { /* Click on dialog. */ ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_update_dialog_download), clickListener.capture()); clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); } @@ -313,15 +313,15 @@ private Intent mockInstallIntent() throws Exception { } private void restartActivity() { - Updates.getInstance().onActivityStopped(mFirstActivity); - Updates.getInstance().onActivityDestroyed(mFirstActivity); - Updates.getInstance().onActivityCreated(mFirstActivity, null); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityStopped(mFirstActivity); + Distribute.getInstance().onActivityDestroyed(mFirstActivity); + Distribute.getInstance().onActivityCreated(mFirstActivity, null); + Distribute.getInstance().onActivityResumed(mFirstActivity); } private void restartProcessAndSdk() { - Updates.unsetInstance(); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.unsetInstance(); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); } @Test @@ -340,12 +340,12 @@ public void startDownloadThenDisable() throws Exception { /* Pause/resume should do nothing excepting mentioning progress. */ verify(mDialog).show(); - Updates.getInstance().onActivityPaused(mFirstActivity); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityResumed(mFirstActivity); verify(mDialog).show(); /* Cancel download by disabling. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); verifyStatic(); @@ -359,7 +359,7 @@ public void startDownloadThenDisable() throws Exception { public void disableWhileStartingDownload() throws Exception { /* Cancel download before async task completes. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); waitDownloadTask(); /* Verify. */ @@ -390,7 +390,7 @@ public void disableWhileProcessingCompletion() throws Exception { completeDownload(); /* Disable before completion. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); waitCheckDownloadTask(); @@ -423,20 +423,20 @@ public void failDownloadRestartNoLauncher() { /* Nothing should happen if just changing activities. */ Activity activity = mock(Activity.class); - Updates.getInstance().onActivityPaused(activity); - Updates.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityPaused(activity); + Distribute.getInstance().onActivityResumed(activity); /* Verify download happened only once. */ verify(mDownloadManager).enqueue(mDownloadRequest); /* Exit app. */ - Updates.getInstance().onActivityPaused(activity); - Updates.getInstance().onActivityStopped(activity); - Updates.getInstance().onActivityDestroyed(activity); + Distribute.getInstance().onActivityPaused(activity); + Distribute.getInstance().onActivityStopped(activity); + Distribute.getInstance().onActivityDestroyed(activity); /* Recreate activity, we'll cache that there is no launcher since no mock intent. */ when(activity.getPackageManager()).thenReturn(mock(PackageManager.class)); - Updates.getInstance().onActivityCreated(activity, null); + Distribute.getInstance().onActivityCreated(activity, null); /* So nothing happens since no launcher restart detected. */ verify(mDownloadManager).enqueue(mDownloadRequest); @@ -531,7 +531,7 @@ public void disableDuringDownload() throws Exception { waitDownloadTask(); /* Disable. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); /* We receive intent from download manager when we remove download. */ verify(mDownloadManager).remove(DOWNLOAD_ID); @@ -552,7 +552,7 @@ public void disableDuringDownload() throws Exception { /* Verify enabling triggers update dialog again. */ verify(mDialog).show(); - Updates.setEnabled(true); + Distribute.setEnabled(true); verify(mDialog, times(2)).show(); } @@ -605,38 +605,38 @@ public void longFailingDownload() throws Exception { /* No download check yet. */ verifyStatic(never()); - AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { @Override public boolean matches(Object argument) { - return argument instanceof Updates.CheckDownloadTask; + return argument instanceof Distribute.CheckDownloadTask; } }), Mockito.anyVararg()); /* Foreground: check still in progress. */ - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mFirstActivity); waitCheckDownloadTask(); verifyStatic(); - AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { @Override public boolean matches(Object argument) { - return argument instanceof Updates.CheckDownloadTask; + return argument instanceof Distribute.CheckDownloadTask; } }), Mockito.anyVararg()); verify(cursor).close(); /* Restart launcher. */ - Updates.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); restartActivity(); /* Verify we don't run the check again. (Only once). */ verifyStatic(); - AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { @Override public boolean matches(Object argument) { - return argument instanceof Updates.CheckDownloadTask; + return argument instanceof Distribute.CheckDownloadTask; } }), Mockito.anyVararg()); @@ -669,7 +669,7 @@ public void disabledWhileCheckingDownloadOnRestart() throws BrokenBarrierExcepti /* Restart app process. Still nothing as background. */ restartProcessAndSdk(); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mFirstActivity); /* Change behavior of get download it to block to simulate the concurrency issue. */ final Semaphore beforeDisabledSemaphore = new Semaphore(0); @@ -699,7 +699,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { beforeDisabledSemaphore.acquireUninterruptibly(); /* Disable now. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); /* Release task. */ afterDisabledSemaphore.release(); @@ -708,7 +708,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { mCheckDownloadAfterSemaphore.acquireUninterruptibly(); /* Verify we don't mark download checked as in progress. */ - assertEquals(false, Whitebox.getInternalState(Updates.getInstance(), "mCheckedDownload")); + assertEquals(false, Whitebox.getInternalState(Distribute.getInstance(), "mCheckedDownload")); } @Test @@ -741,7 +741,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { }); /* Mock success in background. */ - Updates.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); mockSuccessCursor(); mockInstallIntent(); completeDownload(); @@ -751,7 +751,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { beforeDisabledSemaphore.acquireUninterruptibly(); /* Disable now. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); @@ -795,7 +795,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { completeDownload(); mCheckDownloadBeforeSemaphore.release(); beforeStartingActivityLock.acquireUninterruptibly(); - Updates.setEnabled(false); + Distribute.setEnabled(false); disabledLock.release(); mCheckDownloadAfterSemaphore.acquireUninterruptibly(); @@ -832,7 +832,7 @@ public void notifyThenRestartAppTwice() throws Exception { Intent installIntent = mockInstallIntent(); /* In background. */ - Updates.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); /* Mock notification. */ when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); @@ -849,7 +849,7 @@ public void notifyThenRestartAppTwice() throws Exception { PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_NOTIFIED); verify(notificationBuilder).build(); verify(notificationBuilder, never()).getNotification(); - verify(mNotificationManager).notify(eq(Updates.getNotificationId()), any(Notification.class)); + verify(mNotificationManager).notify(eq(Distribute.getNotificationId()), any(Notification.class)); verifyNoMoreInteractions(mNotificationManager); verify(cursor).close(); @@ -867,7 +867,7 @@ public void notifyThenRestartAppTwice() throws Exception { /* Verify U.I shown after restart and workflow completed. */ verify(mContext).startActivity(installIntent); - verify(mNotificationManager).cancel(Updates.getNotificationId()); + verify(mNotificationManager).cancel(Distribute.getNotificationId()); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); @@ -880,7 +880,7 @@ public void notifyThenRestartAppTwice() throws Exception { when(mDownloadManager.enqueue(mDownloadRequest)).thenReturn(DOWNLOAD_ID + 1); restartActivity(); ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder, times(2)).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); + verify(mDialogBuilder, times(2)).setPositiveButton(eq(R.string.mobile_center_distribute_update_dialog_download), clickListener.capture()); clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); waitDownloadTask(); @@ -892,7 +892,7 @@ public void notifyThenRestartAppTwice() throws Exception { verify(mDownloadManager).remove(DOWNLOAD_ID); /* Notification already canceled so no more call, i.e. only once. */ - verify(mNotificationManager).cancel(Updates.getNotificationId()); + verify(mNotificationManager).cancel(Distribute.getNotificationId()); } @Test @@ -904,7 +904,7 @@ public void notifyThenRestartThenInstallerFails() throws Exception { waitDownloadTask(); /* Kill app, this has nothing to do with failure, but we need to test that too. */ - Updates.unsetInstance(); + Distribute.unsetInstance(); /* Process download completion. */ completeDownload(); @@ -928,22 +928,22 @@ public void notifyThenRestartThenInstallerFails() throws Exception { verify(mContext, never()).startActivity(installIntent); verifyStatic(); PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_NOTIFIED); - verify(mNotificationManager).notify(eq(Updates.getNotificationId()), any(Notification.class)); + verify(mNotificationManager).notify(eq(Distribute.getNotificationId()), any(Notification.class)); verifyNoMoreInteractions(mNotificationManager); verify(cursor).getString(2); verify(cursor).close(); /* Restart app should pop install U.I. and cancel notification and pop a new dialog then a new download. */ doThrow(new ActivityNotFoundException()).when(mContext).startActivity(installIntent); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mFirstActivity); /* Wait download manager query. */ waitCheckDownloadTask(); /* Verify workflow completed even on failure to show install U.I. */ verify(mContext).startActivity(installIntent); - verify(mNotificationManager).cancel(Updates.getNotificationId()); + verify(mNotificationManager).cancel(Distribute.getNotificationId()); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verifyStatic(never()); @@ -956,7 +956,7 @@ public void restartDownloadCheckIsLongEnoughToAppCanGoBackgroundAgain() throws E /* Simulate async task. */ waitDownloadTask(); - Updates.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); /* Process download completion to notify. */ completeDownload(); @@ -976,8 +976,8 @@ public void restartDownloadCheckIsLongEnoughToAppCanGoBackgroundAgain() throws E * already notified. */ restartProcessAndSdk(); - Updates.getInstance().onActivityResumed(mFirstActivity); - Updates.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); waitCheckDownloadTask(); verify(mNotificationManager).notify(anyInt(), any(Notification.class)); verify(mContext).startActivity(installIntent); @@ -1011,7 +1011,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { when(notificationBuilder.getNotification()).thenReturn(mock(Notification.class)); /* Make notification happen. */ - Updates.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); completeDownload(); waitCheckDownloadTask(); @@ -1024,7 +1024,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { packageInfo.lastUpdateTime = Long.MAX_VALUE; when(mPackageManager.getPackageInfo(mContext.getPackageName(), 0)).thenReturn(packageInfo); restartProcessAndSdk(); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mFirstActivity); verify(mDownloadManager).remove(DOWNLOAD_ID); /* Verify new release checked (for example what we installed was something else than the upgrade. */ @@ -1039,7 +1039,7 @@ public void failToCheckLastUpdateTimeOnRestart() throws PackageManager.NameNotFo waitDownloadTask(); when(mPackageManager.getPackageInfo(mContext.getPackageName(), 0)).thenThrow(new PackageManager.NameNotFoundException()); restartProcessAndSdk(); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mFirstActivity); /* Verify workflow completed on failure. */ verifyStatic(); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesPlusDownloadReceiverTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributePlusDownloadReceiverTest.java similarity index 83% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesPlusDownloadReceiverTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributePlusDownloadReceiverTest.java index f7b07aaa21..8438012981 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesPlusDownloadReceiverTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributePlusDownloadReceiverTest.java @@ -9,7 +9,7 @@ import org.junit.Test; import static android.app.DownloadManager.ACTION_NOTIFICATION_CLICKED; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -17,7 +17,7 @@ import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.whenNew; -public class UpdatesPlusDownloadReceiverTest extends AbstractUpdatesTest { +public class DistributePlusDownloadReceiverTest extends AbstractDistributeTest { @Test public void resumeAppBeforeStart() throws Exception { @@ -34,7 +34,7 @@ public void resumeAppBeforeStart() throws Exception { public void resumeAfterBeforeStartButBackground() throws Exception { Intent clickIntent = mock(Intent.class); when(clickIntent.getAction()).thenReturn(ACTION_NOTIFICATION_CLICKED); - Updates.getInstance().onStarted(mContext, "", mock(Channel.class)); + Distribute.getInstance().onStarted(mContext, "", mock(Channel.class)); Intent startIntent = mock(Intent.class); whenNew(Intent.class).withArguments(mContext, DeepLinkActivity.class).thenReturn(startIntent); new DownloadManagerReceiver().onReceive(mContext, clickIntent); @@ -47,15 +47,15 @@ public void resumeForegroundThenPause() throws Exception { when(StorageHelper.PreferencesStorage.getString(eq(PREFERENCE_KEY_UPDATE_TOKEN))).thenReturn("mock"); Intent clickIntent = mock(Intent.class); when(clickIntent.getAction()).thenReturn(ACTION_NOTIFICATION_CLICKED); - Updates.getInstance().onStarted(mContext, "", mock(Channel.class)); + Distribute.getInstance().onStarted(mContext, "", mock(Channel.class)); Intent startIntent = mock(Intent.class); whenNew(Intent.class).withArguments(mContext, DeepLinkActivity.class).thenReturn(startIntent); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); new DownloadManagerReceiver().onReceive(mContext, clickIntent); verify(mContext, never()).startActivity(startIntent); /* Then pause and test again. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); new DownloadManagerReceiver().onReceive(mContext, clickIntent); verify(startIntent).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); verify(mContext).startActivity(startIntent); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeTest.java similarity index 93% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeTest.java index aee2ef8ce7..320e63ddf7 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeTest.java @@ -26,7 +26,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.HEADER_API_TOKEN; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.HEADER_API_TOKEN; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; @@ -39,8 +39,8 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("unused") -@PrepareForTest({NetworkStateHelper.class, MobileCenterLog.class, Updates.class}) -public class UpdatesTest { +@PrepareForTest({NetworkStateHelper.class, MobileCenterLog.class, Distribute.class}) +public class DistributeTest { @Rule public PowerMockRule rule = new PowerMockRule(); @@ -112,7 +112,7 @@ public void buildRequestBody() throws Exception { String apiToken = UUIDUtils.randomUUID().toString(); HttpClient.CallTemplate callTemplate = getCallTemplate(appSecret, apiToken); - /* Updates don't have request body. Verify it. */ + /* Distribute don't have request body. Verify it. */ Assert.assertNull(callTemplate.buildRequestBody()); } @@ -133,8 +133,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { return call; } }); - Updates.getInstance().getLatestReleaseDetails(apiToken); - Updates.getInstance().onStarted(mock(Context.class), appSecret, mock(Channel.class)); + Distribute.getInstance().getLatestReleaseDetails(apiToken); + Distribute.getInstance().onStarted(mock(Context.class), appSecret, mock(Channel.class)); return callTemplate.get(); } } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesWarnUnknownSourcesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java similarity index 83% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesWarnUnknownSourcesTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java index 8bca692773..f0fe9a1be6 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/UpdatesWarnUnknownSourcesTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java @@ -25,8 +25,8 @@ import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_DOWNLOAD_STATE; -import static com.microsoft.azure.mobile.distribute.UpdateConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyMapOf; @@ -45,7 +45,7 @@ import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; -public class UpdatesWarnUnknownSourcesTest extends AbstractUpdatesTest { +public class DistributeWarnUnknownSourcesTest extends AbstractDistributeTest { @Mock private AlertDialog mUnknownSourcesDialog; @@ -74,8 +74,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mFirstActivity); /* Mock second dialog. */ when(mDialogBuilder.create()).thenReturn(mUnknownSourcesDialog); @@ -98,7 +98,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { /* Click on first dialog. */ ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_update_dialog_download), clickListener.capture()); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_update_dialog_download), clickListener.capture()); clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); when(mDialog.isShowing()).thenReturn(false); @@ -120,8 +120,8 @@ public void cancelDialogWithBack() { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(mUnknownSourcesDialog).show(); } @@ -140,8 +140,8 @@ public void cancelDialogWithButton() { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(mUnknownSourcesDialog).show(); } @@ -150,7 +150,7 @@ public void cancelDialogWithButton() { public void disableBeforeCancelWithBack() { /* Disable. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); @@ -165,8 +165,8 @@ public void disableBeforeCancelWithBack() { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(mUnknownSourcesDialog).show(); } @@ -175,7 +175,7 @@ public void disableBeforeCancelWithBack() { public void disableBeforeCancelWithButton() { /* Disable. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); @@ -190,8 +190,8 @@ public void disableBeforeCancelWithButton() { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(mUnknownSourcesDialog).show(); } @@ -200,8 +200,8 @@ public void disableBeforeCancelWithButton() { public void coverActivity() { /* Pause/resume should not alter dialog. */ - Updates.getInstance().onActivityPaused(mFirstActivity); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityResumed(mFirstActivity); /* Only once check, and no hiding. */ verify(mDialog).show(); @@ -210,8 +210,8 @@ public void coverActivity() { verify(mUnknownSourcesDialog, never()).hide(); /* Cover activity. Second dialog must be replaced. First one skipped. */ - Updates.getInstance().onActivityPaused(mFirstActivity); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(mDialog, never()).hide(); verify(mUnknownSourcesDialog, times(2)).show(); @@ -225,7 +225,7 @@ public void clickSettingsGoBackWithoutEnabling() throws Exception { Intent intent = mock(Intent.class); whenNew(Intent.class).withArguments(Settings.ACTION_SECURITY_SETTINGS).thenReturn(intent); ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_unknown_sources_dialog_settings), clickListener.capture()); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_unknown_sources_dialog_settings), clickListener.capture()); clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_POSITIVE); when(mUnknownSourcesDialog.isShowing()).thenReturn(false); @@ -233,8 +233,8 @@ public void clickSettingsGoBackWithoutEnabling() throws Exception { verify(mFirstActivity).startActivity(intent); /* Simulate we go back and forth to settings without changing the value. */ - Updates.getInstance().onActivityPaused(mFirstActivity); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityResumed(mFirstActivity); /* Second dialog will be back directly, no update dialog again. */ verify(mDialog).show(); @@ -251,7 +251,7 @@ public void clickSettingsThenEnableThenBack() throws Exception { Intent intent = mock(Intent.class); whenNew(Intent.class).withArguments(Settings.ACTION_SECURITY_SETTINGS).thenReturn(intent); ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_unknown_sources_dialog_settings), clickListener.capture()); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_unknown_sources_dialog_settings), clickListener.capture()); clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_POSITIVE); when(mUnknownSourcesDialog.isShowing()).thenReturn(false); @@ -260,9 +260,9 @@ public void clickSettingsThenEnableThenBack() throws Exception { /* Simulate we go to settings, change value then go back. */ mockStatic(AsyncTaskUtils.class); - Updates.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mFirstActivity); when(InstallerUtils.isUnknownSourcesEnabled(mContext)).thenReturn(true); - Updates.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mFirstActivity); /* No more dialog, start download. */ verify(mDialog).show(); @@ -274,7 +274,7 @@ public void clickSettingsThenEnableThenBack() throws Exception { @Override public boolean matches(Object argument) { - return argument instanceof Updates.DownloadTask; + return argument instanceof Distribute.DownloadTask; } }), anyVararg()); } @@ -287,7 +287,7 @@ public void clickSettingsFailsToNavigate() throws Exception { whenNew(Intent.class).withArguments(Settings.ACTION_SECURITY_SETTINGS).thenReturn(intent); doThrow(new ActivityNotFoundException()).when(mFirstActivity).startActivity(intent); ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_unknown_sources_dialog_settings), clickListener.capture()); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_unknown_sources_dialog_settings), clickListener.capture()); clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_POSITIVE); when(mUnknownSourcesDialog.isShowing()).thenReturn(false); @@ -299,8 +299,8 @@ public void clickSettingsFailsToNavigate() throws Exception { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(mUnknownSourcesDialog).show(); } @@ -309,7 +309,7 @@ public void clickSettingsFailsToNavigate() throws Exception { public void disableThenClickSettingsThenFailsToNavigate() throws Exception { /* Disable. */ - Updates.setEnabled(false); + Distribute.setEnabled(false); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); @@ -318,7 +318,7 @@ public void disableThenClickSettingsThenFailsToNavigate() throws Exception { whenNew(Intent.class).withArguments(Settings.ACTION_SECURITY_SETTINGS).thenReturn(intent); doThrow(new ActivityNotFoundException()).when(mFirstActivity).startActivity(intent); ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_updates_unknown_sources_dialog_settings), clickListener.capture()); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_unknown_sources_dialog_settings), clickListener.capture()); clickListener.getValue().onClick(mUnknownSourcesDialog, DialogInterface.BUTTON_POSITIVE); when(mUnknownSourcesDialog.isShowing()).thenReturn(false); @@ -330,8 +330,8 @@ public void disableThenClickSettingsThenFailsToNavigate() throws Exception { PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify no more calls, e.g. happened only once. */ - Updates.getInstance().onActivityPaused(mock(Activity.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(mDialog).show(); verify(mUnknownSourcesDialog).show(); } @@ -341,10 +341,10 @@ public void restartShowDialog() { /* Restart should check release and show update dialog again. */ when(mDialogBuilder.create()).thenReturn(mDialog); - Updates.unsetInstance(); - Updates.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Updates.getInstance().onActivityResumed(mock(Activity.class)); - Updates.setEnabled(true); + Distribute.unsetInstance(); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.setEnabled(true); verify(mDialog, times(2)).show(); } } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiverIgnoreIntentTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiverIgnoreIntentTest.java index 4550f64882..27a035045e 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiverIgnoreIntentTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiverIgnoreIntentTest.java @@ -15,19 +15,19 @@ import static org.powermock.api.mockito.PowerMockito.verifyStatic; @RunWith(PowerMockRunner.class) -@PrepareForTest(Updates.class) +@PrepareForTest(Distribute.class) public class DownloadManagerReceiverIgnoreIntentTest { @Test public void invalidIntent() { - mockStatic(Updates.class); - when(Updates.getInstance()).thenReturn(mock(Updates.class)); + mockStatic(Distribute.class); + when(Distribute.getInstance()).thenReturn(mock(Distribute.class)); Intent clickIntent = mock(Intent.class); when(clickIntent.getAction()).thenReturn(Intent.ACTION_ANSWER); new DownloadManagerReceiver().onReceive(mock(Context.class), clickIntent); when(clickIntent.getAction()).thenReturn(null); new DownloadManagerReceiver().onReceive(mock(Context.class), clickIntent); verifyStatic(never()); - Updates.getInstance(); + Distribute.getInstance(); } } From 41ff9af0be2ca04c4c04b8e335fea74d0da0c887 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 27 Feb 2017 15:08:53 -0800 Subject: [PATCH 096/142] Fix app store detection on some devices --- .../microsoft/azure/mobile/updates/InstallerUtils.java | 1 + .../azure/mobile/updates/AppStoreDetectionTest.java | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java index 27047220de..66033cd3d0 100644 --- a/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java +++ b/sdk/mobile-center-updates/src/main/java/com/microsoft/azure/mobile/updates/InstallerUtils.java @@ -36,6 +36,7 @@ class InstallerUtils { /* Populate local stores. */ static { LOCAL_STORES.add("adb"); + LOCAL_STORES.add("com.android.packageinstaller"); LOCAL_STORES.add("com.google.android.packageinstaller"); } diff --git a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java index 0fb7cbd62d..5f91b1ae31 100644 --- a/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java +++ b/sdk/mobile-center-updates/src/test/java/com/microsoft/azure/mobile/updates/AppStoreDetectionTest.java @@ -83,4 +83,14 @@ public void localInstallerIsNotStore() { assertFalse(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); verify(mPackageManager).getInstallerPackageName(anyString()); } + + @Test + public void anotherLocalInstallerIsNotStore() { + when(mPackageManager.getInstallerPackageName(anyString())).thenReturn("com.android.packageinstaller"); + assertFalse(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + + /* Check cache. */ + assertFalse(InstallerUtils.isInstalledFromAppStore(LOG_TAG, mContext)); + verify(mPackageManager).getInstallerPackageName(anyString()); + } } From 5708724be72d0212a993ba251015e0756c70c8c0 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Tue, 28 Feb 2017 10:48:05 +0300 Subject: [PATCH 097/142] Cleanup code --- .../azure/mobile/crashes/UncaughtExceptionHandlerTest.java | 1 - .../main/java/com/microsoft/azure/mobile/MobileCenter.java | 6 +++++- .../com/microsoft/azure/mobile/utils/ShutdownHelper.java | 4 ---- .../microsoft/azure/mobile/utils/ShutdownHelperTest.java | 6 +----- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java index c29ad08cfb..2e7717f87a 100644 --- a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java +++ b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java @@ -1,7 +1,6 @@ package com.microsoft.azure.mobile.crashes; import android.content.Context; -import android.os.Process; import android.os.SystemClock; import com.microsoft.azure.mobile.crashes.ingestion.models.ManagedErrorLog; diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index 530bbe9efa..d3df0870db 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -57,6 +57,9 @@ public class MobileCenter { */ private Application mApplication; + /** + * Handler for uncaught exceptions. + */ private UncaughtExceptionHandler mUncaughtExceptionHandler; /** @@ -294,7 +297,7 @@ private synchronized boolean instanceConfigure(Application application, String a /* If parameters are valid, init context related resources. */ StorageHelper.initialize(application); - /* For don't call PreferencesStorage twice. */ + /* Remember state to avoid double call PreferencesStorage. */ boolean enabled = isInstanceEnabled(); /* Init uncaught exception handler. */ @@ -456,6 +459,7 @@ class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread thread, Throwable exception) { if (isEnabled()) { + /* Wait channel to finish saving other logs in background. */ if (mChannel != null) mChannel.shutdown(); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java index fee8b12214..00946ed9db 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/ShutdownHelper.java @@ -13,10 +13,6 @@ public class ShutdownHelper { /* Hide constructor. */ } - public static void shutdown() { - shutdown(1); - } - public static void shutdown(int status) { Process.killProcess(Process.myPid()); System.exit(status); diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java index 06008e672a..051d572eb6 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java @@ -35,14 +35,10 @@ public void shutdown() throws Exception { /* Mock process id */ when(Process.myPid()).thenReturn(123); - ShutdownHelper.shutdown(); + ShutdownHelper.shutdown(999); verifyStatic(); Process.killProcess(123); verifyStatic(); - System.exit(1); - - ShutdownHelper.shutdown(999); - verifyStatic(); System.exit(999); } From 9ce9db44a68e71acd3a9dc87b19dc1ea061c6e9a Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Wed, 1 Mar 2017 04:36:13 +0300 Subject: [PATCH 098/142] Remove extra blank lines. --- .../com/microsoft/azure/mobile/utils/ShutdownHelperTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java index 051d572eb6..5087b5c9d4 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/ShutdownHelperTest.java @@ -41,6 +41,4 @@ public void shutdown() throws Exception { verifyStatic(); System.exit(999); } - - } \ No newline at end of file From f6e0cae999b81dd611164620c341b21c77aa9e17 Mon Sep 17 00:00:00 2001 From: malani Date: Wed, 1 Mar 2017 16:41:44 -0800 Subject: [PATCH 099/142] Updated documentation for Distribute service --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 522b1737b8..b989a78d2e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ The SDK is currently in private beta release and we support the following servic 2. **Crashes**: The Mobile Center SDK will automatically generate a crash log every time your app crashes. The log is first written to the device's storage and when the user starts the app again, the crash report will be forwarded to Mobile Center. Collecting crashes works for both beta and live apps, i.e. those submitted to Google Play or other app stores. Crash logs contain viable information for you to help resolve the issue. The SDK gives you a lot of flexibility how to handle a crash log. As a developer you can collect and add additional information to the report if you like. +3. **Distribute**: Our SDK will let your users install a new version of the app when you distribute it via Mobile Center. With a new version of the app available, the SDK will present an update dialog to the users to either download or ignore the latest version. Once they click "Download", SDK will start the installation process of your application. Note that this feature will NOT work if your app is deployed to the app store. + This document contains the following sections: 1. [Prerequisites](#1-prerequisites) @@ -23,10 +25,11 @@ This document contains the following sections: 3. [Start the SDK](#3-start-the-sdk) 4. [Analytics APIs](#4-analytics-apis) 5. [Crashes APIs](#5-crashes-apis) -6. [Advanced APIs](#6-advanced-apis) -7. [Troubleshooting](#7-troubleshooting) -8. [Contributing](#8-contributing) -9. [Contact](#9-contact) +6. [Distribute APIs](#5-crashes-apis) +7. [Advanced APIs](#6-advanced-apis) +8. [Troubleshooting](#7-troubleshooting) +9. [Contributing](#8-contributing) +10. [Contact](#9-contact) Let's get started with setting up Mobile Center Android SDK in your app to use these services: @@ -43,15 +46,17 @@ The Mobile Center SDK is designed with a modular approach – a developer only n Below are the steps on how to integrate our compiled libraries in your application using Android Studio and Gradle. -1. Open your app level build.gradle file (app/build.gradle) and include the dependencies that you want in your project. Each SDK module needs to be added as a separate dependency in this section. If you would want to use both Analytics and Crashes, add the following lines: +1. Open your app level build.gradle file (app/build.gradle) and include the dependencies that you want in your project. Each SDK module needs to be added as a separate dependency in this section. If you would want to all the modules - Analytics, Crashes and Distribute, add the following lines: ```groovy dependencies { def mobileCenterSdkVersion = '0.5.0' compile "com.microsoft.azure.mobile:mobile-center-analytics:${mobileCenterSdkVersion}" compile "com.microsoft.azure.mobile:mobile-center-crashes:${mobileCenterSdkVersion}" + compile "com.microsoft.azure.mobile:mobile-center-distribute:${mobileCenterSdkVersion}" } ``` +You can remove the dependency line for the service that you don't want to include in your app. 2. Save your build.gradle file and make sure to trigger a Gradle sync in Android Studio. @@ -61,14 +66,19 @@ Now that you've integrated the SDK in your application, it's time to start the S To start the Mobile Center SDK in your app, follow these steps: -1. **Start the SDK:** Mobile Center provides developers with two services to get started – Analytics and Crashes. In order to use these services, you need to opt in for the service(s) that you'd like, meaning by default no services are started and you will have to explicitly call each of them when starting the SDK. Insert the following line inside your app's main activity class' `onCreate` callback. +1. **Start the SDK:** Mobile Center provides developers with these services to get started – Analytics, Crashes and Distribute. In order to use these services, you need to opt in for the service(s) that you'd like, meaning by default no services are started and you will have to explicitly call each of them when starting the SDK. Insert the following line inside your app's main activity class' `onCreate` callback. ```Java - MobileCenter.start(getApplication(), "{Your App Secret}", Analytics.class, Crashes.class); + MobileCenter.start(getApplication(), "{Your App Secret}", Analytics.class, Crashes.class, Distribute.class); ``` You can also copy paste the `start` method call from the Overview page on Mobile Center portal once your app is selected. It already includes the App Secret so that all the data collected by the SDK corresponds to your application. Make sure to replace {Your App Secret} text with the actual value for your application. - The example above shows how to use the `start()` method and include both the Analytics and Crashes services. If you wish not to use Analytics, remove the parameter from the method call above. Note that, unless you explicitly specify each service as parameters in the start method, you can't use that Mobile Center service. Also, the `start()` API can be used only once in the lifecycle of your app – all other calls will log a warning to the console and only the services included in the first call will be available. + The example above shows how to use the `start()` method and include Analytics, Crashes and Distribute services. If you wish not to use feature provided for Distribute service, remove the parameter from the method call above. Note that, unless you explicitly specify each service as parameters in the start method, you can't use that Mobile Center service. Also, the `start()` API can be used only once in the lifecycle of your app – all other calls will log a warning to the console and only the services included in the first call will be available. + + For example - if you jsut want to onboard to Analytics service, you should modify the Start() API call like below: + ```Java + MobileCenter.start(getApplication(), "{Your App Secret}", Analytics.class); + ``` Android Studio will automatically suggest the required import statements once you insert the `start()` method-call, but if you see an error that the class names are not recognized, add the following lines to the import statements in your activity class: @@ -76,6 +86,7 @@ To start the Mobile Center SDK in your app, follow these steps: import com.microsoft.azure.mobile.MobileCenter; import com.microsoft.azure.mobile.analytics.Analytics; import com.microsoft.azure.mobile.crashes.Crashes; + import com.microsoft.azure.mobile.distribute.Distribute; ``` ## 4. Analytics APIs @@ -217,7 +228,27 @@ You create your own Crashes listener and assign it like this: } ``` -## 6. Advanced APIs +## 6. Distribute APIs + +You can easily let your users to get the latest version of your app by integrating `Distribute` module of Mobile Center SDK. All you need to do is pass the module name as a parameter in the `Start()` API call. Once the activity is created, the SDK checks for new updates in the background. If it finds a new update, users will see a dialog with two options - `Download` and `Ignore`. If the user presses `Download`, it will trigger the new version to be installed. + +You can easily provide your own resource strings if you'd like to localize the text displayed in the update dialog. Look at the string files [here](https://github.com/Microsoft/mobile-center-sdk-android/blob/distribute/sdk/mobile-center-distribute/src/main/res/values/strings.xml). Use the same string name and specify the localized value to be reflected in the dialog. + + +* **Enable or disable Distribute:** You can change the enabled state by calling the `Distribute.setEnabled()` method. If you disable it, the SDK will not prompt your users when a new version is available for install. To re-enable it, pass `true` as a parameter in the same method. + + ```Java + Distribute.setEnabled(false); + ``` + + You can also check if the service is enabled or not using the `isEnabled()` method. Note that it will only disable SDK features for Distribute service which is in-app updates for your application and has nothing to do with disabling `Distribute` service from Mobile Center. + + ```Java + Distribute.isEnabled(); + ``` + + +## 7. Advanced APIs * **Debugging**: You can control the amount of log messages that show up from the Mobile Center SDK in LogCat. Use the `MobileCenter.setLogLevel()` API to enable additional logging while debugging. The log levels correspond to the ones defined in `android.util.Log`. By default, it is set it to `ASSERT` for non-debuggable applications and `WARN` for debuggable applications. @@ -237,7 +268,7 @@ You create your own Crashes listener and assign it like this: MobileCenter.setEnabled(false); ``` -## 7. Troubleshooting +## 8. Troubleshooting * **How long to wait for Analytics data to appear on the portal?** @@ -263,17 +294,17 @@ You create your own Crashes listener and assign it like this: Required permissions are automatically merged into your app's manifest by the SDK. -## 8. Contributing +## 9. Contributing We're looking forward to your contributions via pull requests. -### 8.1 Code of Conduct +### 9.1 Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact opencode@microsoft.com with any additional questions or comments. -### 8.2 Contributor License +### 9.2 Contributor License You must sign a [Contributor License Agreement](https://cla.microsoft.com/) before submitting your pull request. To complete the Contributor License Agreement (CLA), you will need to submit a request via the [form](https://cla.microsoft.com/) and then electronically sign the CLA when you receive the email containing the link to the document. You need to sign the CLA only once to cover submission to any Microsoft OSS project. -## 9. Contact +## 10. Contact If you have further questions or are running into trouble that cannot be resolved by any of the steps here, feel free to open a Github issue here or contact us at mobilecentersdk@microsoft.com. From f851f6c6eca4789a1e92dd7dbc366ed11ad62bac Mon Sep 17 00:00:00 2001 From: malani Date: Wed, 1 Mar 2017 16:48:52 -0800 Subject: [PATCH 100/142] Update section number --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b989a78d2e..1506300931 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ This document contains the following sections: 3. [Start the SDK](#3-start-the-sdk) 4. [Analytics APIs](#4-analytics-apis) 5. [Crashes APIs](#5-crashes-apis) -6. [Distribute APIs](#5-crashes-apis) -7. [Advanced APIs](#6-advanced-apis) -8. [Troubleshooting](#7-troubleshooting) -9. [Contributing](#8-contributing) -10. [Contact](#9-contact) +6. [Distribute APIs](#6-distribute-apis) +7. [Advanced APIs](#7-advanced-apis) +8. [Troubleshooting](#8-troubleshooting) +9. [Contributing](#9-contributing) +10. [Contact](#10-contact) Let's get started with setting up Mobile Center Android SDK in your app to use these services: From a7a1b3fd88d3e3ae3b05a91fe07da57ae2b303f2 Mon Sep 17 00:00:00 2001 From: malani Date: Thu, 2 Mar 2017 14:18:11 -0800 Subject: [PATCH 101/142] Addressed code review comments --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1506300931..b4401cff06 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The SDK is currently in private beta release and we support the following servic 2. **Crashes**: The Mobile Center SDK will automatically generate a crash log every time your app crashes. The log is first written to the device's storage and when the user starts the app again, the crash report will be forwarded to Mobile Center. Collecting crashes works for both beta and live apps, i.e. those submitted to Google Play or other app stores. Crash logs contain viable information for you to help resolve the issue. The SDK gives you a lot of flexibility how to handle a crash log. As a developer you can collect and add additional information to the report if you like. -3. **Distribute**: Our SDK will let your users install a new version of the app when you distribute it via Mobile Center. With a new version of the app available, the SDK will present an update dialog to the users to either download or ignore the latest version. Once they click "Download", SDK will start the installation process of your application. Note that this feature will NOT work if your app is deployed to the app store. +3. **Distribute**: Our SDK will let your users install a new version of the app when you distribute it via Mobile Center. With a new version of the app available, the SDK will present an update dialog to the users to either download or ignore the latest version. Once they click "Download", SDK will start the installation process of your application. Note that this feature will `NOT` work if your app is deployed to the app store, if you are developing locally or if the app is a debug build. This document contains the following sections: @@ -46,7 +46,7 @@ The Mobile Center SDK is designed with a modular approach – a developer only n Below are the steps on how to integrate our compiled libraries in your application using Android Studio and Gradle. -1. Open your app level build.gradle file (app/build.gradle) and include the dependencies that you want in your project. Each SDK module needs to be added as a separate dependency in this section. If you would want to all the modules - Analytics, Crashes and Distribute, add the following lines: +1. Open your app level build.gradle file (app/build.gradle) and include the dependencies that you want in your project. Each SDK module needs to be added as a separate dependency in this section. If you want to include all the modules - Analytics, Crashes and Distribute, add the following lines: ```groovy dependencies { @@ -73,9 +73,9 @@ To start the Mobile Center SDK in your app, follow these steps: ``` You can also copy paste the `start` method call from the Overview page on Mobile Center portal once your app is selected. It already includes the App Secret so that all the data collected by the SDK corresponds to your application. Make sure to replace {Your App Secret} text with the actual value for your application. - The example above shows how to use the `start()` method and include Analytics, Crashes and Distribute services. If you wish not to use feature provided for Distribute service, remove the parameter from the method call above. Note that, unless you explicitly specify each service as parameters in the start method, you can't use that Mobile Center service. Also, the `start()` API can be used only once in the lifecycle of your app – all other calls will log a warning to the console and only the services included in the first call will be available. + The example above shows how to use the `start()` method and include Analytics, Crashes and Distribute services. If you wish not to onboard to any of these services, say you dont want to use features provided by Distribute service, remove the parameter from the method call above. Note that, unless you explicitly specify each service as parameters in the start method, you can't use that Mobile Center service. Also, the `start()` API can be used only once in the lifecycle of your app – all other calls will log a warning to the console and only the services included in the first call will be available. - For example - if you jsut want to onboard to Analytics service, you should modify the Start() API call like below: + For example - if you just want to onboard to Analytics service, you should modify the start() API call like below: ```Java MobileCenter.start(getApplication(), "{Your App Secret}", Analytics.class); ``` @@ -230,10 +230,9 @@ You create your own Crashes listener and assign it like this: ## 6. Distribute APIs -You can easily let your users to get the latest version of your app by integrating `Distribute` module of Mobile Center SDK. All you need to do is pass the module name as a parameter in the `Start()` API call. Once the activity is created, the SDK checks for new updates in the background. If it finds a new update, users will see a dialog with two options - `Download` and `Ignore`. If the user presses `Download`, it will trigger the new version to be installed. - -You can easily provide your own resource strings if you'd like to localize the text displayed in the update dialog. Look at the string files [here](https://github.com/Microsoft/mobile-center-sdk-android/blob/distribute/sdk/mobile-center-distribute/src/main/res/values/strings.xml). Use the same string name and specify the localized value to be reflected in the dialog. +You can easily let your users get the latest version of your app by integrating `Distribute` service of Mobile Center SDK. All you need to do is pass the module name as a parameter in the `Start()` API call. Once the activity is created, the SDK checks for new updates in the background. If it finds a new update, users will see a dialog with two options - `Download` and `Ignore`. If the user presses `Download`, it will trigger the new version to be installed. +You can easily provide your own resource strings if you'd like to localize the text displayed in the update dialog. Look at the string files [here](https://github.com/Microsoft/mobile-center-sdk-android/blob/distribute/sdk/mobile-center-distribute/src/main/res/values/strings.xml). Use the same string name and specify the localized value to be reflected in the dialog in your own app resource file. * **Enable or disable Distribute:** You can change the enabled state by calling the `Distribute.setEnabled()` method. If you disable it, the SDK will not prompt your users when a new version is available for install. To re-enable it, pass `true` as a parameter in the same method. From 86261def176a038ae98ab6aee007c8a692c52e80 Mon Sep 17 00:00:00 2001 From: malani Date: Thu, 2 Mar 2017 15:08:24 -0800 Subject: [PATCH 102/142] Changing module word to services --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b4401cff06..3237c7c898 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The Mobile Center SDK is designed with a modular approach – a developer only n Below are the steps on how to integrate our compiled libraries in your application using Android Studio and Gradle. -1. Open your app level build.gradle file (app/build.gradle) and include the dependencies that you want in your project. Each SDK module needs to be added as a separate dependency in this section. If you want to include all the modules - Analytics, Crashes and Distribute, add the following lines: +1. Open your app level build.gradle file (app/build.gradle) and include the dependencies that you want in your project. Each SDK module needs to be added as a separate dependency in this section. If you want to include all the services - Analytics, Crashes and Distribute, add the following lines: ```groovy dependencies { @@ -230,9 +230,9 @@ You create your own Crashes listener and assign it like this: ## 6. Distribute APIs -You can easily let your users get the latest version of your app by integrating `Distribute` service of Mobile Center SDK. All you need to do is pass the module name as a parameter in the `Start()` API call. Once the activity is created, the SDK checks for new updates in the background. If it finds a new update, users will see a dialog with two options - `Download` and `Ignore`. If the user presses `Download`, it will trigger the new version to be installed. +You can easily let your users get the latest version of your app by integrating `Distribute` service of Mobile Center SDK. All you need to do is pass the service name as a parameter in the `Start()` API call. Once the activity is created, the SDK checks for new updates in the background. If it finds a new update, users will see a dialog with two options - `Download` and `Ignore`. If the user presses `Download`, it will trigger the new version to be installed. -You can easily provide your own resource strings if you'd like to localize the text displayed in the update dialog. Look at the string files [here](https://github.com/Microsoft/mobile-center-sdk-android/blob/distribute/sdk/mobile-center-distribute/src/main/res/values/strings.xml). Use the same string name and specify the localized value to be reflected in the dialog in your own app resource file. +You can easily provide your own resource strings if you'd like to localize the text displayed in the update dialog. Look at the string files [here](https://github.com/Microsoft/mobile-center-sdk-android/blob/distribute/sdk/mobile-center-distribute/src/main/res/values/strings.xml). Use the same string name and specify the localized value to be reflected in the dialog in your own app resource files. * **Enable or disable Distribute:** You can change the enabled state by calling the `Distribute.setEnabled()` method. If you disable it, the SDK will not prompt your users when a new version is available for install. To re-enable it, pass `true` as a parameter in the same method. From c732370ff9131a1e67a9cbae8d2315517666e587 Mon Sep 17 00:00:00 2001 From: Ela Malani Date: Thu, 2 Mar 2017 16:42:48 -0800 Subject: [PATCH 103/142] Updating text to add Postpone button There was no description about Postpone button earlier. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3237c7c898..97b0fa7ecb 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ You create your own Crashes listener and assign it like this: ## 6. Distribute APIs -You can easily let your users get the latest version of your app by integrating `Distribute` service of Mobile Center SDK. All you need to do is pass the service name as a parameter in the `Start()` API call. Once the activity is created, the SDK checks for new updates in the background. If it finds a new update, users will see a dialog with two options - `Download` and `Ignore`. If the user presses `Download`, it will trigger the new version to be installed. +You can easily let your users get the latest version of your app by integrating `Distribute` service of Mobile Center SDK. All you need to do is pass the service name as a parameter in the `Start()` API call. Once the activity is created, the SDK checks for new updates in the background. If it finds a new update, users will see a dialog with three options - `Download`,`Postpone` and `Ignore`. If the user presses `Download`, it will trigger the new version to be installed. Postpone will delay the download until the app is opened again. Ignore will not prompt the user again for that particular app version. You can easily provide your own resource strings if you'd like to localize the text displayed in the update dialog. Look at the string files [here](https://github.com/Microsoft/mobile-center-sdk-android/blob/distribute/sdk/mobile-center-distribute/src/main/res/values/strings.xml). Use the same string name and specify the localized value to be reflected in the dialog in your own app resource files. From dc958c7211eb04561a9655a452c2e8e501dd465a Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 2 Mar 2017 17:29:18 -0800 Subject: [PATCH 104/142] Update build dependencies --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- versions.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 3e4b40f1aa..d308892cfc 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:2.3.0' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6c6235a7c3..bb9178e433 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Aug 16 11:54:01 PDT 2016 +#Thu Mar 02 17:25:18 PST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/versions.gradle b/versions.gradle index cb86b8576f..14082d0404 100644 --- a/versions.gradle +++ b/versions.gradle @@ -7,5 +7,5 @@ ext { targetSdkVersion = 25 compileSdkVersion = 25 buildToolsVersion = '25.0.2' - supportLibVersion = '25.1.0' + supportLibVersion = '25.1.1' } From edf358402b0f003920ecae789a7c7634a6214180 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 2 Mar 2017 18:56:25 -0800 Subject: [PATCH 105/142] Javadoc workaround for Android Studio 2.3 --- sdk/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/build.gradle b/sdk/build.gradle index eb88793049..678ee08bb5 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -109,7 +109,8 @@ subprojects { afterEvaluate { source = android.sourceSets.main.java.srcDirs classpath += configurations.javadocDeps - classpath += fileTree(dir: "$buildDir/intermediates/exploded-aar/", include: "**/classes.jar") + // FIXME workaround for Android Studio 2.3.0 / Gradle 3.3, need to find the right fix... + classpath += files("$buildDir/../../mobile-center/build/intermediates/classes/release") //noinspection GroovyAssignabilityCheck classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) javadoc.dependsOn project.assembleRelease From 707be389edee542c7da66644f83b7556322c337c Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 2 Mar 2017 19:48:50 -0800 Subject: [PATCH 106/142] Upgrade support lib again --- versions.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.gradle b/versions.gradle index 14082d0404..e049ae3d92 100644 --- a/versions.gradle +++ b/versions.gradle @@ -7,5 +7,5 @@ ext { targetSdkVersion = 25 compileSdkVersion = 25 buildToolsVersion = '25.0.2' - supportLibVersion = '25.1.1' + supportLibVersion = '25.2.0' } From 0f29296d898501c575da54228b2ed70a692eec8e Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 2 Mar 2017 20:05:22 -0800 Subject: [PATCH 107/142] Fix code analysis and lint issues --- .../main/java/com/microsoft/azure/mobile/MobileCenter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index d3df0870db..d449bcfeac 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -25,7 +25,6 @@ import java.util.Set; import java.util.UUID; -import static android.util.Log.ASSERT; import static android.util.Log.VERBOSE; import static com.microsoft.azure.mobile.utils.MobileCenterLog.NONE; @@ -104,7 +103,7 @@ public static void setWrapperSdk(WrapperSdk wrapperSdk) { * * @return log level as defined by {@link android.util.Log}. */ - @IntRange(from = VERBOSE, to = ASSERT) + @IntRange(from = VERBOSE, to = NONE) public static int getLogLevel() { return MobileCenterLog.getLogLevel(); } @@ -273,6 +272,8 @@ private synchronized boolean isInstanceConfigured() { * @param appSecret a unique and secret key used to identify the application. * @return true if configuration was successful, false otherwise. */ + /* UncaughtExceptionHandler is used by PowerMock but lint does not detect it. */ + @SuppressLint("VisibleForTests") private synchronized boolean instanceConfigure(Application application, String appSecret) { /* Load some global constants. */ From 6920b3a77414fb3fd4ccf9b182e7088a91f5c2c4 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 2 Mar 2017 20:09:25 -0800 Subject: [PATCH 108/142] Upgrade to gradle 3.4 --- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 68 +++++++++++++---------- gradlew.bat | 14 ++--- 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..f6c6e8b8304bbf3ea5e27291f8e8afbde4a01c4f 100644 GIT binary patch delta 24984 zcmZ6yW02<1xAxtfwr$%srfu7{?R(m`ZQFL=-P4}7ZEMEN-noJ@h5!(NbMu`h0g+M?+)DneNcrP+Ezh)a>PRuVyuvSN+d)`Rv`_M^IoFxXpRFC?o9-Qp~1`G3a#E{B=C@^M7{A6R$ zr<2jL&NgWJt7kc}MfHG3ry}_me{IJ~_FRH$fm}JCC4|90jAx0*ylO3H*?UwC z&aLjDL%R-U zy1W7>p;sy>XP>mi))wbe*Q6&wc?aB+l)&taN1`>NwWlQ#sW7FhGoDOdu);@Qs=|HJ z`j%v+Sw!K%Rve?ht{oN!$NaIEg0c5hp*9AFAbr`x4y>Se;p$#iu!ssWmEH2J2&*^H zcvL$BbU!ZDbJlYRlyJ=JXFI#aCv1{!Xgr%Gue#|FMy=u>N%+kq~2Fay2`51x;TbIZ{PZ?he7QU6eBi=_D#bi?HXNKLF|3jOEW5GcBNUB zMhFT{qOf~>uzk@tjYBu88=aHq%D1>Z8iOc3I)lo8X}vP034j?vM7!Z7R2MW$jI9+k z9dUTDu*Ba1BUx4CDpee{yJPfsux#1AQ-51z92L6Qi+8CwC8P%xwdK6km`ct_*>#5j zvl;IXv|$Jza26j)IP!x2NkOajDK6S5)D8-BY=SHtu{bCTwfL2WG}xAgfa!t$K}U>_ zIL&@2ik*2o_4ld9!E*Ge{l#fNXP;7|Kc;8XB#IlR0sy63$q$+kA-WehDsfCxk`ML4 zo!e*RufpB@m-G-(a0xYXzrQgK@{&Y=Ig6`FCd`%?!Aje;io9p{Gm9)GgPJ?sw7hBE zb8SVxtj#+9)3vSLcQN%;0girg7Qzd0Fic1ujnuF8kV)iJ`mja5i6>)}}h0eisU&s#h8|iOdk6>3J2wFpv={m3V znRa-B0$X^UAHY^rtLKL(BC$Hwd)DLk&FJb|V`K|T%ZBpWb^!JG13ZlGKA8mh!d3v% z=6@6YOK-?`o+(6W= zJ-jdcbePabU`~M0hm3Tyz?xu>kip`7ot=qf%m0*W9JHuQS&^>59CZE^>|Ej?QQRB? z6B(bOxG&~U&M&-@In2!v^w=nW=z6q22-q&5L?TkRNq62M`D5ph>@gC6!vR?>{x0gg zKG@P2iWM#GO+`sE%sFhT7>M;hdvmF7n;=4QZH#BcMe#dVv^`ID>bvYS27jk2ULf?kArv4a4yq zPkP0shL~7?c0eED4**gP6@eX425)kXV5FBjkSpPu3l*c_m1IA@yKBJ&iL@c{cbm6e zKPV63n#O3{0o*DNZCdxls(v=*XNen#d`VLhJoMs4KE=X20;l~ zNXu^tnKu&cyyz~BNKMSza|ny^L4HLwC3GkXYW1O~&%m?+_r-RI9gi0HFV=pH42dT&w=+ zrj36=`ak-~(#6=!E;$!s?EmN_lNW2c(;pxpxQHMil>e=hZm}@|hIs!35~Gxkc8xu~ zDW8#)xf~h^Xw}xJz5IR}T^lMy3LQ{E1hWP6A|kb^yQxsqr&IohPGG+4**|$Xar2Mj zgYS1m+s>N4Y(WI&>(85M-z=_&+?%QF_V2IRi*GO|#P66r{ut4ex-q2ziI^msVFa8! zP34p;d2KCP8I}=%(QgIq60D*Ia}N`xy$KKTuxwffpcB3>i`^vP<*#~TA0}|c{7V9TCuU!l z#Sd=><%CJ{Zi~8)ipo&q&L2Za$;qy(r4EZqeynx6HOdx{Mh~g8+yw7iRKXXwN6qc- z9bo6;W8-dZY!mP}Vdr4uZtN_`=c@hF?=37qbr#*dIzhVtw3@kU_f~A&MQ_U`{m}%d z@0HqT|dXV$#%{4}xug8GP-e7KBGlSYyAYE7U&q(~Yc~OR* z7Jfgk1tj35aDZMw`eOv$;dt>AsuvoZajTci8i_ksgns0+ z^p4`)559V*#ydKU{$=K1?d|i;Lo(Wc;ypn7OK#MT?F;x<#h&6za}XoaD&@5K=(NbP zOKecVTQqgdetnRY%}sYsE@1?YP1F0}nbBxXV>Z0;?6xR?m6tv$bCmvao*CfF zB&al%IadtmvrW52nlYlQER`Q?WU=CaebIx0)IU8rK0H2nY{S33xHQ^}dm+i{P@`IT!Nf6wv*j%ZkGQlqE8w*9`LebCmo zQrYS4`!7yEm09nzU9Q#)0sd=XY!#sS6#^zPZ@0Z($wdiQt3+kyf>9tOd1VyRjGPE0 zg1q@_Cep0Uyh)|@&CAr%d6;o4DSa-wH#na6U>r}kc?D<|?Ibl$Qr_gzZ<{A)q5Gan zNQ54CCnC&>)?(;B`CJ&?_rewxh{a_NP%yBHAS8rKC zo+u5ofu1c1Ti_654D%?g`wcLh!$rIzZBr42($v8SKak{h3+GmIZV8)--h1yFdpa9^ zKJ%GZI#4ph8q-wS+PjuQ(C2$}@AJLgUEE zu@>78CVCpYVW+soNAuhsPP?~XY(((lMpw+bPpa4hJ><8uzyATbeRG?WgCCjS6%OU% zwkp6T<|xq3-HS?Ew2k}53svRiMAaTWBJ%;yw)7ol`3kT4Rp-AM>x?f6WqNJa^nUhA z?+@4a%<={8!xA2UD+Yji%^U-3%qi`OpkT^Mq(Dj#l(na6uXFpb!vK$e z-2Tbko9u8?yT65Cuy4Sw$f~-~l0$aTocA39XuB?iBYNboSkxL+umAmehWyf0$>8Dh zdrY*GM0#+bRFD^c9p@ZQKodBO5Ko1JgDsokY!y?Iyn4{G{Tm>oxMho!XN+&>%pIs+ zZ^CXsddH-_=PAAWO_7HtLAh{eh7nv(oYfWGmg~lURLfg>dT{1z#2DsD&hWE$L{f{& zV9xAX^WAsbO5~0G;t|KkKaEGr-2K$I?#hhdjC?r=zQ*_DVX8olz)(VipnKx`Kgj}| zjOprimh-=55g9(22$~O|>xyTI6CkKPzkcyZ;;^s5Qjn(IZjueh5%@15t@?WLWs=P@Br?kK}EFZ@zOKUAzm=Gr|#H1EEb!f;F+xs1(2 zk52EOIIN`)3itaYT*j(Ba}F(Biw$Ls$*4F3soXL~>kQkmo;U&-KPWqb^u_`Qd;;~3 ztJ{=b9lpLz7$SE9P+Z=+T^q3T`je4`+>S7D1jD&obp%7SQ--|B{7!BI>bxFqcVoYv z+Zl;=yIQHS+^YNDu9%nZGl}Cu>S&fucc$fN!)Av!2I_&c z)+{UX1y_VNV)EzNdXI$tXZ>nEopPux9sk~YR;=h|cwKDFt+&h!%&&c3yXOV54Kz_rw$ z(yjZRqptbNa71iikW7kRY+R&vw1~LHlC{N-@6H_%uthL!&Co(u$0e2!&zW*%sG9^_chN?4i1lev!{t%%&!tFe9iX(7N~Q6g zX$=Zf{EVDaaWE~42h^yDE3o2cUh&|h{2qbXa7H&=pU2U#(OuHXN%bD9p0vbkgU2tc z{tk@4C8g!{&LrnmG~Q^0U4;#JJEdltgyV0DLZ4XoTn4HRGG??#4Qsg*eH;=PmwYThxRTCdVDVZXHTVviz#*YCN5?TzE{11k0qz=ek55ztu*_o#jZMCHS{r4?-(`=rZZOlJF?Q2tCg zF$eb8tbraN^w#rJu=y-n?1mb-l!dj!f8aaXdW{Wqchv2b1sd+ggz5eWh`gv-qy(a4 zziC~C#M=pIiblyxj%35LVTNvI&(r`;am4{MI_d@vik6%gmC{E$waDr|2agi5n2>6g z&^@P8y8~s4ma!zjyK}G$+}`+;;WmpEa0kAtx2m{#(E-zD_gQNQmwDLZS8`5p3141{ zhsi55Bc3h>+=)B}$c*X%L;A2Q_Z!jLR@}>~q?F@v++!wX&yt!u@e)Nkx9)||+9p7| z<>a*pot^*8TT!has)ryx{_t%nS#6hIu!~J2;cwdNW1YJ4irF8nh0)9r4bw)y3?If` zYaK*kt<8v;f9u2ZzNyXbb}6DCIBU|C#Pb)r(M9e4r(*_u-J!V5!oIH!FZOgPiY$N^eYzY) zwp3`)^LL`m_TY?K5V(BHR!6Y@&DM7_D@)7zqr;mTW7g_Id-{c_tr31-v_BY+01%vw zT}rJ`9?Wl-xb$0arL$A+q&KHg=1<4i$2S~N6yb4$q@5}oEh#1I-d93arnP$giWXf0 z?7K9$cu2W+db$=$w0ONz(J0`2NxNB7JC{oik6=uXxjs2B6NuG*tkC`pM!gUwj|Q zmtrxdoo(`L=HAg>M1JppeZS6GD}P4bB3kta2VSijer~~zCGWk4A~&Oqx7?ncqb<$@ z0$u6-XaPT0cJ=Ymy^!wq-}&QI8=@V^TtB5knCKKRa>s8>NW1P_torgg6kqi?bY0LTj_%y|n z_X8oC@Pev9m8rOn@ASOpdER!lv;cg)J%J13#M_f6Ht!Kz8Z!iDb1pX~4p@Lqj`o_H z!nNCrCAK%%?;(nQWD>o-@rbe;0#;2UmdsR{>6bp0daelwp&!GDAkdvJng)Xf>in9C zpiH~2JYhDJ@2!-s&f1_`cXoAXf1Ide!>Yr1wbMnYR>>~i>qK#Z?>%DNxP{ys{0_jD z+TP^XEf{LpMD+=ue^=q4Vm1te$9om|6!;$KRkCvAtTfqU1jk-}aP>{616bVtR*od` zL`)pqP8C=}X7XnXI7g?&r9o8DD#1PZZK7poyG7$M_1m#qM7!gp06Ko=j>##@HX$jr zDfr#Xh?l?RHlpwa5I&ypT5JQWmCMH>>XU(e+i-M+*rkKdN5XX3>g{1RVzc7`4K2hg;9Tvdfmh0^F}+&`L-J7>~wQRitslc81IKN|R26p^7@ zBH1j)7f;_yOm9a-b|0DpJ>N1KJ{aF2Rn0~^0K{lwF-stz(xkCzxbzg;>1;%!?{Hq> z{kc!ah(G+ruYSEC)_GZhA4sEzm+mXW$+|pbVLKiJZOSmJJNRh4PIsOF08bz9I;2n@ zpjhpY!8{vr{7*(;<}Z+rEaY}QO;7|0jOX-%hhUls=Nl~=W zqLX8QQ9zd(oRj2GhKITCk4u9(KECM~;#-~vJ0i|-&n%`h|Bms0maN=SAU#~3Fl%~R zxAphEYVB`bQTTd$j~4>%iE#8k1(pQS5S=rh4ZF+HW!w#t#X7zg7-=Yws&p6b+5;e6 zVUPW-hDxxMc^IV{se$rDYV^`xH7Ju_3 zTW%9PI$Ar62Z+krUxHjMm(z$m8M(x7@ZKR~P_#1F-76)=X> zbS+_q(pY;3^m@9D56%lEZ&0&~Nmtk$9*@+gR57o88alGzaW^>=NwB47lY$8~at^&f zrnZgdTZ1v$3@<*gC1KJZwq{Bu+6wE-ZI{`8Uyqkx8P~rL(zm^`4u`cY66)m}&(@;N zZhp<@yJYo6ykh++ioa&!|8EQfvMn zp2O!ueaka}0UiLB#`6~swhuhk>#1ZyB$P9c&>KsbGy6Ol5|KFHk+zpJ;`Cu-D*W5H zA(8gQiDg6!1W%kY9u^r0+7JFY9tvdqf#3WJ`h>#csE zhUHHZ2EGr5|2b>rR%5>`{VMOGskq^4&fc<1gFg0t%Hi4ZOMmv;;oITye&4C@`(cFl)dsJ7+W}9MvM0)l z%_=vZpa>w14*J0s387#^O_?b66LaC-jKWFgc!9LhJR@;1aZug_wUk^Co++?N$}dq3 zm79#xGMPL4fz-PQjc8C(e9yGWuL+H4jEeV8+D|4-k;>`kNWS!b^NSv8e!5ZeiwKw^ z#cd0}AlWb$@Jm)&bIeKZO4bXcW7lK9~$cEx&}G@Z7Q(|_#_`ik8~Mchk$buY62TFN{u#iN(} zCeVlb7FK%Be&2V*6a*@a|o^> z%HuUZrkHPHsXoOH02Z(KM`L8|Ck(zuon*%46PSEKNOd=uIshlLjb;AoEVoDC!U22- z#y1=?j|9!$78rGbD{lPp^%vL!4!NJDhGQN$*TaYee9rxG&qnESKmCIHvP92bC<)cH zHe`xFF(w%^^5UYsevY-@+G{vR`|6B&nh8W;x%*-5EZq6y1Q0i+|3jQY^LMO_G$U;( z@Uts-rrw`uA1TTgk0lQX`*)VO0f4##te4U_!@Z_3O}7u+{yAsJX5OlfDr1E)j-IN$ zuDG43{GGr>0|C4BGA0EnTFT|enT0J|Fa2RW-hg-^tC!+9p}pCu-HwM4b=pYA$c91B z$`{J7qJw%b*|ClN`(JheTPpNa)jGD74FTgOsAj`vdHk@Ro7{+l#{vtt;D9K)OPdXe zbuZ^|%F5|cmtKAjhunmbhWgH5 z`RLfXp0`X4Kh8rsy?T$;xK)81{f?b%)2 zE%7X4`--J!ah~ZphyD&dZ2~rYSb=auGR=V=*#-87qrB^FRGKpkn87WK)S|WF^y*=Y z2)Qg{ZIkDl+jR|NxC!r&_q(;Cgq4kuVdZ1DqRiiL3BT#k{TuT2|pM{ zP$w3R9lv)PbYrmcQRy&cus5o5<@tH%d#`3|u7N5p7VFgUStqBY=Ku&*f@6QM<{J$o zm-uXlCb*Ct`G>nzH)<}eDz19GKOWF6l8#_HY_1N+vh`4_G@u^eJDSB7n$J5fxTQE@ zz3z+rbfk8>{v4AVGa4h^U55=2+>Z6r{H(#5jz%$;Dk z=b~D-s@iGwJ|eBUv*n9RZik|Hy|F(;rPb+GT_kS}&!zx!&(SMCvMjJ}FEA8 zV<=Ywc0@ZwXZTMQ=W7ZvRk)O>*+R?xd4NH^cbxj&HEs2f*T>ya877x?R=CxOvZrH5 zOv_s>5Ah#40sxz5N1``=1f;X6>*|!=4qsxFKH@6Ll0UvJ&l8!v!WC)0i`hK6w;FMH z*?7^(A)y4KzH#bK32>*8=z9S9E7Z7^5NeUqK!(*z3t_LIY8S;FG<@*^Tb*gY!3QS( zM@{Ak>zg+S5{p7gTJ{}t{{9PQ_2J@`4(2! zGt%vPdS4n=cABcs+d5!QNQ6(b3DwalE5|i`tv{2G?ECZM*%HKHP?1%{OLx?GN>(eu z>E|dhXOl@^Tb%xj-Td}$AKQdq@Gz+xkuxVKf+WUc}4i2nz&fDx)fF${J z3U#*mHk-AnER0>IyW(HPbFGW|k9}Ex)Qc0OHqtE6E%c)-C^6Y{r+M!xGy?hmwxVDX zcQZ0&iAR-ID{BjvPxhARp!+Yg+qIcfxwopxWZJ}`uw2?;PbugbN-s5bxFi;NNP+oW z`0dS zY3{QUWh>3ex(Df@rkq-66sLa!dyds4TWgBVjA}#c*@Cf$ihd!+BD$>8|I6C73f;h9 zoME8m4pN3>BSw2SNZMa@>k8Q>;$X`A#w z!wQa7V7V{|Ey8rQ%?!Vb45q)x42t^6|Jv#(>+L8zKx?O_OAB#C_lv$c#klk_puua? zFyl)_XU=Dz^kFo~@DM8pJR3Df^bV3TwnbR7xtwjvh_UgzC=|0=U6?zNunKD6#?!qJ z3f=w*Kxh6E@{P-~b^0WY^e%if)dj(5sOy(;-(S=lP=saf9*~D zZ{*S}yOSB+KeAgPccyia8Ph&jh5^L(Pmi{9TkMQZw8Eu1n)!FM7yC5FL7#X0_U)Mu zmjfAlM`fZ(EqRq^+H?4?xiX%GA2rFyK4m$xROal?!XJWJ``G$<%U5Rq{MURWuzc?p!cyHJk3f%aF^nNo zUN@tdCKq{yryemI*u1fQgv_X`>{i&ARr%rA=Axx)33Gl(#UggL+s+KC z$Mxe2({Ld%DY`!yAkxR^*;luRHy`3Su_w;+(Tm?l5$-s-$LI1=$9_QM_E8*2;-@(F z?EWD1zj#3c|Bn2gzuUnLAaDPY9Q!_hX$;e} zdWj?<4q?QlR%f>%Er={&6zTfIUU*S7KHzM8Y7)vQH-SM1u#U7^2GZ)coSL*U4s~f2 zN4IuqQ9-uYnDkO|T9J~1fU@OHuR9oy-t%vuLC1H^0AEGD0n0OQXDLT~b;1v)tG^Q) zzr^;h`-%^SJ^y5N3`_`8UA$^6=Qpr=cPoofHxzmL85IC7uf;ypgxcHMp+i__refr2 zvVRbGmidkXTFY2Ho*`Ezj_lffl?blU{&Wzw9ouCwd(H4Vx?9#<%6SfXF0A1Y;_>G- z=rdiJ(YOG3v!9S)|F-FiAG@$jdV0t~L%ikCROxeM%&ZT;c1mvu7|UCvkSI({$Z0v~ z)X8Rp0yyP!>T1hh8eB=Tooi^;edQ;{70NB|>w3`vO76jc}}X0|iFZN65cjNTayipS;UrBrwd&+~b90S;4XZS@nKGcoM7v*WUbG4ZA; z=ZF4)z~v?uGH;>u=g4mRX=R^ZAakn-96C5E+I$oY6yl;;Cl?bRcXk}z$Jz@|9od+x zJrJD)vU7Wn^I5A>x)Db8N2RK=y^Y0;FO&934$LY?90b%PQI_Qd$8#k|YuyfKvt?@R zTdV)tZ_gn;CS$I!GB-&!^4%j&MrnCh@@A9(x}JA2f6Hnku&i76=WFG&6USZ6k_1zH z+v7Su)3T*ytK8MqjvBPcH}%D_lUPWpE~>fJGO^X54z;H*rHwlY;J0d zCacU=TvNOToxM|?V!1KAz-%|pWtb1QLvnMIj;zI21;kS2^MFjV|g=XOH#NoF$F{{7R3<@e<7^N@yoO)8hziQKtbJbRUK$7F4LSZ_@A4m%0h8!lQCIY|(Vc&)QRnii zW@wI8+_yD}q%rJWRtqCNRNW#J(CWtkj@e#xgeQ-sullf#@@|==lwI=9$*Yx-+46U& z&e4<(96-j#4RqfU`Fc!W7;hcQuI$;@rxZ#bxkklHEG^ZUr9>Ay7%@yHqTYY@i*_`F zJ<^V}EE2qoo7!PvM47_VPvf)j!O0KfwL<7@4X0ao2ZT)g5+TU0Y0_k zpAp?#DzKktgtO?PCR)3N$>F162g8y@2ZRSyK7U6d=PIfo=GIk^-Dfr&7ANLqW)&K} zBlaB5to#S))8&=-d z7~-z?zPd@6>sMIiiJ6a#;N3<5Xe5pp;-gtq$wy!?5y$b0*X3$7Qwil`vQ0#CN+jtx zLYKUy-?H{gNws0qY@YBY@TY-UE<-fyYAWjb+U(8~=;grN5buu>;T1yF92C!`KMYNE zm8uR9MJ~@Km-~8@oS8EUKt$cXquCR?WgTAOUJ;EG>H(&#Tx!MYtI8m&Y_$rBj=J8Ax(EAMG{!~op!RfIsmyYjy}>gl z(|X(VM)}uOMOK!HN{JM}C6#qVMdgIufT!=thQp33RFlfwMDRivt6EQ)0O}#nv6-{h zNUG^cIZmj48&y-iV7sN(oIrM6dARv=_^Bmz_7<|S@;p>kwA?IXi6Zfpss5AanufIu(d(GT|aqHBx zsJt(;IquV*l$Qb+HPdvL#qyL?9Ib%VYU|E!v7ZEGKj9eR8b^+mg2sNp?+4Hxcr_uB zTt+U|FsSMsd{i8~0@tW*<6--}tnFW?Jad~5ymc{uW=ezCaj=!Y6>rWSogMMreG_H( zNJkWC;W)x9m!_#6%dN~Vy{8$tHxc}ubj0;md`v6Kw;ckA*TeD%%jq1^a))nvinmmqIp(yOYJ-u8g&qlRridnV8G+Sur_Hu&a@H(|y0 zgCe2#q3@%_vDQ=c6>=ruan;y@7fUTew%%}I*^`54qN2tRGNg>abLKu>uMkniy-ulU zNFzra9}E)^v>IVOCHSGE;Op5c zc%ErdPUla7#eA5=%xkAolhH3*=~hnsE{H4GEGAKaViY91evtCqwjAxQ#ObO+EnkX6 z^KZ?0YA2#=s@4=91sm!9s^(u9kQr1Pe7sl%NwDK=cm2A^v*lRk5HP#q)QN5F2&g-IbS$|npF`f(7>K1>D(l@i zjXI6J$bwz{T2&;}EVqZejDV;Yj)#D=y6GAM7=jYS@4XQ=e|1vMk{!D$-p~L1pfT7D zH5|Zrq!)Q}8HIFW(`-*L!7|>Owpt6eUzIKJ^=gU-JQrg@VGp+(Bex%*Ba#4feg8;e z{YAS5?Ln2i)xifAb4v(_zPJv3vP;}D;*T|B7mt$o<@o0BZDVylt6%#k6Q(mx8ACn- z`at_*pm1ur7U~R*d#o2de<1B)xIIHi+*MB{Q=SWn>(>X7=xchsMVpuRYq|J<_XTPm zaKOjedlB}B8CpU^+;M#TAUtkHMeX{aW&rLG&`$oUO`prOV=9n!inv>u-jV;fI=nfC z>^Y2N^&DfPKmDJk$yiWf~<^>a&TwuIw%&%CJ-A8H0BwRBg5RY=h17gGJ^ zCDoQpZ;${hBJ@_=WsRd6Hf6;_o1K%&ZDf}aqO^Jgrv>`Eo8u5+dHWDmuY8^!r}}!G zu}FlM@Jp^ksr3l_YluzG+YyceNZ$EE6lS;HJ@ddhS%QBF^IYK@Gh7QuN%q44erpGL z-$~|-gZ*{F*$&4@@pEJ``NnqCX#9QiqO#?uUg>fJpKr8<4;$}_f87(rFtsdC5#RiA z<#M}Q27Qf=<{3N~*BAgw7UTP|Wu z=IEo=tH*3Pokmq+KJ!-A`i~KJWeguhEL{HaDjM_-4bk8&&MS22jm=!DFLpz0q}i`{ zVNz)(s-!|G=EOzPLNS#aDRNzrT>O^05sJ;vk)#HHhG`#yL%?oGCNatn)09H*>G*zqaea!Z82|7t8Hn&Zg&D9EClJ7>c7lsXBP`FMov`vlvK;cO zguT&F4y2CT+bO*rwh-ihVyPeYL|FvMbi@n*7(cb6Rv{|3OukG#Vh9oJWt2!WBtBeZ z1nGoTAefk0LGqKKKX$I zJNx&c24wa)VW=F^3U?Of_KaWCN2Y{Fbk7?!Yx)3OZ^9h3>cHi-CZS9vtIDtL7&r9f z-8*my&U~~8WcmO&-#;=dd(0Fz&EiI-HKDp<$UWR%B+b(dj{k44SCFGcL zLTOzE)|1&z?N>Nt&uabe^j#ZB>#aDfBa%|}t^Ho~XOE~LV`7<%F@v!ECZNtq@>iR4 zcm8m|^5-Vay~mL=7hcg)X}~tkYy>o#IyYv4f)=Vgf4=VvtF>Gx8tD3X?7S8$OXV$N zs4w^;nd`wO72KQn+$8vicAtn~eL_1OkV=!<4+D**Qob?6VOg?8Ohl0x5)W5%#olJS zqqwlr{UEGNJ)4uW)^@G258!ZsTQ+no+p4c4FBa{-)S^>{a-lQBUMEN>tEDrwUL&6L z=g5ZAkbHZoG+S;V`@lj|KD{!O%tyYy^6~Gfg?zI>MCzAQ4;N;nt#O&Q16nq&%E3^n zWOLUAu}O;nY060lgI2x(RCL*yKS83|&}wNtC#jFVWO$xb&5}tL13)(xFLd;P5rk(4 zmdrwnE|Zb*6QWm8I~grnzb$iImk8-X4lOG-V>>CP?c!JhA$UK~D;#sJCNs>hoVQn_ z9dvfu>4?5BQjFNYV0e*3){z+%b3wZf*UmZ5$*O!Vm7QZUVk|A4M}cdf!vxJ)T=I!q zbxk@sNW!>*EB2|x58%j3CU-z^&$3mu)=~(g8r4+Z;$aJyPH!(Skx3~b_83<4GqnDv z=fkN(8IY>wev_qoKpSNSAuJQcM$2-^qHpOm(Qy@(d(PKPTV!u`kOBKrO2TNHKJX+Z zu0-V+Qy|ZwH$D>KgJUo`m-97;TEwJ`O`y<|Tz>exUK@@c3=n}Jk)GFJW?s(SOFdjn zvz*)_thrKzZJsf>l6QHDUU6cl8){{H6aLI0YdQh0DhXCrXF^3iP{!_qu zHC$p^pCxHS2B6(NnJ9qa|G4EdHWTt@C%_8k*-qQPVt0dU%FYe z{Vu2e`SFw>jj&Df0|iP&iQy12+)lkW%X&sU!4A)!%TX-en0QP$dBw&J$WCW{ZmQ3m zRTeE>M(I}Fh*Z!t5_RFuWhZNtKM~t#rZ#J4e{}D* zPe(W@067OkaF_R0gFm9Cf1Z`SS?bd8!`9o59pFuB()r6m*X{b_^&vA$zrE(~m&ZgU zb-807J6wr2_moXwgUb%VLXF;Yx14e^Ws4GYEw{VKnz&-tauM1Bud##lf)0I4l|tz9 z@*Ysu><*Hu@kQ;yMq#JOm{B%#@?O=$)1#$Cnt{*U-rTu5cZOi3y3%`zRlfA()H`*% z9AH?@E*wnz852-9ulny_%$0ee3O59Qh0^ddl+>r&RwF+dj52z#*x7|g+N-h)^zGVv z_Yd#X`V#?Iz)!)xihBgV;t9ijyhZ^c!^%#$kkxecFIWHut(k3vm4m*jW+CX&P02qAAs*)t#jXijoPn#VrSQ}$`9*C_0^$IGRs%J zPm#UD@CfSE{t|3Bnv2{t4P}XKE$b}l!7fx}EmXwHK-$EB8h-_?P-_#m@S}yD40GP( zGsD89IaU^|8j+*SV>y5kCj%#D2G&{!%ve2Z~!eJ z`kBREs1BKC8>S&5yeMA*ER|^AvAQMxTH}=%AbE5%M7wF9t}PLs>neITlaLLci;Isg zSWB*fY6~u*eN znBh#{@+R8g)_?1gpphz~<8Fg*0|+m%LcoE+9z!SC%V?m7v8EK0HD@y#u}Ie;JZC?c zOTcWJG84HxLRVTU(xUF3hc7IvLP#Z-F*$3&PI6HS%ejsL@~O1y*IrR4z$~pnu4jOz z5@E6n7TuEssO^oZ1?b@eGN2mNk;2;Cp4EcV{}du1C08=${xYIn2;+a#T?n0FjXH?oN@ElukuN>5@iTBpgHq2^nDoq`Nx>B%~#V2I&TAP>_!Q zf$ROjT)%Ixb=KN@o#%P$>{>H>zwM*4a{>D;TqAyGax@$#uR7{yZ6BU-P5V!Ho=6~- zKdV-dp$1PkK9FR}l(7B6(`lI%kuJ|=ZxjBb23B)RYUmxqlHw6K-6HiFYDQ?xN4M4l z{Z-E#oOnfn4600Llwa8e1egke_KB4ItJsfHkh(f2I3eq2gBCrCx$$Mro4-G4sKi^W z>fGlu;%(@_I%^I%XpwawZUXgVIT=$p@c#rkc3YD(1$dDYoavyOU)4yNBv z(vL@N(y&nR&_b=3b(CP3lMT{4`*n~+J#dDL3q}zh$RGgoQTg<&V5)f_xv zUO8&Wz76tf8(*w*#o5#PbmTDlIHF^IZB=OUS<8TP2+iXuSvH0~(v4!rySr{<)}wcu zLz3(4^StzbvpK_5H<<`p7kQJ>xhc#-LcaJMStvFHYH9bcFDP!rrNu87+G;d2D5*qw zkXI?~rSkjINQ}^z^qjBJ!M7i~hpXJ_>SHj%t9k_KKgJqw3T^voky$5R6@@1?{+-&w zj9dTLdpX%^J9~qJ8a-lB%`V;~)RfL_$N* z=GxJkMn+s=c&zJke_P3@2o>sO7qR26Z)_1@s)I#jX^|VYAN*hvcRyt)Z8mxEcG&I_ z6J_iMJVRI~Y3$gk6vo?|+`qw6}qu1TfedSH+{m z%-1T$u?qUXFz<0%OiuFaJsx(WW{Hbg|5E&B{^fU$6l3d(8}f$jgjG1SY1QS;W0l!= z`%6&qYE zT0m;L!5K;ECr;$hsKD~6r02evwl0f&*~0;6PSVr2)Eqc)kNm14h#tPy7Lo=ivd-O5XM^{9u)Fydxy+Rsuh&>_w z3_=g--S4UugwRvnJPJAqE)bQ;99jbJpjn0PJ9OZ(gLlxEce$kRRWCV=Zu~fHGsI;_ z(dM^{91_Iaczf&1d*)spDKiRv4?K9rh9H#NKiR!EdJxCu&5C%xHHuq5)8IWcw*d=R zwxmoHE3NT2@tWKCd3EUW9jQCkHzrl~!yrZ z(Cyt)IZ>o%Vn&IOhQ%5lay3V&X&HXDxsgq!&2G_;8#&#{guddBLHvEpw58zQO=O6- zXEwDEDVnE>`~Wcpsa-vl-#zCdXKV>&=&Pyu<%jkhiajQ~=4-$vfh|74ANC+Q2D?CuKvC6DW3vq2qIXpP@UcPaG#IFHxnW~puPRG zpW@BL7L|ExVt(O4VR&KEa`Vx*&h#Pgra|k|R&BnxKJ2DHl`+DhiXM%I;8*-MLj5rJ z?yx%zkkC^h^ERc5`x??@3fXtIKkDXEq*#3Y6^%b(IjJ7Jon@cx`84}`so-afK1ZIn zIYG`O!$*E~NKamhWk*O)6O_>hRIGP{!>0==_u!ndGisV^PY;%e+0*s#hCfR~X7rXR z-$meT-%mPyDW=Hs04yP^Q2dhJ@htCs^~(0j{&plsqXl7}%3&X_&0K9<$bMWx)AC`R z{TR*6mWbz+QsxPDxcIneWdZ^v!|kV+ z^Wj=5lvH_`JV16xt%0WU*}9^z|(#X-xQh&RnG{+*HO0CETv zySVmza^dg1xW|ENGiA;ch7kfJ$VRUhBCr>5I(}15Y=r4H41$|vgnypH1o#Lcbc7_y z#IFr*Jgrr!_Z^wXqYm$t&~-uUI#Ilm{8!UKEC#-a9ohn6rbgP=u z+)c$(h*5&Vqw1qcNIcF2!{-OZR`=Oxw3hj$_>y+vbU`R{67~$OXe%Ys{8}K?+WrWP zBeZt>N|yZrVf2IcBxl$2;@CGU5bl-T@n_QKeX_k)VnVzF>Z_5bShO_C2Cfn|DQgP$ zVOz$#2JF65(pGm#M0I33OIJ|LyKtpf7>S79Jm~&X zy=M0*cFNgvGz{7pykb-vs-kldxz>I?MUv{JGj=MYlL;5_f0=| zYKXh0nDeu#D=R=EK%Y(IUa+KWJFarK!)8Ey{N#j2oBZYy_~l5T!K7UDeD@M;@`Yuj z*ejxL6KE{oNc^5th_nc2&Y~l?&v5q_5k;5oo_uj|Inuc#;ty$`1C7^GFhL-8SdpwC z#GBwd*}3EgpI?XKAF$=$&|%Dgbx4)osVP#8`!=A|yra7zvd}~v<|;4B7c&&*%4k2< zA6=&Qe84rlEMijl#)@j}rt4O1f9?K3{g+Zsm4Kgxk~1q)`*o*2TgP>6u=5uOe4yB7 zVj?le&p~jHKz&2@s3Hhib^Oj)MrU(`M1eg^9te>ees&gvo6Aa?2zg<7lEf?1=`;Po)C z^;ny;?J*cPN0*~&0vbSAGY!Sf%4$J>XwFMl&%F4Hu1b8p?IR5|YkOX+3ZmK|LIi=% zH?iOKgXSEN-Yu9T@Q`U>A@Wvk5faay8#oA|NsJIo+?n6OtfQ5zXT?nn4=@T3N%%G2 z8Q%Bk?uCg`)A1LC^;8L^k%kMJQHE&D%Fs!lXppxb6dk_{R} zC#o%=m)x4AxrODmx%1n`5QXX4$y&%HRe;w%w(H&=25NQa3d^pesz!+aalW}A-x$5- z@KfgdkqJfqySihyFo-^UevVbr_8Ha{WGbrbR1Ni4fE_^;yD$<} zSLf&D`D;3-s2$xWJq4vxy`9c5IkUyItxC8IG(y9TBHD_5SDmL)S->4sAz5ToWbetu zxo9+E#LH6Tm1R&rrddDP(3&HLuC;$`fZne1lsD~jj3tAv1>#w+-=zo$QKo-7hf1d8 zr?PY7NF$Szp^k@xJwSR~kn9D7b*Ylb;zV3-%vs)f`JG{OhnWhA$X@XhnDcAP7Fp%3uP#^Tt0%o(TA zk#kp{Dwyr5qv^Oc3zHXi$rkoN7wI3x>XSvdfvx;%bcDaYF?oxAEPAW~Vc|U0)-jno zFjLh#j8`Kgd05>~Bw)_?KuG>~Sx8N;N29dkCdxx4ayEhZ5Yl|MIZ{u3V<(-@-OV0j z&R-daLeo-V&(IsKbzV3S&t6^hJ{Q#-9rBTPQWWwGT(WtslH5 z7&51W{dwjocAKL>Zr@w(aEQOrjCD2pbbJW;`9sk(ihE4Rn zj%yXTty`(mAbI=b<~gzM(rlgj<0vyeLt$u>`;IyN(^ZB%r0#Yl>>fzVMt|^m^hSks zkWcmP;TUByC!N8x=3^d`YEU`#w@0NJ>6{Mc-bY-MLOK}5zS)tn)PmoW?PA!1!#8B^ z9SCYM2Hj5PO=;8^lDpwxNmdm(t6o6F3TML0VU2Jl?`-k5siE%>)Z%6*)XEOLN1ybL zT}?ZZCXx9f;E2Ug;{@9N2zz2yna{j-X}T=riGL@8M&wrA&M;Wtm$L9X||^M zx|xB$Q*>tde>vK!v6_&?YT8V^lY`2?57QKMqCQg#${LW9C@_1m$I26;9~;d)X@{$J z+#K%9EjM2&eaAp;+teM$)Kl5wAyfx-_~m0Iqdem;qz7)rIa_ixZ{aAAWaGstB)@lUO7=&J_vjk~ zNfxuh3|@l+2@T634?SPg5uM&$S;NCww#F5)1WQAohDev=TrbVe>9<>2Zz>|Jb=!!F|QiexSzJsC3F8R344+Z5N zj;BfXB~HRZ{i4D7z7V3MN!KYZ+l*&=LB%Ta`2(}wltBg}cl_tk4?CRwdG7XNR(8jQ zPxtS{7e|IYc;sFOSO2E&vSAaQaAQJ3CETK9h1j^`v4?d*DOzjzlG0&gX)UZXZQzC; z#JsC$n!5F88Fgws@sDsZuCnwqT0D=%dC_@~w+g&CFM2(|&czV)e4l`I)RNXX$KOO) zi@yzBe~=COboW-2b?@jabHuaNW*#m`NN$aGJbJNh#z@ciKq$b!%Ei^4_SY9ZX(CI< zk;x3P_{?!q*hWH*(YxmrBn62y%!!7spN z{~A&GDtC!istxZflGYE;G)y-xx$=G^bz-w$L8}ty`qJE5v`pVNYEZ0?v=LgztE9qZ zt%Cp(H07fJAEa{0*Zo*!WRB6OQ7YT7*t9>ClvJ_mGFp>>AwJk-s4>ATT0?oLL^8pd znra_g$T-`Rn7_f&;b%#Zc}73y(!473CPr=>o!2%^gps)DDj7w7w#JK$I%>3nZ-<(s z&ryd}XXe=Qm@?hS@)C=^aFO-D_rce!t+^&2LdgYQ4Qk zr%MwhT_N}+Kb<90Q*7c^R;<)11lH7$-#$A{5A&)?1uv^Gy3e`~V_{6#Sz4 z&{6s9Urg(mHG}Zn`z5xnX9@7mg6_C=_VQEYl1iq|l%bZ~Vxn$ilyiG5L2x6PKL0DV z!}V&HksMJYCWg$JEI_Aku<=BFZLs*=aCid? zW;Y~0{WiQU>rRny??{;$Uiy==@$}cj_0=OmS>5Jox>a(k&Lp2_O$4l^zgHYTj z4h<=)xdZ>N2#gHWnvk0nHUocNq!zrlj9tWD#XRbUY?SwSdceJU!j9R?dK>!N?e};b ziD31y(_ZUFZ3*yQmIL5yZl^BD=96OXry$$t?)SU)pA75#w4RxaZJIQ0T9hbLP_QP4 zMNUbNTfDbv+1d7Pe1vy|%v?2})azltu~C&{bGg#V<4y=c>W zwm%66gFw;PFr?Q+Fh}UkQgyEs@Shv?7YKg4M-|Gzje1l7B@gR!WrVeLF_j*MsQ-I> z_!4Af0nGsHT?QW3OOTm)@pUtfe0Pwpw|jy*5ZZ4`bP(DDnT7w}^1n2D>A4`vb-orf z8ULD*k|b8=ZRj9S0Fd2Q;!;0h2W9D&T_^f=5?f_E(lwHN1;)Mv3JBBzTpAI(BthX| z7*-!qX+n)Og0_&n6NDh{`*kA-rnWdhc)wUGieUXol^Vi(TC2J1(X04c2zTn0tw4D1R+GtuA~S?W}3W0CcXNyxaUoefEEZJcxXT{wLZG5ezf@C z-h%<05^qg5z05KU;DG{rX-qA^p~A)9-PNg8@-P4xlQc{-(ZtEbT{+I9=09_b)RbcgBsUUyk{lD@a zUm;_F>O~gh>$3mLxOoK!YIT1YmwEpz{{O^_kX`t9RaP#J#B=^v6GBeqD=<)_M1s9) z;bFPv$A9w=UcuQg0W1+Lppo!89w86m6&R=y+`PnZlM?)8&{dWu5J&)lB#D1X62_qY zi)2G<^Pvn01kwS9f-nLJiR_OiY*=zD)phF-(rh4bZc{39z`Dw>00QwMuTZ2l)_;}u1f{t~~@j1E(2#lFt@?@h2PjxpOMN34bBI^N39{Anrx8Uh}M zD=<)t;r`bXSXTprj`SD2)jGg<1p!kc4FcyS&n3rz80)VvBiyh+AmQ=NM2JrpwxN6&fIkH0CPcipF@UFp#Way#m;b+&3lNMLPhK!$|?V>i|jJY%tAsG}sCJ=FLB&>=K}P2}phlGpS*P zeT4rnR2MI=);b`y=at_;Ey5aB96||G_d@(^7bJw`4}oO+3?L~YeCJET^`#JjK;{e( z7+^u)u>K9$1wPJBzb6wIei0=d_Ih2stn}{%@kbv{T3fHk&E&7h%D4h-DV&0S>_WdXc35s%&|~8am_= zj63Nf1^nQGz3OCuiNbGQcNAfbz7hh|M#5n~+vO1CAoX6jaTEii>Xo`K1Yyla5CV?_ zzWLwQ!{Td*Y5?nLfw9VoKrl;!#pH4S9l@^73)R-Cz*~A4W*1`rO8$27QabnYOU`_w zy>6RqAMw&E;Jr5xS#bnavIUp;;T{A$7m<}8I$+utKq5Z^KA_|h&sB{Fv)8%1p>a~Gk}-7j$LNJ{-pr)L=`|j9|E?v`m#F}v{NE{ zB&m*zU_7X~RDA4u>+e{uw_PCI>Vb|$2J1E<|EqH$?otqxHUfJVhA02*8~)v&T;F*B z6W+C=)d&Kax>?(qb2(d>Sy(%|ayvRdbF^@FwYG54Q~?9C*M(DnTfn9z2f%_r{|5@I BQA7X$ delta 24409 zcmZ6yV~{4%(kle;NpIBuYn4*Tu!Z$Mo1=SSYiR%S zIsMBe29%;af}X;M3k^{9c6BqiS8{Q5GIw#aHh2B^=H)Hy?q+4~;AU-V?B?jgU}|UV z>RP61?}#Fd6o5e$F}`?IWmK28wdSLC679HXgaaKH18S&OeG@rJY z-g^$)PU#g@19&oCg9l9H-Dm48B0e(9WrSkGunH&g! zO7*j<%61h)qVnzT6DXd7&{$JruxORyxSBE3wWnRC^8A^&BO!+8 zYg7uB^}L7dJg_kv7up;Wx%Ip3A9^x76)-ke82=lyDV#6|{|kaGJ^Fbd{|ke7NBY~N;6Ols{=t&@XrNTFmuAqZ#=Oqn#2 zoeOt_)r&()bspRqVg9oSjEM@371%5+8%K;joF-}52?=LT(*&%Wup|I0AQNS+G@g=Y z-knVK=FCUY-Hq&%JXGZ@jhVchR)S&8RyRAzHc2UsDK5AU%cGzeQym(;oufKiya!N2 zBrGFSIhWI&&t|uZqv|I7+-4JQL71*YAWwn&hs+DrB0ZX&dRD0?rX-{~O#{MR{hgIH zdapt&yE7xwz*nmzO>3&vr75?}%$=RXf%>D@W^{|u?Ab&ZSCyAd-+tO`MWjfUIW^Y? zxl+*?3T3O<0WTXH)rJ1H&dSz@&J=)?Rcu}rjxS5q&A`{rHWTyb{lJ^ubUCwtSl1(G z7ApZc#lb>?Y^5t10^TwleOT!w_&CToHd3zY>5!Qf);Mw@6=ZhhFCq~%@Zmgr^O#C( zE%znFRWir?=8g6U>Y(hz=?jKrh*`c`u3avW$4fLDQJI-bhV= zUh)HuK}OP`YCtdXvAkTf;qAvQHI9kjSu=4F%T2uN2wpT4o}=?KNqV(e_^O?m-lCmJ z99))$S$!AjZLYW-D~s5hb9LX1NWgf4r+IJ0+xNTBSj5EVLsD!)k|iKNgE}m<{4?7b zVfZLNSqTH3y?L;{Dgp~;NHZrG>QLo+DX3 z-0!LyWG2R5zBKpjCJ2CDmqyU(Ewns*jeJ_&TUbMLbrnZ|nzSmOQe$2ney|MzTW-+0 ziW<#7gRRf7?%z_W>4STofSA$Wnd;i`cA7S9G?-ppBNHUC#*pO?@tY6fOH1+{bP8$A z;~HpRY>y9$gp4BZED_5?QpDtn)5-p~eISeZjxA0FF}p(-j3U5vS3OnVIn|Tb#-M(? z(?Jp(UUT`LK!v0pC%X~TNnt4NcWx*t;ym+p3EHmL<$F;E@FXkf-QX>C>ii%l6TDzT z5VcxpuwJ;Uiy&%wLt*<>Z|}Y0Tg7vBqU>7{}|(8mYTpf+$?J)wiQfW z*4S1j=XIV4l>vaD{E|oV&+WW1O#`BDmr|MCBMygNKQHMu+iamnAsA6>QhwMZwH-{D zH%0Mmi&6W|jw`-9$`QJ8@y;a{8O1oo;jSxE{W}SPo&kjys{yxS<0A#@2Y0!1OjdW; zex5&Jz!3h~Pe)4zch*3pPkm438JY=bWAZbkS8(z(rkD5kw`hK) z3nECXUr7FIOE4s}s9{3#n9Q6;eg)_=9e0Jrlvm_C2LWYWEXx3KJjPfr;sDAraq2Ns z+HdX;N-CSbWYh*Kmr)weyU6YI15jlc{L{O~t>>PVHoftXWP0J8?qOz(;5C(}?Kbpr zpp?=#S%xMbIgW4rc0D$WWu08tp7v? zqT9jHEXb50k}1&zcgs7bgqd;LakQeN7raY+55dI`YYK3O3}X$>O?dvLdhz5envL1A z@VV_fbD#gnbDh85|NHfU8TkFKJR1mmV3-jV&oK`6p(?cK-ZM%DgN4O`gD*@0$6IK@;HLws0h`i zRc~Zv@!0IN7?I=UFtHHp?Qx^)w4mT{_-%#BRu_5&MOs6oR>o^{0^=Yp#vT^b#yBlW z8G)Hha+cjLuKk4GMCJNWiajhdYGt8A9lcU}NU=Pm?Qy$%9hs_i@SQI;X2ew`0?^QM zXv>()nkKnW*)!`eG?EPqQP$)Nx0T&~={j*UUolg&tSv07ZJG!*N47y;a6u2WuS048 zqGp}MW^io7F?Z=Ei4$TOTw`@&*wz_CcT6R|dv$s=N zf#y~Z7Eo@~Wuc)4#cR{Tqrt@5(md&b$QyqXdbT_SV`*oa-h40PKI|HM5{UrmEv<(S zu9pHlOS_S27=CC1C>7kQGZ7vq5m<1>C#-Xxa0Izq)S2-fxspLfR_1YpwiuQ;o^ z^bf7m^e;Zp_b)&22PiqqjwifJ3%lJQW(BRh4uJHb$Rc32>|Ee6U~1)V)hB{VQZyKC zM^`?w%fbU#OwY<#vE#*e`=$I_+}4|>q{eNZry1SZzYA?MwCu8^`bgM|;YlhFa^-nn zd;R1LzVPS0Li{SFd2^ob0ovqc;R&3B-04X5p{*)$bTGO#SS}9fmPmyDX+EQNHZ^A~ z{jEs_NU&~)Exlzp@h3%|jVynTt%s!}Yru`k#Y6BkB&_injujyr9p_TrUn}r>2Dvu~ zun-bI*sfmIe>aUQ*5!@f- zYi~$71+Dhpp|4SU0FrhAiHKlf!-+SnZM2A{%qBh99x5a&wrLQt|d1`*KPONkLpKr?Z{%oj8{^yn;-(HKlqFkZEtx6=qnIgOw z2<4rqxJY9Zml2UW3^H%X@dO}C^Djs~*68B7xlh0KNUGN_01p&`?GDa6yJNyTqq3cf zvyfcZK|GKj#w!Zb0(%&em^UUnyAqW2Z%@lt6#N8*=7IeXR{Vn~rOq&n%Q1?<3W^O* z@JF0`1eQr6@93gjLq~c?Z)?iEGJu9OSgj?WydG17qy$&bp>6oTkpEpa%eZ#?ftJNUKsw zqu#HI)iwN~QP}FVwIy|=x$Ew!83}hmrF!!R14;daz|^50X$21WodD9msju5kv-}F# z$9ZSHH^a%gpVRU8Xa1WLICpH|h%0)CAa_&+55i%Dx{pXyIsAl5ZnNF231t)z<2<6A zh_6dV388taf~UNbdZWuyE7628F6>7ZpD+cmd{BWSj5#`X_5&%1Jydn}!zoA_wG{8h zA3Zbs@xvRxZ-z6BJc?2FqYh^rUkqy)xmQ>I$%Zq;%-Kh}$YJ)AJ#`$v&%^4+9e=y` z|K&^>PB?Ne%I!@V-2;OVs}%##ah`C}+t#A$3OA=waxJgg4iTSqc9Z;-B%AUrN;>?+ zsydwVlrb(DaaK`I>1vvRMke84#o1=3%nG|njl%?chJKCTDmT6==cz%P?gsuzK`~x{ zN07V6)7D^Q`%x@#eEw=~tIg)e8lL7qM0LrA?7hPT|5hA4ruJ4Fyif>8tJwNM9$p!j znTGA=ucJqEe_%YYWZzD7Z^U-VfGfVmYq}O$^GxeAx22`+&dXcaDGQKjlatShM=15^ z#&GaBAG&-h}@~Af-!DxaS-0?U~{xCbb ztC!BGD%P?1o`d$N;(c^9INhVndNCFi_Gqs|&3o1P7o4D_L^DkQaSiNyS+rr)vu$tj z!Ay^3=f=A{+JHKMIjBOrJ4NA@OmRG?ma ztd0)&Wr*JDC7FJ~z-xr&QeJ}z_F`M?1l$07)Epe*o>^E}*UhWVqHn6b|7!VJs7`a5 zQhq{_A+=L#T%WpAs+cFxq;D)iwKKf0wlf|cJs>M_8`A?g-GZn(g71}f0n1CkY#Fgw z&)~eKBB*?iKsyKdk#>M#ogx57u(v@j6921#20P(0XG?bWQKqx6b0<|_qL3$ zKQ;Dk*K7k+TSAg^s7Tl8b3#?`gLsk*@T*Roz6Bl@{#siE98Y>DoRXOG_4!J8-h?w> zump(0m35bqOUTfA&z5yLU0w!?;Y>pAkyPalxKgFnz;fu{nOEM*F>bEWd)xMbaXhdF z9X$QHe0OKohU9(f9;c?adp#Vk((@m9v;n(bGNc1oi&u>8Sp+v+*DhxtUrq<0P};?y zIjwsDv&0;K?eR=G_DHlIeS5*x(rCg@IQctlcV`HCXiPERHZ8h*5osnkX>dg>qf<5b zR1+Ryox!;gjoh^a$W%9k0>imrfQkkMQC3oSJ2NNF^~Q%+r}>A1lUbi|y%sLFxNWQ> z&_w~r8o@@G>_0gE$|@?)bu4GG;Lt+FmHW~_C6xKHqKnOiaUkWT449x_ZwmR<2UFCq zu)I>dI$OazQ<(z5XU~Rmx*K~bPrpJhnPo|x|;~C z@r=_U7emx7G(~!@L-t^9I2fE0^nS@sG+_YX`P@+zRcwB1rzmcj%FPnJNM;5z-CU1S zl|iw8?0`>p{sM^PCX_`rHpPmf;}OW>K@S9#xW;8wbt&Dg@VC~lr{1#3dW?yty(&5u z@thJX@k!saE3pPSBdUk=S_bX+@KI8JqS(LPFHohd39K#aN{mCq&xS>w@S~IED5e6O z{rufP^;y%s^ZjeuNyElX*_^O z3D4t?EHxAv^QWA@Xv~VdiS6^7(R$r=r;;D|9kE^HA=ejd{{A3Ev`G|@Ef?UBY*^O{ z@y;cRzYN3Q65$E;Qv%-Q1>oO3X^lsuwfUh<{r!I?rd)gNySOR7g7g4bKeUrJLCT)Z zO}b%Pq@gKWF%s_xP8=k7>BJ4B#Cs;9OEb^p*lW_;xn!|Aofw(bekI*SjoJhgdUa*# z1*#1j|Mu6Swzjra4O_>no4&rc*S+menm2%j2WO5M(h=jpx5cS%?%ChmJ3m8%@6Tn) zC@W$BQlY?H#u#NBNgTj1I~arw*m`WT0;iCLpy3`2@gZj(oN5r&+%B0d1ypZBwKNOo zfFvKIkhfDS`vL|Mlr7D2;wuYg{_*J=Xx>=RtT~Y)0f^}4XCkfj&TWSZYJG%k4IgCNyLoU=f+#bq! z|4{kpl*^_GdjSBoIrj_|0ok8<2u6I{y?2TJzM(r{;1I8w{_a)HM@FWocA^u)ZzRip z=ej+mEtRIqg~a&`EpL+xo6WfDc?M)h5?s|K%qj+{2sl1Rr-`$lcF5e^=kpXb+p~j57FfH44 zM9ITnI3F(6>FT>rmLl6HK%I0sz;gVa`&GOgyQf5!#^#A_I}@pl34=ZPn}@%AyLq)! z*^2KFBoXjb$7WjI7JZStl(2Yp%+B4lVXm9m`|{BEi>+xh7Ms4xswZiq)KrNN3GpI{ z-e)NF&aGnok|jlsRi8V?kpx+a5Q-dbmj!30bkwcA20}NF5^u}cqa3B%e9V+~3a=vP z0jDXdlO&aQPR(Pgv``uA8r8yArk|6NL=F}2kr^;tz1iCYts3o8(duy#lg?l(FP=pq zDfSYcD;(A!>iIO`K+#5x0eTmSP%^|Mt6=l&T@opTBo1cHgsP1lAy>H7Y{%Mjf?E!5 z7we#%uJ+D)j{+fD1y!3z`gdpPP;F`_cyihNnWZVp5EXCRIAuPDM8tx7IYojhjj66b z+Z_-{;k0Swfg>%e;Q<8^-OQJ5vP%u?2G1X@9IoznQx%UO9I*Gg3f2EA5GdT?Ru01C zPG*!XAXB~iO0$r;X|3hnbdN!5?D2fEWn;>RAVrT=v~TqyL0rYPz6vkFlFHPBkLWXO z0dgtlqG80RvTV2Iqku~Q?7y2uwIDI0&ke}krqS-1PU6RpebJY&vQ+VFX?1IDrCfz@ zt`@^*91QoFkhwtKUIw4^qCsY{^Gaa#>4{4;54}MKj0B;nE$LaMlARJ;v?sLY zaGH!TC0Z6!Te0(+cBjc;mFP1im&Zt#Q^MCYVHP%zMB~Pat;kV;Pej%xt+Yc;n*lIL z&pjKBLpgDLHl&lJY4H@bwTZTB*GEv5cbl)F_n=g8&fPjl`OmQ2MM)SYJ=~{axl$X8 z7E>2d%|@`opI$Z(bd$h?`iiL`(xcQkV+(l656FVs9;nV^Jq0e07b^)wQ>(O)6>Fz` zR^=SnO*hI5+bUY^aM5BZb=|bJkpLVd;ib!=F=b53(&g7TppQq>gqOi4(4Y(X&AaUP z!y;6I=sioJN3MFN)a%F}5;HQ-=KjXp*s`{XDLCe0$!K^|_&M!>2qR|x5ww_~}{k3N`EElSzqqO}+>v~RO4!&B~`(`#JVA~B&0Q%$H- zbwMnx7zMd&(yp@MYbZtP>=-)R;TVi*qnPAbBL0#k_h;<1`XXC(pHKqz3yFS$ z*l{(dsp9$S)*J_=3!gu=Lp|#KL5^x46niE2XTOgv(i{(U)qnW*qi;S&WM)BYrO*9^ zPZ%FF3dQS_q0ZOZ+|?^p(SDkzd_OqAJZ?jCoxqWOgb%mX`$Gnb9|4*l3TbZ-`V$Pd z<{?9#HDByM-BWM(0cm$9j#91#Q*SXSh?QSN-*C9U7LMYoGR-C8?1n75ul;l>={qzQ zPBX~PY!5q=R!p2_MTKoHEaa9={WH@C18_6&H0s#PISpr{ZSkdkXD`P1^DI@+&&*xb zNMtdO0Q^5s&NV)A`~X65uZdIpN8fV13RGg*o+xxjI?pN6g^$Bu_G!1ondd|R$rDK< z^xI}ydRDUvefqtkuE~X>Vd2ANSrhUmj=>do1+}~7?q~h*H3ux_*`<=*W=mWC@>O+x zlor`%F}0R%H`utMSx@@GGuz!LE({%i9J*b~Uc5&2vR@i`1b~RG?3KFgtWal^Vp2TR zbn@){K&y*Th}ezmrRlbKF}~`H>@+FzQ3C~db^2)QuF>-F=4uUE>GTnnEajAFrSRO_ z+ELMq$NDu7vow7=7KWsSPcr{%J|RKX z4=BO1$K^*3C_uqtPVjG`9&B02qQNt4y9LT>=kQ+)ssU#a-Nnu;#Q7((8P#8OcgQr7 z>H=f))L;5(1C&N|1o*br(K_As*!?Z=?2qsxB!F3sxQzij?ZITj`GJpUNk7} zqWs<{;7z)Ykyt(|PUVwbS|%t8k|SC891ylT zucQAYO$nYBRDP)jfSU3vkXJtDz!eOP3QA&M>ikqb6aN)C>`?zHD?HZxQW#q*nPN)= zz!N?>f&qA0C3#H08~@n*ILOT5qX_oWI(RFV2`);){M9(we|Vu8nD-><$oJ9}`pCIR zN|c=Ku)2mkiM6zUJ9&pYX-=&wlb3(Y1w?CtQQumXx?f2Utll!-%}}8`l}C%WcV? zarFWc&dJ~9EY=3ls6GeH%3~AV+m?DkXt{X8O%Q9s?UW_-!ba8VV9Ic9`3 zpzn(z^dxUEMxdqGJaY=p7vbF9C$uD*vH*z&Yu}9ruKw#4O(IQJ5UbDA61$bWX)e~y zMCuKqV1jVgd9IH%Of9)!g%=+>0#RyA=hSSCOBZOPGuc7`V_lZ@L{U$|6iN4~vC|AbOvMODMDvl0|zh_CTjYD(5vkwGh3TQZ@ zd@g65k4j3ySbbN#3P=j_uP<6l&I4EU%8hL6V8f2)(Rz?bD*8Sy`h3Q060uY)tU#;` z7SB2t^GUcC92V%Ai(MCw;+{MmO@Lp-oblbYRg-PM8e=L^56!D_>8k=sotbR{5LtSJ zD^v;7kd+0%FJ!u-Gwa7R3H-BRPQ2pYM7V9Y6?2nsDC1|m6n^dCUQo39-n-*dc|GK6 z3J8IpgrHf`;&Emf5D>jE#W6N;$;o)17(b?I;+I25*dY{%`r>>n%#F&#l>?Tr6O9RG z%Tapsq&ZDXcZ#Gt7V5-ab0$V!bEMKN2cq-{o&)mx0|}B&p}?hnxvGl`%Iif~sK|b# zumLls*!BrK3Zk!_PMgZqA$}i+^b2QiX%IYvKhe@wLsV84l=DlDCd4JS!11JqxR9IG z6=SQxPhxJk)Vp_@pXiAjmI4@si~V?V`sI9u$%SOkIX2cq+D_bfccEu3gy#L-FfC@w zvfMcL{GQosUrkQdn;_2AB?%ow41q{F(G&zlez}LZ>FACE&00OsJt^di&A;0|#q@fA zT3e_y4HQ@~JYjg>asOmY8TD5{$;|OMeT7T~Wb4DypS8j#YSeh8e*j{>g!dOet$vyD z5Im!QU>drHDm2hTVLUJE-Sx3CRLbcrC|cUV(=4$&UO5O7a{c%yT~s6!x6~F*T6KtA z$5@fH0pn~R4BfCrFqD6^f`)g8&XM4sLM?uvmB*Pan6C&^xG$kweJ*&?i`$`k2>qIF z{9}Ms706In_6*;h@C7J#TdU<<_+~;Ofv6!Ugd`pn+Y;X|1!b}&CchxAy$_ZM?H5NJ zuB{n-+4g!m?21NM@;oTl0~HiWC^Z`cpiPo_t%#_RK=92kzT}euok#&Kq+OsOaz!bW2L=fAROb7*2JR)VwvIwT zY&$0WBJtMhN=H~cE_?%5U|LPtL&Z9Xko6gAmukd!}CV{I@@$q+;&c` zSs}vvP4$AxN4|CmVa?s>mk#!}`Dw*W&_=H=nc>Yp{TPD;|CXeoo;pyuhWY%m9PMAK z0pYK>IK{Ws%}Xd1(BU${*Aq3|^4`}p~UBs(>*qGf2`7=O(@0f|0KmAbv`&ujMkT25NI&r_F zu-u*G2wx-VywfWY&VVB0(B-=Er+1VA z#5g}&i~%s(_d$B0<^{V~SGva%;(5el4P2P%=2r zoDI<`4{0M7VvP{S=9osYpzEP^G8yKbJs*;&(%98I6dz&37wev{-a^v^oonDQT;sAv za$p)|g6+%o`0t1s5b3!K6io%))c>?^|>c?GQ~Jy0Lo zqECJxbORNMAbEjb%a!6A1_3E4!*aGQH^4rJR?*>exo1!OfK0g2Gm*z^;L0s=mHc8e z_1W&|#3h{AgU2c1y2NC$R7Ie8p{&3}=mIwc4RWMsJ3J140Ub0|2pH*EMw8_nmk1zx zkk2D@jDJuOJk6&-qJ+Gj4-qf9QOh z4cZ5?=!5TE3z~ku=q-_XAF^yeWQ~wY;V+O3K*-^n)XG1U=ZaAHf>aF&Fh^13yztwI9B86f=WsPx)EW&VSDva?Tg z2`~==D%M7pEnEZE9YLXb?eFWDZSu53G*XfKYj(C=8=3O;JYRSlxe>`tzz0Zsgt4>w zDN-4+M9np3UCE_>X0#56`L^AnfDucbW$Wn(d^fz@USaiMN&O)_xv4w*-PqES^LcSM?CE;XJ@@-^j?a^W@NTZhWe2*J$u&QR zc_Z%?I#$@5j_+wBhDYepfnfKScqOb;ZS&dUU(o;7t9A$FqDKA+_-WApzs#RRjStY> z)ta{@uj8C6h~!7XHi4r|0pHcICyjcH_*5KFJ5Lp@Jtu>QpTvz5`$ z!5H4)C%^y6_-6&M=uvML!uhz0=$Vzp>pC~nXZrmy@5l(;8$@XS?yM=nXhGJ?XsjVY zdup~b+#CB(J}{OJhh)Z{);b*i_5tu6BuBhxr6FZ7k(D6ZqO6(Q8L19IzDPN7CcHO} zMvT9_D~(CMYYkDpD1Er-N?<0VsDSvXW*2QPgYXsIhXXl}$uR@RG0z&&cFl0KtgYDR zk^N+5slC9jF1_~?q21`y(-Psa*156SS5fDQ7wBC62(%P?$}i7twQ6gylL8!S(r0U> zf%oVPVa^flM_*S!*{f?LcyK!CXQdRoQD$T3ta)l?;o)V`d;m3T>jXUUuJkr8KKrf2rp zsUz~pAuzTVwU^?%d7AAQ6#+Yo0nFZYX$}?P-f?~~NXX_8WbpY1{)pF*z+ZnqTF>KhL1~45x6Qt&)3~Wp z>G|wpQcTmfDK*BbYmZGd*O9D`>U5N6;R{Y}(W(A4MibB|ay2AxBC5q;tKB&@L86iB zJ4JH_CwJH1=*X2>dQxY#be0_Tx6#$oo*cBy@rpJ;tpf^|yYGJ>MOJ4kqK$%d zn#gzRoh@oE<=)lfj;F+VY__tUjRcB#{c6}^efOt_(8LaHk^=jqIaESQQ=q!@_2zMn zsq|K5$w>N&A0hPee_AMZ)u>BYu~*9@gb226%3hI?cbMr}s)@8n6WF2h#p^HpK-bUz zt|9(B2_4YKjRCG&W0vR^uJL)o?}IVi&r251pm-M)OO9}ifEt$N5>Sbok0Sn3@J5}^ z7A2Xk)a4GPAORvW?_*C7XNNHbgHk2GnQ{J^XK^1sE#hx-g*y^kLO>|{ef@mXT|r^ z1Ax3@T8qO09WuY*Thr$+XN@2a7+4t8(p@U#Ug0a}DgFu`dE*c;&)^9!=d8Il!e z-0ywzx(I>5`eS~bD7-ywne<$-``~?#qn!`L|1Nh@YaL|E|GY}1SU^Bj|DUS~9gwE! zZG@+e{a3zu(t%?+)FHW@nwpN>erZ;GdN-X^S`}wv)ov-fcy02QO?%@ybaRsc3Wkb` zrWlU|O-#J5L-+zBGO!q96!-2^;9&^V@&0XAj-zGU9v*O=^Vav)clYi;?)&-M6;Tkz zs1N1wm_A?!c~Ccm>Q`RC4!RgN0sx}uz(KofJfQ>Jse#ZD@d)3`5(&MNF;WKa`B-0A z?bryMd!K&T*Nls7*!Sf5)68q2(T^w)aq3O9`O!GbpDYmjV~=rI67)UOk1+9Om}MG* ziFO~-+>0tO;)IuSc>LRoEKqVZdOY+SRX8_ZF*+|{`dXFc(+#KqnK1tS0T6nRmz4Ku z_Npx?fH|_sg*BM+`0WQ|2z3wx>_;8Q2zj3d)K4?5`0>z2)E#K6E^PVpC z(N4GzU5vSpPQDL&^!Xl?`8rG#5K0L7fcnK2SU_F5zl4jAeS3q0e}Rv;!OyeP;cMmP zRB7pDCMR5Z$70{Gmb9XR3cy#MmMf*;pw?QlFI`GcZ}za7jIB2^&kwAhvr3Q@`8icO zo8Xu~jqaU2Z=c)6o#V6hbZa&I^{C$`+EYRrGRz-_!p{qS6uex{T$iIEqDZTCnwj5( zG8tdA!!z5UbFh^ywe&+1!D{RVMUQa1JcH(sKclsbO-nf?QRM2Q15g}obIxhvvnYKf zqd=>@Vx5VKhgOZDe2Mojm!N1dt$4a=_x>k^k;kGTu3j_C;kacHw6v4S#fN(f{TiM9 z&_L7l=Huu8e9;OYsvKIkAGy$SHzX-TG%Fu-cNL45<%`Qq-zmz1F4`3jnr#awE^@CQ zECR`$;-pl8zVZV<1E9CHAzheyHHrp_7`x}+u&eGT zOR@5fvyzyt$Wd_>dFPc7mFeP_4ELebwk9!~!af*mK%c@%eLlF3?XR?T(l24Lr_3Dk z`NSxc{TuQf^Rh$K2M5Yqr~+wxgLR3LOpq_P z)D%$XpUX2#<{l{o1AbL1o~L6X2LDRyrBhp2`VvuDYuHXi?UBu4nquQXaqoesi$yajZ-YGp=Ag)g^o@gOo{T}1=rOt8Ho3kh#mtn=mT6W|Z zADg4j2cWgPOpCgpDkS$%5S-Z+WnW{lDxcrX{cMG$vH;XKHHULi*Q}k=;NFzhuBIPs{K2}7D07uRH zcjx8kJJ6Uoa{Tx+hPcN3Vj36rE(y-I@En*pdOcTqdl=g)ub;W#Hu;;wBx(M-h&S@& z$@@+Mju&cEM)Y!c(p3TinM*idt`raX6o)C>I)8Z8XY0{3%_*=x7xWI8pXt7r%Ptcv z1;A(n4RF~|1DJZ53d}t^(^qsgjch8-fJz+=C6z3?8v>SbPuBvTEsI}Qz@TzY{+g+Y zLoNi`Y_9jURtTj}3XxBs((PNRvHUFr-g9lf&$Z`@U(AWP>K2`8m;SYN&Q6-lpaQ;?WrpF5Svcb@%b7&7Q0y6gxI|qw3&Zmpw7)*537=?S)F}Vxj z(_%(*?ljrkny%Nc^^=Vfh5k{mKx4WVxTC2~o7WgM;q77v*N(-BZ)@zQuBH$0GQfzk zqbnhM;SeDV4V1V0r>4P+RtYl7?K!&yJ@Ccsv7iK z3c$Ik^X-mm%OUY|%Y~mqIPI@%BNaD2eX|edY0r++^g})u**E$1f`sdq3(Th5>RS2*H+9sS1xODw3b(V@t@aZ z+osNMi&SlG2u-(ep>9eKgTM%Y23;LSdTHrArgKa&4L|A2Fwm2C9NmZdzOpR79u;`J zGk2G|2Y3T8IaX?1#QUb^1TVr&M&rEKUaE+iBS zxEP3w!rC0n0vM=5?$!w_@Ud#JyPrJ3}!K927G*I|IWZrlxc0W4ZKk_4F*hsEV-hwA98M5?5<@KBBiT zD@L40MJyf22_M&&ZVeS6c(<=aTZzlN2bUUFCHMP3g8xm1Lvc!Ln*Whu5%7O(nC3rh zczyyR#eV`Bkf#Odg*J}<1F5jJY@^NvT{@f!3cHdx7Z}$xh@_1|l!#Pl5oF(+i*a#( zp|NwLe3Widu@SW_)s(`;uW*Ex%SE{vEq{$hso?2zH+#{?`(f$n&a+D|V%U#3d(-Rv z-RFG=uPE?zUjnoi6Gzj2$bgP}&(hVa$Izg3FM|#aK+y}-q~jXx ze0zWJ0e4hVrf8bFm+SW* zCPyev4l~M1{Nu>zljk)7e)7N8%ji8PPjCZv7ytto;~i>#xckcY`hxwGQ3DM}kncr$ z0i{Rd?@`eON0u-F^~Hz-NR?aOg>8K`$4&r%<9N_;k54x*!rS%L)%7MV(m79g!NM>z zzW~Z@8_MGQ1V&IY2Wx)ob0>S@xJtMXRYGrHNkdypO$q;S#gO>s{{H4-Mi4^|_$(>S z4o;k8ojnNb)g>gDpZzIQS(>}}@z~zV1~Tbl1z{AniZpBSIK)5Y_g24f9w&|he9Z%p z6_s=^B|;dFeV796@3$ir>bbQoY`I=1dCWG&Ic69{?jG?d-(GeVh5T4^&xN6+KiBgT zDpVDM%s$Lp%p6itb_|^op|K(*6B(GyBwxcXvoqw(pTzdw#G^YeMTg_SIzION-pJ-r z5GN`jj?5Ym9831gybG+aA|;)!PEPv>YBkd)&pZ~CJDLfTHHYSyctni^HhXK0Xr$x9A#h(7U0B@?&dpQ z70GO`5GKHvdj%Sl=H61m>`Mt+!()OMr}?{Z{cOx7sYbp%_OY9fP1s->-)INWIP?w4 z~ssNE{!G4nih#wCw+wV zCv)&9)*2^A~ z7j~Nt+@=#^-3IFyJ&wbeJWf2|kV2ybR-X)hU}V`%y`C4F{U?pLRG!*|BaVfd?F(i= z;~p1&VDv81Rj=^Ci^gWb68CQcZRO$Hdq}w8ks{1b?IGhkZHmpQ$jwqp$?l)d@PWfS zoPWQ?+DIKCz*juj!DHO2-7~`f9Rq-+`*b#_uAOMbV$K~m?($(LWc^_R*ao_~s0=?j z=R6;=w(N4qHz{^V=%z@U#-g(mKm1IGF$mQ`KC0JROUK}17Z}KP#G#AO8C9ECK+d{T zl*y|vTDfm&zxh)bAyu4dc`VEj%5a3m=~hR?RSfr)v#X-a zO}Yl#Irs@U>lPY?J!6@XMUpb}<>HrHib^aSD4TMj#r+e7T1YP0loM>V^ZBP>N!KmP zr8u`mmj#?p*kw&|(H1MC+>4{ul@sl?P%{oV#A`F&jxl%+dq>4o920MdF<2E}#~mY| zaGvEj z%LFVLBPvr@X=)4#889=(>ih4hgw4G%AzgWURO>u^6_DBi7j+{Eh|}K7Jvk?ebS*ct z7d{_N_XOgYCkWJ``ka)e4I0e52z-i`>H)feCL0>WGBc|^Zg(uEc`kxShm=Q77GWIwvU$FB;$QW&648PS4 zuNh?``(@~r&|am%ow#TI!|qwE%c^_+nTpPXoUp)b#Qlf&yhi^}2q7tQR93S&+e}_b z3uw~z)1wV<$Z$UA?q9y$u}r%cwQrGLR(zM6FQyNolHr}dOZPZVo^w9C9$BaGRnB8Y zw?^LpAPr;CcHn#B$bWunvHt8HaH^vKD%%DZ0F$lZ(05U-KQ(oz--w7KNwBss1uoZkE1QjQRpg#wQ#agGEC|l*$mpROKwfSx}yVC z%yP&21k1WpVxeOPa)h(z8<04v;7Y$uWUkfKU^1$Iq~88b+MY4LpSJ4azOY2~gTC_7 zF9G5g+N7&LluUmn?p@g+i7}$B=r@v#^E+`N62#oeNGSN-)6R`-D4fv3>!Gk2migFpFnKYTplALbr7NS^G(skN zhdS`JlyT~7LJPmw$%fiX7Q4<>5yH1EJMjp16uG{pO5f4UDG7p-+cz8cB`iGHOlVD4 z`)DB>@7Wf@-_$5SL;>0lxZQ|1JLb4FzK^5s8Y<1iUGlpz01X-{)4W4i{-em7t9 zdn$>EjTK|xLTARMqoGSjV?2kCoCvuzvo1@-yAQ7DZFy5RJ5#;S|NFFvVc{Kvs}69^V(ObWo|17v1_luwanpT zZ1EzQx+F?oVd& zM~fnxnbmtz|H&VYd=K$vX;(f8rEl~y=?+i|G44K6l4XN--p|Ijqr7Q%pMy4>UXjF3 zI8Z0}3mBgNQb~k;Pc9gyU6#u z#7_t=gAP2;#ozxrN*5N&@ zo%GmQaH>f!JYy*chiyh0LFak>-F4nNf*=T&;{{<7bna_@fV&5TckwV=gyi5 z1nb7=1~3~SzAi9Tw+h=yZRj|84<3GIw9~s6sNC<3*yp1+PI_9Zhe`A`YgI79?OY7E z*c9-(?Qon$ow5U;!L6#27p+3jDna8@9qkA6wtl=8EOf&E`3EB;8i=N-KdN=f*anAx z@$M1zc?HEhDza-*)>eq4O5FnqZ#;H_2`Ig`+~n?xAOel}h2EVcHV$moF<)|sTtY_{6-?b1v6%Z109J)6f@dzQM_Z}TZs0AGfC>L|gQP-Q}(N;1Sz~btif=8Wsq8 zeW+cN<`&J+mX{}scg2-O|91)aE{s)sSTfe%?#FFxG`he@jYlJ5iORuhDmgm=L7Wo% z3cNS&gbsX=1{+dH&4?>au?|m{Jd%Tr>uz{6 ztdkHw>}LO1R8;wpypKg*am>-p<4F^#^_;m|P?w>ZrFT;DkwyOcwcbtN``<&S;WKkH zb1@*L>BSVY%h*YcL3sSncj+Vb2;K0^cqua#B$FI76kN7QI9NxkaAm;eQmnBd`1zL5 zcT4v@a#%k|u4rAo80>LCS->@6zDXhX_$wVhgiq$)bhd~iv2rl|!0JVCze!M3JdLv= zr1UBL=_g2c;ftq4T>WplAl3CJzJem(7&vBm9J67p1; zjhHFyjcLJ&5sx|N(;Rj_JEOevYO(Z#$;U0KiHhdEIV24}DJ}TD>D17h zLuwy36pcR1mVFP;IF|5VzIa$7iD!VnMDF7Cq>=fKGuBbjYHty2I@huzAvi;px?p>? zB8z%^L9HN4(noIBRv=cOFSSXCK&xJFT~|+QnI+6nvebXs($>kQ8EQiwMUk^_|6PS7g2w7IuVhv1TcyOOqL;519`MCgiwRg0V5L!%{TO+O3*@3L-OX zja!-1#5neOB*T!@R)LAWd6P>{kNS#nqMbS;Jpl%W1~~=>`;V<0Sw;fr4-Kfh#?jDo z4)kI6)lDKYBvK$M6L3NSsp~EWQDV)+`2_zt(GEVs8#-Q9GBbP&gRTztga&Rc#h=_? zvKZ!B(wN65*SdlcD~1NLW@Sd^C$uND6PsxZ+muFw0$&~;{kOP!>)Gb5&4DE7GbnDm z|A{`t?rtTv)abkb1JoR{3idQy^U{Rp5aIS1rnh7B%14sNY;f9++~H;ERDpzO!g~xw zSE+fQ?yJ#|uj<==87E@V1-4g=M4#y&1cfNPqCMamm1W4;={Xl-`pS5}KDL7Q`00b8 z<6w-%5v8UGkD@@VC4&S0bc0HL%~t-{bgD#sqD+H*X51yieW)DnlJPz^?)ARo><8n= zk75fUvYe|uRBQ?{sRcIi_I^D=OoYq_)DK5>sNjcOBOOy+G;GeaaqkG79avja9 zBduSn7m}LINV-F3-i6nc6;{F9bILZ~dRfn#GbJLrgN!5xO*SH1tyKx)StsM)VVf>! zl6T*bu`6icw|MCWuVnW3^>Tp^*55iGt5mVNYUVSg-e`aRSf%XTf<&ByrX5F{pvyG= z;Y=xQLefOFNMi=&$3(>5utQ3Z)+eUDm!P52{6e}TQndkS`as^t+84qdi-Zh0oz;oF zJgU=F{CExwyX}VtrRSXC40HoGNd+`bujjjVW$(df%aqt5lJ0Q68TZo3q^w<;Oi^#! z(MJc=ghK=Fb_Fie1(^`pAepAleMvFKj}L9g3*)HX4xYG%xoh8kFC*KTFnH4Wn7z}Z zf5ym6Ny2#@dRJF(jc_P-ytfJnR zK~7Ul%UHN2`$CKY;@+);xk}^wCvzXE-|j!ueHBIDrAb&`!KD-8cGlGYV&6Q-7->*ehd&c-j+){s<>`_$(P5$0&Lc3kwSakGkjyG+U=N!mf|!TJxywN<<+L;rJC{nszvUbl*{m9x1;sN zIQ?7S+1!lbgrhk_u{F{X9Z&Di;CFnh{G=YMyfn}XY(bmn)eI$k9*rzAwE~61AI*~* ziya@3*h-H@nXilz5}N1w@!GYxOz74v8gc9s8Jd5Qt~f3&dbqqRws9DeOzUT7*ikaG zIfE$v(sPTebJA^>XE^PNM^Jt*F72)c#I@&Q*}Y`a$Le{Jw_RS;8t@o7TWB24nlVPd zt}so4BGkV&_i5&s@|aPHmwM{zBWAZMDpQ0c^x>yLpbtcs=Cg~2(bt5z z>YCrBgmR7Kl!>grZEj|gQ0T)hc|12_$(Y)yhpLvQiaM*xZSR0s{gkU~S>{O`Mu+Ry z(gtg~Z8>h^wYr0^+w=DnwU}Ba#M%y5(fYF31Y~eBu~?bXZ7qn*_a3eKw=OJ3V~ATz z;7PHpG$HPpOSP3bx8mCzgf2_+$qQLt-ui^OI};P?+9T=HFf|eudPJ4Cn_0c@ca(We z5gM>$ddx$Veo(s5;g22RD&cixA6bbT=h(xbTtmiO54*T`~nQM$K0SzdyHm|D@qj@k&90jf^wN7k4wc-NGt$oeUv^dc&tNh9eR(>bC0m=wMSh*e3ccC@&H?pFc&%Q2d!X;Q6Gqv(uZ24wf}mz-nrOX0hG5{$`gjP<{A^SQs^q7t zC*h(3v&{z3FNEUyX27-Xn4S8Wma5cfI0Q~$Xn(ki@$JUpS6dENHpXtKro6Do?Eo7Z ze_KxfN7Q@V>P-#8y`vNjo{W!{x*`{$4{?IBVbn*DNXZ(G*=CLj^uK^@B-QL?uer-A ziaz7Q5YSnbg;ePI->sSg-?~#(M;L{{uM%IfPiOC$a1xt1ax@mNDX>@gOc5tAy@_1p z0mb^geffSvOe6Y@5=cr#vi0X>etE@*31rNHgseA9!!mcY208j#pW=kPd2GU`3eBpV zN>%+HSf6IldSSlHpJRT&AHljyeS>XYpwT+;im&%^vi&;5^}47|347-LQ8#tCDqALj za>PL-)1y_we2Gj)vd=Hr-BvdDA8z(P-`i&zMjW(gDq!);E(Wvs5tBD0^=z-QD?KQ9 z5S~QF^k$1mzvH@`-}vGyR~xL2mKY75a*rs^%Vn`|z~8ZI#FUDZQx@exnWaxGSwYQ` znpgSNW8x8e7*A=w&MLG|e#b(;#b~FOhwA_WmfFC{i2?KM<8sv>9=gsmq-Ln9rZq4) zz}Gi8va0uaXhh#rP0L77*g#OYN?5d7SV-5?pHx>+SWtAG7u-Lhu62p0R!cx#vrlbO zYq)QXmv`u5-=NmZbu5&3yomrE%=0tJkYT_X6D9@*9R@66oB=8p-{RgSND>*)s!pl! z7%Jk}XOz`DmgAzJQIp%ANTMeG_4s94lhZ+ZF+qW{2iQq*UDd(slP!0BuDWS0&YliQ@6@ zaF|GCUZ8{iAtz>HP#B)e#v$~IPz^b4pGua_L7R#$J=te}+# z8e=h0?KcwX2vEy5&2-qP<~nu&;t1|DO9bLcC546O(!e~+iK~a4a&S?iPIt8! zDFiyK02>2C5_aIv45NVIS1b8+qPY#o9mYp>L*dttRv+Y%hZeTX?QAMk`wIIlwE982 z8MLxeUp+7zRZ_2elj43cq6`lhBB6VzaH2!e^jUfJE z4*W$TT7M!Oylrl}LjG?ctgwp|4+(gO6h;EU5M4MIP(pHyxM`mYP)!0SCvg5cYQmW? ze`w+M(4g9*tL6T(2DbddF|dAV=TiKwoeP_N#f(a_xozT83}EMglj9EuK;n-Bknj+r z1PIIQrNgNSx5hnVAL(U9B|_It{liofj!$rA?@7hR_jgXwmG@9V%w%UEK!*<&Yfb$R z5M7K975IhX44A>p4AX(*qJ?kKgAAV!bY@T?K=KbOx_A{TYa1QVWBjP?Jrn7m#=zL- z!N6eqV+tgWjty`C0=8W7V1m8Gs2KD&5dMH_!o5V!f?9^7LK^AXLnsjpDxt08IO5JIG*Ja9mW3Gn;cl4R9SaLS6^|xB=Sk zV+iDl19q974wmo=4TJuz{ST-nJk*GPU>Q0)fbE2z2Xc-&+rT>derF14i?(L{G6g5qxbpScu&fkQw`*6Za21roj)VoT^Tm*PN0`}--(a0zuXZE*C7%5N_amILQ z6v;^C2w3pId`1~j8Q*e}eoz5AMFU_JLSy{q413|k1d|*(*b8>@k5-bWPcV`3_ug^&ruT&x&=fA zMw^Go(9qV|KlVVTw{HUur2he!2fC*`22|&IDF62GE&>M4;`m<%7Sd0Qf|zRvkTPOm z98075>39HR@smaNC~8B5U5#tifvn&FcL~TP@Yhijo)ml5@L~_He><(^EH#SpfCUHa zzoFk(1Y`tO-^cs&>TT_W>_hZx=a6fuWhgNNddeU z14Ba&jV-d`%sY8K=-(tH3?eB5s?J6tSpfOpsZ|^WZEOME5CX)AJ|Cv4&k~V~U_%>l zA!nnPu^yH(d=9YveM%C!VXa)6h9;HCmS6W5y0?BCeq!Y+*bzNjLz g@%w%UXe%pPd*|9 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730b4..e95643d6a2 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line From beee79c2d0d5110550fc67e8a6ee3788b97ea51e Mon Sep 17 00:00:00 2001 From: Ela Malani Date: Fri, 3 Mar 2017 11:57:29 -0800 Subject: [PATCH 109/142] Renaming Start() API to start() --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97b0fa7ecb..80cf20a6b2 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ You create your own Crashes listener and assign it like this: ## 6. Distribute APIs -You can easily let your users get the latest version of your app by integrating `Distribute` service of Mobile Center SDK. All you need to do is pass the service name as a parameter in the `Start()` API call. Once the activity is created, the SDK checks for new updates in the background. If it finds a new update, users will see a dialog with three options - `Download`,`Postpone` and `Ignore`. If the user presses `Download`, it will trigger the new version to be installed. Postpone will delay the download until the app is opened again. Ignore will not prompt the user again for that particular app version. +You can easily let your users get the latest version of your app by integrating `Distribute` service of Mobile Center SDK. All you need to do is pass the service name as a parameter in the `start()` API call. Once the activity is created, the SDK checks for new updates in the background. If it finds a new update, users will see a dialog with three options - `Download`,`Postpone` and `Ignore`. If the user presses `Download`, it will trigger the new version to be installed. Postpone will delay the download until the app is opened again. Ignore will not prompt the user again for that particular app version. You can easily provide your own resource strings if you'd like to localize the text displayed in the update dialog. Look at the string files [here](https://github.com/Microsoft/mobile-center-sdk-android/blob/distribute/sdk/mobile-center-distribute/src/main/res/values/strings.xml). Use the same string name and specify the localized value to be reflected in the dialog in your own app resource files. From 821dc46da4a169ed6cce47d55614d8bbf9c0c8ec Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Fri, 3 Mar 2017 17:01:28 -0800 Subject: [PATCH 110/142] Add all project dependencies to source when build generates javadoc --- sdk/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/build.gradle b/sdk/build.gradle index 678ee08bb5..40db8c5284 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -108,9 +108,8 @@ subprojects { task javadoc(type: Javadoc) { afterEvaluate { source = android.sourceSets.main.java.srcDirs + source += configurations.compile.dependencies.withType(ProjectDependency).dependencyProject.android.sourceSets.main.java.srcDirs classpath += configurations.javadocDeps - // FIXME workaround for Android Studio 2.3.0 / Gradle 3.3, need to find the right fix... - classpath += files("$buildDir/../../mobile-center/build/intermediates/classes/release") //noinspection GroovyAssignabilityCheck classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) javadoc.dependsOn project.assembleRelease From fcebcc90c7c88e48e85826362b21281402cf46c9 Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Fri, 3 Mar 2017 17:26:07 -0800 Subject: [PATCH 111/142] Add dependency projects to classpath not source --- sdk/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/build.gradle b/sdk/build.gradle index 40db8c5284..9980b1e752 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -108,8 +108,8 @@ subprojects { task javadoc(type: Javadoc) { afterEvaluate { source = android.sourceSets.main.java.srcDirs - source += configurations.compile.dependencies.withType(ProjectDependency).dependencyProject.android.sourceSets.main.java.srcDirs classpath += configurations.javadocDeps + configurations.compile.dependencies.withType(ProjectDependency).dependencyProject.buildDir.each { dir -> classpath += files("${dir}/intermediates/classes/release") } //noinspection GroovyAssignabilityCheck classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) javadoc.dependsOn project.assembleRelease From e779e7a2e5c44da5acb3199af030f797ca08e4aa Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 6 Mar 2017 18:04:04 -0800 Subject: [PATCH 112/142] Encrypt update token --- .../azure/mobile/distribute/Distribute.java | 26 +- .../distribute/AbstractDistributeTest.java | 25 +- .../DistributeBeforeApiSuccessTest.java | 65 ++- .../mobile/utils/crypto/CryptoAesHandler.java | 64 +++ .../mobile/utils/crypto/CryptoConstants.java | 75 +++ .../mobile/utils/crypto/CryptoHandler.java | 53 ++ .../utils/crypto/CryptoNoOpHandler.java | 30 ++ .../mobile/utils/crypto/CryptoRsaHandler.java | 92 ++++ .../mobile/utils/crypto/CryptoUtils.java | 493 ++++++++++++++++++ .../crypto/CryptoDefaultFactoryTest.java | 55 ++ .../CryptoDefaultKeyGeneratorMockTest.java | 46 ++ .../azure/mobile/utils/crypto/CryptoTest.java | 363 +++++++++++++ 12 files changed, 1379 insertions(+), 8 deletions(-) create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoConstants.java create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoHandler.java create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoNoOpHandler.java create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java create mode 100644 sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java create mode 100644 sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultKeyGeneratorMockTest.java create mode 100644 sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java index 456cd98567..147c608c3b 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java @@ -41,6 +41,7 @@ import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.NetworkStateHelper; import com.microsoft.azure.mobile.utils.UUIDUtils; +import com.microsoft.azure.mobile.utils.crypto.CryptoUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; @@ -55,7 +56,6 @@ import static android.content.Context.DOWNLOAD_SERVICE; import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE; import static android.util.Log.VERBOSE; -import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DEFAULT_API_URL; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DEFAULT_INSTALL_URL; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_COMPLETED; @@ -79,6 +79,7 @@ import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.distribute.DistributeConstants.SERVICE_NAME; import static com.microsoft.azure.mobile.distribute.DistributeConstants.UPDATE_SETUP_PATH_FORMAT; +import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; /** * Distribute service. @@ -511,7 +512,18 @@ private synchronized void resumeDistributeWorkflow() { /* Check if we have previous stored the update token. */ String updateToken = PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN); if (updateToken != null) { - getLatestReleaseDetails(updateToken); + + /* Decrypt token. */ + CryptoUtils.DecryptedData decryptedData = CryptoUtils.getInstance(mContext).decrypt(updateToken); + String newEncryptedData = decryptedData.getNewEncryptedData(); + + /* Store new encrypted value if updated. */ + if (newEncryptedData != null) { + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, newEncryptedData); + } + + /* Check latest release. */ + getLatestReleaseDetails(decryptedData.getDecryptedData()); return; } @@ -623,9 +635,6 @@ private synchronized void completeWorkflow() { /* * Store update token and possibly trigger application update check. - * TODO encrypt token, but where to store encryption key? If it's retrieved from server, - * how do we protect server call to get the key in the first place? - * Even having the encryption key temporarily in memory is risky as that can be heap dumped. */ synchronized void storeUpdateToken(@NonNull String updateToken, @NonNull String requestId) { @@ -635,7 +644,8 @@ synchronized void storeUpdateToken(@NonNull String updateToken, @NonNull String mBeforeStartUpdateToken = updateToken; mBeforeStartRequestId = requestId; } else if (requestId.equals(PreferencesStorage.getString(PREFERENCE_KEY_REQUEST_ID))) { - PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, updateToken); + String encryptedToken = CryptoUtils.getInstance(mContext).encrypt(updateToken); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, encryptedToken); PreferencesStorage.remove(PREFERENCE_KEY_REQUEST_ID); MobileCenterLog.debug(LOG_TAG, "Stored update token."); cancelPreviousTasks(); @@ -713,6 +723,9 @@ private synchronized void handleApiCallFailure(Object releaseCallId, Exception e if (mCheckReleaseCallId == releaseCallId) { MobileCenterLog.error(LOG_TAG, "Failed to check latest release:", e); completeWorkflow(); + if (!HttpUtils.isRecoverableError(e)) { + PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); + } } } @@ -1109,6 +1122,7 @@ private synchronized void markDownloadStillInProgress(CheckDownloadTask task) { /** * Remove a previously downloaded file and any notification. */ + @SuppressLint("VisibleForTests") private synchronized void removeDownload(long downloadId) { cancelNotification(mContext); AsyncTaskUtils.execute(LOG_TAG, new RemoveDownloadTask(), downloadId); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java index 924c8b03e9..25da232a1f 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java @@ -14,6 +14,7 @@ import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.NetworkStateHelper; import com.microsoft.azure.mobile.utils.UUIDUtils; +import com.microsoft.azure.mobile.utils.crypto.CryptoUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import org.junit.Before; @@ -33,6 +34,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -41,7 +43,7 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("WeakerAccess") -@PrepareForTest({Distribute.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, NetworkStateHelper.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, InstallerUtils.class, Toast.class}) +@PrepareForTest({Distribute.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, NetworkStateHelper.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, CryptoUtils.class, InstallerUtils.class, Toast.class}) public class AbstractDistributeTest { static final String TEST_HASH = HashUtils.sha256("com.contoso:1.2.3:6"); @@ -77,6 +79,9 @@ public class AbstractDistributeTest { NetworkStateHelper mNetworkStateHelper; + @Mock + CryptoUtils mCryptoUtils; + @Before @SuppressLint("ShowToast") @SuppressWarnings("ResourceType") @@ -136,6 +141,24 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { } }); + /* Mock Crypto to not crypt. */ + mockStatic(CryptoUtils.class); + when(CryptoUtils.getInstance(any(Context.class))).thenReturn(mCryptoUtils); + when(mCryptoUtils.decrypt(anyString())).thenAnswer(new Answer() { + + @Override + public CryptoUtils.DecryptedData answer(InvocationOnMock invocation) throws Throwable { + return new CryptoUtils.DecryptedData(invocation.getArguments()[0].toString(), null); + } + }); + when(mCryptoUtils.encrypt(anyString())).thenAnswer(new Answer() { + + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + return invocation.getArguments()[0].toString(); + } + }); + /* Dialog. */ whenNew(AlertDialog.Builder.class).withAnyArguments().thenReturn(mDialogBuilder); when(mDialogBuilder.create()).thenReturn(mDialog); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java index 8af35dd6f8..0d9207723d 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java @@ -15,6 +15,7 @@ import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.UUIDUtils; +import com.microsoft.azure.mobile.utils.crypto.CryptoUtils; import org.json.JSONException; import org.junit.Test; @@ -413,7 +414,44 @@ public void disableWhileCheckingRelease() throws Exception { } @Test - public void checkReleaseFails() throws Exception { + public void checkReleaseFailsRecoverable() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallFailed(new HttpException(503)); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); + + /* Trigger call. */ + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on failure we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + + /* The error was recoverable, keep token. */ + verifyStatic(never()); + PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); + + /* After that if we resume app nothing happens. */ + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + + @Test + public void checkReleaseFailsNotRecoverable() throws Exception { /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -439,6 +477,10 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + /* The error was unrecoverable, get rid of token. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); + /* After that if we resume app nothing happens. */ Distribute.getInstance().onActivityPaused(mock(Activity.class)); Distribute.getInstance().onActivityResumed(mock(Activity.class)); @@ -583,4 +625,25 @@ public void run() { verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); verify(mDialog, never()).show(); } + + @Test + public void storeBetterEncryptedToken() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some encrypted token"); + when(mCryptoUtils.decrypt("some encrypted token")).thenReturn(new CryptoUtils.DecryptedData("some token", "some better encrypted token")); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + HashMap headers = new HashMap<>(); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); + + /* Trigger call. */ + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify storage was updated with new encrypted value. */ + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some better encrypted token"); + } } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java new file mode 100644 index 0000000000..d290363182 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java @@ -0,0 +1,64 @@ +package com.microsoft.azure.mobile.utils.crypto; + +import android.content.Context; +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.support.annotation.RequiresApi; + +import java.security.KeyStore; +import java.util.Calendar; + +import javax.crypto.spec.IvParameterSpec; + +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.AES_KEY_SIZE; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_KEY_STORE; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.CIPHER_AES; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ENCRYPT_KEY_LIFETIME_IN_YEARS; +import static javax.crypto.Cipher.DECRYPT_MODE; +import static javax.crypto.Cipher.ENCRYPT_MODE; + +@RequiresApi(Build.VERSION_CODES.M) +class CryptoAesHandler implements CryptoHandler { + + @Override + public String getAlgorithm() { + return CIPHER_AES + "/" + AES_KEY_SIZE; + } + + @Override + public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, String alias, Context context) throws Exception { + Calendar writeExpiry = Calendar.getInstance(); + writeExpiry.add(Calendar.YEAR, ENCRYPT_KEY_LIFETIME_IN_YEARS); + CryptoUtils.IKeyGenerator keyGenerator = cryptoFactory.getKeyGenerator(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE); + keyGenerator.init(new KeyGenParameterSpec.Builder(alias, + KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .setKeySize(AES_KEY_SIZE) + .setKeyValidityForOriginationEnd(writeExpiry.getTime()) + .build()); + keyGenerator.generateKey(); + } + + @Override + public byte[] encrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] input) throws Exception { + CryptoUtils.ICipher cipher = cryptoFactory.getCipher(CIPHER_AES, null); + cipher.init(ENCRYPT_MODE, ((KeyStore.SecretKeyEntry) keyStoreEntry).getSecretKey()); + byte[] cipherIV = cipher.getIV(); + byte[] output = cipher.doFinal(input); + byte[] encryptedBytes = new byte[cipherIV.length + output.length]; + System.arraycopy(cipherIV, 0, encryptedBytes, 0, cipherIV.length); + System.arraycopy(output, 0, encryptedBytes, cipherIV.length, output.length); + return encryptedBytes; + } + + @Override + public byte[] decrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] data) throws Exception { + CryptoUtils.ICipher cipher = cryptoFactory.getCipher(CIPHER_AES, null); + int blockSize = cipher.getBlockSize(); + IvParameterSpec ivParameterSpec = new IvParameterSpec(data, 0, blockSize); + cipher.init(DECRYPT_MODE, ((KeyStore.SecretKeyEntry) keyStoreEntry).getSecretKey(), ivParameterSpec); + return cipher.doFinal(data, blockSize, data.length - blockSize); + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoConstants.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoConstants.java new file mode 100644 index 0000000000..022f2289c0 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoConstants.java @@ -0,0 +1,75 @@ +package com.microsoft.azure.mobile.utils.crypto; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.security.keystore.KeyProperties; +import android.support.annotation.RequiresApi; + +/** + * Various constants used in the cryptography package. + */ +class CryptoConstants { + + /** + * Android key store name. + */ + static final String ANDROID_KEY_STORE = "AndroidKeyStore"; + + /** + * Keystore alias prefix to use for Mobile Center SDK. + */ + static final String KEYSTORE_ALIAS_PREFIX = "mobile.center"; + + /** + * Keystore alias separator. + */ + static final String ALIAS_SEPARATOR = "."; + + /** + * Separator between algorithm and encrypted data. + */ + static final String ALGORITHM_DATA_SEPARATOR = ":"; + + /** + * Encoding charset used for bytes/string conversion. + */ + static final String CHARSET = "UTF-8"; + + /** + * How long an encryption key is valid for producing new encrypted data. + * Decrypting is always allowed and needed for rollover and migration. + */ + static final int ENCRYPT_KEY_LIFETIME_IN_YEARS = 1; + + /** + * Key size for AES. + */ + static final int AES_KEY_SIZE = 256; + + /** + * Key size for RSA. + */ + static final int RSA_KEY_SIZE = 2048; + + /** + * Cipher used for AES. + */ + @RequiresApi(Build.VERSION_CODES.M) + static final String CIPHER_AES = KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7; + + /** + * Cipher used for RSA. + */ + @SuppressLint("InlinedApi") + static final String CIPHER_RSA = KeyProperties.KEY_ALGORITHM_RSA + "/" + KeyProperties.BLOCK_MODE_ECB + "/" + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1; + + /** + * RSA provider when running on old devices. + */ + static final String ANDROID_RSA_OLD_PROVIDER = "AndroidOpenSSL"; + + /** + * RSA provider when running on M devices. + */ + static final String ANDROID_RSA_M_PROVIDER = "AndroidKeyStoreBCWorkaround"; +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoHandler.java new file mode 100644 index 0000000000..037b451435 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoHandler.java @@ -0,0 +1,53 @@ +package com.microsoft.azure.mobile.utils.crypto; + +import android.content.Context; + +import java.security.KeyStore; + +/** + * Specification for implementations of cryptographic utilities. + */ +interface CryptoHandler { + + /** + * Get algorithm to store along encrypted data. + * + * @return algorithm to store along encrypted data. + */ + String getAlgorithm(); + + /** + * Generate a new key. + * + * @param cryptoFactory crypto factory. + * @param apiLevel Android API level. + * @param alias keystore alias. + * @param context application context. + * @throws Exception if an error occurs. + */ + void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, String alias, Context context) throws Exception; + + /** + * Encrypt data. + * + * @param cryptoFactory crypto factory. + * @param apiLevel Android API level. + * @param keyStoreEntry key store. + * @param data data to encrypt. + * @return encrypted bytes. + * @throws Exception if an error occurs. + */ + byte[] encrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] data) throws Exception; + + /** + * Decrypt data. + * + * @param cryptoFactory crypto factory. + * @param apiLevel Android API level. + * @param keyStoreEntry key store. + * @param data data to decrypt. + * @return decrypted bytes. + * @throws Exception if an error occurs. + */ + byte[] decrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] data) throws Exception; +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoNoOpHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoNoOpHandler.java new file mode 100644 index 0000000000..3e85eb222a --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoNoOpHandler.java @@ -0,0 +1,30 @@ +package com.microsoft.azure.mobile.utils.crypto; + +import android.content.Context; + +import java.security.KeyStore; + +/** + * Handler that does not actually encrypt anything. + */ +class CryptoNoOpHandler implements CryptoHandler { + + @Override + public String getAlgorithm() { + return "None"; + } + + @Override + public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, String alias, Context context) { + } + + @Override + public byte[] encrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] data) { + return data; + } + + @Override + public byte[] decrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] data) { + return data; + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java new file mode 100644 index 0000000000..82ee034b00 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java @@ -0,0 +1,92 @@ +package com.microsoft.azure.mobile.utils.crypto; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; +import android.security.keystore.KeyProperties; +import android.support.annotation.RequiresApi; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.CertificateExpiredException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; + +import javax.security.auth.x500.X500Principal; + +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_KEY_STORE; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_RSA_M_PROVIDER; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_RSA_OLD_PROVIDER; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.CIPHER_RSA; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ENCRYPT_KEY_LIFETIME_IN_YEARS; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.RSA_KEY_SIZE; +import static javax.crypto.Cipher.DECRYPT_MODE; +import static javax.crypto.Cipher.ENCRYPT_MODE; + +@RequiresApi(Build.VERSION_CODES.KITKAT) +class CryptoRsaHandler implements CryptoHandler { + + @Override + public String getAlgorithm() { + return CIPHER_RSA + "/" + RSA_KEY_SIZE; + } + + /* + * We don't run this code prior to Android 4.4 hence no 4.3 secure random problem. + */ + @Override + @SuppressWarnings("deprecation") + @SuppressLint({"InlinedApi", "TrulyRandom"}) + public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, String alias, Context context) throws Exception { + Calendar writeExpiry = Calendar.getInstance(); + writeExpiry.add(Calendar.YEAR, ENCRYPT_KEY_LIFETIME_IN_YEARS); + KeyPairGenerator generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE); + generator.initialize(new KeyPairGeneratorSpec.Builder(context) + .setAlias(alias) + .setSubject(new X500Principal("CN=" + alias)) + .setStartDate(new Date()) + .setEndDate(writeExpiry.getTime()) + .setSerialNumber(BigInteger.TEN) + .setKeySize(RSA_KEY_SIZE) + .build()); + generator.generateKeyPair(); + } + + /** + * Get new cipher. + */ + private CryptoUtils.ICipher getCipher(CryptoUtils.ICryptoFactory cipherFactory, int apiLevel) throws Exception { + String provider; + if (apiLevel >= Build.VERSION_CODES.M) { + provider = ANDROID_RSA_M_PROVIDER; + } else { + provider = ANDROID_RSA_OLD_PROVIDER; + } + return cipherFactory.getCipher(CIPHER_RSA, provider); + } + + @Override + public byte[] encrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] input) throws Exception { + CryptoUtils.ICipher cipher = getCipher(cryptoFactory, apiLevel); + KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStoreEntry; + X509Certificate certificate = (X509Certificate) privateKeyEntry.getCertificate(); + try { + certificate.checkValidity(); + } catch (CertificateExpiredException e) { + throw new InvalidKeyException(e); + } + cipher.init(ENCRYPT_MODE, certificate.getPublicKey()); + return cipher.doFinal(input); + } + + @Override + public byte[] decrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] data) throws Exception { + CryptoUtils.ICipher cipher = getCipher(cryptoFactory, apiLevel); + cipher.init(DECRYPT_MODE, ((KeyStore.PrivateKeyEntry) keyStoreEntry).getPrivateKey()); + return cipher.doFinal(data); + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java new file mode 100644 index 0000000000..d3c8989c35 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java @@ -0,0 +1,493 @@ +package com.microsoft.azure.mobile.utils.crypto; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.Base64; + +import com.microsoft.azure.mobile.utils.MobileCenterLog; + +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyStore; +import java.security.Provider; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +import static com.microsoft.azure.mobile.MobileCenter.LOG_TAG; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ALGORITHM_DATA_SEPARATOR; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ALIAS_SEPARATOR; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_KEY_STORE; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.CHARSET; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.KEYSTORE_ALIAS_PREFIX; + +/** + * Tool to encrypt/decrypt strings seamlessly. + */ +public class CryptoUtils { + + @VisibleForTesting + static final ICryptoFactory DEFAULT_CRYPTO_FACTORY = new ICryptoFactory() { + + @Override + public IKeyGenerator getKeyGenerator(String algorithm, String provider) throws Exception { + final KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm, provider); + return new IKeyGenerator() { + + @Override + public void init(AlgorithmParameterSpec parameters) throws Exception { + keyGenerator.init(parameters); + } + + @Override + public SecretKey generateKey() { + return keyGenerator.generateKey(); + } + }; + } + + @Override + public ICipher getCipher(String transformation, String provider) throws Exception { + final Cipher cipher = Cipher.getInstance(transformation, provider); + return new ICipher() { + + @Override + public void init(int opMode, Key key) throws Exception { + cipher.init(opMode, key); + } + + @Override + public void init(int opMode, Key key, AlgorithmParameterSpec params) throws Exception { + cipher.init(opMode, key, params); + } + + @Override + public byte[] doFinal(byte[] input) throws Exception { + return cipher.doFinal(input); + } + + @Override + public byte[] doFinal(byte[] input, int inputOffset, int inputLength) throws Exception { + return cipher.doFinal(input, inputOffset, inputLength); + } + + @Override + public byte[] getIV() { + return cipher.getIV(); + } + + @Override + public int getBlockSize() { + return cipher.getBlockSize(); + } + + @Override + public String getAlgorithm() { + return cipher.getAlgorithm(); + } + + @Override + public String getProvider() { + return cipher.getProvider().getName(); + } + }; + } + }; + + /** + * Shared instance. + */ + @SuppressLint("StaticFieldLeak") + private static CryptoUtils sInstance; + + /** + * Application context. + */ + private final Context mContext; + + /** + * Crypto factory. + */ + private final ICryptoFactory mCryptoFactory; + + /** + * Android API level. + */ + private final int mApiLevel; + + /** + * Android key store or null if could not use it. + */ + private final KeyStore mKeyStore; + + /** + * Supported crypto handlers. Ordered, first one is the preferred one. + */ + private Map mCryptoHandlers = new LinkedHashMap<>(); + + /** + * Init. + * + * @param context any context. + */ + private CryptoUtils(@NonNull Context context) { + this(context, DEFAULT_CRYPTO_FACTORY, Build.VERSION.SDK_INT); + } + + @VisibleForTesting + @TargetApi(Build.VERSION_CODES.M) + CryptoUtils(@NonNull Context context, @NonNull ICryptoFactory cryptoFactory, int apiLevel) { + + /* Store application context. */ + mContext = context.getApplicationContext(); + mCryptoFactory = cryptoFactory; + mApiLevel = apiLevel; + + /* Load Android secure key store if available. */ + KeyStore keyStore = null; + if (apiLevel >= Build.VERSION_CODES.KITKAT) + try { + keyStore = KeyStore.getInstance(ANDROID_KEY_STORE); + keyStore.load(null); + } catch (Exception e) { + MobileCenterLog.error(LOG_TAG, "Cannot use secure keystore on this device."); + } + mKeyStore = keyStore; + + /* We have to use AES to be compliant but it's available only after Android M. */ + if (keyStore != null && apiLevel >= Build.VERSION_CODES.M) { + try { + registerHandler(new CryptoAesHandler()); + } catch (Exception e) { + MobileCenterLog.error(LOG_TAG, "Cannot use modern encryption on this device."); + } + } + + /* + * Even if we're not going to use it on modern devices to decrypt, + * we may have to decrypt stored data that was encrypted before the firmware was upgraded. + * So we load this handler in every case. + */ + if (keyStore != null) { + try { + registerHandler(new CryptoRsaHandler()); + } catch (Exception e) { + MobileCenterLog.error(LOG_TAG, "Cannot use old encryption on this device."); + } + } + + /* Add the fake handler at the end of the list no matter what. */ + CryptoNoOpHandler cryptoNoOpHandler = new CryptoNoOpHandler(); + mCryptoHandlers.put(cryptoNoOpHandler.getAlgorithm(), new CryptoHandlerEntry(0, cryptoNoOpHandler)); + } + + /** + * Get unique instance. + * + * @param context any context. + * @return unique instance. + */ + public static CryptoUtils getInstance(@NonNull Context context) { + if (sInstance == null) + sInstance = new CryptoUtils(context); + return sInstance; + } + + @VisibleForTesting + ICryptoFactory getCryptoFactory() { + return mCryptoFactory; + } + + /** + * Register handler and create alias the first time. + */ + private void registerHandler(@NonNull CryptoHandler handler) throws Exception { + + /* Check which of the potential aliases is the more recent one, the one to use. */ + String alias0 = getAlias(handler, 0); + String alias1 = getAlias(handler, 1); + Date aliasDate0 = mKeyStore.getCreationDate(alias0); + Date aliasDate1 = mKeyStore.getCreationDate(alias1); + int index = 0; + String alias = alias0; + if (aliasDate1 != null && aliasDate1.after(aliasDate0)) { + index = 1; + alias = alias1; + } + + /* If it's the first time we use the preferred handler, create the alias. */ + if (mCryptoHandlers.isEmpty() && !mKeyStore.containsAlias(alias)) { + MobileCenterLog.debug(LOG_TAG, "Creating alias: " + alias); + handler.generateKey(mCryptoFactory, mApiLevel, alias, mContext); + } + + /* Register the handler. */ + MobileCenterLog.debug(LOG_TAG, "Using " + alias); + mCryptoHandlers.put(handler.getAlgorithm(), new CryptoHandlerEntry(index, handler)); + } + + @NonNull + private String getAlias(CryptoHandler handler, int index) { + return KEYSTORE_ALIAS_PREFIX + ALIAS_SEPARATOR + index + ALIAS_SEPARATOR + handler.getAlgorithm(); + } + + /** + * Get key store entry for the corresponding handler. + */ + @Nullable + private KeyStore.Entry getKeyStoreEntry(@NonNull CryptoHandlerEntry handlerEntry) throws Exception { + if (mKeyStore == null) + return null; + String alias = getAlias(handlerEntry.mCryptoHandler, handlerEntry.mAliasIndex); + return mKeyStore.getEntry(alias, null); + } + + /** + * Encrypt data. + * + * @param data data to encrypt. + * @return encrypted data, or original data on internal failure or if null. + */ + @Nullable + public String encrypt(@Nullable String data) { + if (data == null) + return null; + try { + + /* Get preferred crypto handler. */ + CryptoHandlerEntry handlerEntry = mCryptoHandlers.values().iterator().next(); + CryptoHandler handler = handlerEntry.mCryptoHandler; + try { + + /* Attempt encryption. */ + KeyStore.Entry keyStoreEntry = getKeyStoreEntry(handlerEntry); + byte[] encryptedBytes = handler.encrypt(mCryptoFactory, mApiLevel, keyStoreEntry, data.getBytes(CHARSET)); + String encryptedString = Base64.encodeToString(encryptedBytes, Base64.DEFAULT); + + /* + * Store algorithm for crypto agility alongside the data. + * We also use that information in decrypt in case of firmware/sdk upgrade. + */ + return handler.getAlgorithm() + ALGORITHM_DATA_SEPARATOR + encryptedString; + } catch (InvalidKeyException e) { + + /* When key expires, switch to another alias. */ + MobileCenterLog.debug(LOG_TAG, "Alias expired: " + handlerEntry.mAliasIndex); + handlerEntry.mAliasIndex ^= 1; + String newAlias = getAlias(handler, handlerEntry.mAliasIndex); + + /* If this is the second time we switch, we delete the previous key. */ + if (mKeyStore.containsAlias(newAlias)) { + MobileCenterLog.debug(LOG_TAG, "Deleting alias: " + newAlias); + mKeyStore.deleteEntry(newAlias); + } + + /* Generate new key. */ + MobileCenterLog.debug(LOG_TAG, "Creating alias: " + newAlias); + handler.generateKey(mCryptoFactory, mApiLevel, newAlias, mContext); + + /* And encrypt using that new key. */ + return encrypt(data); + } + } catch (Exception e) { + + /* Return data as is. */ + MobileCenterLog.error(LOG_TAG, "Failed to encrypt data."); + return data; + } + } + + /** + * Decrypt data. + * + * @param data data to decrypt. + * @return decrypted data. + */ + @NonNull + public DecryptedData decrypt(@Nullable String data) { + + /* Handle null for convenience. */ + if (data == null) + return new DecryptedData(null, null); + + /* Guess what algorithm was used in case the data was encrypted using an old SDK or old firmware. */ + String[] dataSplit = data.split(ALGORITHM_DATA_SEPARATOR); + CryptoHandlerEntry handlerEntry = dataSplit.length == 2 ? mCryptoHandlers.get(dataSplit[0]) : null; + CryptoHandler cryptoHandler = handlerEntry == null ? null : handlerEntry.mCryptoHandler; + try { + if (cryptoHandler == null) { + throw new IllegalStateException("Could not find crypto handler that was used for the specified data."); + } + KeyStore.Entry keyStoreEntry = getKeyStoreEntry(handlerEntry); + byte[] decryptedBytes = cryptoHandler.decrypt(mCryptoFactory, mApiLevel, keyStoreEntry, Base64.decode(dataSplit[1], Base64.DEFAULT)); + String decryptedString = new String(decryptedBytes, CHARSET); + String newEncryptedData = null; + if (cryptoHandler != mCryptoHandlers.values().iterator().next().mCryptoHandler) { + newEncryptedData = encrypt(decryptedString); + } + return new DecryptedData(decryptedString, newEncryptedData); + } catch (Exception e) { + + /* Return data as is. */ + MobileCenterLog.error(LOG_TAG, "Failed to decrypt data."); + return new DecryptedData(data, null); + } + } + + /** + * Crypto factory. + */ + interface ICryptoFactory { + + /** + * Adapt {@link KeyGenerator#getInstance(String, Provider)}. + */ + IKeyGenerator getKeyGenerator(String algorithm, String provider) throws Exception; + + /** + * Adapt {@link Cipher#getInstance(String, Provider)}. + */ + ICipher getCipher(String algorithm, String provider) throws Exception; + } + + /** + * Adapter for KeyGenerator. + */ + interface IKeyGenerator { + + /** + * Adapt {@link KeyGenerator#init(AlgorithmParameterSpec)}. + */ + void init(AlgorithmParameterSpec parameters) throws Exception; + + /** + * Adapt {@link KeyGenerator#generateKey()}. + */ + SecretKey generateKey(); + } + + /** + * Adapter for cipher. + */ + interface ICipher { + + /** + * Adapt {@link Cipher#init(int, Key)}. + */ + void init(int opMode, Key key) throws Exception; + + /** + * Adapt {@link Cipher#init(int, Key, AlgorithmParameterSpec)}. + */ + void init(int opMode, Key key, AlgorithmParameterSpec params) throws Exception; + + /** + * Adapt {@link Cipher#doFinal(byte[])}. + */ + byte[] doFinal(byte[] input) throws Exception; + + /** + * Adapt {@link Cipher#doFinal(byte[], int, int)}. + */ + byte[] doFinal(byte[] input, int inputOffset, int inputLength) throws Exception; + + /** + * Adapt {@link Cipher#getIV()}}. + */ + byte[] getIV(); + + /** + * Adapt {@link Cipher#getBlockSize()}. + */ + int getBlockSize(); + + /** + * Adapt {@link Cipher#getAlgorithm()}. + */ + @VisibleForTesting + String getAlgorithm(); + + /** + * Adapt {@link Cipher#getProvider()}. + */ + @VisibleForTesting + String getProvider(); + } + + /** + * Internal structure for the register handler entries. + */ + private static class CryptoHandlerEntry { + + /** + * Keystore alias index, 0 or 1. + */ + int mAliasIndex; + + /** + * Crypto handler. + */ + CryptoHandler mCryptoHandler; + + /** + * Init. + */ + CryptoHandlerEntry(int aliasIndex, CryptoHandler cryptoHandler) { + mAliasIndex = aliasIndex; + mCryptoHandler = cryptoHandler; + } + } + + /** + * Decrypted data returned by {@link #decrypt(String)}. + */ + public static class DecryptedData { + + /** + * Decrypted data. + */ + String mDecryptedData; + + /** + * Better encrypted data. + */ + String mNewEncryptedData; + + /** + * Init. + */ + @VisibleForTesting + public DecryptedData(String decryptedData, String newEncryptedData) { + mDecryptedData = decryptedData; + mNewEncryptedData = newEncryptedData; + } + + /** + * Get decrypted data. + * + * @return decrypted data, or original data if null or failed to decrypt. + */ + public String getDecryptedData() { + return mDecryptedData; + } + + /** + * Get new encrypted data. + * + * @return new encrypted data to use if input was encrypted using an old cipher, or null. + */ + public String getNewEncryptedData() { + return mNewEncryptedData; + } + } +} diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java new file mode 100644 index 0000000000..0494ad868a --- /dev/null +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java @@ -0,0 +1,55 @@ +package com.microsoft.azure.mobile.utils.crypto; + +import android.content.Context; + +import org.junit.Test; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import static javax.crypto.Cipher.DECRYPT_MODE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.powermock.api.mockito.PowerMockito.mock; + +/** + * This class covers the default crypto factory implementation. + * This is separate from other tests as we don't use PowerMockRule here. + */ +public class CryptoDefaultFactoryTest { + + @Test + public void coverNoOpHandlerGenerate() throws Exception { + new CryptoNoOpHandler().generateKey(CryptoUtils.DEFAULT_CRYPTO_FACTORY, 0, null, null); + } + + @Test + public void coverSingleton() { + CryptoUtils instance = CryptoUtils.getInstance(mock(Context.class)); + assertSame(instance, CryptoUtils.getInstance(mock(Context.class))); + assertSame(CryptoUtils.DEFAULT_CRYPTO_FACTORY, instance.getCryptoFactory()); + } + + @Test + public void coverDefaultCipherParameterPassing() throws Exception { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(128); + SecretKey secretKey = keyGenerator.generateKey(); + String algorithm = "AES/CBC/PKCS5Padding"; + Cipher cipherTmp = Cipher.getInstance(algorithm); + String provider = cipherTmp.getProvider().getName(); + CryptoUtils.ICipher encryptCipher = CryptoUtils.DEFAULT_CRYPTO_FACTORY.getCipher(algorithm, provider); + assertEquals(algorithm, encryptCipher.getAlgorithm()); + assertEquals(provider, encryptCipher.getProvider()); + encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey); + byte[] data = encryptCipher.doFinal("test".getBytes("UTF-8")); + CryptoUtils.ICipher decryptCipher = CryptoUtils.DEFAULT_CRYPTO_FACTORY.getCipher(algorithm, provider); + decryptCipher.init(DECRYPT_MODE, secretKey, new IvParameterSpec(encryptCipher.getIV())); + assertEquals("test", new String(decryptCipher.doFinal(data), "UTF-8")); + assertEquals("test", new String(decryptCipher.doFinal(data, 0, data.length), "UTF-8")); + assertEquals(decryptCipher.getIV().length, decryptCipher.getBlockSize()); + } +} + diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultKeyGeneratorMockTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultKeyGeneratorMockTest.java new file mode 100644 index 0000000000..9c71142476 --- /dev/null +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultKeyGeneratorMockTest.java @@ -0,0 +1,46 @@ +package com.microsoft.azure.mobile.utils.crypto; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.KeyGenerator; + +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * This class is the only way to cover the default KeyGenerator implementation, + * it cannot use PowerMockRule or real java implementation. + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest(KeyGenerator.class) +public class CryptoDefaultKeyGeneratorMockTest { + + @Before + public void setUp() throws Exception { + mockStatic(KeyGenerator.class); + when(KeyGenerator.getInstance(anyString(), anyString())).thenReturn(mock(KeyGenerator.class)); + } + + @Test + public void checkParametersPassingForKeyGenerator() throws Exception { + mockStatic(KeyGenerator.class); + KeyGenerator mockKeyGenerator = mock(KeyGenerator.class); + when(KeyGenerator.getInstance(anyString(), anyString())).thenReturn(mockKeyGenerator); + CryptoUtils.IKeyGenerator keyGenerator = CryptoUtils.DEFAULT_CRYPTO_FACTORY.getKeyGenerator("AES", "MockProvider"); + AlgorithmParameterSpec parameters = mock(AlgorithmParameterSpec.class); + keyGenerator.init(parameters); + verify(mockKeyGenerator).init(parameters); + keyGenerator.generateKey(); + verify(mockKeyGenerator).generateKey(); + } +} + diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java new file mode 100644 index 0000000000..68379d3630 --- /dev/null +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java @@ -0,0 +1,363 @@ +package com.microsoft.azure.mobile.utils.crypto; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; +import android.security.keystore.KeyGenParameterSpec; +import android.util.Base64; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; + +import java.math.BigInteger; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; + +import javax.crypto.BadPaddingException; +import javax.security.auth.x500.X500Principal; + +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.AES_KEY_SIZE; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ALGORITHM_DATA_SEPARATOR; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_KEY_STORE; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.CIPHER_AES; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.CIPHER_RSA; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.RSA_KEY_SIZE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.doThrow; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@SuppressLint("NewApi") +@SuppressWarnings("deprecation") +@PowerMockIgnore({"javax.security.auth.x500.*"}) +@PrepareForTest({KeyStore.class, KeyPairGenerator.class, Base64.class, CryptoUtils.class, CryptoRsaHandler.class, CryptoAesHandler.class}) +public class CryptoTest { + + @Rule + public PowerMockRule mPowerMockRule = new PowerMockRule(); + + @Mock + private KeyStore mKeyStore; + + @Mock + private X509Certificate mRsaCert; + + @Mock + private KeyPairGeneratorSpec.Builder mRsaBuilder; + + @Mock + private KeyGenParameterSpec.Builder mAesBuilder; + + @Mock + private Context mContext; + + @Mock + private CryptoUtils.ICryptoFactory mCryptoFactory; + + @Mock + private CryptoUtils.ICipher mCipher; + + @Before + @SuppressWarnings("WrongConstant") + public void setUp() throws Exception { + when(mContext.getApplicationContext()).thenReturn(mContext); + mockStatic(KeyStore.class); + mockStatic(KeyPairGenerator.class); + mockStatic(Base64.class); + when(Base64.encodeToString(any(byte[].class), anyInt())).thenAnswer(new Answer() { + + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + return new String((byte[]) invocation.getArguments()[0]); + } + }); + when(Base64.decode(anyString(), anyInt())).thenAnswer(new Answer() { + + @Override + public byte[] answer(InvocationOnMock invocation) throws Throwable { + return invocation.getArguments()[0].toString().getBytes(); + } + }); + when(KeyStore.getInstance(ANDROID_KEY_STORE)).thenReturn(mKeyStore); + + /* Mock some RSA specifics. */ + whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(mRsaBuilder); + when(mRsaBuilder.setAlias(anyString())).thenReturn(mRsaBuilder); + when(mRsaBuilder.setSubject(any(X500Principal.class))).thenReturn(mRsaBuilder); + when(mRsaBuilder.setStartDate(any(Date.class))).thenReturn(mRsaBuilder); + when(mRsaBuilder.setEndDate(any(Date.class))).thenReturn(mRsaBuilder); + when(mRsaBuilder.setSerialNumber(any(BigInteger.class))).thenReturn(mRsaBuilder); + when(mRsaBuilder.setKeySize(anyInt())).thenReturn(mRsaBuilder); + when(KeyPairGenerator.getInstance(anyString(), anyString())).thenReturn(mock(KeyPairGenerator.class)); + KeyStore.PrivateKeyEntry rsaKey = mock(KeyStore.PrivateKeyEntry.class); + when(mKeyStore.getEntry(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return String.valueOf(argument).contains(CIPHER_RSA); + } + }), any(KeyStore.ProtectionParameter.class))).thenReturn(rsaKey); + when(rsaKey.getCertificate()).thenReturn(mRsaCert); + when(mCipher.doFinal(any(byte[].class))).thenAnswer(new Answer() { + + @Override + public byte[] answer(InvocationOnMock invocation) throws Throwable { + return (byte[]) invocation.getArguments()[0]; + } + }); + + /* Mock some AES specifics. */ + KeyStore.SecretKeyEntry aesKey = mock(KeyStore.SecretKeyEntry.class); + whenNew(KeyGenParameterSpec.Builder.class).withAnyArguments().thenReturn(mAesBuilder); + when(mAesBuilder.setBlockModes(anyString())).thenReturn(mAesBuilder); + when(mAesBuilder.setEncryptionPaddings(anyString())).thenReturn(mAesBuilder); + when(mAesBuilder.setKeySize(anyInt())).thenReturn(mAesBuilder); + when(mAesBuilder.setKeyValidityForOriginationEnd(any(Date.class))).thenReturn(mAesBuilder); + when(mAesBuilder.build()).thenReturn(mock(KeyGenParameterSpec.class)); + when(mKeyStore.getEntry(argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return String.valueOf(argument).contains(CIPHER_AES); + } + }), any(KeyStore.ProtectionParameter.class))).thenReturn(aesKey); + when(mCryptoFactory.getKeyGenerator(anyString(), anyString())).thenReturn(mock(CryptoUtils.IKeyGenerator.class)); + final byte[] mockInitVector = "IV".getBytes(); + when(mCipher.getBlockSize()).thenReturn(mockInitVector.length); + when(mCipher.getIV()).thenReturn(mockInitVector); + when(mCipher.doFinal(any(byte[].class), anyInt(), anyInt())).thenAnswer(new Answer() { + + @Override + public byte[] answer(InvocationOnMock invocation) throws Throwable { + byte[] input = (byte[]) invocation.getArguments()[0]; + int offset = (int) invocation.getArguments()[1]; + int length = (int) invocation.getArguments()[2]; + byte[] data = new byte[length]; + System.arraycopy(input, offset, data, 0, length); + return data; + } + }); + + /* Mock ciphers. */ + when(mCryptoFactory.getCipher(anyString(), anyString())).thenReturn(mCipher); + } + + @Test + public void initCryptoConstants() { + assertNotNull(new CryptoConstants()); + } + + @Test + public void nullData() { + CryptoUtils cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1); + assertNull(cryptoUtils.encrypt(null)); + CryptoUtils.DecryptedData nullDecryptedData = cryptoUtils.decrypt(null); + assertNull(nullDecryptedData.getDecryptedData()); + assertNull(nullDecryptedData.getNewEncryptedData()); + } + + private void verifyNoCrypto(int apiLevel) throws Exception { + CryptoUtils cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, apiLevel); + String encrypted = cryptoUtils.encrypt("anything"); + assertEquals("None" + ALGORITHM_DATA_SEPARATOR + "anything", encrypted); + CryptoUtils.DecryptedData decryptedData = cryptoUtils.decrypt(encrypted); + assertEquals("anything", decryptedData.getDecryptedData()); + assertNull(decryptedData.getNewEncryptedData()); + } + + @Test + public void noCryptoInIceCreamSandwich() throws Exception { + verifyNoCrypto(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1); + verifyStatic(never()); + KeyStore.getInstance(anyString()); + } + + @Test + public void keyStoreNotFound() throws Exception { + when(KeyStore.getInstance(ANDROID_KEY_STORE)).thenThrow(new KeyStoreException()); + verifyNoCrypto(Build.VERSION_CODES.KITKAT); + verifyStatic(); + KeyStore.getInstance(anyString()); + } + + @Test + public void rsaFailsToLoadWhenPreferred() throws Exception { + when(KeyPairGenerator.getInstance(anyString(), anyString())).thenThrow(new NoSuchAlgorithmException()); + verifyNoCrypto(Build.VERSION_CODES.KITKAT); + verifyStatic(); + KeyStore.getInstance(anyString()); + } + + @Test + public void aesFailsToLoadWhenPreferred() throws Exception { + when(mCryptoFactory.getKeyGenerator(anyString(), anyString())).thenThrow(new NoSuchAlgorithmException()); + verifyRsaPreferred(Build.VERSION_CODES.M); + } + + @Test + public void decryptUnknownAlgorithm() throws Exception { + CryptoUtils cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1); + CryptoUtils.DecryptedData data = cryptoUtils.decrypt("rot13:caesar"); + assertEquals("rot13:caesar", data.getDecryptedData()); + assertNull(data.getNewEncryptedData()); + data = cryptoUtils.decrypt(":"); + assertEquals(":", data.getDecryptedData()); + assertNull(data.getNewEncryptedData()); + } + + @Test + public void failsToEncrypt() throws Exception { + CryptoUtils cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, Build.VERSION_CODES.KITKAT); + when(mCipher.doFinal(any(byte[].class))).thenThrow(new BadPaddingException()); + String data = "anythingThatWouldMakeTheCipherFailForSomeReason"; + String encryptedData = cryptoUtils.encrypt(data); + assertEquals(data, encryptedData); + CryptoUtils.DecryptedData decryptedData = cryptoUtils.decrypt(encryptedData); + assertEquals(data, decryptedData.getDecryptedData()); + assertNull(decryptedData.getNewEncryptedData()); + } + + private void verifyRsaPreferred(int apiLevel) throws Exception { + CryptoUtils cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, apiLevel); + String encrypted = cryptoUtils.encrypt("anything"); + assertEquals(CIPHER_RSA + "/" + RSA_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "anything", encrypted); + CryptoUtils.DecryptedData decryptedData = cryptoUtils.decrypt(encrypted); + assertEquals("anything", decryptedData.getDecryptedData()); + assertNull(decryptedData.getNewEncryptedData()); + + /* Test old data encryption upgrade. */ + CryptoUtils.DecryptedData oldDecryptedData = cryptoUtils.decrypt("None:oldData"); + assertEquals("oldData", oldDecryptedData.getDecryptedData()); + assertEquals(CIPHER_RSA + "/" + RSA_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "oldData", oldDecryptedData.getNewEncryptedData()); + + /* Check we can still read data after expiration. */ + doThrow(new CertificateExpiredException()).doNothing().when(mRsaCert).checkValidity(); + decryptedData = cryptoUtils.decrypt(encrypted); + assertEquals("anything", decryptedData.getDecryptedData()); + assertNull(decryptedData.getNewEncryptedData()); + + /* But encrypt will use another cert. */ + encrypted = cryptoUtils.encrypt("anything"); + assertEquals(CIPHER_RSA + "/" + RSA_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "anything", encrypted); + + /* Verify another cert was created. */ + ArgumentCaptor alias = ArgumentCaptor.forClass(String.class); + verify(mRsaBuilder, times(2)).setAlias(alias.capture()); + String alias0 = alias.getAllValues().get(0); + String alias1 = alias.getAllValues().get(1); + assertNotEquals(alias0, alias1); + verify(mKeyStore).getEntry(alias1, null); + + /* Count how many times alias0 was used to test interactions after more easily... */ + alias = ArgumentCaptor.forClass(String.class); + verify(mKeyStore, atLeastOnce()).getEntry(alias.capture(), any(KeyStore.ProtectionParameter.class)); + int alias0count = 0; + for (String aliasValue : alias.getAllValues()) { + if (aliasValue.equals(alias0)) { + alias0count++; + } + } + + /* If we restart crypto utils it must pick up the second cert. */ + when(mKeyStore.containsAlias(alias0)).thenReturn(true); + when(mKeyStore.containsAlias(alias1)).thenReturn(true); + Calendar calendar = Calendar.getInstance(); + when(mKeyStore.getCreationDate(alias0)).thenReturn(calendar.getTime()); + calendar.add(Calendar.YEAR, 1); + when(mKeyStore.getCreationDate(alias1)).thenReturn(calendar.getTime()); + cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, apiLevel); + encrypted = cryptoUtils.encrypt("anything"); + assertEquals(CIPHER_RSA + "/" + RSA_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "anything", encrypted); + + /* Check alias0 no more used and that we used second alias to encrypt that value. */ + verify(mKeyStore, times(alias0count)).getEntry(alias0, null); + verify(mKeyStore, times(2)).getEntry(alias1, null); + + /* Roll over a second time. */ + doThrow(new CertificateExpiredException()).doNothing().when(mRsaCert).checkValidity(); + encrypted = cryptoUtils.encrypt("anything"); + assertEquals(CIPHER_RSA + "/" + RSA_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "anything", encrypted); + + /* Verify another cert was created with reusing first alias name, deleting old one. */ + alias = ArgumentCaptor.forClass(String.class); + verify(mRsaBuilder, times(3)).setAlias(alias.capture()); + assertNotEquals(alias0, alias1); + assertEquals(alias0, alias.getAllValues().get(2)); + verify(mKeyStore).deleteEntry(alias0); + verify(mKeyStore, times(alias0count + 1)).getEntry(alias0, null); + verify(mKeyStore, times(3)).getEntry(alias1, null); + + /* Check that it will reload alias0 again after restart. */ + calendar.add(Calendar.YEAR, 1); + when(mKeyStore.getCreationDate(alias0)).thenReturn(calendar.getTime()); + cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, apiLevel); + encrypted = cryptoUtils.encrypt("anything"); + assertEquals(CIPHER_RSA + "/" + RSA_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "anything", encrypted); + verify(mKeyStore, times(alias0count + 2)).getEntry(alias0, null); + verify(mKeyStore, times(3)).getEntry(alias1, null); + } + + @Test + public void rsaPreferredInKitKat() throws Exception { + verifyRsaPreferred(Build.VERSION_CODES.KITKAT); + } + + @Test + public void aesPreferredOnM() throws Exception { + + /* Encrypt. */ + CryptoUtils cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, Build.VERSION_CODES.M); + String encrypted = cryptoUtils.encrypt("anything"); + + /* The init vector is encoded alongside data, in the mock setup it's just a word. */ + assertEquals(CIPHER_AES + "/" + AES_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "IV" + "anything", encrypted); + CryptoUtils.DecryptedData decryptedData = cryptoUtils.decrypt(encrypted); + assertEquals("anything", decryptedData.getDecryptedData()); + assertNull(decryptedData.getNewEncryptedData()); + + /* Test old data encryption upgrade. */ + CryptoUtils.DecryptedData oldDecryptedData = cryptoUtils.decrypt("None:oldData"); + assertEquals("oldData", oldDecryptedData.getDecryptedData()); + assertEquals(CIPHER_AES + "/" + AES_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "IV" + "oldData", oldDecryptedData.getNewEncryptedData()); + CryptoUtils.DecryptedData oldDecryptedRsaData = cryptoUtils.decrypt(CIPHER_RSA + "/" + RSA_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "oldRsaData"); + assertEquals("oldRsaData", oldDecryptedRsaData.getDecryptedData()); + assertEquals(CIPHER_AES + "/" + AES_KEY_SIZE + ALGORITHM_DATA_SEPARATOR + "IV" + "oldRsaData", oldDecryptedRsaData.getNewEncryptedData()); + + /* Verify we created the alias only for AES, RSA is read only with existing aliases only. */ + ArgumentCaptor alias = ArgumentCaptor.forClass(String.class); + verify(mKeyStore).containsAlias(alias.capture()); + assertTrue(alias.getValue().contains(CIPHER_AES)); + } +} + From b50c5d43aee9e41955c4286c2c7939b5bdb203c9 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 6 Mar 2017 18:28:57 -0800 Subject: [PATCH 113/142] Fix code analysis --- .../azure/mobile/crashes/CrashesListener.java | 3 +-- .../azure/mobile/crashes/CrashesTest.java | 12 ++++----- .../crashes/UncaughtExceptionHandlerTest.java | 1 + .../distribute/AbstractDistributeTest.java | 2 +- .../distribute/AppStoreDetectionTest.java | 1 + .../distribute/DeepLinkActivityTest.java | 1 + .../distribute/DistributeDownloadTest.java | 1 + .../DistributeWarnUnknownSourcesTest.java | 1 + .../DatabasePersistenceAndroidTest.java | 10 +++---- .../mobile/utils/crypto/CryptoAesHandler.java | 2 +- .../mobile/utils/crypto/CryptoHandler.java | 3 +-- .../utils/crypto/CryptoNoOpHandler.java | 2 +- .../mobile/utils/crypto/CryptoRsaHandler.java | 2 +- .../mobile/utils/crypto/CryptoUtils.java | 27 +++++++++---------- .../mobile/utils/storage/StorageHelper.java | 2 +- .../azure/mobile/MobileCenterTest.java | 2 +- .../DatabasePersistenceAsyncTest.java | 2 +- .../crypto/CryptoDefaultFactoryTest.java | 2 +- .../azure/mobile/utils/crypto/CryptoTest.java | 4 +-- 19 files changed, 41 insertions(+), 39 deletions(-) diff --git a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/CrashesListener.java b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/CrashesListener.java index 60438967cb..128a6cd15f 100644 --- a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/CrashesListener.java +++ b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/CrashesListener.java @@ -3,7 +3,6 @@ import android.support.annotation.UiThread; import android.support.annotation.WorkerThread; -import com.microsoft.azure.mobile.crashes.model.ErrorAttachment; import com.microsoft.azure.mobile.crashes.model.ErrorReport; /** @@ -30,7 +29,7 @@ public interface CrashesListener { @UiThread boolean shouldAwaitUserConfirmation(); - /** + /* * Called to get additional information to be attached to a crash report before sending. * Attachment is an optional so this method can also return null. * diff --git a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java index 3318eb58a2..9b169b3937 100644 --- a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java +++ b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/CrashesTest.java @@ -93,11 +93,11 @@ public class CrashesTest { private Looper mMockLooper; - private static void assertErrorEquals(ManagedErrorLog errorLog, Throwable throwable, ErrorReport report) { + private static void assertErrorEquals(ManagedErrorLog errorLog, ErrorReport report) { assertNotNull(report); assertEquals(errorLog.getId().toString(), report.getId()); assertEquals(errorLog.getErrorThreadName(), report.getThreadName()); - assertEquals(throwable, report.getThrowable()); + assertEquals(CrashesTest.EXCEPTION, report.getThrowable()); assertEquals(errorLog.getToffset() - errorLog.getAppLaunchTOffset(), report.getAppStartTime().getTime()); assertEquals(errorLog.getToffset(), report.getAppErrorTime().getTime()); assertEquals(errorLog.getDevice(), report.getDevice()); @@ -651,17 +651,17 @@ public void getChannelListener() throws IOException, ClassNotFoundException { Crashes.setListener(new AbstractCrashesListener() { @Override public void onBeforeSending(ErrorReport report) { - assertErrorEquals(mErrorLog, EXCEPTION, report); + assertErrorEquals(mErrorLog, report); } @Override public void onSendingSucceeded(ErrorReport report) { - assertErrorEquals(mErrorLog, EXCEPTION, report); + assertErrorEquals(mErrorLog, report); } @Override public void onSendingFailed(ErrorReport report, Exception e) { - assertErrorEquals(mErrorLog, EXCEPTION, report); + assertErrorEquals(mErrorLog, report); } }); @@ -777,7 +777,7 @@ public void buildErrorReport() throws IOException, ClassNotFoundException { Crashes crashes = Crashes.getInstance(); ErrorReport report = crashes.buildErrorReport(mErrorLog); - assertErrorEquals(mErrorLog, EXCEPTION, report); + assertErrorEquals(mErrorLog, report); mErrorLog.setId(UUIDUtils.randomUUID()); report = crashes.buildErrorReport(mErrorLog); diff --git a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java index 9201b8add0..bd471a84af 100644 --- a/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java +++ b/sdk/mobile-center-crashes/src/test/java/com/microsoft/azure/mobile/crashes/UncaughtExceptionHandlerTest.java @@ -119,6 +119,7 @@ public void registerWorks() { } @Test + @SuppressWarnings("WeakerAccess") public void handleExceptionAndPassOn() { mExceptionHandler.register(); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java index 25da232a1f..9380ac2a82 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java @@ -42,7 +42,7 @@ import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; -@SuppressWarnings("WeakerAccess") +@SuppressWarnings({"WeakerAccess", "CanBeFinal"}) @PrepareForTest({Distribute.class, PreferencesStorage.class, MobileCenterLog.class, MobileCenter.class, NetworkStateHelper.class, BrowserUtils.class, UUIDUtils.class, ReleaseDetails.class, TextUtils.class, CryptoUtils.class, InstallerUtils.class, Toast.class}) public class AbstractDistributeTest { diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java index 8d2fe44b13..a621a15038 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AppStoreDetectionTest.java @@ -19,6 +19,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@SuppressWarnings("CanBeFinal") @RunWith(PowerMockRunner.class) public class AppStoreDetectionTest { diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java index 9d1ce00612..753f6b485d 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DeepLinkActivityTest.java @@ -20,6 +20,7 @@ import static org.powermock.api.mockito.PowerMockito.verifyStatic; @RunWith(PowerMockRunner.class) +@SuppressWarnings("CanBeFinal") @PrepareForTest(Distribute.class) public class DeepLinkActivityTest { diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java index f7d32e2542..878881c555 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java @@ -77,6 +77,7 @@ import static org.powermock.api.mockito.PowerMockito.when; import static org.powermock.api.mockito.PowerMockito.whenNew; +@SuppressWarnings("CanBeFinal") @PrepareForTest(AsyncTaskUtils.class) public class DistributeDownloadTest extends AbstractDistributeTest { diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java index f0fe9a1be6..664ded5eec 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java @@ -45,6 +45,7 @@ import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; +@SuppressWarnings("CanBeFinal") public class DistributeWarnUnknownSourcesTest extends AbstractDistributeTest { @Mock diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/persistence/DatabasePersistenceAndroidTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/persistence/DatabasePersistenceAndroidTest.java index 66ebbe3205..8737146a5e 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/persistence/DatabasePersistenceAndroidTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/persistence/DatabasePersistenceAndroidTest.java @@ -344,11 +344,11 @@ public void getLogs() throws PersistenceException, IOException { persistence.putLog("test", log); /* Get. */ - getAllLogs(persistence, "test", numberOfLogs, sizeForGetLogs); + getAllLogs(persistence, numberOfLogs, sizeForGetLogs); /* Clear ids, we should be able to get the logs again in the same sequence. */ persistence.clearPendingLogState(); - getAllLogs(persistence, "test", numberOfLogs, sizeForGetLogs); + getAllLogs(persistence, numberOfLogs, sizeForGetLogs); /* Count. */ assertEquals(10, persistence.countLogs("test")); @@ -366,19 +366,19 @@ public void getLogs() throws PersistenceException, IOException { } } - private void getAllLogs(DatabasePersistence persistence, String group, int numberOfLogs, int sizeForGetLogs) { + private void getAllLogs(DatabasePersistence persistence, int numberOfLogs, int sizeForGetLogs) { List outputLogs = new ArrayList<>(); int expected = 0; do { numberOfLogs -= expected; - persistence.getLogs(group, sizeForGetLogs, outputLogs); + persistence.getLogs("test", sizeForGetLogs, outputLogs); expected = Math.min(Math.max(numberOfLogs, 0), sizeForGetLogs); assertEquals(expected, outputLogs.size()); outputLogs.clear(); } while (numberOfLogs > 0); /* Get should be 0 now. */ - persistence.getLogs(group, sizeForGetLogs, outputLogs); + persistence.getLogs("test", sizeForGetLogs, outputLogs); assertEquals(0, outputLogs.size()); } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java index d290363182..75d4f7570d 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java @@ -27,7 +27,7 @@ public String getAlgorithm() { } @Override - public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, String alias, Context context) throws Exception { + public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, String alias, Context context) throws Exception { Calendar writeExpiry = Calendar.getInstance(); writeExpiry.add(Calendar.YEAR, ENCRYPT_KEY_LIFETIME_IN_YEARS); CryptoUtils.IKeyGenerator keyGenerator = cryptoFactory.getKeyGenerator(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoHandler.java index 037b451435..9d2958d120 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoHandler.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoHandler.java @@ -20,12 +20,11 @@ interface CryptoHandler { * Generate a new key. * * @param cryptoFactory crypto factory. - * @param apiLevel Android API level. * @param alias keystore alias. * @param context application context. * @throws Exception if an error occurs. */ - void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, String alias, Context context) throws Exception; + void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, String alias, Context context) throws Exception; /** * Encrypt data. diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoNoOpHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoNoOpHandler.java index 3e85eb222a..9c949dc97d 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoNoOpHandler.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoNoOpHandler.java @@ -15,7 +15,7 @@ public String getAlgorithm() { } @Override - public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, String alias, Context context) { + public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, String alias, Context context) { } @Override diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java index 82ee034b00..a721bda251 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java @@ -41,7 +41,7 @@ public String getAlgorithm() { @Override @SuppressWarnings("deprecation") @SuppressLint({"InlinedApi", "TrulyRandom"}) - public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, String alias, Context context) throws Exception { + public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, String alias, Context context) throws Exception { Calendar writeExpiry = Calendar.getInstance(); writeExpiry.add(Calendar.YEAR, ENCRYPT_KEY_LIFETIME_IN_YEARS); KeyPairGenerator generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java index d3c8989c35..a2f0cbf295 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java @@ -22,7 +22,6 @@ import javax.crypto.Cipher; import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; import static com.microsoft.azure.mobile.MobileCenter.LOG_TAG; import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ALGORITHM_DATA_SEPARATOR; @@ -50,8 +49,8 @@ public void init(AlgorithmParameterSpec parameters) throws Exception { } @Override - public SecretKey generateKey() { - return keyGenerator.generateKey(); + public void generateKey() { + keyGenerator.generateKey(); } }; } @@ -133,7 +132,7 @@ public String getProvider() { /** * Supported crypto handlers. Ordered, first one is the preferred one. */ - private Map mCryptoHandlers = new LinkedHashMap<>(); + private final Map mCryptoHandlers = new LinkedHashMap<>(); /** * Init. @@ -228,7 +227,7 @@ private void registerHandler(@NonNull CryptoHandler handler) throws Exception { /* If it's the first time we use the preferred handler, create the alias. */ if (mCryptoHandlers.isEmpty() && !mKeyStore.containsAlias(alias)) { MobileCenterLog.debug(LOG_TAG, "Creating alias: " + alias); - handler.generateKey(mCryptoFactory, mApiLevel, alias, mContext); + handler.generateKey(mCryptoFactory, alias, mContext); } /* Register the handler. */ @@ -294,7 +293,7 @@ public String encrypt(@Nullable String data) { /* Generate new key. */ MobileCenterLog.debug(LOG_TAG, "Creating alias: " + newAlias); - handler.generateKey(mCryptoFactory, mApiLevel, newAlias, mContext); + handler.generateKey(mCryptoFactory, newAlias, mContext); /* And encrypt using that new key. */ return encrypt(data); @@ -373,7 +372,7 @@ interface IKeyGenerator { /** * Adapt {@link KeyGenerator#generateKey()}. */ - SecretKey generateKey(); + void generateKey(); } /** @@ -389,6 +388,7 @@ interface ICipher { /** * Adapt {@link Cipher#init(int, Key, AlgorithmParameterSpec)}. */ + @SuppressWarnings("SameParameterValue") void init(int opMode, Key key, AlgorithmParameterSpec params) throws Exception; /** @@ -430,14 +430,13 @@ interface ICipher { private static class CryptoHandlerEntry { /** - * Keystore alias index, 0 or 1. + * Crypto handler. */ - int mAliasIndex; - + final CryptoHandler mCryptoHandler; /** - * Crypto handler. + * Keystore alias index, 0 or 1. */ - CryptoHandler mCryptoHandler; + int mAliasIndex; /** * Init. @@ -456,12 +455,12 @@ public static class DecryptedData { /** * Decrypted data. */ - String mDecryptedData; + final String mDecryptedData; /** * Better encrypted data. */ - String mNewEncryptedData; + final String mNewEncryptedData; /** * Init. diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/storage/StorageHelper.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/storage/StorageHelper.java index 2ef516283c..1b4c8542e4 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/storage/StorageHelper.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/storage/StorageHelper.java @@ -180,7 +180,7 @@ public static void putInt(@NonNull String key, int value) { * @param key The key for which the value is to be retrieved. * @return The value of {@code key} or 0L if key is not set. */ - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"}) public static long getLong(@NonNull String key) { return getLong(key, 0L); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index 51e41237db..0a6bdd95bf 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -58,7 +58,7 @@ import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "CanBeFinal"}) @PrepareForTest({ MobileCenter.class, MobileCenter.UncaughtExceptionHandler.class, diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/persistence/DatabasePersistenceAsyncTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/persistence/DatabasePersistenceAsyncTest.java index 6135773985..22af0e7617 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/persistence/DatabasePersistenceAsyncTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/persistence/DatabasePersistenceAsyncTest.java @@ -35,7 +35,7 @@ import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "CanBeFinal"}) @PrepareForTest(DatabasePersistenceAsync.class) public class DatabasePersistenceAsyncTest { diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java index 0494ad868a..513a4c26d5 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java @@ -22,7 +22,7 @@ public class CryptoDefaultFactoryTest { @Test public void coverNoOpHandlerGenerate() throws Exception { - new CryptoNoOpHandler().generateKey(CryptoUtils.DEFAULT_CRYPTO_FACTORY, 0, null, null); + new CryptoNoOpHandler().generateKey(CryptoUtils.DEFAULT_CRYPTO_FACTORY, null, null); } @Test diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java index 68379d3630..be71796bd5 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java @@ -59,7 +59,7 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressLint("NewApi") -@SuppressWarnings("deprecation") +@SuppressWarnings({"deprecation", "CanBeFinal"}) @PowerMockIgnore({"javax.security.auth.x500.*"}) @PrepareForTest({KeyStore.class, KeyPairGenerator.class, Base64.class, CryptoUtils.class, CryptoRsaHandler.class, CryptoAesHandler.class}) public class CryptoTest { @@ -187,7 +187,7 @@ public void nullData() { assertNull(nullDecryptedData.getNewEncryptedData()); } - private void verifyNoCrypto(int apiLevel) throws Exception { + private void verifyNoCrypto(int apiLevel) { CryptoUtils cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, apiLevel); String encrypted = cryptoUtils.encrypt("anything"); assertEquals("None" + ALGORITHM_DATA_SEPARATOR + "anything", encrypted); From f24f672d52952e07e95311aaa7b72316ec43bc11 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 6 Mar 2017 19:00:15 -0800 Subject: [PATCH 114/142] Fix AES provider parameter passing --- .../azure/mobile/utils/crypto/CryptoAesHandler.java | 5 +++-- .../azure/mobile/utils/crypto/CryptoConstants.java | 8 ++++---- .../azure/mobile/utils/crypto/CryptoRsaHandler.java | 8 ++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java index 75d4f7570d..a2543b813c 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoAesHandler.java @@ -15,6 +15,7 @@ import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_KEY_STORE; import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.CIPHER_AES; import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ENCRYPT_KEY_LIFETIME_IN_YEARS; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.PROVIDER_ANDROID_M; import static javax.crypto.Cipher.DECRYPT_MODE; import static javax.crypto.Cipher.ENCRYPT_MODE; @@ -43,7 +44,7 @@ public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, String alias, @Override public byte[] encrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] input) throws Exception { - CryptoUtils.ICipher cipher = cryptoFactory.getCipher(CIPHER_AES, null); + CryptoUtils.ICipher cipher = cryptoFactory.getCipher(CIPHER_AES, PROVIDER_ANDROID_M); cipher.init(ENCRYPT_MODE, ((KeyStore.SecretKeyEntry) keyStoreEntry).getSecretKey()); byte[] cipherIV = cipher.getIV(); byte[] output = cipher.doFinal(input); @@ -55,7 +56,7 @@ public byte[] encrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, Ke @Override public byte[] decrypt(CryptoUtils.ICryptoFactory cryptoFactory, int apiLevel, KeyStore.Entry keyStoreEntry, byte[] data) throws Exception { - CryptoUtils.ICipher cipher = cryptoFactory.getCipher(CIPHER_AES, null); + CryptoUtils.ICipher cipher = cryptoFactory.getCipher(CIPHER_AES, PROVIDER_ANDROID_M); int blockSize = cipher.getBlockSize(); IvParameterSpec ivParameterSpec = new IvParameterSpec(data, 0, blockSize); cipher.init(DECRYPT_MODE, ((KeyStore.SecretKeyEntry) keyStoreEntry).getSecretKey(), ivParameterSpec); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoConstants.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoConstants.java index 022f2289c0..628247622c 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoConstants.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoConstants.java @@ -64,12 +64,12 @@ class CryptoConstants { static final String CIPHER_RSA = KeyProperties.KEY_ALGORITHM_RSA + "/" + KeyProperties.BLOCK_MODE_ECB + "/" + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1; /** - * RSA provider when running on old devices. + * Cipher provider when running on old devices. */ - static final String ANDROID_RSA_OLD_PROVIDER = "AndroidOpenSSL"; + static final String PROVIDER_ANDROID_OLD = "AndroidOpenSSL"; /** - * RSA provider when running on M devices. + * Cipher provider when running on M devices. */ - static final String ANDROID_RSA_M_PROVIDER = "AndroidKeyStoreBCWorkaround"; + static final String PROVIDER_ANDROID_M = "AndroidKeyStoreBCWorkaround"; } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java index a721bda251..6076dd6d19 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoRsaHandler.java @@ -19,10 +19,10 @@ import javax.security.auth.x500.X500Principal; import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_KEY_STORE; -import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_RSA_M_PROVIDER; -import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ANDROID_RSA_OLD_PROVIDER; import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.CIPHER_RSA; import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.ENCRYPT_KEY_LIFETIME_IN_YEARS; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.PROVIDER_ANDROID_M; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.PROVIDER_ANDROID_OLD; import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.RSA_KEY_SIZE; import static javax.crypto.Cipher.DECRYPT_MODE; import static javax.crypto.Cipher.ENCRYPT_MODE; @@ -62,9 +62,9 @@ public void generateKey(CryptoUtils.ICryptoFactory cryptoFactory, String alias, private CryptoUtils.ICipher getCipher(CryptoUtils.ICryptoFactory cipherFactory, int apiLevel) throws Exception { String provider; if (apiLevel >= Build.VERSION_CODES.M) { - provider = ANDROID_RSA_M_PROVIDER; + provider = PROVIDER_ANDROID_M; } else { - provider = ANDROID_RSA_OLD_PROVIDER; + provider = PROVIDER_ANDROID_OLD; } return cipherFactory.getCipher(CIPHER_RSA, provider); } From 364a7a89ccec04980249e9d0b1991776020132be Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Tue, 7 Mar 2017 15:37:45 +0300 Subject: [PATCH 115/142] Add start service log --- .../mobile/AbstractMobileCenterService.java | 6 +- .../microsoft/azure/mobile/MobileCenter.java | 27 +++++++ .../ingestion/models/StartServiceLog.java | 81 +++++++++++++++++++ .../ingestion/models/json/JSONUtils.java | 23 ++++++ .../models/json/StartServiceLogFactory.java | 12 +++ 5 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLog.java create mode 100644 sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/StartServiceLogFactory.java diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java index 32356d21eb..af424672b0 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java @@ -25,16 +25,16 @@ public abstract class AbstractMobileCenterService implements MobileCenterService /** * Number of metrics queue items which will trigger synchronization. */ - private static final int DEFAULT_TRIGGER_COUNT = 50; + static final int DEFAULT_TRIGGER_COUNT = 50; /** * Maximum time interval in milliseconds after which a synchronize will be triggered, regardless of queue size. */ - private static final int DEFAULT_TRIGGER_INTERVAL = 3 * 1000; + static final int DEFAULT_TRIGGER_INTERVAL = 3 * 1000; /** * Maximum number of requests being sent for the group. */ - private static final int DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS = 3; + static final int DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS = 3; /** * Channel instance. diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index d449bcfeac..ea1b3950c8 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -9,6 +9,7 @@ import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.channel.DefaultChannel; +import com.microsoft.azure.mobile.ingestion.models.StartServiceLog; import com.microsoft.azure.mobile.ingestion.models.WrapperSdk; import com.microsoft.azure.mobile.ingestion.models.json.DefaultLogSerializer; import com.microsoft.azure.mobile.ingestion.models.json.LogFactory; @@ -20,16 +21,27 @@ import com.microsoft.azure.mobile.utils.ShutdownHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import static android.util.Log.VERBOSE; +import static com.microsoft.azure.mobile.AbstractMobileCenterService.DEFAULT_TRIGGER_COUNT; +import static com.microsoft.azure.mobile.AbstractMobileCenterService.DEFAULT_TRIGGER_INTERVAL; +import static com.microsoft.azure.mobile.AbstractMobileCenterService.DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS; import static com.microsoft.azure.mobile.utils.MobileCenterLog.NONE; public class MobileCenter { + /** + * Group for sending logs. + */ + @VisibleForTesting + static final String CORE_GROUP = "group_core"; + /** * TAG used in logging. */ @@ -311,6 +323,7 @@ private synchronized boolean instanceConfigure(Application application, String a mLogSerializer = new DefaultLogSerializer(); mChannel = new DefaultChannel(application, appSecret, mLogSerializer); mChannel.setEnabled(enabled); + mChannel.addGroup(CORE_GROUP, DEFAULT_TRIGGER_COUNT, DEFAULT_TRIGGER_INTERVAL, DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS, null); if (mLogUrl != null) mChannel.setLogUrl(mLogUrl); MobileCenterLog.logAssert(LOG_TAG, "Mobile Center SDK configured successfully."); @@ -336,17 +349,20 @@ private final synchronized void startServices(Class startedServices = new ArrayList<>(); for (Class service : services) { if (service == null) { MobileCenterLog.warn(LOG_TAG, "Skipping null service, please check your varargs/array does not contain any null reference."); } else { try { startService((MobileCenterService) service.getMethod("getInstance").invoke(null)); + startedServices.add(service.getSimpleName()); } catch (Exception e) { MobileCenterLog.error(LOG_TAG, "Failed to get service instance '" + service.getName() + "', skipping it.", e); } } } + queueStartService(startedServices); } /** @@ -378,6 +394,17 @@ private final synchronized void configureAndStartServices(Application applicatio startServices(services); } + /** + * Send started services. + * + * @param services started services. + */ + private synchronized void queueStartService(List services) { + StartServiceLog startServiceLog = new StartServiceLog(); + startServiceLog.setServices(services); + mChannel.enqueue(startServiceLog, CORE_GROUP); + } + /** * Implements {@link #isEnabled()}. */ diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLog.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLog.java new file mode 100644 index 0000000000..735ec8d46b --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLog.java @@ -0,0 +1,81 @@ +package com.microsoft.azure.mobile.ingestion.models; + +import com.microsoft.azure.mobile.ingestion.models.json.JSONUtils; + +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONStringer; + +import java.util.List; + +/** + * Describe a MobileCenter.start call from the SDK. + */ +public class StartServiceLog extends AbstractLog { + + /** + * Log type. + */ + public static final String TYPE = "start_service"; + + private static final String SERVICES = "services"; + + /** + * The list of services of the MobileCenter start call. + */ + private List services; + + @Override + public String getType() { + return TYPE; + } + + /** + * Get the services value. + * + * @return the services value + */ + public List getServices() { + return this.services; + } + + /** + * Set the services value. + * + * @param services the services value to set + */ + public void setServices(List services) { + this.services = services; + } + + @Override + public void read(JSONObject object) throws JSONException { + super.read(object); + setServices(JSONUtils.readStringArray(object, SERVICES)); + } + + @Override + public void write(JSONStringer writer) throws JSONException { + super.write(writer); + JSONUtils.writeStringArray(writer, SERVICES, getServices()); + } + + @Override + @SuppressWarnings("SimplifiableIfStatement") + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + StartServiceLog that = (StartServiceLog) o; + + return services != null ? services.equals(that.services) : that.services == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (services != null ? services.hashCode() : 0); + return result; + } +} diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java index 926b8835a7..be246814cd 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java @@ -9,6 +9,7 @@ import org.json.JSONObject; import org.json.JSONStringer; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -65,6 +66,18 @@ public static List readArray(JSONObject object, String key, return array; } + public static List readStringArray(JSONObject object, String key) throws JSONException { + + JSONArray jArray = object.optJSONArray(key); + if (jArray == null) + return null; + List array = new ArrayList<>(jArray.length()); + for (int i = 0; i < jArray.length(); i++) { + array.add(jArray.getString(i)); + } + return array; + } + public static void write(JSONStringer writer, String key, Object value) throws JSONException { if (value != null) writer.key(key).value(value); @@ -90,4 +103,14 @@ public static void writeArray(JSONStringer writer, String key, List value) throws JSONException { + if (value != null) { + writer.key(key).array(); + for (String i : value) { + writer.value(i); + } + writer.endArray(); + } + } } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/StartServiceLogFactory.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/StartServiceLogFactory.java new file mode 100644 index 0000000000..fa80764c11 --- /dev/null +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/StartServiceLogFactory.java @@ -0,0 +1,12 @@ +package com.microsoft.azure.mobile.ingestion.models.json; + +import com.microsoft.azure.mobile.ingestion.models.Log; +import com.microsoft.azure.mobile.ingestion.models.StartServiceLog; + +public class StartServiceLogFactory implements LogFactory { + + @Override + public Log create() { + return new StartServiceLog(); + } +} From 657d5b6a1a5039620b109e473bad6870ae52e305 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Tue, 7 Mar 2017 17:27:44 +0300 Subject: [PATCH 116/142] Add start service log tests --- .../models/json/JSONUtilsAndroidTest.java | 28 ++++++++++ .../models/json/LogSerializerAndroidTest.java | 30 +++++++++++ .../microsoft/azure/mobile/MobileCenter.java | 15 ++++-- .../azure/mobile/MobileCenterTest.java | 47 +++++++++------- .../ingestion/models/StartServiceLogTest.java | 53 +++++++++++++++++++ 5 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLogTest.java diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtilsAndroidTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtilsAndroidTest.java index fda9d5acc4..ffe9b04cbb 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtilsAndroidTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtilsAndroidTest.java @@ -105,6 +105,34 @@ public void writeReadArray() throws JSONException { assertNull(writer.toString()); } + @Test + public void writeReadStringArray() throws JSONException { + + /* Create a test list. */ + final List list = new ArrayList<>(); + list.add("FIRST"); + list.add("SECOND"); + + /* Write to JSON object. */ + JSONStringer writer = new JSONStringer(); + writer.object(); + JSONUtils.writeStringArray(writer, "list", list); + writer.endObject(); + + /* Convert to string. */ + String json = writer.toString(); + assertNotNull(json); + + /* Read a JSON object and verify. */ + JSONObject object = new JSONObject(json); + assertEquals(list, JSONUtils.readStringArray(object, "list")); + + /* Test null value. */ + writer = new JSONStringer(); + JSONUtils.writeStringArray(writer, "null", null); + assertNull(writer.toString()); + } + @Test public void readKeyNotExists() throws JSONException { diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/LogSerializerAndroidTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/LogSerializerAndroidTest.java index d2bbb5cdb1..707ccaf75d 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/LogSerializerAndroidTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/LogSerializerAndroidTest.java @@ -3,13 +3,18 @@ import com.microsoft.azure.mobile.AndroidTestUtils; import com.microsoft.azure.mobile.ingestion.models.Log; import com.microsoft.azure.mobile.ingestion.models.LogContainer; +import com.microsoft.azure.mobile.ingestion.models.StartServiceLog; +import com.microsoft.azure.mobile.utils.UUIDUtils; import junit.framework.Assert; import org.json.JSONException; import org.junit.Test; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; +import java.util.UUID; import static com.microsoft.azure.mobile.ingestion.models.json.MockLog.MOCK_LOG_TYPE; import static com.microsoft.azure.mobile.test.TestUtils.TAG; @@ -49,4 +54,29 @@ public void deserializeUnknownType() throws JSONException { android.util.Log.v(TAG, payload); new DefaultLogSerializer().deserializeLog(payload); } + + @Test + public void startServiceLog() throws JSONException { + LogContainer expectedContainer = new LogContainer(); + List logs = new ArrayList<>(); + { + StartServiceLog log = new StartServiceLog(); + List services = new ArrayList<>(); + services.add("FIRST"); + services.add("SECOND"); + log.setServices(services); + logs.add(log); + } + expectedContainer.setLogs(logs); + UUID sid = UUIDUtils.randomUUID(); + for (Log log : logs) { + log.setSid(sid); + } + + LogSerializer serializer = new DefaultLogSerializer(); + serializer.addLogFactory(StartServiceLog.TYPE, new StartServiceLogFactory()); + String payload = serializer.serializeContainer(expectedContainer); + LogContainer actualContainer = serializer.deserializeContainer(payload); + Assert.assertEquals(expectedContainer, actualContainer); + } } \ No newline at end of file diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index ea1b3950c8..3b57a32327 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -14,6 +14,7 @@ import com.microsoft.azure.mobile.ingestion.models.json.DefaultLogSerializer; import com.microsoft.azure.mobile.ingestion.models.json.LogFactory; import com.microsoft.azure.mobile.ingestion.models.json.LogSerializer; +import com.microsoft.azure.mobile.ingestion.models.json.StartServiceLogFactory; import com.microsoft.azure.mobile.utils.DeviceInfoHelper; import com.microsoft.azure.mobile.utils.IdHelper; import com.microsoft.azure.mobile.utils.MobileCenterLog; @@ -321,6 +322,7 @@ private synchronized boolean instanceConfigure(Application application, String a /* Init channel. */ mLogSerializer = new DefaultLogSerializer(); + mLogSerializer.addLogFactory(StartServiceLog.TYPE, new StartServiceLogFactory()); mChannel = new DefaultChannel(application, appSecret, mLogSerializer); mChannel.setEnabled(enabled); mChannel.addGroup(CORE_GROUP, DEFAULT_TRIGGER_COUNT, DEFAULT_TRIGGER_INTERVAL, DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS, null); @@ -355,14 +357,16 @@ private final synchronized void startServices(Class 0) + queueStartService(startedServices); } /** @@ -370,10 +374,10 @@ private final synchronized void startServices(Class logFactories = service.getLogFactories(); if (logFactories != null) { @@ -385,6 +389,7 @@ private synchronized void startService(@NonNull MobileCenterService service) { if (isInstanceEnabled()) mApplication.registerActivityLifecycleCallbacks(service); MobileCenterLog.info(LOG_TAG, service.getClass().getSimpleName() + " service started."); + return true; } @SafeVarargs diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index ac77020bae..28d32a50bb 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -9,6 +9,7 @@ import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.channel.DefaultChannel; +import com.microsoft.azure.mobile.ingestion.models.StartServiceLog; import com.microsoft.azure.mobile.ingestion.models.WrapperSdk; import com.microsoft.azure.mobile.ingestion.models.json.LogFactory; import com.microsoft.azure.mobile.utils.DeviceInfoHelper; @@ -62,9 +63,10 @@ @PrepareForTest({ MobileCenter.class, MobileCenter.UncaughtExceptionHandler.class, - Channel.class, + DefaultChannel.class, Constants.class, MobileCenterLog.class, + StartServiceLog.class, StorageHelper.class, StorageHelper.PreferencesStorage.class, IdHelper.class, @@ -83,14 +85,20 @@ public class MobileCenterTest { @Mock private Iterator mDataBaseScannerIterator; + @Mock + private DefaultChannel mChannel; + private Application application; @Before - public void setUp() { + public void setUp() throws Exception { MobileCenter.unsetInstance(); DummyService.sharedInstance = null; AnotherDummyService.sharedInstance = null; + mChannel = mock(DefaultChannel.class); + whenNew(DefaultChannel.class).withAnyArguments().thenReturn(mChannel); + application = mock(Application.class); when(application.getApplicationContext()).thenReturn(application); @@ -182,6 +190,7 @@ public void useDummyServiceTest() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); + verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } @Test @@ -198,6 +207,7 @@ public void useDummyServiceTestSplitCall() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); + verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } @Test @@ -212,6 +222,7 @@ public void configureAndStartTwiceTest() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); + verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } @Test @@ -227,9 +238,9 @@ public void configureTwiceTest() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); + verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } - @Test public void startTwoServicesTest() { MobileCenter.start(application, DUMMY_APP_SECRET, DummyService.class, AnotherDummyService.class); @@ -248,6 +259,7 @@ public void startTwoServicesTest() { verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } + verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } @Test @@ -269,6 +281,7 @@ public void startTwoServicesSplit() { verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } + verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } @Test @@ -291,6 +304,7 @@ public void startTwoServicesSplitEvenMore() { verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } + verify(mChannel, times(2)).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } @Test @@ -311,6 +325,7 @@ public void startTwoServicesWithSomeInvalidReferences() { verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } + verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } @Test @@ -333,6 +348,7 @@ public void startTwoServicesWithSomeInvalidReferencesSplit() { verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } + verify(mChannel, times(2)).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } @Test @@ -358,6 +374,7 @@ public void startServiceTwice() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); + verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); } @Test @@ -381,9 +398,7 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { /* Start MobileCenter SDK */ MobileCenter.start(application, DUMMY_APP_SECRET, DummyService.class, AnotherDummyService.class); - Channel channel = mock(Channel.class); MobileCenter mobileCenter = MobileCenter.getInstance(); - mobileCenter.setChannel(channel); /* Verify services are enabled by default */ Set services = mobileCenter.getServices(); @@ -402,7 +417,7 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { } verify(dummyService, never()).setInstanceEnabled(anyBoolean()); verify(anotherDummyService, never()).setInstanceEnabled(anyBoolean()); - verify(channel).setEnabled(true); + verify(mChannel, times(2)).setEnabled(true); /* Verify disabling base disables all services */ MobileCenter.setEnabled(false); @@ -414,7 +429,7 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { verify(anotherDummyService).setInstanceEnabled(false); verify(application).unregisterActivityLifecycleCallbacks(dummyService); verify(application).unregisterActivityLifecycleCallbacks(anotherDummyService); - verify(channel).setEnabled(false); + verify(mChannel).setEnabled(false); /* Verify re-enabling base re-enables all services */ MobileCenter.setEnabled(true); @@ -426,7 +441,7 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { verify(anotherDummyService).setInstanceEnabled(true); verify(application, times(2)).registerActivityLifecycleCallbacks(dummyService); verify(application, times(2)).registerActivityLifecycleCallbacks(anotherDummyService); - verify(channel, times(2)).setEnabled(true); + verify(mChannel, times(3)).setEnabled(true); /* Verify that disabling one service leaves base and other services enabled */ dummyService.setInstanceEnabled(false); @@ -442,7 +457,7 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { } verify(dummyService, times(2)).setInstanceEnabled(true); verify(anotherDummyService).setInstanceEnabled(true); - verify(channel, times(3)).setEnabled(true); + verify(mChannel, times(4)).setEnabled(true); /* Enable service after the SDK is disabled. */ MobileCenter.setEnabled(false); @@ -455,7 +470,7 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { PowerMockito.verifyStatic(); MobileCenterLog.error(eq(MobileCenter.LOG_TAG), anyString()); assertFalse(MobileCenter.isEnabled()); - verify(channel, times(2)).setEnabled(false); + verify(mChannel, times(2)).setEnabled(false); /* Disable back via main class. */ MobileCenter.setEnabled(false); @@ -463,7 +478,7 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable { for (MobileCenterService service : services) { assertFalse(service.isInstanceEnabled()); } - verify(channel, times(3)).setEnabled(false); + verify(mChannel, times(3)).setEnabled(false); /* Check factories / channel only once interactions. */ verify(dummyService).getLogFactories(); @@ -486,9 +501,7 @@ public void enableBeforeConfiguredTest() { public void disablePersisted() { when(StorageHelper.PreferencesStorage.getBoolean(KEY_ENABLED, true)).thenReturn(false); MobileCenter.start(application, DUMMY_APP_SECRET, DummyService.class, AnotherDummyService.class); - Channel channel = mock(Channel.class); MobileCenter mobileCenter = MobileCenter.getInstance(); - mobileCenter.setChannel(channel); /* Verify services are enabled by default but MobileCenter is disabled. */ assertFalse(MobileCenter.isEnabled()); @@ -511,9 +524,7 @@ public void disablePersisted() { public void disablePersistedAndDisable() { when(StorageHelper.PreferencesStorage.getBoolean(KEY_ENABLED, true)).thenReturn(false); MobileCenter.start(application, DUMMY_APP_SECRET, DummyService.class, AnotherDummyService.class); - Channel channel = mock(Channel.class); MobileCenter mobileCenter = MobileCenter.getInstance(); - mobileCenter.setChannel(channel); /* Its already disabled so disable should have no effect on MobileCenter but should disable services. */ MobileCenter.setEnabled(false); @@ -661,19 +672,17 @@ public void uncaughtExceptionHandler() { verifyStatic(); Thread.setDefaultUncaughtExceptionHandler(eq(handler)); - Channel channel = mock(Channel.class); Thread thread = mock(Thread.class); Throwable exception = mock(Throwable.class); - MobileCenter.getInstance().setChannel(channel); handler.uncaughtException(thread, exception); - verify(channel).shutdown(); + verify(mChannel).shutdown(); verify(defaultUncaughtExceptionHandler).uncaughtException(eq(thread), eq(exception)); MobileCenter.setEnabled(false); verifyStatic(); Thread.setDefaultUncaughtExceptionHandler(eq(defaultUncaughtExceptionHandler)); handler.uncaughtException(thread, exception); - verify(channel, times(1)).shutdown(); + verify(mChannel, times(1)).shutdown(); when(Thread.getDefaultUncaughtExceptionHandler()).thenReturn(null); MobileCenter.setEnabled(true); diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLogTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLogTest.java new file mode 100644 index 0000000000..e07d3053fe --- /dev/null +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLogTest.java @@ -0,0 +1,53 @@ +package com.microsoft.azure.mobile.ingestion.models; + + +import com.microsoft.azure.mobile.test.TestUtils; +import com.microsoft.azure.mobile.utils.UUIDUtils; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static com.microsoft.azure.mobile.test.TestUtils.checkEquals; +import static com.microsoft.azure.mobile.test.TestUtils.checkNotEquals; + +@SuppressWarnings("unused") +public class StartServiceLogTest { + + @Test + public void compareDifferentType() { + TestUtils.compareSelfNullClass(new StartServiceLog()); + } + + @Test + public void compare() { + + /* Empty objects. */ + StartServiceLog a = new StartServiceLog(); + StartServiceLog b = new StartServiceLog(); + checkEquals(a, b); + checkEquals(a.getType(), StartServiceLog.TYPE); + + UUID sid = UUIDUtils.randomUUID(); + a.setSid(sid); + checkNotEquals(a, b); + b.setSid(sid); + checkEquals(a, b); + + /* Services. */ + List services = new ArrayList<>(); + services.add("FIRST"); + services.add("SECOND"); + a.setServices(services); + checkEquals(a.getServices(), services); + checkNotEquals(a, b); + b.setServices(new ArrayList()); + checkNotEquals(a, b); + b.setServices(services); + checkEquals(a, b); + } + + +} \ No newline at end of file From 7485ee8524193ffdbb1d16d1fd351a4393bd7898 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Tue, 7 Mar 2017 17:30:39 +0300 Subject: [PATCH 117/142] Remove blank line --- .../microsoft/azure/mobile/ingestion/models/json/JSONUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java index be246814cd..b9ea73e4be 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java @@ -67,7 +67,6 @@ public static List readArray(JSONObject object, String key, } public static List readStringArray(JSONObject object, String key) throws JSONException { - JSONArray jArray = object.optJSONArray(key); if (jArray == null) return null; From 850f2c334fdb8ca7ff004c873f2b6520b69cf014 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Tue, 7 Mar 2017 18:00:45 +0300 Subject: [PATCH 118/142] Fix code coverage --- .../azure/mobile/ingestion/models/json/JSONUtilsAndroidTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtilsAndroidTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtilsAndroidTest.java index ffe9b04cbb..dda2b699a3 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtilsAndroidTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtilsAndroidTest.java @@ -126,6 +126,7 @@ public void writeReadStringArray() throws JSONException { /* Read a JSON object and verify. */ JSONObject object = new JSONObject(json); assertEquals(list, JSONUtils.readStringArray(object, "list")); + assertNull(JSONUtils.readStringArray(object, "missing")); /* Test null value. */ writer = new JSONStringer(); From d61e774755779277af6dcb5252927b4b0abb91bf Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 7 Mar 2017 17:47:23 -0800 Subject: [PATCH 119/142] Don't show update dialog for incompatible release (api level) --- .../mobile/distribute/ReleaseDetailsTest.java | 63 ++++++++++++++++++- .../azure/mobile/distribute/Distribute.java | 9 ++- .../mobile/distribute/ReleaseDetails.java | 23 +++++-- .../DistributeBeforeDownloadTest.java | 51 +++++++++++++++ 4 files changed, 138 insertions(+), 8 deletions(-) diff --git a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java index 3c08a12cc3..4561e45ce9 100644 --- a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java +++ b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java @@ -18,6 +18,7 @@ public void parse() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -26,6 +27,7 @@ public void parse() throws JSONException { assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); + assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); } @@ -35,6 +37,7 @@ public void missingId() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails.parse(json); @@ -47,18 +50,20 @@ public void invalidId() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails.parse(json); } @Test - public void acceptIdAsStringAsAndroidJsonDoesThat() throws JSONException { + public void acceptIdAsString() throws JSONException { String json = "{" + "id: '42'," + "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -67,6 +72,7 @@ public void acceptIdAsStringAsAndroidJsonDoesThat() throws JSONException { assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); + assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); } @@ -76,6 +82,7 @@ public void missingVersion() throws JSONException { "id: 42," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails.parse(json); @@ -88,6 +95,7 @@ public void invalidVersion() throws JSONException { "version: true," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails.parse(json); @@ -99,6 +107,7 @@ public void missingShortVersion() throws JSONException { "id: 42," + "version: '14'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails.parse(json); @@ -110,6 +119,7 @@ public void missingReleaseNotes() throws JSONException { "id: 42," + "version: '14'," + "short_version: '2.1.5'," + + "android_min_api_level: 19," + "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -118,6 +128,7 @@ public void missingReleaseNotes() throws JSONException { assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertNull(releaseDetails.getReleaseNotes()); + assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); } @@ -127,6 +138,7 @@ public void nullReleaseNotes() throws JSONException { "id: 42," + "version: '14'," + "release_notes: null," + + "android_min_api_level: 19," + "short_version: '2.1.5'," + "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + "}"; @@ -136,9 +148,55 @@ public void nullReleaseNotes() throws JSONException { assertEquals(14, releaseDetails.getVersion()); assertEquals("2.1.5", releaseDetails.getShortVersion()); assertNull(releaseDetails.getReleaseNotes()); + assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); } + @Test(expected = JSONException.class) + public void missingApiLevel() throws JSONException { + String json = "{" + + "id: 42," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + + @Test + public void acceptApiLevelAsString() throws JSONException { + String json = "{" + + "id: 42," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: '19'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails releaseDetails = ReleaseDetails.parse(json); + assertNotNull(releaseDetails); + assertEquals(42, releaseDetails.getId()); + assertEquals(14, releaseDetails.getVersion()); + assertEquals("2.1.5", releaseDetails.getShortVersion()); + assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); + assertEquals(19, releaseDetails.getMinApiLevel()); + assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + } + + @Test(expected = JSONException.class) + public void invalidApiLevel() throws JSONException { + String json = "{" + + "id: 42," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: '4.0.3'," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + @Test(expected = JSONException.class) public void missingDownloadUrl() throws JSONException { String json = "{" + @@ -146,6 +204,7 @@ public void missingDownloadUrl() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19" + "}"; ReleaseDetails.parse(json); } @@ -157,6 +216,7 @@ public void missingDownloadUrlScheme() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + "download_url: 'someFile'" + "}"; ReleaseDetails.parse(json); @@ -169,6 +229,7 @@ public void invalidDownloadUrlScheme() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + "download_url: 'ftp://someFile'" + "}"; ReleaseDetails.parse(json); diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java index 456cd98567..9e9bdaf94e 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java @@ -55,7 +55,6 @@ import static android.content.Context.DOWNLOAD_SERVICE; import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE; import static android.util.Log.VERBOSE; -import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DEFAULT_API_URL; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DEFAULT_INSTALL_URL; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_COMPLETED; @@ -79,6 +78,7 @@ import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.distribute.DistributeConstants.SERVICE_NAME; import static com.microsoft.azure.mobile.distribute.DistributeConstants.UPDATE_SETUP_PATH_FORMAT; +import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; /** * Distribute service. @@ -728,7 +728,10 @@ private synchronized void handleApiCallSuccess(Object releaseCallId, ReleaseDeta int releaseId = releaseDetails.getId(); if (releaseId == PreferencesStorage.getInt(PREFERENCE_KEY_IGNORED_RELEASE_ID, INVALID_RELEASE_IDENTIFIER)) { MobileCenterLog.debug(LOG_TAG, "This release is ignored id=" + releaseId); - } else { + } + + /* Check minimum Android API level. */ + else if (Build.VERSION.SDK_INT >= releaseDetails.getMinApiLevel()) { /* Check version code is equals or higher and hash is different. */ MobileCenterLog.debug(LOG_TAG, "Check version code."); @@ -749,6 +752,8 @@ private synchronized void handleApiCallSuccess(Object releaseCallId, ReleaseDeta } catch (PackageManager.NameNotFoundException e) { MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); } + } else { + MobileCenterLog.info(LOG_TAG, "This device is not compatible with the latest release."); } /* If update dialog was not shown or scheduled, complete workflow. */ diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java index 19135a91bf..7870f050c2 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java @@ -22,6 +22,8 @@ class ReleaseDetails { private static final String DOWNLOAD_URL = "download_url"; + private static final String MIN_API_LEVEL = "android_min_api_level"; + /** * ID identifying this unique release. */ @@ -46,6 +48,11 @@ class ReleaseDetails { */ private String releaseNotes; + /** + * The release's minimum required Android API level. + */ + private int minApiLevel; + /** * The URL that hosts the binary for this release. */ @@ -62,13 +69,10 @@ static ReleaseDetails parse(String json) throws JSONException { JSONObject object = new JSONObject(json); ReleaseDetails releaseDetails = new ReleaseDetails(); releaseDetails.id = object.getInt(ID); - try { - releaseDetails.version = Integer.parseInt(object.getString(VERSION)); - } catch (NumberFormatException e) { - throw new JSONException(e.getMessage()); - } + releaseDetails.version = object.getInt(VERSION); releaseDetails.shortVersion = object.getString(SHORT_VERSION); releaseDetails.releaseNotes = object.isNull(RELEASE_NOTES) ? null : object.getString(RELEASE_NOTES); + releaseDetails.minApiLevel = object.getInt(MIN_API_LEVEL); releaseDetails.downloadUrl = Uri.parse(object.getString(DOWNLOAD_URL)); String scheme = releaseDetails.downloadUrl.getScheme(); if (scheme == null || !scheme.startsWith("http")) { @@ -115,6 +119,15 @@ String getReleaseNotes() { return releaseNotes; } + /** + * Get the minApiLevel value. + * + * @return the minApiLevel value + */ + int getMinApiLevel() { + return minApiLevel; + } + /** * Get the downloadUrl value. * diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java index a36060eba7..7c0d67aa76 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java @@ -4,14 +4,17 @@ import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageManager; +import android.os.Build; import com.microsoft.azure.mobile.channel.Channel; import com.microsoft.azure.mobile.http.HttpClient; import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.test.TestUtils; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; +import org.junit.After; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; @@ -85,6 +88,47 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); } + @Test + public void moreRecentWithIncompatibleMinApiLevel() throws Exception { + + /* Mock we already have token. */ + TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.JELLY_BEAN_MR2); + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + HashMap headers = new HashMap<>(); + headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn(4); + when(releaseDetails.getVersion()).thenReturn(7); + when(releaseDetails.getMinApiLevel()).thenReturn(Build.VERSION_CODES.KITKAT); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify on incompatible version we complete workflow. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + verify(mDialogBuilder, never()).create(); + verify(mDialog, never()).show(); + + /* After that if we resume app nothing happens. */ + Distribute.getInstance().onActivityPaused(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); + verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } + @Test public void olderVersionCode() throws Exception { @@ -105,7 +149,9 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(5); + when(releaseDetails.getMinApiLevel()).thenReturn(Build.VERSION_CODES.M); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.N_MR1); /* Trigger call. */ Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); @@ -691,4 +737,9 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verifyStatic(never()); AsyncTaskUtils.execute(anyString(), any(Distribute.DownloadTask.class), Mockito.anyVararg()); } + + @After + public void tearDown() throws Exception { + TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", 0); + } } From 75fbaab5570743d3369c38281a269a224fc71a0d Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 8 Mar 2017 11:14:26 -0800 Subject: [PATCH 120/142] Address minor comments from #349 discussion --- .../microsoft/azure/mobile/utils/crypto/CryptoUtils.java | 2 +- .../mobile/utils/crypto/CryptoDefaultFactoryTest.java | 7 ++++--- .../microsoft/azure/mobile/utils/crypto/CryptoTest.java | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java index a2f0cbf295..4ee6fe390a 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/crypto/CryptoUtils.java @@ -236,7 +236,7 @@ private void registerHandler(@NonNull CryptoHandler handler) throws Exception { } @NonNull - private String getAlias(CryptoHandler handler, int index) { + private String getAlias(@NonNull CryptoHandler handler, int index) { return KEYSTORE_ALIAS_PREFIX + ALIAS_SEPARATOR + index + ALIAS_SEPARATOR + handler.getAlgorithm(); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java index 513a4c26d5..6b8d645719 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoDefaultFactoryTest.java @@ -9,6 +9,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; +import static com.microsoft.azure.mobile.utils.crypto.CryptoConstants.CHARSET; import static javax.crypto.Cipher.DECRYPT_MODE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; @@ -44,11 +45,11 @@ public void coverDefaultCipherParameterPassing() throws Exception { assertEquals(algorithm, encryptCipher.getAlgorithm()); assertEquals(provider, encryptCipher.getProvider()); encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] data = encryptCipher.doFinal("test".getBytes("UTF-8")); + byte[] data = encryptCipher.doFinal("test".getBytes(CHARSET)); CryptoUtils.ICipher decryptCipher = CryptoUtils.DEFAULT_CRYPTO_FACTORY.getCipher(algorithm, provider); decryptCipher.init(DECRYPT_MODE, secretKey, new IvParameterSpec(encryptCipher.getIV())); - assertEquals("test", new String(decryptCipher.doFinal(data), "UTF-8")); - assertEquals("test", new String(decryptCipher.doFinal(data, 0, data.length), "UTF-8")); + assertEquals("test", new String(decryptCipher.doFinal(data), CHARSET)); + assertEquals("test", new String(decryptCipher.doFinal(data, 0, data.length), CHARSET)); assertEquals(decryptCipher.getIV().length, decryptCipher.getBlockSize()); } } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java index be71796bd5..f5278ead28 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/crypto/CryptoTest.java @@ -334,7 +334,7 @@ public void rsaPreferredInKitKat() throws Exception { } @Test - public void aesPreferredOnM() throws Exception { + public void aesPreferredInM() throws Exception { /* Encrypt. */ CryptoUtils cryptoUtils = new CryptoUtils(mContext, mCryptoFactory, Build.VERSION_CODES.M); From 0b1a13505ad3f3ad336998df58be27b5d543483f Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Tue, 7 Mar 2017 18:46:15 -0800 Subject: [PATCH 121/142] Allow update to different hash when version code is the same --- .../mobile/distribute/ReleaseDetailsTest.java | 89 ++++++++++++++++--- .../azure/mobile/distribute/Distribute.java | 9 +- .../mobile/distribute/ReleaseDetails.java | 18 ++++ .../DistributeBeforeDownloadTest.java | 13 ++- 4 files changed, 106 insertions(+), 23 deletions(-) diff --git a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java index 4561e45ce9..9fc65dbad5 100644 --- a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java +++ b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java @@ -19,7 +19,8 @@ public void parse() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); @@ -29,6 +30,7 @@ public void parse() throws JSONException { assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @Test(expected = JSONException.class) @@ -38,7 +40,8 @@ public void missingId() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); } @@ -51,7 +54,8 @@ public void invalidId() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); } @@ -64,7 +68,8 @@ public void acceptIdAsString() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); @@ -74,6 +79,7 @@ public void acceptIdAsString() throws JSONException { assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @Test(expected = JSONException.class) @@ -83,7 +89,8 @@ public void missingVersion() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); } @@ -96,7 +103,8 @@ public void invalidVersion() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); } @@ -108,7 +116,8 @@ public void missingShortVersion() throws JSONException { "version: '14'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); } @@ -120,7 +129,8 @@ public void missingReleaseNotes() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "android_min_api_level: 19," + - "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'https://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); @@ -130,6 +140,7 @@ public void missingReleaseNotes() throws JSONException { assertNull(releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @Test @@ -140,7 +151,8 @@ public void nullReleaseNotes() throws JSONException { "release_notes: null," + "android_min_api_level: 19," + "short_version: '2.1.5'," + - "download_url: 'https://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'https://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); @@ -150,6 +162,7 @@ public void nullReleaseNotes() throws JSONException { assertNull(releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @Test(expected = JSONException.class) @@ -159,7 +172,8 @@ public void missingApiLevel() throws JSONException { "version: '14'," + "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); } @@ -172,7 +186,8 @@ public void acceptApiLevelAsString() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: '19'," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); assertNotNull(releaseDetails); @@ -182,6 +197,7 @@ public void acceptApiLevelAsString() throws JSONException { assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @Test(expected = JSONException.class) @@ -192,7 +208,8 @@ public void invalidApiLevel() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: '4.0.3'," + - "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); } @@ -205,6 +222,7 @@ public void missingDownloadUrl() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19" + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); } @@ -217,7 +235,8 @@ public void missingDownloadUrlScheme() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + - "download_url: 'someFile'" + + "download_url: 'someFile'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); } @@ -230,7 +249,49 @@ public void invalidDownloadUrlScheme() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + - "download_url: 'ftp://someFile'" + + "download_url: 'ftp://someFile'," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + + "}"; + ReleaseDetails.parse(json); + } + + @Test(expected = JSONException.class) + public void missingPackageHashes() throws JSONException { + String json = "{" + + "id: 42," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + + "}"; + ReleaseDetails.parse(json); + } + + @Test(expected = JSONException.class) + public void emptyPackageHashes() throws JSONException { + String json = "{" + + "id: 42," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: []" + + "}"; + ReleaseDetails.parse(json); + } + + @Test(expected = JSONException.class) + public void invalidPackageHashes() throws JSONException { + String json = "{" + + "id: 42," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "package_hashes: '9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60'" + "}"; ReleaseDetails.parse(json); } diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java index 9e9bdaf94e..458bfe77bb 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java @@ -734,7 +734,7 @@ private synchronized void handleApiCallSuccess(Object releaseCallId, ReleaseDeta else if (Build.VERSION.SDK_INT >= releaseDetails.getMinApiLevel()) { /* Check version code is equals or higher and hash is different. */ - MobileCenterLog.debug(LOG_TAG, "Check version code."); + MobileCenterLog.debug(LOG_TAG, "Check if more recent."); PackageManager packageManager = mContext.getPackageManager(); try { PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0); @@ -769,10 +769,9 @@ else if (Build.VERSION.SDK_INT >= releaseDetails.getMinApiLevel()) { * @return true if latest release on server should be used. */ private boolean isMoreRecent(PackageInfo packageInfo, ReleaseDetails releaseDetails) { -// TODO when releaseHash is exposed in JSON. -// if (releaseDetails.getVersion() == packageInfo.versionCode) { -// return !releaseDetails.getReleaseHash().equals(computeHash(mContext, packageInfo)); -// } + if (releaseDetails.getVersion() == packageInfo.versionCode) { + return !releaseDetails.getReleaseHash().equals(computeHash(mContext, packageInfo)); + } return releaseDetails.getVersion() > packageInfo.versionCode; } diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java index 7870f050c2..aeb9f20674 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java @@ -24,6 +24,8 @@ class ReleaseDetails { private static final String MIN_API_LEVEL = "android_min_api_level"; + private static final String PACKAGE_HASHES = "package_hashes"; + /** * ID identifying this unique release. */ @@ -58,6 +60,11 @@ class ReleaseDetails { */ private Uri downloadUrl; + /** + * Release hash. + */ + private String releaseHash; + /** * Parse a JSON string describing release details. * @@ -78,6 +85,7 @@ static ReleaseDetails parse(String json) throws JSONException { if (scheme == null || !scheme.startsWith("http")) { throw new JSONException("Invalid download_url scheme."); } + releaseDetails.releaseHash = object.getJSONArray(PACKAGE_HASHES).getString(0); return releaseDetails; } @@ -137,4 +145,14 @@ int getMinApiLevel() { Uri getDownloadUrl() { return downloadUrl; } + + /** + * Get the release hash value. + * + * @return the releaseHash value. + */ + @NonNull + String getReleaseHash() { + return releaseHash; + } } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java index 7c0d67aa76..b9f7dcb0ed 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java @@ -168,10 +168,13 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { Distribute.getInstance().onActivityPaused(mock(Activity.class)); Distribute.getInstance().onActivityResumed(mock(Activity.class)); verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + + /* Verify release hash was not even considered. */ + verify(releaseDetails, never()).getReleaseHash(); } @Test - public void sameVersionCode() throws Exception { + public void sameVersionCodeSameHash() throws Exception { /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -190,6 +193,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(6); + when(releaseDetails.getReleaseHash()).thenReturn(TEST_HASH); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ @@ -210,7 +214,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } @Test - public void moreRecentVersionWithoutReleaseNotesDialog() throws Exception { + public void moreRecentVersionCodeWithoutReleaseNotesDialog() throws Exception { /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -270,7 +274,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } @Test - public void moreRecentVersionWithReleaseNotesDialog() throws Exception { + public void sameVersionDifferentHashWithReleaseNotesDialog() throws Exception { /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -288,7 +292,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); - when(releaseDetails.getVersion()).thenReturn(7); + when(releaseDetails.getVersion()).thenReturn(6); + when(releaseDetails.getReleaseHash()).thenReturn("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60"); when(releaseDetails.getReleaseNotes()).thenReturn("mock"); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); From b417377059d4d8ed37adfe01e64a5e2e91079731 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 8 Mar 2017 12:07:33 -0800 Subject: [PATCH 122/142] Change some debug messages after #353 comment --- .../com/microsoft/azure/mobile/distribute/Distribute.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java index 458bfe77bb..aca01bcb98 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java @@ -734,7 +734,7 @@ private synchronized void handleApiCallSuccess(Object releaseCallId, ReleaseDeta else if (Build.VERSION.SDK_INT >= releaseDetails.getMinApiLevel()) { /* Check version code is equals or higher and hash is different. */ - MobileCenterLog.debug(LOG_TAG, "Check if more recent."); + MobileCenterLog.debug(LOG_TAG, "Check if latest release is more recent."); PackageManager packageManager = mContext.getPackageManager(); try { PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0); @@ -747,10 +747,10 @@ else if (Build.VERSION.SDK_INT >= releaseDetails.getMinApiLevel()) { } return; } else { - MobileCenterLog.debug(LOG_TAG, "Latest server version is not more recent."); + MobileCenterLog.debug(LOG_TAG, "Latest release is not more recent."); } } catch (PackageManager.NameNotFoundException e) { - MobileCenterLog.error(LOG_TAG, "Could not compare versions.", e); + MobileCenterLog.error(LOG_TAG, "Could not compare release versions.", e); } } else { MobileCenterLog.info(LOG_TAG, "This device is not compatible with the latest release."); From 93268579727144eba37b01f8ee31beabd73c6a9c Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Thu, 9 Mar 2017 14:15:39 +0300 Subject: [PATCH 123/142] Promote `getServiceName` to `MobileCenterService` interface --- .../com/microsoft/azure/mobile/analytics/Analytics.java | 2 +- .../java/com/microsoft/azure/mobile/crashes/Crashes.java | 2 +- .../azure/mobile/AbstractMobileCenterService.java | 7 ------- .../main/java/com/microsoft/azure/mobile/MobileCenter.java | 6 ++++-- .../com/microsoft/azure/mobile/MobileCenterService.java | 7 +++++++ .../azure/mobile/AbstractMobileCenterServiceTest.java | 2 +- .../java/com/microsoft/azure/mobile/MobileCenterTest.java | 6 +++--- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java b/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java index 166b460113..3a93550d73 100644 --- a/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java +++ b/sdk/mobile-center-analytics/src/main/java/com/microsoft/azure/mobile/analytics/Analytics.java @@ -231,7 +231,7 @@ protected String getGroupName() { } @Override - protected String getServiceName() { + public String getServiceName() { return SERVICE_NAME; } diff --git a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java index 64ae27aae2..11d8d2245e 100644 --- a/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java +++ b/sdk/mobile-center-crashes/src/main/java/com/microsoft/azure/mobile/crashes/Crashes.java @@ -379,7 +379,7 @@ protected String getGroupName() { } @Override - protected String getServiceName() { + public String getServiceName() { return SERVICE_NAME; } diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java index af424672b0..8eb28b737a 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java @@ -137,13 +137,6 @@ public Map getLogFactories() { */ protected abstract String getGroupName(); - /** - * Gets a name of the service. - * - * @return The name of the service. - */ - protected abstract String getServiceName(); - /** * Gets a tag of the logger. * diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index 3b57a32327..202303a58c 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -351,14 +351,16 @@ private final synchronized void startServices(Class startedServices = new ArrayList<>(); for (Class service : services) { if (service == null) { MobileCenterLog.warn(LOG_TAG, "Skipping null service, please check your varargs/array does not contain any null reference."); } else { try { - if (startService((MobileCenterService) service.getMethod("getInstance").invoke(null))) { - startedServices.add(service.getSimpleName()); + MobileCenterService serviceInstance = (MobileCenterService) service.getMethod("getInstance").invoke(null); + if (startService(serviceInstance)) { + startedServices.add(serviceInstance.getServiceName()); } } catch (Exception e) { MobileCenterLog.error(LOG_TAG, "Failed to get service instance '" + service.getName() + "', skipping it.", e); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java index c1f15502c1..9f13d92fea 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenterService.java @@ -30,6 +30,13 @@ public interface MobileCenterService extends Application.ActivityLifecycleCallba */ void setInstanceEnabled(boolean enabled); + /** + * Gets a name of the service. + * + * @return The name of the service. + */ + String getServiceName(); + /** * Factories for logs sent by this service. * diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java index 12402b9023..844ca0e2c6 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java @@ -46,7 +46,7 @@ protected String getGroupName() { } @Override - protected String getServiceName() { + public String getServiceName() { return "Test"; } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index 28d32a50bb..75cc7e6af7 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -711,7 +711,7 @@ protected String getGroupName() { } @Override - protected String getServiceName() { + public String getServiceName() { return "Dummy"; } @@ -746,7 +746,7 @@ protected String getGroupName() { } @Override - protected String getServiceName() { + public String getServiceName() { return "AnotherDummy"; } @@ -764,7 +764,7 @@ protected String getGroupName() { } @Override - protected String getServiceName() { + public String getServiceName() { return "Invalid"; } From 6f7f690a24aa43dfe3deb634e21973e0f418c30a Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Thu, 9 Mar 2017 14:17:13 +0300 Subject: [PATCH 124/142] Moved constants to a constant file to avoid coupling --- .../mobile/AbstractMobileCenterService.java | 17 +++-------------- .../com/microsoft/azure/mobile/Constants.java | 14 ++++++++++++++ .../microsoft/azure/mobile/MobileCenter.java | 6 +++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java index 8eb28b737a..d85648dad7 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/AbstractMobileCenterService.java @@ -12,6 +12,9 @@ import java.util.Map; +import static com.microsoft.azure.mobile.Constants.DEFAULT_TRIGGER_COUNT; +import static com.microsoft.azure.mobile.Constants.DEFAULT_TRIGGER_INTERVAL; +import static com.microsoft.azure.mobile.Constants.DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS; import static com.microsoft.azure.mobile.MobileCenter.LOG_TAG; import static com.microsoft.azure.mobile.utils.PrefStorageConstants.KEY_ENABLED; @@ -22,20 +25,6 @@ public abstract class AbstractMobileCenterService implements MobileCenterService */ private static final String PREFERENCE_KEY_SEPARATOR = "_"; - /** - * Number of metrics queue items which will trigger synchronization. - */ - static final int DEFAULT_TRIGGER_COUNT = 50; - - /** - * Maximum time interval in milliseconds after which a synchronize will be triggered, regardless of queue size. - */ - static final int DEFAULT_TRIGGER_INTERVAL = 3 * 1000; - /** - * Maximum number of requests being sent for the group. - */ - static final int DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS = 3; - /** * Channel instance. */ diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/Constants.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/Constants.java index 9428dcb53c..049c02235d 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/Constants.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/Constants.java @@ -22,6 +22,20 @@ public class Constants { */ public static boolean APPLICATION_DEBUGGABLE = false; + /** + * Number of metrics queue items which will trigger synchronization. + */ + public static final int DEFAULT_TRIGGER_COUNT = 50; + + /** + * Maximum time interval in milliseconds after which a synchronize will be triggered, regardless of queue size. + */ + public static final int DEFAULT_TRIGGER_INTERVAL = 3 * 1000; + /** + * Maximum number of requests being sent for the group. + */ + public static final int DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS = 3; + /** * Initializes constants from the given context. The context is used to set * the package name, version code, and the files path. diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java index 202303a58c..286c60bd0e 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/MobileCenter.java @@ -30,9 +30,9 @@ import java.util.UUID; import static android.util.Log.VERBOSE; -import static com.microsoft.azure.mobile.AbstractMobileCenterService.DEFAULT_TRIGGER_COUNT; -import static com.microsoft.azure.mobile.AbstractMobileCenterService.DEFAULT_TRIGGER_INTERVAL; -import static com.microsoft.azure.mobile.AbstractMobileCenterService.DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS; +import static com.microsoft.azure.mobile.Constants.DEFAULT_TRIGGER_COUNT; +import static com.microsoft.azure.mobile.Constants.DEFAULT_TRIGGER_INTERVAL; +import static com.microsoft.azure.mobile.Constants.DEFAULT_TRIGGER_MAX_PARALLEL_REQUESTS; import static com.microsoft.azure.mobile.utils.MobileCenterLog.NONE; public class MobileCenter { From d520ab168579d9e27eef291e4776f2f6f82ae3f4 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Thu, 9 Mar 2017 14:17:56 +0300 Subject: [PATCH 125/142] Fix parameter name --- .../azure/mobile/ingestion/models/json/JSONUtils.java | 8 ++++---- .../mobile/ingestion/models/StartServiceLogTest.java | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java index b9ea73e4be..3ffb7b70e9 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/models/json/JSONUtils.java @@ -103,11 +103,11 @@ public static void writeArray(JSONStringer writer, String key, List value) throws JSONException { - if (value != null) { + public static void writeStringArray(JSONStringer writer, String key, List values) throws JSONException { + if (values != null) { writer.key(key).array(); - for (String i : value) { - writer.value(i); + for (String value : values) { + writer.value(value); } writer.endArray(); } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLogTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLogTest.java index e07d3053fe..226d550673 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLogTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/ingestion/models/StartServiceLogTest.java @@ -48,6 +48,4 @@ public void compare() { b.setServices(services); checkEquals(a, b); } - - } \ No newline at end of file From bf60b5995407cc752c5236c7f2d11e72b65ebb53 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Thu, 9 Mar 2017 14:19:41 +0300 Subject: [PATCH 126/142] Add check what arguments are passed to `setServices` --- .../azure/mobile/MobileCenterTest.java | 63 ++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java index 75cc7e6af7..2d8c683610 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/MobileCenterTest.java @@ -29,8 +29,10 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; @@ -88,6 +90,9 @@ public class MobileCenterTest { @Mock private DefaultChannel mChannel; + @Mock + private StartServiceLog mStartServiceLog; + private Application application; @Before @@ -99,6 +104,9 @@ public void setUp() throws Exception { mChannel = mock(DefaultChannel.class); whenNew(DefaultChannel.class).withAnyArguments().thenReturn(mChannel); + mStartServiceLog = mock(StartServiceLog.class); + whenNew(StartServiceLog.class).withAnyArguments().thenReturn(mStartServiceLog); + application = mock(Application.class); when(application.getApplicationContext()).thenReturn(application); @@ -190,7 +198,10 @@ public void useDummyServiceTest() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); - verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + verify(mChannel).enqueue(eq(mStartServiceLog), eq(MobileCenter.CORE_GROUP)); + List services = new ArrayList<>(); + services.add(service.getServiceName()); + verify(mStartServiceLog).setServices(eq(services)); } @Test @@ -207,7 +218,10 @@ public void useDummyServiceTestSplitCall() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); - verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + verify(mChannel).enqueue(eq(mStartServiceLog), eq(MobileCenter.CORE_GROUP)); + List services = new ArrayList<>(); + services.add(service.getServiceName()); + verify(mStartServiceLog).setServices(eq(services)); } @Test @@ -222,7 +236,10 @@ public void configureAndStartTwiceTest() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); - verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + verify(mChannel).enqueue(eq(mStartServiceLog), eq(MobileCenter.CORE_GROUP)); + List services = new ArrayList<>(); + services.add(service.getServiceName()); + verify(mStartServiceLog).setServices(eq(services)); } @Test @@ -238,7 +255,10 @@ public void configureTwiceTest() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); - verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + verify(mChannel).enqueue(eq(mStartServiceLog), eq(MobileCenter.CORE_GROUP)); + List services = new ArrayList<>(); + services.add(service.getServiceName()); + verify(mStartServiceLog).setServices(eq(services)); } @Test @@ -259,7 +279,11 @@ public void startTwoServicesTest() { verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } - verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + verify(mChannel).enqueue(eq(mStartServiceLog), eq(MobileCenter.CORE_GROUP)); + List services = new ArrayList<>(); + services.add(DummyService.getInstance().getServiceName()); + services.add(AnotherDummyService.getInstance().getServiceName()); + verify(mStartServiceLog).setServices(eq(services)); } @Test @@ -281,7 +305,11 @@ public void startTwoServicesSplit() { verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } - verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + verify(mChannel).enqueue(eq(mStartServiceLog), eq(MobileCenter.CORE_GROUP)); + List services = new ArrayList<>(); + services.add(DummyService.getInstance().getServiceName()); + services.add(AnotherDummyService.getInstance().getServiceName()); + verify(mStartServiceLog).setServices(eq(services)); } @Test @@ -305,6 +333,12 @@ public void startTwoServicesSplitEvenMore() { verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } verify(mChannel, times(2)).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + List services1 = new ArrayList<>(); + services1.add(DummyService.getInstance().getServiceName()); + verify(mStartServiceLog).setServices(eq(services1)); + List services2 = new ArrayList<>(); + services2.add(DummyService.getInstance().getServiceName()); + verify(mStartServiceLog).setServices(eq(services2)); } @Test @@ -325,7 +359,11 @@ public void startTwoServicesWithSomeInvalidReferences() { verify(AnotherDummyService.getInstance()).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } - verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + verify(mChannel).enqueue(eq(mStartServiceLog), eq(MobileCenter.CORE_GROUP)); + List services = new ArrayList<>(); + services.add(DummyService.getInstance().getServiceName()); + services.add(AnotherDummyService.getInstance().getServiceName()); + verify(mStartServiceLog).setServices(eq(services)); } @Test @@ -349,6 +387,12 @@ public void startTwoServicesWithSomeInvalidReferencesSplit() { verify(application).registerActivityLifecycleCallbacks(AnotherDummyService.getInstance()); } verify(mChannel, times(2)).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + List services1 = new ArrayList<>(); + services1.add(DummyService.getInstance().getServiceName()); + verify(mStartServiceLog).setServices(eq(services1)); + List services2 = new ArrayList<>(); + services2.add(DummyService.getInstance().getServiceName()); + verify(mStartServiceLog).setServices(eq(services2)); } @Test @@ -374,7 +418,10 @@ public void startServiceTwice() { verify(service).getLogFactories(); verify(service).onChannelReady(any(Context.class), notNull(Channel.class)); verify(application).registerActivityLifecycleCallbacks(service); - verify(mChannel).enqueue(any(StartServiceLog.class), eq(MobileCenter.CORE_GROUP)); + verify(mChannel).enqueue(eq(mStartServiceLog), eq(MobileCenter.CORE_GROUP)); + List services = new ArrayList<>(); + services.add(DummyService.getInstance().getServiceName()); + verify(mStartServiceLog).setServices(eq(services)); } @Test From 7ab68ac035d37511d94bd75ef83e65f0b76eed27 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Thu, 9 Mar 2017 14:20:18 +0300 Subject: [PATCH 127/142] Bump version to 0.6.0 --- versions.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.gradle b/versions.gradle index e049ae3d92..d4a534ebde 100644 --- a/versions.gradle +++ b/versions.gradle @@ -2,7 +2,7 @@ ext { versionCode = 14 - versionName = '0.5.1' + versionName = '0.6.0' minSdkVersion = 15 targetSdkVersion = 25 compileSdkVersion = 25 From a67a3fc6b21d5fc2ad782e2cc1942546752e5d74 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Thu, 9 Mar 2017 15:02:17 +0300 Subject: [PATCH 128/142] Add cause check in recoverable error detection --- .../azure/mobile/ingestion/http/HttpUtilsAndroidTest.java | 3 +++ .../microsoft/azure/mobile/ingestion/http/HttpUtils.java | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java index 2122da8fb3..544973e362 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java @@ -3,6 +3,7 @@ import org.junit.Test; import java.io.EOFException; +import java.io.IOException; import java.io.InterruptedIOException; import java.net.MalformedURLException; import java.net.PortUnreachableException; @@ -37,6 +38,8 @@ public void isRecoverableErrorTest() { assertTrue(isRecoverableError(new UnknownHostException())); assertTrue(isRecoverableError(new RejectedExecutionException())); assertFalse(isRecoverableError(new MalformedURLException())); + assertFalse(isRecoverableError(new IOException())); + assertTrue(isRecoverableError(new IOException(new EOFException()))); for (int i = 0; i <= 4; i++) assertTrue(isRecoverableError(new HttpException(500 + i))); for (int i = 2; i <= 6; i++) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java index 93659e684a..ad06178573 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java @@ -57,6 +57,14 @@ public static boolean isRecoverableError(Throwable t) { if (type.isAssignableFrom(t.getClass())) return true; + /* Check the cause. */ + Throwable cause = t.getCause(); + if (cause != null) { + for (Class type : RECOVERABLE_EXCEPTIONS) + if (type.isAssignableFrom(cause.getClass())) + return true; + } + /* Check corner cases. */ if (t instanceof SSLException) { String message = t.getMessage(); From 566bbe8d4577245199afb5fc419e8ca4f31462a0 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Thu, 9 Mar 2017 15:23:33 +0300 Subject: [PATCH 129/142] Add another test case --- .../azure/mobile/ingestion/http/HttpUtilsAndroidTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java index 544973e362..d0bcff130d 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java @@ -40,6 +40,7 @@ public void isRecoverableErrorTest() { assertFalse(isRecoverableError(new MalformedURLException())); assertFalse(isRecoverableError(new IOException())); assertTrue(isRecoverableError(new IOException(new EOFException()))); + assertFalse(isRecoverableError(new IOException(new Exception()))); for (int i = 0; i <= 4; i++) assertTrue(isRecoverableError(new HttpException(500 + i))); for (int i = 2; i <= 6; i++) From 4157b82148f57532bbb101948d182a7aa7e0355c Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Fri, 10 Mar 2017 12:52:35 +0300 Subject: [PATCH 130/142] Fix code coverage --- .../main/java/com/microsoft/azure/mobile/Constants.java | 1 + .../azure/mobile/channel/DefaultChannelTest.java | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/Constants.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/Constants.java index 049c02235d..843fdcbaaf 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/Constants.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/Constants.java @@ -31,6 +31,7 @@ public class Constants { * Maximum time interval in milliseconds after which a synchronize will be triggered, regardless of queue size. */ public static final int DEFAULT_TRIGGER_INTERVAL = 3 * 1000; + /** * Maximum number of requests being sent for the group. */ diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java index 760a8623c2..6a2794c061 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/channel/DefaultChannelTest.java @@ -69,6 +69,10 @@ public void invalidGroup() throws Persistence.PersistenceException { verify(log, never()).setDevice(any(Device.class)); verify(log, never()).setToffset(anyLong()); verify(persistence, never()).putLog(TEST_GROUP, log); + + /* Trying remove group that not registered. */ + channel.removeGroup(TEST_GROUP); + verify(mHandler, never()).removeCallbacks(any(Runnable.class)); } @Test @@ -159,6 +163,10 @@ public void analyticsSuccess() throws Persistence.PersistenceException, Interrup /* Check total timers. */ verify(mHandler, times(3)).postDelayed(any(Runnable.class), eq(BATCH_TIME_INTERVAL)); verify(mHandler).removeCallbacks(any(Runnable.class)); + + /* Check channel clear clear */ + channel.clear(TEST_GROUP); + verify(mockPersistence).deleteLogs(eq(TEST_GROUP)); } @NonNull From 8316f56d3d118d0d8de621713776aa2fe0a70578 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 10 Mar 2017 12:39:08 -0800 Subject: [PATCH 131/142] Fix merge issue --- .../java/com/microsoft/azure/mobile/distribute/Distribute.java | 2 +- .../azure/mobile/AbstractMobileCenterServiceTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java index ac2bdedfbc..6b3698b617 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java @@ -312,7 +312,7 @@ protected String getGroupName() { } @Override - protected String getServiceName() { + public String getServiceName() { return SERVICE_NAME; } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java index 980bf42eef..1615747bbd 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/AbstractMobileCenterServiceTest.java @@ -172,13 +172,14 @@ public void getGroupName() { @Test public void optionalGroup() { service = new AbstractMobileCenterService() { + @Override protected String getGroupName() { return null; } @Override - protected String getServiceName() { + public String getServiceName() { return "Test"; } From 3f8102f35abfce566ea1db5b5c1322d71f41ccf5 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 13 Mar 2017 18:39:46 -0700 Subject: [PATCH 132/142] Document distribute specific permission --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 80cf20a6b2..159b88251e 100644 --- a/README.md +++ b/README.md @@ -288,11 +288,13 @@ You can easily provide your own resource strings if you'd like to localize the t * **What Android permissions are required for the SDK?** Depending on the services you use, the following permissions are required: - - Analytics, Crashes: `INTERNET`, `ACCESS_NETWORK_STATE` + - All services: `INTERNET`, `ACCESS_NETWORK_STATE` + - Distribute: `REQUEST_INSTALL_PACKAGES` Required permissions are automatically merged into your app's manifest by the SDK. - + None of these permissions require user approval at runtime, those are all install time permissions. + ## 9. Contributing We're looking forward to your contributions via pull requests. From 6ef754c098acd77bb2537bccd3c4f3af5602c887 Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Wed, 15 Mar 2017 13:45:44 -0700 Subject: [PATCH 133/142] Remove 401 status code from recoverable error list --- .../azure/mobile/ingestion/http/HttpUtilsAndroidTest.java | 3 +-- .../com/microsoft/azure/mobile/ingestion/http/HttpUtils.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java index d0bcff130d..1c148d89b8 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java @@ -43,12 +43,11 @@ public void isRecoverableErrorTest() { assertFalse(isRecoverableError(new IOException(new Exception()))); for (int i = 0; i <= 4; i++) assertTrue(isRecoverableError(new HttpException(500 + i))); - for (int i = 2; i <= 6; i++) + for (int i = 1; i <= 6; i++) assertFalse(isRecoverableError(new HttpException(400 + i))); assertTrue(isRecoverableError(new HttpException(408))); assertFalse(isRecoverableError(new HttpException(413))); assertTrue(isRecoverableError(new HttpException(429))); - assertTrue(isRecoverableError(new HttpException(401))); assertTrue(isRecoverableError(new SSLException("Write error: ssl=0x59c28f90: I/O error during system call, Connection timed out"))); assertFalse(isRecoverableError(new SSLHandshakeException("java.security.cert.CertPathValidatorException: Trust anchor for certification path not found."))); assertFalse(isRecoverableError(new SSLException(null, new CertPathValidatorException("Trust anchor for certification path not found.")))); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java index ad06178573..2ee142225b 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/ingestion/http/HttpUtils.java @@ -49,7 +49,7 @@ public static boolean isRecoverableError(Throwable t) { if (t instanceof HttpException) { HttpException exception = (HttpException) t; int code = exception.getStatusCode(); - return code >= 500 || code == 408 || code == 429 || code == 401; + return code >= 500 || code == 408 || code == 429; } /* Check for a generic exception to retry. */ From 60473a890175e0d53c71a5a118a0737c1f760a57 Mon Sep 17 00:00:00 2001 From: Jae Lim Date: Wed, 15 Mar 2017 14:14:42 -0700 Subject: [PATCH 134/142] Add 400 to the test --- .../azure/mobile/ingestion/http/HttpUtilsAndroidTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java index 1c148d89b8..e541dac320 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/ingestion/http/HttpUtilsAndroidTest.java @@ -43,7 +43,7 @@ public void isRecoverableErrorTest() { assertFalse(isRecoverableError(new IOException(new Exception()))); for (int i = 0; i <= 4; i++) assertTrue(isRecoverableError(new HttpException(500 + i))); - for (int i = 1; i <= 6; i++) + for (int i = 0; i <= 6; i++) assertFalse(isRecoverableError(new HttpException(400 + i))); assertTrue(isRecoverableError(new HttpException(408))); assertFalse(isRecoverableError(new HttpException(413))); From b49cd61a46600121cebadd7f2c5961bec2266549 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 15 Mar 2017 16:27:16 -0700 Subject: [PATCH 135/142] Implement forced update + refactoring --- README.md | 2 +- .../mobile/distribute/ReleaseDetailsTest.java | 62 ++ .../src/main/AndroidManifest.xml | 1 + .../mobile/distribute/CheckDownloadTask.java | 178 +++++ .../azure/mobile/distribute/Distribute.java | 620 ++++++++---------- .../distribute/DistributeConstants.java | 25 + .../mobile/distribute/DistributeUtils.java | 164 +++++ .../distribute/DownloadManagerReceiver.java | 2 +- .../mobile/distribute/DownloadProgress.java | 39 ++ .../azure/mobile/distribute/DownloadTask.java | 58 ++ .../mobile/distribute/ReleaseDetails.java | 23 +- .../mobile/distribute/RemoveDownloadTask.java | 41 ++ .../src/main/res/values/strings.xml | 3 + .../AbstractDistributeAfterDownloadTest.java | 330 ++++++++++ .../distribute/AbstractDistributeTest.java | 6 + .../DistributeBeforeApiSuccessTest.java | 69 +- .../DistributeBeforeDownloadTest.java | 41 +- .../distribute/DistributeDownloadTest.java | 343 +--------- ...ibuteTest.java => DistributeHttpTest.java} | 2 +- .../DistributeMandatoryDownloadTest.java | 347 ++++++++++ ...antsTest.java => DistributeUtilsTest.java} | 3 +- .../DistributeWarnUnknownSourcesTest.java | 54 +- .../azure/mobile/utils/HandlerUtilsTest.java | 9 +- .../azure/mobile/utils/HandlerUtils.java | 15 +- .../azure/mobile/utils/UUIDUtilsTest.java | 2 - 25 files changed, 1698 insertions(+), 741 deletions(-) create mode 100644 sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/CheckDownloadTask.java create mode 100644 sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeUtils.java create mode 100644 sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadProgress.java create mode 100644 sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadTask.java create mode 100644 sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/RemoveDownloadTask.java create mode 100644 sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeAfterDownloadTest.java rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{DistributeTest.java => DistributeHttpTest.java} (99%) create mode 100644 sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java rename sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/{DistributeConstantsTest.java => DistributeUtilsTest.java} (72%) diff --git a/README.md b/README.md index 159b88251e..a4e979d8f9 100644 --- a/README.md +++ b/README.md @@ -289,7 +289,7 @@ You can easily provide your own resource strings if you'd like to localize the t * **What Android permissions are required for the SDK?** Depending on the services you use, the following permissions are required: - All services: `INTERNET`, `ACCESS_NETWORK_STATE` - - Distribute: `REQUEST_INSTALL_PACKAGES` + - Distribute: `REQUEST_INSTALL_PACKAGES`, `DOWNLOAD_WITHOUT_NOTIFICATION` Required permissions are automatically merged into your app's manifest by the SDK. diff --git a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java index 9fc65dbad5..da4d3ba7f3 100644 --- a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java +++ b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ReleaseDetailsTest.java @@ -6,8 +6,10 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; public class ReleaseDetailsTest { @@ -20,6 +22,7 @@ public void parse() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -30,6 +33,7 @@ public void parse() throws JSONException { assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertFalse(releaseDetails.isMandatoryUpdate()); assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @@ -41,6 +45,7 @@ public void missingId() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); @@ -55,6 +60,7 @@ public void invalidId() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); @@ -69,6 +75,7 @@ public void acceptIdAsString() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -79,6 +86,7 @@ public void acceptIdAsString() throws JSONException { assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertFalse(releaseDetails.isMandatoryUpdate()); assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @@ -90,6 +98,7 @@ public void missingVersion() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); @@ -104,6 +113,7 @@ public void invalidVersion() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); @@ -117,6 +127,7 @@ public void missingShortVersion() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); @@ -130,6 +141,7 @@ public void missingReleaseNotes() throws JSONException { "short_version: '2.1.5'," + "android_min_api_level: 19," + "download_url: 'https://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -140,6 +152,7 @@ public void missingReleaseNotes() throws JSONException { assertNull(releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertFalse(releaseDetails.isMandatoryUpdate()); assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @@ -152,6 +165,7 @@ public void nullReleaseNotes() throws JSONException { "android_min_api_level: 19," + "short_version: '2.1.5'," + "download_url: 'https://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -162,6 +176,7 @@ public void nullReleaseNotes() throws JSONException { assertNull(releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("https://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertFalse(releaseDetails.isMandatoryUpdate()); assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @@ -173,6 +188,7 @@ public void missingApiLevel() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); @@ -187,6 +203,7 @@ public void acceptApiLevelAsString() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: '19'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails releaseDetails = ReleaseDetails.parse(json); @@ -197,6 +214,7 @@ public void acceptApiLevelAsString() throws JSONException { assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); assertEquals(19, releaseDetails.getMinApiLevel()); assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertFalse(releaseDetails.isMandatoryUpdate()); assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); } @@ -209,6 +227,7 @@ public void invalidApiLevel() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: '4.0.3'," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); @@ -236,6 +255,7 @@ public void missingDownloadUrlScheme() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'someFile'," + + "mandatory_update: false," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); @@ -250,6 +270,45 @@ public void invalidDownloadUrlScheme() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'ftp://someFile'," + + "mandatory_update: false," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + + "}"; + ReleaseDetails.parse(json); + } + + @Test + public void mandatoryUpdate() throws JSONException { + String json = "{" + + "id: 42," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: true," + + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + + "}"; + ReleaseDetails releaseDetails = ReleaseDetails.parse(json); + assertNotNull(releaseDetails); + assertEquals(42, releaseDetails.getId()); + assertEquals(14, releaseDetails.getVersion()); + assertEquals("2.1.5", releaseDetails.getShortVersion()); + assertEquals("Fix a critical bug, this text was entered in Mobile Center portal.", releaseDetails.getReleaseNotes()); + assertEquals(19, releaseDetails.getMinApiLevel()); + assertEquals(Uri.parse("http://download.thinkbroadband.com/1GB.zip"), releaseDetails.getDownloadUrl()); + assertTrue(releaseDetails.isMandatoryUpdate()); + assertEquals("9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60", releaseDetails.getReleaseHash()); + } + + @Test(expected = JSONException.class) + public void missingMandatoryUpdate() throws JSONException { + String json = "{" + + "id: 42," + + "version: '14'," + + "short_version: '2.1.5'," + + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + + "android_min_api_level: 19," + + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + "package_hashes: ['9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60']" + "}"; ReleaseDetails.parse(json); @@ -263,6 +322,7 @@ public void missingPackageHashes() throws JSONException { "short_version: '2.1.5'," + "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + + "mandatory_update: false," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'" + "}"; ReleaseDetails.parse(json); @@ -277,6 +337,7 @@ public void emptyPackageHashes() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: []" + "}"; ReleaseDetails.parse(json); @@ -291,6 +352,7 @@ public void invalidPackageHashes() throws JSONException { "release_notes: 'Fix a critical bug, this text was entered in Mobile Center portal.'," + "android_min_api_level: 19," + "download_url: 'http://download.thinkbroadband.com/1GB.zip'," + + "mandatory_update: false," + "package_hashes: '9f52199c986d9210842824df695900e1656180946212bd5e8978501a5b732e60'" + "}"; ReleaseDetails.parse(json); diff --git a/sdk/mobile-center-distribute/src/main/AndroidManifest.xml b/sdk/mobile-center-distribute/src/main/AndroidManifest.xml index 801862eb68..f32b745612 100644 --- a/sdk/mobile-center-distribute/src/main/AndroidManifest.xml +++ b/sdk/mobile-center-distribute/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.microsoft.azure.mobile.distribute"> + { + + /** + * Context. + */ + private final Context mContext; + + /** + * Download identifier to inspect. + */ + private final long mDownloadId; + + /** + * Flag just to check progress. + */ + private final boolean mCheckProgress; + + /** + * Release details. + */ + private ReleaseDetails mReleaseDetails; + + /** + * Init. + * + * @param context context. + * @param downloadId download identifier. + * @param checkProgress check progress only. + * @param releaseDetails release details. + */ + CheckDownloadTask(Context context, long downloadId, boolean checkProgress, ReleaseDetails releaseDetails) { + mContext = context; + mDownloadId = downloadId; + mCheckProgress = checkProgress; + mReleaseDetails = releaseDetails; + } + + @SuppressWarnings("deprecation") + private static Uri getFileUriOnOldDevices(Cursor cursor) throws IllegalArgumentException { + return Uri.parse("file://" + cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME))); + } + + @Override + protected DownloadProgress doInBackground(Void... params) { + + /* + * Completion might be triggered in background before MobileCenter.start + * if application was killed after starting download. + * + * We still want to generate the notification: if we can find the data in preferences + * that means they were not deleted, and thus that the sdk was not disabled. + */ + MobileCenterLog.debug(LOG_TAG, "Check download id=" + mDownloadId); + Distribute distribute = Distribute.getInstance(); + if (!distribute.isStarted()) { + MobileCenterLog.debug(LOG_TAG, "Called before onStart, init storage"); + StorageHelper.initialize(mContext); + mReleaseDetails = DistributeUtils.loadCachedReleaseDetails(); + } + + /* Check intent data is what we expected. */ + long expectedDownloadId = DistributeUtils.getStoredDownloadId(); + if (expectedDownloadId == INVALID_DOWNLOAD_IDENTIFIER || expectedDownloadId != mDownloadId) { + MobileCenterLog.debug(LOG_TAG, "Ignoring download identifier we didn't expect, id=" + mDownloadId); + return null; + } + + /* Query download manager. */ + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + try { + Cursor cursor = downloadManager.query(new DownloadManager.Query().setFilterById(mDownloadId)); + if (cursor == null) { + throw new NoSuchElementException(); + } + try { + if (!cursor.moveToFirst()) { + throw new NoSuchElementException(); + } + int status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)); + if (status == DownloadManager.STATUS_FAILED) { + throw new IllegalStateException(); + } + if (status != DownloadManager.STATUS_SUCCESSFUL || mCheckProgress) { + distribute.markDownloadStillInProgress(this); + long totalSize = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); + long currentSize = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); + MobileCenterLog.verbose(LOG_TAG, "currentSize=" + currentSize + " totalSize=" + totalSize); + return new DownloadProgress(currentSize, totalSize); + } + + /* Build install intent. */ + String localUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)); + MobileCenterLog.debug(LOG_TAG, "Download was successful for id=" + mDownloadId + " uri=" + localUri); + Intent intent = DistributeUtils.getInstallIntent(Uri.parse(localUri)); + boolean installerFound = false; + if (intent.resolveActivity(mContext.getPackageManager()) == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + intent = DistributeUtils.getInstallIntent(getFileUriOnOldDevices(cursor)); + installerFound = intent.resolveActivity(mContext.getPackageManager()) != null; + } + } else { + installerFound = true; + } + if (!installerFound) { + MobileCenterLog.error(LOG_TAG, "Installer not found"); + distribute.completeWorkflow(this); + return null; + } + + /* Check if a should install now. */ + if (!distribute.notifyDownload(mContext, this, intent)) { + + /* + * This start call triggers strict mode in U.I. thread so it + * needs to be done here without synchronizing + * (not to block methods waiting on synchronized on U.I. thread) + * so yes we could launch install and SDK being disabled... + * + * This corner case cannot be avoided without triggering + * strict mode exception. + */ + MobileCenterLog.info(LOG_TAG, "Show install UI now."); + mContext.startActivity(intent); + if (mReleaseDetails != null && mReleaseDetails.isMandatoryUpdate()) { + distribute.setInstalling(this); + } else { + distribute.completeWorkflow(this); + } + } + } finally { + cursor.close(); + } + } catch (RuntimeException e) { + MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + mDownloadId); + distribute.completeWorkflow(this); + } + return null; + } + + @Override + protected void onPostExecute(DownloadProgress result) { + if (result != null) { + Distribute.getInstance().updateProgressDialog(this, result); + } + } + + /** + * Get context. + * + * @return context. + */ + Context getContext() { + return mContext; + } +} diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java index 6b3698b617..1f965dda29 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java @@ -3,21 +3,21 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; +import android.app.Dialog; import android.app.DownloadManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.os.SystemClock; import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -37,10 +37,9 @@ import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; -import com.microsoft.azure.mobile.utils.HashUtils; +import com.microsoft.azure.mobile.utils.HandlerUtils; import com.microsoft.azure.mobile.utils.MobileCenterLog; import com.microsoft.azure.mobile.utils.NetworkStateHelper; -import com.microsoft.azure.mobile.utils.UUIDUtils; import com.microsoft.azure.mobile.utils.crypto.CryptoUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper; import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; @@ -49,38 +48,37 @@ import java.lang.ref.WeakReference; import java.net.URL; +import java.text.NumberFormat; import java.util.HashMap; import java.util.Map; -import java.util.NoSuchElementException; -import static android.content.Context.DOWNLOAD_SERVICE; import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE; import static android.util.Log.VERBOSE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.CHECK_PROGRESS_TIME_INTERVAL; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DEFAULT_API_URL; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DEFAULT_INSTALL_URL; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_COMPLETED; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_ENQUEUED; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_INSTALLING; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_NOTIFIED; import static com.microsoft.azure.mobile.distribute.DistributeConstants.GET_LATEST_RELEASE_PATH_FORMAT; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.HANDLER_TOKEN_CHECK_PROGRESS; import static com.microsoft.azure.mobile.distribute.DistributeConstants.HEADER_API_TOKEN; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.INVALID_DOWNLOAD_IDENTIFIER; import static com.microsoft.azure.mobile.distribute.DistributeConstants.INVALID_RELEASE_IDENTIFIER; import static com.microsoft.azure.mobile.distribute.DistributeConstants.LOG_TAG; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_PLATFORM; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_PLATFORM_VALUE; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_REDIRECT_ID; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_RELEASE_HASH; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.MEBIBYTE_IN_BYTES; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_ID; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_TIME; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_RELEASE_DETAILS; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_REQUEST_ID; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.distribute.DistributeConstants.SERVICE_NAME; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.UPDATE_SETUP_PATH_FORMAT; +import static com.microsoft.azure.mobile.distribute.DistributeUtils.getStoredDownloadState; import static com.microsoft.azure.mobile.http.DefaultHttpClient.METHOD_GET; + /** * Distribute service. */ @@ -90,7 +88,7 @@ public class Distribute extends AbstractMobileCenterService { * Shared instance. */ @SuppressLint("StaticFieldLeak") - private static Distribute sInstance = null; + private static Distribute sInstance; /** * Current install base URL. @@ -158,6 +156,16 @@ public class Distribute extends AbstractMobileCenterService { */ private AlertDialog mUnknownSourcesDialog; + /** + * Last download progress dialog that was shown. + */ + private ProgressDialog mProgressDialog; + + /** + * Mandatory download completed in app notification. + */ + private AlertDialog mCompletedDownloadDialog; + /** * Last activity that did show a dialog. * Used to avoid replacing a dialog in same screen as it causes flickering. @@ -248,64 +256,6 @@ public static void setApiUrl(String apiUrl) { getInstance().setInstanceApiUrl(apiUrl); } - /** - * Get the intent used to open installation U.I. - * - * @param fileUri downloaded file URI from the download manager. - * @return intent to open installation U.I. - */ - @NonNull - private static Intent getInstallIntent(Uri fileUri) { - Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); - intent.setData(fileUri); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - return intent; - } - - /** - * Get the notification identifier for downloads. - * - * @return notification identifier for downloads. - */ - @VisibleForTesting - static int getNotificationId() { - return Distribute.class.getName().hashCode(); - } - - @SuppressWarnings("deprecation") - private static Uri getFileUriOnOldDevices(Cursor cursor) throws IllegalArgumentException { - return Uri.parse("file://" + cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME))); - } - - @SuppressWarnings("deprecation") - private static Notification buildNotification(Notification.Builder builder) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return builder.build(); - } else { - return builder.getNotification(); - } - } - - /** - * Get download identifier from storage. - * - * @return download identifier or negative value if not found. - */ - private static long getStoredDownloadId() { - return StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); - } - - /** - * Get download state from storage. - * - * @return download state (completed by default). - */ - private static int getStoredDownloadState() { - return PreferencesStorage.getInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_COMPLETED); - } - @Override protected String getGroupName() { return null; @@ -329,6 +279,13 @@ public synchronized void onStarted(@NonNull Context context, @NonNull String app resumeDistributeWorkflow(); } + /** + * Check if distribute started. + */ + boolean isStarted() { + return mAppSecret != null; + } + @Override public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) { @@ -361,6 +318,7 @@ public synchronized void onActivityResumed(Activity activity) { @Override public synchronized void onActivityPaused(Activity activity) { mForegroundActivity = null; + hideProgressDialog(); } @Override @@ -404,6 +362,8 @@ private synchronized void cancelPreviousTasks() { } mUpdateDialog = null; mUnknownSourcesDialog = null; + mProgressDialog = null; + mCompletedDownloadDialog = null; mReleaseDetails = null; if (mDownloadTask != null) { mDownloadTask.cancel(true); @@ -414,11 +374,12 @@ private synchronized void cancelPreviousTasks() { mCheckDownloadTask = null; } mCheckedDownload = false; - long downloadId = getStoredDownloadId(); + long downloadId = DistributeUtils.getStoredDownloadId(); if (downloadId >= 0) { MobileCenterLog.debug(LOG_TAG, "Removing download and notification id=" + downloadId); removeDownload(downloadId); } + PreferencesStorage.remove(PREFERENCE_KEY_RELEASE_DETAILS); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_TIME); @@ -453,40 +414,72 @@ private synchronized void resumeDistributeWorkflow() { return; } - /* If we have a pending or notified download, check it. */ - if (getStoredDownloadState() != DOWNLOAD_STATE_COMPLETED) { - if (mCheckedDownload) { + /* Load cached release details if process restarted and we have such a cache. */ + int downloadState = getStoredDownloadState(); + if (mReleaseDetails == null && downloadState != DOWNLOAD_STATE_COMPLETED) { + mReleaseDetails = DistributeUtils.loadCachedReleaseDetails(); + } + + /* If process restarted during workflow. */ + if (downloadState != DOWNLOAD_STATE_COMPLETED && !mCheckedDownload) { + + /* Discard release if application updated. Then immediately check release. */ + long lastUpdateTime; + try { + lastUpdateTime = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).lastUpdateTime; + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.debug(LOG_TAG, "Could not check last update time.", e); + completeWorkflow(); return; - } else { - - /* Discard download if application updated. Then immediately check release. */ - long lastUpdateTime; - try { - lastUpdateTime = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).lastUpdateTime; - } catch (PackageManager.NameNotFoundException e) { - MobileCenterLog.debug(LOG_TAG, "Could not check last update time.", e); - completeWorkflow(); - return; - } - if (lastUpdateTime > StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_TIME)) { - MobileCenterLog.debug(LOG_TAG, "Discarding previous download as application updated."); - cancelPreviousTasks(); - } + } + if (lastUpdateTime > StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_TIME)) { + MobileCenterLog.debug(LOG_TAG, "Discarding previous download as application updated."); + cancelPreviousTasks(); + } + + /* Otherwise check currently processed release. */ + else { - /* Otherwise check download. */ - else { - mCheckedDownload = true; - checkDownload(mContext, getStoredDownloadId()); + /* Don't do it twice per process life time for this release. */ + mCheckedDownload = true; + + /* + * And check release, this will show install U.I. if file valid. + * If we are waiting for a mandatory update download, + * skip this step as we'll show progress dialog instead. + */ + if (mReleaseDetails == null || !mReleaseDetails.isMandatoryUpdate() || downloadState != DOWNLOAD_STATE_ENQUEUED) { + checkDownload(mContext, DistributeUtils.getStoredDownloadId(), false); return; } } } - /* If we were waiting after API call to resume app to show/resume the dialog do it now. */ + /* + * If we got a release information but application backgrounded then resumed, + * check what dialog to restore. + */ if (mReleaseDetails != null) { - /* Restore the U.I. state after a rotation or if activity covered by another one. */ - if (mUnknownSourcesDialog != null) { + /* If we go back to application without installing the mandatory update. */ + if (downloadState == DOWNLOAD_STATE_INSTALLING) { + + /* Show a new modal dialog with only install button. */ + showMandatoryDownloadReadyDialog(); + } + + /* If we are still downloading. */ + else if (downloadState == DOWNLOAD_STATE_ENQUEUED) { + + /* Refresh mandatory dialog progress or do nothing otherwise. */ + if (mReleaseDetails.isMandatoryUpdate()) { + showDownloadProgress(); + checkDownload(mContext, DistributeUtils.getStoredDownloadId(), true); + } + } + + /* If we were showing unknown sources dialog, restore it. */ + else if (mUnknownSourcesDialog != null) { /* * Resume click download step if last time we were showing unknown source dialog. @@ -495,9 +488,10 @@ private synchronized void resumeDistributeWorkflow() { * otherwise restore dialog if activity rotated or was covered. */ enqueueDownloadOrShowUnknownSourcesDialog(mReleaseDetails); - } else { + } - /* Or restore update dialog if that's the last thing we did before being paused. */ + /* Or restore update dialog if that's the last thing we did before being paused. */ + else { showUpdateDialog(); } return; @@ -528,64 +522,13 @@ private synchronized void resumeDistributeWorkflow() { } /* If not, open browser to update setup. */ - if (mBrowserOpenedOrAborted) { - return; - } - - /* - * If network is disconnected, browser will fail so wait. - * Also we can't just wait for network to be up and launch browser at that time - * as it's unpredictable and will interrupt the user, so just wait next relaunch. - */ - if (!NetworkStateHelper.getSharedInstance(mContext).isNetworkConnected()) { - MobileCenterLog.info(LOG_TAG, "Postpone enabling in app updates via browser as network is disconnected."); - completeWorkflow(); - return; - } - - /* Compute hash. */ - String releaseHash; - try { - releaseHash = computeHash(mContext); - } catch (PackageManager.NameNotFoundException e) { - MobileCenterLog.error(LOG_TAG, "Could not get package info", e); + if (!mBrowserOpenedOrAborted) { + DistributeUtils.updateSetupUsingBrowser(mForegroundActivity, mInstallUrl, mAppSecret); mBrowserOpenedOrAborted = true; - return; } - - /* Generate request identifier. */ - String requestId = UUIDUtils.randomUUID().toString(); - - /* Build URL. */ - String url = mInstallUrl; - url += String.format(UPDATE_SETUP_PATH_FORMAT, mAppSecret); - url += "?" + PARAMETER_RELEASE_HASH + "=" + releaseHash; - url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); - url += "&" + PARAMETER_REQUEST_ID + "=" + requestId; - url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; - MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to url=" + url); - - /* Store request id. */ - PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId); - - /* Open browser, remember that whatever the outcome to avoid opening it twice. */ - BrowserUtils.openBrowser(url, mForegroundActivity); - mBrowserOpenedOrAborted = true; } } - @NonNull - private String computeHash(@NonNull Context context) throws PackageManager.NameNotFoundException { - PackageManager packageManager = context.getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); - return computeHash(context, packageInfo); - } - - @NonNull - private String computeHash(@NonNull Context context, @NonNull PackageInfo packageInfo) { - return HashUtils.sha256(context.getPackageName() + ":" + packageInfo.versionName + ":" + packageInfo.versionCode); - } - /** * Reset all variables that matter to restart checking a new release on launcher activity restart. * @@ -602,9 +545,9 @@ private synchronized void completeWorkflow(ReleaseDetails releaseDetails) { * * @param task to check if state changed and that the call should be ignored. */ - private synchronized void completeWorkflow(CheckDownloadTask task) { + synchronized void completeWorkflow(CheckDownloadTask task) { if (task == mCheckDownloadTask) { - cancelNotification(task.mContext); + cancelNotification(task.getContext()); completeWorkflow(); } } @@ -616,19 +559,21 @@ private synchronized void cancelNotification(Context context) { if (getStoredDownloadState() == DOWNLOAD_STATE_NOTIFIED) { MobileCenterLog.debug(LOG_TAG, "Delete notification"); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(getNotificationId()); + notificationManager.cancel(DistributeUtils.getNotificationId()); } } /** * Reset all variables that matter to restart checking a new release on launcher activity restart. */ - private synchronized void completeWorkflow() { + synchronized void completeWorkflow() { + PreferencesStorage.remove(PREFERENCE_KEY_RELEASE_DETAILS); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); mCheckReleaseApiCall = null; mCheckReleaseCallId = null; mUpdateDialog = null; mUnknownSourcesDialog = null; + hideProgressDialog(); mReleaseDetails = null; mWorkflowCompleted = true; } @@ -701,7 +646,7 @@ public void onBeforeCalling(URL url, Map headers) { @Override public void onCallSucceeded(String payload) { try { - handleApiCallSuccess(releaseCallId, ReleaseDetails.parse(payload)); + handleApiCallSuccess(releaseCallId, payload, ReleaseDetails.parse(payload)); } catch (JSONException e) { onCallFailed(e); } @@ -732,7 +677,7 @@ private synchronized void handleApiCallFailure(Object releaseCallId, Exception e /** * Handle API call success. */ - private synchronized void handleApiCallSuccess(Object releaseCallId, ReleaseDetails releaseDetails) { + private synchronized void handleApiCallSuccess(Object releaseCallId, String rawReleaseDetails, ReleaseDetails releaseDetails) { /* Check if state did not change. */ if (mCheckReleaseCallId == releaseCallId) { @@ -755,6 +700,7 @@ else if (Build.VERSION.SDK_INT >= releaseDetails.getMinApiLevel()) { /* Show update dialog. */ mReleaseDetails = releaseDetails; + PreferencesStorage.putString(PREFERENCE_KEY_RELEASE_DETAILS, rawReleaseDetails); if (mForegroundActivity != null) { showUpdateDialog(); } @@ -783,7 +729,7 @@ else if (Build.VERSION.SDK_INT >= releaseDetails.getMinApiLevel()) { */ private boolean isMoreRecent(PackageInfo packageInfo, ReleaseDetails releaseDetails) { if (releaseDetails.getVersion() == packageInfo.versionCode) { - return !releaseDetails.getReleaseHash().equals(computeHash(mContext, packageInfo)); + return !releaseDetails.getReleaseHash().equals(DistributeUtils.computeReleaseHash(mContext, packageInfo)); } return releaseDetails.getVersion() > packageInfo.versionCode; } @@ -791,24 +737,23 @@ private boolean isMoreRecent(PackageInfo packageInfo, ReleaseDetails releaseDeta /** * Check if dialog should be restored in the new activity. Hiding previous dialog version if any. * - * @param alertDialog existing dialog if any, always returning true when null. + * @param dialog existing dialog if any, always returning true when null. * @return true if a new dialog should be displayed, false otherwise. */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private boolean shouldRefreshDialog(@Nullable AlertDialog alertDialog) { + private boolean shouldRefreshDialog(@Nullable Dialog dialog) { /* We could be in another activity now, refresh dialog. */ - if (alertDialog != null) { + if (dialog != null) { /* Nothing to if resuming same activity with dialog already displayed. */ - if (alertDialog.isShowing()) { + if (dialog.isShowing()) { if (mForegroundActivity == mLastActivityWithDialog.get()) { MobileCenterLog.debug(LOG_TAG, "Previous dialog is still being shown in the same activity."); return false; } /* Otherwise replace dialog. */ - alertDialog.hide(); + dialog.hide(); } } return true; @@ -817,14 +762,11 @@ private boolean shouldRefreshDialog(@Nullable AlertDialog alertDialog) { /** * Show dialog and remember which activity displayed it for later U.I. state change. * - * @param dialogBuilder dialog builder that prepared the new dialog. - * @return the dialog that is shown. + * @param alertDialog dialog. */ - private AlertDialog showAndRememberDialogActivity(AlertDialog.Builder dialogBuilder) { - AlertDialog alertDialog = dialogBuilder.create(); + private void showAndRememberDialogActivity(AlertDialog alertDialog) { alertDialog.show(); mLastActivityWithDialog = new WeakReference<>(mForegroundActivity); - return alertDialog; } /** @@ -852,22 +794,27 @@ public void onClick(DialogInterface dialog, int which) { enqueueDownloadOrShowUnknownSourcesDialog(releaseDetails); } }); - dialogBuilder.setNegativeButton(R.string.mobile_center_distribute_update_dialog_ignore, new DialogInterface.OnClickListener() { + if (releaseDetails.isMandatoryUpdate()) { + dialogBuilder.setCancelable(false); + } else { + dialogBuilder.setNegativeButton(R.string.mobile_center_distribute_update_dialog_ignore, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - ignoreRelease(releaseDetails); - } - }); - dialogBuilder.setNeutralButton(R.string.mobile_center_distribute_update_dialog_postpone, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ignoreRelease(releaseDetails); + } + }); + dialogBuilder.setNeutralButton(R.string.mobile_center_distribute_update_dialog_postpone, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - completeWorkflow(releaseDetails); - } - }); - setOnCancelListener(dialogBuilder, releaseDetails); - mUpdateDialog = showAndRememberDialogActivity(dialogBuilder); + @Override + public void onClick(DialogInterface dialog, int which) { + completeWorkflow(releaseDetails); + } + }); + setOnCancelListener(dialogBuilder, releaseDetails); + } + mUpdateDialog = dialogBuilder.create(); + showAndRememberDialogActivity(mUpdateDialog); } /** @@ -895,14 +842,18 @@ private synchronized void showUnknownSourcesDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mForegroundActivity); dialogBuilder.setMessage(R.string.mobile_center_distribute_unknown_sources_dialog_message); final ReleaseDetails releaseDetails = mReleaseDetails; - dialogBuilder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + if (releaseDetails.isMandatoryUpdate()) { + dialogBuilder.setCancelable(false); + } else { + dialogBuilder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - completeWorkflow(releaseDetails); - } - }); - setOnCancelListener(dialogBuilder, releaseDetails); + @Override + public void onClick(DialogInterface dialog, int which) { + completeWorkflow(releaseDetails); + } + }); + setOnCancelListener(dialogBuilder, releaseDetails); + } /* We use generic OK button as we can't promise we can navigate to settings. */ dialogBuilder.setPositiveButton(R.string.mobile_center_distribute_unknown_sources_dialog_settings, new DialogInterface.OnClickListener() { @@ -912,7 +863,8 @@ public void onClick(DialogInterface dialog, int which) { goToSettings(releaseDetails); } }); - mUnknownSourcesDialog = showAndRememberDialogActivity(dialogBuilder); + mUnknownSourcesDialog = dialogBuilder.create(); + showAndRememberDialogActivity(mUnknownSourcesDialog); } /** @@ -979,8 +931,11 @@ private synchronized void enqueueDownloadOrShowUnknownSourcesDialog(final Releas if (releaseDetails == mReleaseDetails) { if (InstallerUtils.isUnknownSourcesEnabled(mContext)) { MobileCenterLog.debug(LOG_TAG, "Schedule download..."); + if (releaseDetails.isMandatoryUpdate()) { + showDownloadProgress(); + } mCheckedDownload = true; - mDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new DownloadTask(releaseDetails)); + mDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new DownloadTask(mContext, releaseDetails)); } else { showUnknownSourcesDialog(); } @@ -1002,33 +957,38 @@ private void showDisabledToast() { /** * Persist download state. * - * @param downloadManager download manager. - * @param task current task to check state change. - * @param downloadRequestId download identifier. - * @param enqueueTime time just before enqueuing download. + * @param downloadManager download manager. + * @param task current task to check state change. + * @param downloadId download identifier. + * @param enqueueTime time just before enqueuing download. */ @WorkerThread - private synchronized void storeDownloadRequestId(DownloadManager downloadManager, DownloadTask task, long downloadRequestId, long enqueueTime) { + synchronized void storeDownloadRequestId(DownloadManager downloadManager, DownloadTask task, long downloadId, long enqueueTime) { /* Check for if state changed and task not canceled in time. */ if (mDownloadTask == task) { /* Delete previous download. */ - long previousDownloadId = getStoredDownloadId(); + long previousDownloadId = DistributeUtils.getStoredDownloadId(); if (previousDownloadId >= 0) { MobileCenterLog.debug(LOG_TAG, "Delete previous download id=" + previousDownloadId); downloadManager.remove(previousDownloadId); } /* Store new download identifier. */ - PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, downloadRequestId); + PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_ID, downloadId); PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_ENQUEUED); PreferencesStorage.putLong(PREFERENCE_KEY_DOWNLOAD_TIME, enqueueTime); + + /* Start monitoring progress for mandatory update. */ + if (mReleaseDetails.isMandatoryUpdate()) { + checkDownload(mContext, downloadId, true); + } } else { /* State changed quickly, cancel download. */ - MobileCenterLog.debug(LOG_TAG, "State changed while downloading, cancel id=" + downloadRequestId); - downloadManager.remove(downloadRequestId); + MobileCenterLog.debug(LOG_TAG, "State changed while downloading, cancel id=" + downloadId); + downloadManager.remove(downloadId); } } @@ -1055,13 +1015,14 @@ synchronized void resumeApp(@NonNull Context context) { /** * Check a download state and take action depending on that state. * - * @param context any application context. - * @param downloadId download identifier from DownloadManager. + * @param context any application context. + * @param downloadId download identifier from DownloadManager. + * @param checkProgress true to only check progress, false to also process install if done. */ - synchronized void checkDownload(@NonNull Context context, long downloadId) { + synchronized void checkDownload(@NonNull Context context, long downloadId, boolean checkProgress) { /* Querying download manager and even the start intent are detected by strict mode so we do that in background. */ - mCheckDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new CheckDownloadTask(context, downloadId)); + mCheckDownloadTask = AsyncTaskUtils.execute(LOG_TAG, new CheckDownloadTask(context, downloadId, checkProgress, mReleaseDetails)); } /** @@ -1074,7 +1035,7 @@ synchronized void checkDownload(@NonNull Context context, long downloadId) { * @param intent prepared install intent. * @return false if install U.I should be shown now, true if a notification was posted or if the task was canceled. */ - private synchronized boolean notifyDownload(Context context, CheckDownloadTask task, Intent intent) { + synchronized boolean notifyDownload(Context context, CheckDownloadTask task, Intent intent) { /* Check state. */ if (task != mCheckDownloadTask) { @@ -1100,10 +1061,10 @@ private synchronized boolean notifyDownload(Context context, CheckDownloadTask t .setContentText(context.getString(R.string.mobile_center_distribute_download_successful_notification_message)) .setSmallIcon(context.getApplicationInfo().icon) .setContentIntent(PendingIntent.getActivities(context, 0, new Intent[]{intent}, 0)); - Notification notification = buildNotification(builder); + Notification notification = DistributeUtils.buildNotification(builder); notification.flags |= Notification.FLAG_AUTO_CANCEL; NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(getNotificationId(), notification); + notificationManager.notify(DistributeUtils.getNotificationId(), notification); PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_NOTIFIED); /* Reset check download flag to show install U.I. on resume if notification ignored. */ @@ -1116,7 +1077,7 @@ private synchronized boolean notifyDownload(Context context, CheckDownloadTask t * * @param task task to check for a state change. */ - private synchronized void markDownloadStillInProgress(CheckDownloadTask task) { + synchronized void markDownloadStillInProgress(CheckDownloadTask task) { if (task == mCheckDownloadTask) { MobileCenterLog.verbose(LOG_TAG, "Download is still in progress..."); mCheckedDownload = true; @@ -1129,174 +1090,115 @@ private synchronized void markDownloadStillInProgress(CheckDownloadTask task) { @SuppressLint("VisibleForTests") private synchronized void removeDownload(long downloadId) { cancelNotification(mContext); - AsyncTaskUtils.execute(LOG_TAG, new RemoveDownloadTask(), downloadId); + AsyncTaskUtils.execute(LOG_TAG, new RemoveDownloadTask(mContext, downloadId)); } /** - * Removing a download triggers strict mode exception in U.I. thread. + * Show download progress. */ - @VisibleForTesting - class RemoveDownloadTask extends AsyncTask { - - @Override - protected Void doInBackground(Long... params) { + private void showDownloadProgress() { + mProgressDialog = new ProgressDialog(mForegroundActivity); + mProgressDialog.setTitle(R.string.mobile_center_distribute_downloading_mandatory_update); + mProgressDialog.setCancelable(false); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setProgressNumberFormat(null); + mProgressDialog.setProgressPercentFormat(null); + showAndRememberDialogActivity(mProgressDialog); + } - /* This special cleanup task does not require any cancellation on state change as a previous download will never be reused. */ - DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); - downloadManager.remove(params[0]); - return null; + /** + * Hide progress dialog and stop updating. + */ + private synchronized void hideProgressDialog() { + if (mProgressDialog != null) { + final Dialog progressDialog = mProgressDialog; + mProgressDialog = null; + HandlerUtils.runOnUiThread(new Runnable() { + + @Override + public void run() { + progressDialog.hide(); + } + }); + HandlerUtils.getMainHandler().removeCallbacksAndMessages(HANDLER_TOKEN_CHECK_PROGRESS); } } /** - * The download manager API triggers strict mode exception in U.I. thread. + * Update progress dialog for mandatory update. */ - @VisibleForTesting - class DownloadTask extends AsyncTask { + synchronized void updateProgressDialog(CheckDownloadTask task, DownloadProgress downloadProgress) { + + /* If not canceled and U.I. context did not change. */ + if (task == mCheckDownloadTask && mForegroundActivity != null && mProgressDialog != null) { + + /* If file size is known update downloadProgress bar. */ + if (downloadProgress.getTotalSize() >= 0) { + if (mProgressDialog.isIndeterminate()) { + mProgressDialog.setProgressPercentFormat(NumberFormat.getPercentInstance()); + mProgressDialog.setProgressNumberFormat(mForegroundActivity.getString(R.string.mobile_center_distribute_download_progress_number_format)); + mProgressDialog.setIndeterminate(false); + mProgressDialog.setMax((int) (downloadProgress.getTotalSize() / MEBIBYTE_IN_BYTES)); + } + mProgressDialog.setProgress((int) (downloadProgress.getCurrentSize() / MEBIBYTE_IN_BYTES)); + } - /** - * Release details to check. - */ - private final ReleaseDetails mReleaseDetails; + /* And schedule the next check. */ + HandlerUtils.getMainHandler().postAtTime(new Runnable() { - /** - * Init. - * - * @param releaseDetails release details associated to this check. - */ - DownloadTask(ReleaseDetails releaseDetails) { - mReleaseDetails = releaseDetails; + @Override + public void run() { + checkDownload(mContext, DistributeUtils.getStoredDownloadId(), true); + } + }, HANDLER_TOKEN_CHECK_PROGRESS, SystemClock.uptimeMillis() + CHECK_PROGRESS_TIME_INTERVAL); } + } - @Override - protected Void doInBackground(Void[] params) { - - /* Download file. */ - Uri downloadUrl = mReleaseDetails.getDownloadUrl(); - MobileCenterLog.debug(LOG_TAG, "Start downloading new release, url=" + downloadUrl); - DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); - DownloadManager.Request request = new DownloadManager.Request(downloadUrl); - long enqueueTime = System.currentTimeMillis(); - long downloadRequestId = downloadManager.enqueue(request); - storeDownloadRequestId(downloadManager, this, downloadRequestId, enqueueTime); - return null; + /** + * Show modal dialog with install button if mandatory update ready and user cancelled install. + */ + private synchronized void showMandatoryDownloadReadyDialog() { + if (shouldRefreshDialog(mCompletedDownloadDialog)) { + final ReleaseDetails releaseDetails = mReleaseDetails; + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mForegroundActivity); + dialogBuilder.setCancelable(false); + dialogBuilder.setTitle(R.string.mobile_center_distribute_update_dialog_title); + dialogBuilder.setMessage(R.string.mobile_center_distribute_download_successful_notification_message); + dialogBuilder.setPositiveButton(R.string.mobile_center_distribute_install, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + installMandatoryUpdate(releaseDetails); + } + }); + mCompletedDownloadDialog = dialogBuilder.create(); + showAndRememberDialogActivity(mCompletedDownloadDialog); } } /** - * Inspect a pending or completed download. - * This uses APIs that would trigger strict mode exception if used in U.I. thread. + * Install mandatory update after clicking on the install dialog button. + * + * @param releaseDetails release details. */ - @VisibleForTesting - class CheckDownloadTask extends AsyncTask { - - /** - * Context. - */ - private final Context mContext; - - /** - * Download identifier to inspect. - */ - private final long mDownloadId; - - /** - * Init. - * - * @param context context. - * @param downloadId download identifier. - */ - CheckDownloadTask(Context context, long downloadId) { - mContext = context; - mDownloadId = downloadId; + private synchronized void installMandatoryUpdate(ReleaseDetails releaseDetails) { + if (releaseDetails == mReleaseDetails) { + checkDownload(mContext, DistributeUtils.getStoredDownloadId(), false); + } else { + showDisabledToast(); } + } - @Override - protected Void doInBackground(Void... params) { - - /* - * Completion might be triggered in background before MobileCenter.start - * if application was killed after starting download. - * - * We still want to generate the notification: if we can find the data in preferences - * that means they were not deleted, and thus that the sdk was not disabled. - */ - MobileCenterLog.debug(LOG_TAG, "Check download id=" + mDownloadId); - if (mAppSecret == null) { - MobileCenterLog.debug(LOG_TAG, "Called before onStart, init storage"); - StorageHelper.initialize(mContext); - } - - /* Check intent data is what we expected. */ - long expectedDownloadId = getStoredDownloadId(); - if (expectedDownloadId == INVALID_DOWNLOAD_IDENTIFIER || expectedDownloadId != mDownloadId) { - MobileCenterLog.debug(LOG_TAG, "Ignoring download identifier we didn't expect, id=" + mDownloadId); - return null; - } - - /* Query download manager. */ - DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); - try { - Cursor cursor = downloadManager.query(new DownloadManager.Query().setFilterById(mDownloadId)); - if (cursor == null) { - throw new NoSuchElementException(); - } - try { - if (!cursor.moveToFirst()) { - throw new NoSuchElementException(); - } - int status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)); - if (status == DownloadManager.STATUS_FAILED) { - throw new IllegalStateException(); - } - if (status != DownloadManager.STATUS_SUCCESSFUL) { - markDownloadStillInProgress(this); - return null; - } - - /* Build install intent. */ - String localUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)); - MobileCenterLog.debug(LOG_TAG, "Download was successful for id=" + mDownloadId + " uri=" + localUri); - Intent intent = getInstallIntent(Uri.parse(localUri)); - boolean installerFound = false; - if (intent.resolveActivity(mContext.getPackageManager()) == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - intent = getInstallIntent(getFileUriOnOldDevices(cursor)); - installerFound = intent.resolveActivity(mContext.getPackageManager()) != null; - } - } else { - installerFound = true; - } - if (!installerFound) { - MobileCenterLog.error(LOG_TAG, "Installer not found"); - completeWorkflow(this); - return null; - } - - /* Check if a should install now. */ - if (!notifyDownload(mContext, this, intent)) { - - /* - * This start call triggers strict mode in U.I. thread so it - * needs to be done here without synchronizing - * (not to block methods waiting on synchronized on U.I. thread) - * so yes we could launch install and SDK being disabled... - * - * This corner case cannot be avoided without triggering - * strict mode exception. - */ - MobileCenterLog.info(LOG_TAG, "Show install UI now."); - mContext.startActivity(intent); - completeWorkflow(this); - } - } finally { - cursor.close(); - } - } catch (RuntimeException e) { - MobileCenterLog.error(LOG_TAG, "Failed to download update id=" + mDownloadId); - completeWorkflow(this); - } - return null; + /** + * Update download state to installing if state did not change. + * + * @param task current task to check state change. + */ + synchronized void setInstalling(CheckDownloadTask task) { + if (task == mCheckDownloadTask) { + cancelNotification(task.getContext()); + PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_INSTALLING); } } } diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java index a3cea76288..c559c52194 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java @@ -108,6 +108,26 @@ final class DistributeConstants { */ static final int DOWNLOAD_STATE_NOTIFIED = 2; + /** + * State used for mandatory update to block app until user installs the app. + */ + static final int DOWNLOAD_STATE_INSTALLING = 3; + + /** + * Token used for handler callbacks to check download progress. + */ + static final String HANDLER_TOKEN_CHECK_PROGRESS = SERVICE_NAME + ".handler_token_check_progress"; + + /** + * How often to check download progress in millis. + */ + static final long CHECK_PROGRESS_TIME_INTERVAL = 1000; + + /** + * 1 MiB in bytes (this not a megabyte). + */ + static final float MEBIBYTE_IN_BYTES = 1024 * 1024; + /** * Base key for stored preferences. */ @@ -139,6 +159,11 @@ final class DistributeConstants { */ static final String PREFERENCE_KEY_IGNORED_RELEASE_ID = PREFERENCE_PREFIX + "ignored_release_id"; + /** + * Preference key to store release details. + */ + static final String PREFERENCE_KEY_RELEASE_DETAILS = PREFERENCE_PREFIX + "release_details"; + /** * Preference key to store download start time. Used to avoid showing install U.I. of a completed * download if we already updated (the download workflow can work across process restarts). diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeUtils.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeUtils.java new file mode 100644 index 0000000000..f7cbc54b3a --- /dev/null +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeUtils.java @@ -0,0 +1,164 @@ +package com.microsoft.azure.mobile.distribute; + +import android.app.Activity; +import android.app.Notification; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; + +import com.microsoft.azure.mobile.utils.HashUtils; +import com.microsoft.azure.mobile.utils.MobileCenterLog; +import com.microsoft.azure.mobile.utils.NetworkStateHelper; +import com.microsoft.azure.mobile.utils.UUIDUtils; +import com.microsoft.azure.mobile.utils.storage.StorageHelper; + +import org.json.JSONException; + +import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_COMPLETED; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.INVALID_DOWNLOAD_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.LOG_TAG; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_PLATFORM; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_PLATFORM_VALUE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_REDIRECT_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_RELEASE_HASH; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_RELEASE_DETAILS; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_REQUEST_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.UPDATE_SETUP_PATH_FORMAT; + +/** + * Some static util methods to avoid the main file getting too big. + */ +class DistributeUtils { + + /** + * Get the intent used to open installation U.I. + * + * @param fileUri downloaded file URI from the download manager. + * @return intent to open installation U.I. + */ + @NonNull + static Intent getInstallIntent(Uri fileUri) { + Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + intent.setData(fileUri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + return intent; + } + + /** + * Get the notification identifier for downloads. + * + * @return notification identifier for downloads. + */ + static int getNotificationId() { + return Distribute.class.getName().hashCode(); + } + + @SuppressWarnings("deprecation") + static Notification buildNotification(Notification.Builder builder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return builder.build(); + } else { + return builder.getNotification(); + } + } + + /** + * Get download identifier from storage. + * + * @return download identifier or negative value if not found. + */ + static long getStoredDownloadId() { + return StorageHelper.PreferencesStorage.getLong(PREFERENCE_KEY_DOWNLOAD_ID, INVALID_DOWNLOAD_IDENTIFIER); + } + + /** + * Get download state from storage. + * + * @return download state (completed by default). + */ + static int getStoredDownloadState() { + return StorageHelper.PreferencesStorage.getInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_COMPLETED); + } + + @NonNull + static String computeReleaseHash(@NonNull Context context, @NonNull PackageInfo packageInfo) { + return HashUtils.sha256(context.getPackageName() + ":" + packageInfo.versionName + ":" + packageInfo.versionCode); + } + + /** + * Update setup using browser. + * + * @param activity activity from which to start browser. + * @param installUrl base install site URL. + * @param appSecret application secret. + */ + static void updateSetupUsingBrowser(Activity activity, String installUrl, String appSecret) { + + /* + * If network is disconnected, browser will fail so wait. + * Also we can't just wait for network to be up and launch browser at that time + * as it's unpredictable and will interrupt the user, so just wait next relaunch. + */ + if (!NetworkStateHelper.getSharedInstance(activity).isNetworkConnected()) { + MobileCenterLog.info(LOG_TAG, "Postpone enabling in app updates via browser as network is disconnected."); + Distribute.getInstance().completeWorkflow(); + return; + } + + /* Compute hash. */ + String releaseHash; + try { + PackageManager packageManager = activity.getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(activity.getPackageName(), 0); + releaseHash = computeReleaseHash(activity, packageInfo); + } catch (PackageManager.NameNotFoundException e) { + MobileCenterLog.error(LOG_TAG, "Could not get package info", e); + return; + } + + /* Generate request identifier. */ + String requestId = UUIDUtils.randomUUID().toString(); + + /* Build URL. */ + String url = installUrl; + url += String.format(UPDATE_SETUP_PATH_FORMAT, appSecret); + url += "?" + PARAMETER_RELEASE_HASH + "=" + releaseHash; + url += "&" + PARAMETER_REDIRECT_ID + "=" + activity.getPackageName(); + url += "&" + PARAMETER_REQUEST_ID + "=" + requestId; + url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; + MobileCenterLog.debug(LOG_TAG, "No token, need to open browser to url=" + url); + + /* Store request id. */ + StorageHelper.PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId); + + /* Open browser, remember that whatever the outcome to avoid opening it twice. */ + BrowserUtils.openBrowser(url, activity); + } + + /** + * Get release details from cache if any. + * + * @return release details from cache or null. + */ + static ReleaseDetails loadCachedReleaseDetails() { + String cachedReleaseDetails = StorageHelper.PreferencesStorage.getString(PREFERENCE_KEY_RELEASE_DETAILS); + if (cachedReleaseDetails != null) { + try { + return ReleaseDetails.parse(cachedReleaseDetails); + } catch (JSONException e) { + MobileCenterLog.error(LOG_TAG, "Invalid cached release details.", e); + StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_RELEASE_DETAILS); + } + } + return null; + } +} diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java index 38f1933472..881d412b25 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadManagerReceiver.java @@ -28,7 +28,7 @@ public void onReceive(Context context, Intent intent) { */ else if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); - Distribute.getInstance().checkDownload(context, downloadId); + Distribute.getInstance().checkDownload(context, downloadId, false); } } } diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadProgress.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadProgress.java new file mode 100644 index 0000000000..2da3be726c --- /dev/null +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadProgress.java @@ -0,0 +1,39 @@ +package com.microsoft.azure.mobile.distribute; + +/** + * Class to hold current download progress status. + */ +class DownloadProgress { + + /** + * Number of bytes downloaded so far. + */ + private long mCurrentSize; + + /** + * Expected file size. + */ + private long mTotalSize; + + /** + * Init. + */ + DownloadProgress(long currentSize, long totalSize) { + mCurrentSize = currentSize; + mTotalSize = totalSize; + } + + /** + * @return Number of bytes downloaded so far. + */ + long getCurrentSize() { + return mCurrentSize; + } + + /** + * @return Expected file size. + */ + long getTotalSize() { + return mTotalSize; + } +} diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadTask.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadTask.java new file mode 100644 index 0000000000..88e85f03b7 --- /dev/null +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DownloadTask.java @@ -0,0 +1,58 @@ +package com.microsoft.azure.mobile.distribute; + +import android.app.DownloadManager; +import android.content.Context; +import android.net.Uri; +import android.os.AsyncTask; + +import com.microsoft.azure.mobile.utils.MobileCenterLog; + +import static android.content.Context.DOWNLOAD_SERVICE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.LOG_TAG; + +/** + * The download manager API triggers strict mode exception in U.I. thread. + */ +class DownloadTask extends AsyncTask { + + /** + * Context. + */ + private final Context mContext; + + /** + * Release details to check. + */ + private final ReleaseDetails mReleaseDetails; + + /** + * Init. + * + * @param context context. + * @param releaseDetails release details associated to this check. + */ + DownloadTask(Context context, ReleaseDetails releaseDetails) { + mContext = context; + mReleaseDetails = releaseDetails; + } + + @Override + protected Void doInBackground(Void[] params) { + + /* Download file. */ + Uri downloadUrl = mReleaseDetails.getDownloadUrl(); + MobileCenterLog.debug(LOG_TAG, "Start downloading new release, url=" + downloadUrl); + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE); + DownloadManager.Request request = new DownloadManager.Request(downloadUrl); + + /* Hide mandatory download to prevent canceling via notification cancel or download U.I. delete. */ + if (mReleaseDetails.isMandatoryUpdate()) { + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN); + request.setVisibleInDownloadsUi(false); + } + long enqueueTime = System.currentTimeMillis(); + long downloadRequestId = downloadManager.enqueue(request); + Distribute.getInstance().storeDownloadRequestId(downloadManager, this, downloadRequestId, enqueueTime); + return null; + } +} diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java index aeb9f20674..1e80f26902 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ReleaseDetails.java @@ -20,9 +20,11 @@ class ReleaseDetails { private static final String RELEASE_NOTES = "release_notes"; + private static final String MIN_API_LEVEL = "android_min_api_level"; + private static final String DOWNLOAD_URL = "download_url"; - private static final String MIN_API_LEVEL = "android_min_api_level"; + private static final String MANDATORY_UPDATE = "mandatory_update"; private static final String PACKAGE_HASHES = "package_hashes"; @@ -60,6 +62,11 @@ class ReleaseDetails { */ private Uri downloadUrl; + /** + * Mandatory update. + */ + private boolean mandatoryUpdate; + /** * Release hash. */ @@ -85,6 +92,7 @@ static ReleaseDetails parse(String json) throws JSONException { if (scheme == null || !scheme.startsWith("http")) { throw new JSONException("Invalid download_url scheme."); } + releaseDetails.mandatoryUpdate = object.getBoolean(MANDATORY_UPDATE); releaseDetails.releaseHash = object.getJSONArray(PACKAGE_HASHES).getString(0); return releaseDetails; } @@ -92,7 +100,7 @@ static ReleaseDetails parse(String json) throws JSONException { /** * Get the id value. * - * @return the id value. + * @return the id value */ int getId() { return id; @@ -146,10 +154,19 @@ Uri getDownloadUrl() { return downloadUrl; } + /** + * Get the mandatory update value. + * + * @return mandatory update value + */ + boolean isMandatoryUpdate() { + return mandatoryUpdate; + } + /** * Get the release hash value. * - * @return the releaseHash value. + * @return the releaseHash value */ @NonNull String getReleaseHash() { diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/RemoveDownloadTask.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/RemoveDownloadTask.java new file mode 100644 index 0000000000..cbc26a2c36 --- /dev/null +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/RemoveDownloadTask.java @@ -0,0 +1,41 @@ +package com.microsoft.azure.mobile.distribute; + +import android.app.DownloadManager; +import android.content.Context; +import android.os.AsyncTask; + +/** + * Removing a download triggers strict mode exception in U.I. thread. + */ +class RemoveDownloadTask extends AsyncTask { + + /** + * Context. + */ + private final Context mContext; + + /** + * Download identifier to inspect. + */ + private final long mDownloadId; + + /** + * Init. + * + * @param context context. + * @param downloadId download identifier to remove. + */ + RemoveDownloadTask(Context context, long downloadId) { + mContext = context; + mDownloadId = downloadId; + } + + @Override + protected Void doInBackground(Void... params) { + + /* This special cleanup task does not require any cancellation on state change as a previous download will never be reused. */ + DownloadManager downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); + downloadManager.remove(mDownloadId); + return null; + } +} diff --git a/sdk/mobile-center-distribute/src/main/res/values/strings.xml b/sdk/mobile-center-distribute/src/main/res/values/strings.xml index 94d7e1ae8a..f78f8509f2 100644 --- a/sdk/mobile-center-distribute/src/main/res/values/strings.xml +++ b/sdk/mobile-center-distribute/src/main/res/values/strings.xml @@ -10,4 +10,7 @@ Distribute was disabled For security, your device is set to block installation of apps obtained from unknown sources. Settings + Downloading mandatory update + %1$d MB of %2$d MB + Install \ No newline at end of file diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeAfterDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeAfterDownloadTest.java new file mode 100644 index 0000000000..9f812be3c0 --- /dev/null +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeAfterDownloadTest.java @@ -0,0 +1,330 @@ +package com.microsoft.azure.mobile.distribute; + +import android.app.DownloadManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; + +import com.microsoft.azure.mobile.channel.Channel; +import com.microsoft.azure.mobile.http.HttpClient; +import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; +import com.microsoft.azure.mobile.http.ServiceCall; +import com.microsoft.azure.mobile.http.ServiceCallback; +import com.microsoft.azure.mobile.test.TestUtils; +import com.microsoft.azure.mobile.utils.AsyncTaskUtils; +import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; + +import org.junit.After; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + +import static android.app.DownloadManager.EXTRA_DOWNLOAD_ID; +import static android.content.Context.NOTIFICATION_SERVICE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_COMPLETED; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.INVALID_DOWNLOAD_IDENTIFIER; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_RELEASE_DETAILS; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.doAnswer; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@SuppressWarnings("CanBeFinal") +@PrepareForTest({AsyncTaskUtils.class, DistributeUtils.class, DownloadTask.class, CheckDownloadTask.class, RemoveDownloadTask.class}) +public class AbstractDistributeAfterDownloadTest extends AbstractDistributeTest { + + static final long DOWNLOAD_ID = 42; + + static final ArgumentMatcher sCheckCompleteTask = new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument instanceof CheckDownloadTask; + } + }; + + @Mock + Uri mDownloadUrl; + + @Mock + DownloadManager mDownloadManager; + + @Mock + NotificationManager mNotificationManager; + + @Mock + DownloadManager.Request mDownloadRequest; + + AtomicReference mDownloadTask; + + Semaphore mCheckDownloadBeforeSemaphore; + + Semaphore mCheckDownloadAfterSemaphore; + + AtomicReference mCompletionTask; + + private Semaphore mDownloadBeforeSemaphore; + + private Semaphore mDownloadAfterSemaphore; + + void setUpDownload(boolean mandatoryUpdate) throws Exception { + + /* Allow unknown sources. */ + when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); + + /* Mock download manager. */ + when(mContext.getSystemService(Context.DOWNLOAD_SERVICE)).thenReturn(mDownloadManager); + whenNew(DownloadManager.Request.class).withAnyArguments().thenReturn(mDownloadRequest); + when(mDownloadManager.enqueue(mDownloadRequest)).thenReturn(DOWNLOAD_ID); + + /* Mock notification manager. */ + when(mContext.getSystemService(NOTIFICATION_SERVICE)).thenReturn(mNotificationManager); + + /* Mock updates to storage. */ + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn((Long) invocation.getArguments()[1]); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.putLong(eq(PREFERENCE_KEY_DOWNLOAD_ID), anyLong()); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn(INVALID_DOWNLOAD_IDENTIFIER); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn((Integer) invocation.getArguments()[1]); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.putInt(eq(PREFERENCE_KEY_DOWNLOAD_STATE), anyInt()); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn(DOWNLOAD_STATE_COMPLETED); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn(invocation.getArguments()[1].toString()); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.putString(eq(PREFERENCE_KEY_RELEASE_DETAILS), anyString()); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(PreferencesStorage.getString(invocation.getArguments()[0].toString())).thenReturn(null); + return null; + } + }).when(PreferencesStorage.class); + PreferencesStorage.remove(PREFERENCE_KEY_RELEASE_DETAILS); + + /* Mock everything that triggers a download. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn(4); + when(releaseDetails.getVersion()).thenReturn(7); + when(releaseDetails.getDownloadUrl()).thenReturn(mDownloadUrl); + when(releaseDetails.isMandatoryUpdate()).thenReturn(mandatoryUpdate); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + mockStatic(AsyncTaskUtils.class); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mActivity); + + /* Mock download asyncTask. */ + mDownloadBeforeSemaphore = new Semaphore(0); + mDownloadAfterSemaphore = new Semaphore(0); + mDownloadTask = new AtomicReference<>(); + when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument instanceof DownloadTask; + } + }), Mockito.anyVararg())).then(new Answer() { + + @Override + public DownloadTask answer(InvocationOnMock invocation) throws Throwable { + final DownloadTask task = spy((DownloadTask) invocation.getArguments()[1]); + mDownloadTask.set(task); + new Thread() { + + @Override + public void run() { + mDownloadBeforeSemaphore.acquireUninterruptibly(); + task.doInBackground(null); + mDownloadAfterSemaphore.release(); + } + }.start(); + return task; + } + }); + + /* Mock remove download async task. */ + when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { + + @Override + public boolean matches(Object argument) { + return argument instanceof RemoveDownloadTask; + } + }), Mockito.anyVararg())).then(new Answer() { + + @Override + public RemoveDownloadTask answer(InvocationOnMock invocation) throws Throwable { + final RemoveDownloadTask task = (RemoveDownloadTask) invocation.getArguments()[1]; + task.doInBackground(); + return task; + } + }); + + /* Mock download completion async task. */ + mCheckDownloadBeforeSemaphore = new Semaphore(0); + mCheckDownloadAfterSemaphore = new Semaphore(0); + mCompletionTask = new AtomicReference<>(); + when(AsyncTaskUtils.execute(anyString(), argThat(sCheckCompleteTask), Mockito.anyVararg())).then(new Answer() { + + @Override + public CheckDownloadTask answer(InvocationOnMock invocation) throws Throwable { + final CheckDownloadTask task = spy((CheckDownloadTask) invocation.getArguments()[1]); + mCompletionTask.set(task); + new Thread() { + + @Override + public void run() { + mCheckDownloadBeforeSemaphore.acquireUninterruptibly(); + task.onPostExecute(task.doInBackground()); + mCheckDownloadAfterSemaphore.release(); + } + }.start(); + return task; + } + }); + + /* Click on dialog. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_update_dialog_download), clickListener.capture()); + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + } + + void waitDownloadTask() { + mDownloadBeforeSemaphore.release(); + mDownloadAfterSemaphore.acquireUninterruptibly(); + } + + void waitCheckDownloadTask() { + mCheckDownloadBeforeSemaphore.release(); + mCheckDownloadAfterSemaphore.acquireUninterruptibly(); + } + + void completeDownload() { + Intent completionIntent = mock(Intent.class); + when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); + new DownloadManagerReceiver().onReceive(mContext, completionIntent); + } + + @NonNull + Cursor mockSuccessCursor() { + Cursor cursor = mock(Cursor.class); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); + when(cursor.moveToFirst()).thenReturn(true); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)).thenReturn(0); + when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_SUCCESSFUL); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)).thenReturn(1); + when(cursor.getString(1)).thenReturn("content://downloads/all_downloads/" + DOWNLOAD_ID); + return cursor; + } + + @NonNull + Notification.Builder mockNotificationBuilderChain() throws Exception { + Notification.Builder notificationBuilder = mock(Notification.Builder.class); + whenNew(Notification.Builder.class).withAnyArguments().thenReturn(notificationBuilder); + when(notificationBuilder.setTicker(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentTitle(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentText(anyString())).thenReturn(notificationBuilder); + when(notificationBuilder.setSmallIcon(anyInt())).thenReturn(notificationBuilder); + when(notificationBuilder.setContentIntent(any(PendingIntent.class))).thenReturn(notificationBuilder); + return notificationBuilder; + } + + @NonNull + Intent mockInstallIntent() throws Exception { + Intent installIntent = mock(Intent.class); + whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); + when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); + return installIntent; + } + + void restartActivity() { + Distribute.getInstance().onActivityStopped(mActivity); + Distribute.getInstance().onActivityDestroyed(mActivity); + Distribute.getInstance().onActivityCreated(mActivity, null); + Distribute.getInstance().onActivityResumed(mActivity); + } + + void restartProcessAndSdk() { + Distribute.unsetInstance(); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + } + + @After + public void tearDown() throws Exception { + TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", 0); + } +} diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java index 9380ac2a82..9cf7de3869 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/AbstractDistributeTest.java @@ -1,6 +1,7 @@ package com.microsoft.azure.mobile.distribute; import android.annotation.SuppressLint; +import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -62,6 +63,9 @@ public class AbstractDistributeTest { @Mock Context mContext; + @Mock + Activity mActivity; + @Mock PackageManager mPackageManager; @@ -114,8 +118,10 @@ public Void answer(InvocationOnMock invocation) throws Throwable { /* Mock package manager. */ when(mContext.getPackageName()).thenReturn("com.contoso"); + when(mActivity.getPackageName()).thenReturn("com.contoso"); when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo); when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mActivity.getPackageManager()).thenReturn(mPackageManager); PackageInfo packageInfo = mock(PackageInfo.class); when(mPackageManager.getPackageInfo("com.contoso", 0)).thenReturn(packageInfo); Whitebox.setInternalState(packageInfo, "versionName", "1.2.3"); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java index 0d9207723d..ff8c72660d 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java @@ -61,13 +61,11 @@ public class DistributeBeforeApiSuccessTest extends AbstractDistributeTest { /** * Shared code to mock a restart of an activity considered to be the launcher. */ - private static void restartResumeLauncher(Activity activity) { + private void restartResumeLauncher(Activity activity) { Intent intent = mock(Intent.class); - PackageManager packageManager = mock(PackageManager.class); - when(activity.getPackageManager()).thenReturn(packageManager); - when(packageManager.getLaunchIntentForPackage(anyString())).thenReturn(intent); + when(mPackageManager.getLaunchIntentForPackage(anyString())).thenReturn(intent); ComponentName componentName = mock(ComponentName.class); - when(intent.resolveActivity(packageManager)).thenReturn(componentName); + when(intent.resolveActivity(mPackageManager)).thenReturn(componentName); when(componentName.getClassName()).thenReturn(activity.getClass().getName()); Distribute.getInstance().onActivityPaused(activity); Distribute.getInstance().onActivityStopped(activity); @@ -147,21 +145,20 @@ public void postponeBrowserIfNoNetwork() throws Exception { /* Check browser not opened if no network. */ when(mNetworkStateHelper.isNetworkConnected()).thenReturn(false); Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Distribute.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityResumed(mActivity); verifyStatic(never()); BrowserUtils.openBrowser(anyString(), any(Activity.class)); /* If network comes back, we don't open network unless we restart app. */ when(mNetworkStateHelper.isNetworkConnected()).thenReturn(true); - Distribute.getInstance().onActivityPaused(mock(Activity.class)); - Distribute.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onActivityPaused(mActivity); + Distribute.getInstance().onActivityResumed(mActivity); verifyStatic(never()); BrowserUtils.openBrowser(anyString(), any(Activity.class)); /* Restart should open browser if still have network. */ when(UUIDUtils.randomUUID()).thenReturn(UUID.randomUUID()); - Activity activity = mock(Activity.class); - restartResumeLauncher(activity); + restartResumeLauncher(mActivity); verifyStatic(); BrowserUtils.openBrowser(anyString(), any(Activity.class)); } @@ -178,8 +175,7 @@ public void happyPathUntilHangingCall() throws Exception { /* Start and resume: open browser. */ Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Activity activity = mock(Activity.class); - Distribute.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityResumed(mActivity); verifyStatic(); String url = DistributeConstants.DEFAULT_INSTALL_URL; url += String.format(UPDATE_SETUP_PATH_FORMAT, "a"); @@ -187,15 +183,15 @@ public void happyPathUntilHangingCall() throws Exception { url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(url, mActivity); verifyStatic(); PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); /* If browser already opened, activity changed must not recall it. */ - Distribute.getInstance().onActivityPaused(activity); - Distribute.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityPaused(mActivity); + Distribute.getInstance().onActivityResumed(mActivity); verifyStatic(); - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(url, mActivity); verifyStatic(); PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); @@ -222,8 +218,8 @@ public boolean matches(Object argument) { }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* If call already made, activity changed must not recall it. */ - Distribute.getInstance().onActivityPaused(activity); - Distribute.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityPaused(mActivity); + Distribute.getInstance().onActivityResumed(mActivity); /* Verify behavior. */ verifyStatic(); @@ -243,16 +239,16 @@ public boolean matches(Object argument) { }), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); /* Call is still in progress. If we restart app, nothing happens we still wait. */ - restartResumeLauncher(activity); - Distribute.getInstance().onActivityPaused(activity); - Distribute.getInstance().onActivityStopped(activity); - Distribute.getInstance().onActivityDestroyed(activity); - Distribute.getInstance().onActivityCreated(activity, mock(Bundle.class)); - Distribute.getInstance().onActivityResumed(activity); + restartResumeLauncher(mActivity); + Distribute.getInstance().onActivityPaused(mActivity); + Distribute.getInstance().onActivityStopped(mActivity); + Distribute.getInstance().onActivityDestroyed(mActivity); + Distribute.getInstance().onActivityCreated(mActivity, mock(Bundle.class)); + Distribute.getInstance().onActivityResumed(mActivity); /* Verify behavior not changed. */ verifyStatic(); - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(url, mActivity); verifyStatic(); PreferencesStorage.putString(PREFERENCE_KEY_UPDATE_TOKEN, "some token"); verifyStatic(); @@ -284,8 +280,7 @@ public void setUrls() throws Exception { /* Start and resume: open browser. */ Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Activity activity = mock(Activity.class); - Distribute.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityResumed(mActivity); verifyStatic(); String url = "http://mock"; url += String.format(UPDATE_SETUP_PATH_FORMAT, "a"); @@ -293,7 +288,7 @@ public void setUrls() throws Exception { url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(url, mActivity); verifyStatic(); PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); @@ -314,19 +309,14 @@ public boolean matches(Object argument) { public void computeHashFailsWhenOpeningBrowser() throws Exception { /* Mock package manager. */ - Context context = mock(Context.class); - PackageManager packageManager = mock(PackageManager.class); - when(context.getPackageName()).thenReturn("com.contoso"); - when(context.getPackageManager()).thenReturn(packageManager); - when(context.getApplicationInfo()).thenReturn(mApplicationInfo); - when(packageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); + when(mPackageManager.getPackageInfo("com.contoso", 0)).thenThrow(new PackageManager.NameNotFoundException()); /* Start and resume: open browser. */ - Distribute.getInstance().onStarted(context, "a", mock(Channel.class)); - Distribute.getInstance().onActivityResumed(mock(Activity.class)); + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mActivity); /* Verify only tried once. */ - verify(packageManager).getPackageInfo("com.contoso", 0); + verify(mPackageManager).getPackageInfo("com.contoso", 0); /* And verify we didn't open browser. */ verifyStatic(never()); @@ -342,8 +332,7 @@ public void disableBeforeStoreToken() { UUID requestId = UUID.randomUUID(); when(UUIDUtils.randomUUID()).thenReturn(requestId); Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Activity activity = mock(Activity.class); - Distribute.getInstance().onActivityResumed(activity); + Distribute.getInstance().onActivityResumed(mActivity); verifyStatic(); String url = DistributeConstants.DEFAULT_INSTALL_URL; url += String.format(UPDATE_SETUP_PATH_FORMAT, "a"); @@ -351,7 +340,7 @@ public void disableBeforeStoreToken() { url += "&" + PARAMETER_REDIRECT_ID + "=" + mContext.getPackageName(); url += "&" + PARAMETER_REQUEST_ID + "=" + requestId.toString(); url += "&" + PARAMETER_PLATFORM + "=" + PARAMETER_PLATFORM_VALUE; - BrowserUtils.openBrowser(url, activity); + BrowserUtils.openBrowser(url, mActivity); verifyStatic(); PreferencesStorage.putString(PREFERENCE_KEY_REQUEST_ID, requestId.toString()); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java index b9f7dcb0ed..3f99a683a6 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java @@ -29,6 +29,7 @@ import static com.microsoft.azure.mobile.distribute.DistributeConstants.INVALID_RELEASE_IDENTIFIER; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_IGNORED_RELEASE_ID; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_RELEASE_DETAILS; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; import static org.mockito.Matchers.any; @@ -468,6 +469,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify. */ verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_RELEASE_DETAILS); /* Verify no more calls, e.g. happened only once. */ Distribute.getInstance().onActivityPaused(mock(Activity.class)); @@ -740,7 +743,43 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify no download scheduled. */ verifyStatic(never()); - AsyncTaskUtils.execute(anyString(), any(Distribute.DownloadTask.class), Mockito.anyVararg()); + AsyncTaskUtils.execute(anyString(), any(DownloadTask.class), Mockito.anyVararg()); + } + + @Test + public void mandatoryUpdateDialog() throws Exception { + + /* Mock we already have token. */ + when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); + HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); + whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + + @Override + public ServiceCall answer(InvocationOnMock invocation) throws Throwable { + ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); + return mock(ServiceCall.class); + } + }); + ReleaseDetails releaseDetails = mock(ReleaseDetails.class); + when(releaseDetails.getId()).thenReturn(4); + when(releaseDetails.getVersion()).thenReturn(7); + when(releaseDetails.isMandatoryUpdate()).thenReturn(true); + when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); + + /* Trigger call. */ + Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + Distribute.getInstance().onActivityResumed(mock(Activity.class)); + + /* Verify release notes persisted. */ + verifyStatic(); + PreferencesStorage.putString(PREFERENCE_KEY_RELEASE_DETAILS, "mock"); + + /* Verify dialog. */ + verify(mDialogBuilder, never()).setNeutralButton(anyString(), any(DialogInterface.OnClickListener.class)); + verify(mDialogBuilder, never()).setNegativeButton(anyString(), any(DialogInterface.OnClickListener.class)); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_update_dialog_download), any(DialogInterface.OnClickListener.class)); + verify(mDialogBuilder).setCancelable(false); } @After diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java index 878881c555..d3c029460a 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeDownloadTest.java @@ -4,11 +4,8 @@ import android.app.Activity; import android.app.DownloadManager; import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.ComponentName; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -17,23 +14,15 @@ import android.database.Cursor; import android.net.Uri; import android.os.Build; -import android.support.annotation.NonNull; import com.microsoft.azure.mobile.channel.Channel; -import com.microsoft.azure.mobile.http.HttpClient; -import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; -import com.microsoft.azure.mobile.http.ServiceCall; -import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.test.TestUtils; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatcher; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.internal.util.reflection.Whitebox; import org.mockito.invocation.InvocationOnMock; @@ -42,287 +31,39 @@ import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicReference; import static android.app.DownloadManager.EXTRA_DOWNLOAD_ID; -import static android.content.Context.NOTIFICATION_SERVICE; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_COMPLETED; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_ENQUEUED; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_NOTIFIED; import static com.microsoft.azure.mobile.distribute.DistributeConstants.INVALID_DOWNLOAD_IDENTIFIER; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_ID; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_TIME; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.powermock.api.mockito.PowerMockito.doAnswer; -import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyNew; import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.when; import static org.powermock.api.mockito.PowerMockito.whenNew; -@SuppressWarnings("CanBeFinal") -@PrepareForTest(AsyncTaskUtils.class) -public class DistributeDownloadTest extends AbstractDistributeTest { - - private static final long DOWNLOAD_ID = 42; - - @Mock - private Uri mDownloadUrl; - - @Mock - private DownloadManager mDownloadManager; - - @Mock - private NotificationManager mNotificationManager; - - @Mock - private DownloadManager.Request mDownloadRequest; - - @Mock - private Activity mFirstActivity; - - private Semaphore mDownloadBeforeSemaphore; - - private Semaphore mDownloadAfterSemaphore; - - private AtomicReference mDownloadTask; - - private Semaphore mCheckDownloadBeforeSemaphore; - - private Semaphore mCheckDownloadAfterSemaphore; - - private AtomicReference mCompletionTask; +public class DistributeDownloadTest extends AbstractDistributeAfterDownloadTest { @Before public void setUpDownload() throws Exception { - - /* Allow unknown sources. */ - when(InstallerUtils.isUnknownSourcesEnabled(any(Context.class))).thenReturn(true); - - /* Mock download manager. */ - when(mContext.getSystemService(Context.DOWNLOAD_SERVICE)).thenReturn(mDownloadManager); - whenNew(DownloadManager.Request.class).withAnyArguments().thenReturn(mDownloadRequest); - when(mDownloadManager.enqueue(mDownloadRequest)).thenReturn(DOWNLOAD_ID); - - /* Mock notification manager. */ - when(mContext.getSystemService(NOTIFICATION_SERVICE)).thenReturn(mNotificationManager); - - /* Mock updates to storage. */ - doAnswer(new Answer() { - - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - when(PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn((Long) invocation.getArguments()[1]); - return null; - } - }).when(PreferencesStorage.class); - PreferencesStorage.putLong(eq(PREFERENCE_KEY_DOWNLOAD_ID), anyLong()); - doAnswer(new Answer() { - - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - when(PreferencesStorage.getLong(invocation.getArguments()[0].toString(), INVALID_DOWNLOAD_IDENTIFIER)).thenReturn(INVALID_DOWNLOAD_IDENTIFIER); - return null; - } - }).when(PreferencesStorage.class); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_ID); - doAnswer(new Answer() { - - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn((Integer) invocation.getArguments()[1]); - return null; - } - }).when(PreferencesStorage.class); - PreferencesStorage.putInt(eq(PREFERENCE_KEY_DOWNLOAD_STATE), anyInt()); - doAnswer(new Answer() { - - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - when(PreferencesStorage.getInt(invocation.getArguments()[0].toString(), DOWNLOAD_STATE_COMPLETED)).thenReturn(DOWNLOAD_STATE_COMPLETED); - return null; - } - }).when(PreferencesStorage.class); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); - - /* Mock everything that triggers a download. */ - when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); - HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); - whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { - - @Override - public ServiceCall answer(InvocationOnMock invocation) throws Throwable { - ((ServiceCallback) invocation.getArguments()[4]).onCallSucceeded("mock"); - return mock(ServiceCall.class); - } - }); - ReleaseDetails releaseDetails = mock(ReleaseDetails.class); - when(releaseDetails.getId()).thenReturn(4); - when(releaseDetails.getVersion()).thenReturn(7); - when(releaseDetails.getDownloadUrl()).thenReturn(mDownloadUrl); - when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); - mockStatic(AsyncTaskUtils.class); - Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Distribute.getInstance().onActivityResumed(mFirstActivity); - - /* Mock download asyncTask. */ - mDownloadBeforeSemaphore = new Semaphore(0); - mDownloadAfterSemaphore = new Semaphore(0); - mDownloadTask = new AtomicReference<>(); - when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { - - @Override - public boolean matches(Object argument) { - return argument instanceof Distribute.DownloadTask; - } - }), Mockito.anyVararg())).then(new Answer() { - - @Override - public Distribute.DownloadTask answer(InvocationOnMock invocation) throws Throwable { - final Distribute.DownloadTask task = spy((Distribute.DownloadTask) invocation.getArguments()[1]); - mDownloadTask.set(task); - new Thread() { - - @Override - public void run() { - mDownloadBeforeSemaphore.acquireUninterruptibly(); - task.doInBackground(null); - mDownloadAfterSemaphore.release(); - } - }.start(); - return task; - } - }); - - /* Mock remove download async task. */ - when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { - - @Override - public boolean matches(Object argument) { - return argument instanceof Distribute.RemoveDownloadTask; - } - }), Mockito.anyVararg())).then(new Answer() { - - @Override - public Distribute.RemoveDownloadTask answer(InvocationOnMock invocation) throws Throwable { - final Distribute.RemoveDownloadTask task = (Distribute.RemoveDownloadTask) invocation.getArguments()[1]; - task.doInBackground((Long) invocation.getArguments()[2]); - return task; - } - }); - - /* Mock download completion async task. */ - mCheckDownloadBeforeSemaphore = new Semaphore(0); - mCheckDownloadAfterSemaphore = new Semaphore(0); - mCompletionTask = new AtomicReference<>(); - when(AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { - - @Override - public boolean matches(Object argument) { - return argument instanceof Distribute.CheckDownloadTask; - } - }), Mockito.anyVararg())).then(new Answer() { - - @Override - public Distribute.CheckDownloadTask answer(InvocationOnMock invocation) throws Throwable { - final Distribute.CheckDownloadTask task = spy((Distribute.CheckDownloadTask) invocation.getArguments()[1]); - mCompletionTask.set(task); - new Thread() { - - @Override - public void run() { - mCheckDownloadBeforeSemaphore.acquireUninterruptibly(); - task.doInBackground(); - mCheckDownloadAfterSemaphore.release(); - } - }.start(); - return task; - } - }); - - /* Click on dialog. */ - ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); - verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_update_dialog_download), clickListener.capture()); - clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); - } - - private void waitDownloadTask() { - mDownloadBeforeSemaphore.release(); - mDownloadAfterSemaphore.acquireUninterruptibly(); - } - - private void waitCheckDownloadTask() { - mCheckDownloadBeforeSemaphore.release(); - mCheckDownloadAfterSemaphore.acquireUninterruptibly(); - } - - private void completeDownload() { - Intent completionIntent = mock(Intent.class); - when(completionIntent.getAction()).thenReturn(DownloadManager.ACTION_DOWNLOAD_COMPLETE); - when(completionIntent.getLongExtra(eq(EXTRA_DOWNLOAD_ID), anyLong())).thenReturn(DOWNLOAD_ID); - new DownloadManagerReceiver().onReceive(mContext, completionIntent); - } - - @NonNull - private Cursor mockSuccessCursor() { - Cursor cursor = mock(Cursor.class); - when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); - when(cursor.moveToFirst()).thenReturn(true); - when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)).thenReturn(0); - when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_SUCCESSFUL); - when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)).thenReturn(1); - when(cursor.getString(1)).thenReturn("content://downloads/all_downloads/" + DOWNLOAD_ID); - return cursor; - } - - @NonNull - private Notification.Builder mockNotificationBuilderChain() throws Exception { - Notification.Builder notificationBuilder = mock(Notification.Builder.class); - whenNew(Notification.Builder.class).withAnyArguments().thenReturn(notificationBuilder); - when(notificationBuilder.setTicker(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentTitle(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentText(anyString())).thenReturn(notificationBuilder); - when(notificationBuilder.setSmallIcon(anyInt())).thenReturn(notificationBuilder); - when(notificationBuilder.setContentIntent(any(PendingIntent.class))).thenReturn(notificationBuilder); - return notificationBuilder; - } - - @NonNull - private Intent mockInstallIntent() throws Exception { - Intent installIntent = mock(Intent.class); - whenNew(Intent.class).withArguments(Intent.ACTION_INSTALL_PACKAGE).thenReturn(installIntent); - when(installIntent.resolveActivity(any(PackageManager.class))).thenReturn(mock(ComponentName.class)); - return installIntent; - } - - private void restartActivity() { - Distribute.getInstance().onActivityStopped(mFirstActivity); - Distribute.getInstance().onActivityDestroyed(mFirstActivity); - Distribute.getInstance().onActivityCreated(mFirstActivity, null); - Distribute.getInstance().onActivityResumed(mFirstActivity); - } - - private void restartProcessAndSdk() { - Distribute.unsetInstance(); - Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); + setUpDownload(false); } @Test @@ -341,8 +82,8 @@ public void startDownloadThenDisable() throws Exception { /* Pause/resume should do nothing excepting mentioning progress. */ verify(mDialog).show(); - Distribute.getInstance().onActivityPaused(mFirstActivity); - Distribute.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityPaused(mActivity); + Distribute.getInstance().onActivityResumed(mActivity); verify(mDialog).show(); /* Cancel download by disabling. */ @@ -580,7 +321,7 @@ public void successInForeground() throws Exception { } @Test - public void longFailingDownload() throws Exception { + public void longFailingDownloadForOptionalDownload() throws Exception { /* Simulate async task. */ waitDownloadTask(); @@ -593,12 +334,11 @@ public void longFailingDownload() throws Exception { when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_RUNNING); /* Restart launcher, nothing happens. */ - when(mFirstActivity.getPackageManager()).thenReturn(mPackageManager); Intent launcherIntent = mock(Intent.class); when(mPackageManager.getLaunchIntentForPackage(anyString())).thenReturn(launcherIntent); ComponentName launcher = mock(ComponentName.class); when(launcherIntent.resolveActivity(mPackageManager)).thenReturn(launcher); - when(launcher.getClassName()).thenReturn(mFirstActivity.getClass().getName()); + when(launcher.getClassName()).thenReturn(mActivity.getClass().getName()); restartActivity(); /* Restart app process. Still nothing as background. */ @@ -606,40 +346,22 @@ public void longFailingDownload() throws Exception { /* No download check yet. */ verifyStatic(never()); - AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { - - @Override - public boolean matches(Object argument) { - return argument instanceof Distribute.CheckDownloadTask; - } - }), Mockito.anyVararg()); + AsyncTaskUtils.execute(anyString(), argThat(sCheckCompleteTask), Mockito.anyVararg()); /* Foreground: check still in progress. */ - Distribute.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mActivity); waitCheckDownloadTask(); verifyStatic(); - AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { - - @Override - public boolean matches(Object argument) { - return argument instanceof Distribute.CheckDownloadTask; - } - }), Mockito.anyVararg()); + AsyncTaskUtils.execute(anyString(), argThat(sCheckCompleteTask), Mockito.anyVararg()); verify(cursor).close(); /* Restart launcher. */ - Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mActivity); restartActivity(); /* Verify we don't run the check again. (Only once). */ verifyStatic(); - AsyncTaskUtils.execute(anyString(), argThat(new ArgumentMatcher() { - - @Override - public boolean matches(Object argument) { - return argument instanceof Distribute.CheckDownloadTask; - } - }), Mockito.anyVararg()); + AsyncTaskUtils.execute(anyString(), argThat(sCheckCompleteTask), Mockito.anyVararg()); /* Download eventually fails. */ when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_FAILED); @@ -668,9 +390,9 @@ public void disabledWhileCheckingDownloadOnRestart() throws BrokenBarrierExcepti when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)).thenReturn(0); when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_RUNNING); - /* Restart app process. Still nothing as background. */ + /* Restart app process and resume. */ restartProcessAndSdk(); - Distribute.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mActivity); /* Change behavior of get download it to block to simulate the concurrency issue. */ final Semaphore beforeDisabledSemaphore = new Semaphore(0); @@ -742,7 +464,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { }); /* Mock success in background. */ - Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mActivity); mockSuccessCursor(); mockInstallIntent(); completeDownload(); @@ -833,7 +555,7 @@ public void notifyThenRestartAppTwice() throws Exception { Intent installIntent = mockInstallIntent(); /* In background. */ - Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mActivity); /* Mock notification. */ when(mPackageManager.getApplicationInfo(mContext.getPackageName(), 0)).thenReturn(mock(ApplicationInfo.class)); @@ -850,17 +572,17 @@ public void notifyThenRestartAppTwice() throws Exception { PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_NOTIFIED); verify(notificationBuilder).build(); verify(notificationBuilder, never()).getNotification(); - verify(mNotificationManager).notify(eq(Distribute.getNotificationId()), any(Notification.class)); + verify(mNotificationManager).notify(eq(DistributeUtils.getNotificationId()), any(Notification.class)); verifyNoMoreInteractions(mNotificationManager); verify(cursor).close(); /* Launch app should pop install U.I. and cancel notification. */ - when(mFirstActivity.getPackageManager()).thenReturn(mPackageManager); + when(mActivity.getPackageManager()).thenReturn(mPackageManager); Intent launcherIntent = mock(Intent.class); when(mPackageManager.getLaunchIntentForPackage(anyString())).thenReturn(launcherIntent); ComponentName launcher = mock(ComponentName.class); when(launcherIntent.resolveActivity(mPackageManager)).thenReturn(launcher); - when(launcher.getClassName()).thenReturn(mFirstActivity.getClass().getName()); + when(launcher.getClassName()).thenReturn(mActivity.getClass().getName()); restartActivity(); /* Wait again. */ @@ -868,8 +590,10 @@ public void notifyThenRestartAppTwice() throws Exception { /* Verify U.I shown after restart and workflow completed. */ verify(mContext).startActivity(installIntent); - verify(mNotificationManager).cancel(Distribute.getNotificationId()); + verify(mNotificationManager).cancel(DistributeUtils.getNotificationId()); verifyStatic(); + + /* Verify workflow completed. */ PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); /* Verify however downloaded file was kept. */ @@ -893,7 +617,7 @@ public void notifyThenRestartAppTwice() throws Exception { verify(mDownloadManager).remove(DOWNLOAD_ID); /* Notification already canceled so no more call, i.e. only once. */ - verify(mNotificationManager).cancel(Distribute.getNotificationId()); + verify(mNotificationManager).cancel(DistributeUtils.getNotificationId()); } @Test @@ -929,7 +653,7 @@ public void notifyThenRestartThenInstallerFails() throws Exception { verify(mContext, never()).startActivity(installIntent); verifyStatic(); PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_NOTIFIED); - verify(mNotificationManager).notify(eq(Distribute.getNotificationId()), any(Notification.class)); + verify(mNotificationManager).notify(eq(DistributeUtils.getNotificationId()), any(Notification.class)); verifyNoMoreInteractions(mNotificationManager); verify(cursor).getString(2); verify(cursor).close(); @@ -937,14 +661,14 @@ public void notifyThenRestartThenInstallerFails() throws Exception { /* Restart app should pop install U.I. and cancel notification and pop a new dialog then a new download. */ doThrow(new ActivityNotFoundException()).when(mContext).startActivity(installIntent); Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Distribute.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mActivity); /* Wait download manager query. */ waitCheckDownloadTask(); /* Verify workflow completed even on failure to show install U.I. */ verify(mContext).startActivity(installIntent); - verify(mNotificationManager).cancel(Distribute.getNotificationId()); + verify(mNotificationManager).cancel(DistributeUtils.getNotificationId()); verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); verifyStatic(never()); @@ -957,7 +681,7 @@ public void restartDownloadCheckIsLongEnoughToAppCanGoBackgroundAgain() throws E /* Simulate async task. */ waitDownloadTask(); - Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mActivity); /* Process download completion to notify. */ completeDownload(); @@ -977,8 +701,8 @@ public void restartDownloadCheckIsLongEnoughToAppCanGoBackgroundAgain() throws E * already notified. */ restartProcessAndSdk(); - Distribute.getInstance().onActivityResumed(mFirstActivity); - Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityResumed(mActivity); + Distribute.getInstance().onActivityPaused(mActivity); waitCheckDownloadTask(); verify(mNotificationManager).notify(anyInt(), any(Notification.class)); verify(mContext).startActivity(installIntent); @@ -1012,7 +736,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { when(notificationBuilder.getNotification()).thenReturn(mock(Notification.class)); /* Make notification happen. */ - Distribute.getInstance().onActivityPaused(mFirstActivity); + Distribute.getInstance().onActivityPaused(mActivity); completeDownload(); waitCheckDownloadTask(); @@ -1025,7 +749,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { packageInfo.lastUpdateTime = Long.MAX_VALUE; when(mPackageManager.getPackageInfo(mContext.getPackageName(), 0)).thenReturn(packageInfo); restartProcessAndSdk(); - Distribute.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mActivity); verify(mDownloadManager).remove(DOWNLOAD_ID); /* Verify new release checked (for example what we installed was something else than the upgrade. */ @@ -1040,15 +764,10 @@ public void failToCheckLastUpdateTimeOnRestart() throws PackageManager.NameNotFo waitDownloadTask(); when(mPackageManager.getPackageInfo(mContext.getPackageName(), 0)).thenThrow(new PackageManager.NameNotFoundException()); restartProcessAndSdk(); - Distribute.getInstance().onActivityResumed(mFirstActivity); + Distribute.getInstance().onActivityResumed(mActivity); /* Verify workflow completed on failure. */ verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); } - - @After - public void tearDown() throws Exception { - TestUtils.setInternalState(Build.VERSION.class, "SDK_INT", 0); - } } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeHttpTest.java similarity index 99% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeHttpTest.java index 320e63ddf7..b5685ac17b 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeHttpTest.java @@ -40,7 +40,7 @@ @SuppressWarnings("unused") @PrepareForTest({NetworkStateHelper.class, MobileCenterLog.class, Distribute.class}) -public class DistributeTest { +public class DistributeHttpTest { @Rule public PowerMockRule rule = new PowerMockRule(); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java new file mode 100644 index 0000000000..6cf5c186ec --- /dev/null +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java @@ -0,0 +1,347 @@ +package com.microsoft.azure.mobile.distribute; + +import android.app.DownloadManager; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.database.Cursor; +import android.os.Handler; +import android.os.SystemClock; +import android.support.annotation.NonNull; + +import com.microsoft.azure.mobile.utils.HandlerUtils; +import com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; + +import org.json.JSONException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import java.util.concurrent.Semaphore; + +import static com.microsoft.azure.mobile.distribute.DistributeConstants.CHECK_PROGRESS_TIME_INTERVAL; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_INSTALLING; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.HANDLER_TOKEN_CHECK_PROGRESS; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.MEBIBYTE_IN_BYTES; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_RELEASE_DETAILS; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.powermock.api.mockito.PowerMockito.doAnswer; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyStatic; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@PrepareForTest({SystemClock.class, HandlerUtils.class}) +public class DistributeMandatoryDownloadTest extends AbstractDistributeAfterDownloadTest { + + @Mock + private ProgressDialog mProgressDialog; + + @Mock + private Handler mHandler; + + @Before + public void setUpDownload() throws Exception { + + /* Mock some dialog methods. */ + whenNew(ProgressDialog.class).withAnyArguments().thenReturn(mProgressDialog); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + when(mProgressDialog.isIndeterminate()).thenReturn((Boolean) invocation.getArguments()[0]); + return null; + } + }).when(mProgressDialog).setIndeterminate(anyBoolean()); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Mockito.when(mDialog.isShowing()).thenReturn(true); + return null; + } + }).when(mDialog).show(); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Mockito.when(mDialog.isShowing()).thenReturn(false); + return null; + } + }).when(mDialog).hide(); + + /* Mock time for Handler.post. */ + mockStatic(SystemClock.class); + when(SystemClock.uptimeMillis()).thenReturn(1L); + + /* Mock Handler. */ + mockStatic(HandlerUtils.class); + when(mHandler.postAtTime(any(Runnable.class), eq(HANDLER_TOKEN_CHECK_PROGRESS), anyLong())).then(new Answer() { + + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + ((Runnable) invocation.getArguments()[0]).run(); + return true; + } + }); + when(HandlerUtils.getMainHandler()).thenReturn(mHandler); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + ((Runnable) invocation.getArguments()[0]).run(); + return null; + } + }).when(HandlerUtils.class); + HandlerUtils.runOnUiThread(any(Runnable.class)); + + /* Set up common download test. */ + setUpDownload(true); + } + + @NonNull + private Cursor mockProgressCursor(long progress) { + Cursor cursor = mock(Cursor.class); + when(mDownloadManager.query(any(DownloadManager.Query.class))).thenReturn(cursor); + when(cursor.moveToFirst()).thenReturn(true); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)).thenReturn(0); + when(cursor.getInt(0)).thenReturn(DownloadManager.STATUS_RUNNING); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)).thenReturn(1); + when(cursor.getLong(1)).thenReturn(progress); + when(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)).thenReturn(2); + when(cursor.getLong(2)).thenReturn(progress > 0 ? (long) (100 * MEBIBYTE_IN_BYTES) : -1); + return cursor; + } + + @Test + public void longMandatoryDownloadAndInstallAcrossRestarts() throws Exception { + + /* Dialog shown upon clicking on download. */ + verify(mProgressDialog).setCancelable(false); + verify(mProgressDialog).show(); + + /* Mock initial progress where file size is still unknown. */ + Cursor cursor = mockProgressCursor(-1); + waitDownloadTask(); + waitCheckDownloadTask(); + verify(cursor).close(); + verify(mHandler).postAtTime(any(Runnable.class), eq(HANDLER_TOKEN_CHECK_PROGRESS), eq(CHECK_PROGRESS_TIME_INTERVAL + 1)); + verify(mProgressDialog, never()).setProgress(anyInt()); + + /* Mock some progress. */ + mockProgressCursor((long) (17 * MEBIBYTE_IN_BYTES)); + waitCheckDownloadTask(); + verify(mProgressDialog).setProgress(17); + + /* Mock further progress. */ + mockProgressCursor((long) (42 * MEBIBYTE_IN_BYTES)); + waitCheckDownloadTask(); + verify(mProgressDialog).setProgress(42); + + /* Pause hides dialog and pauses updates. */ + Distribute.getInstance().onActivityPaused(mActivity); + verify(mProgressDialog).hide(); + verify(mHandler).removeCallbacksAndMessages(HANDLER_TOKEN_CHECK_PROGRESS); + + /* Unblock current task that will not reschedule a new one. */ + waitCheckDownloadTask(); + + /* Check no more timer and progress update while paused. */ + verify(mProgressDialog).setProgress(42); + verify(mHandler, times(3)).postAtTime(any(Runnable.class), eq(HANDLER_TOKEN_CHECK_PROGRESS), eq(CHECK_PROGRESS_TIME_INTERVAL + 1)); + + /* Reusing dialog on resume. */ + Distribute.getInstance().onActivityResumed(mActivity); + verify(mProgressDialog, times(2)).show(); + + /* On restart progress is restored. */ + mProgressDialog = mock(ProgressDialog.class); + whenNew(ProgressDialog.class).withAnyArguments().thenReturn(mProgressDialog); + restartProcessAndSdk(); + + /* Unblock the previous task now that we are paused. */ + waitCheckDownloadTask(); + + /* Resume shows a new dialog as process restarted. */ + Distribute.getInstance().onActivityResumed(mActivity); + verify(mProgressDialog).show(); + waitCheckDownloadTask(); + verify(mProgressDialog).setProgress(42); + + /* Download eventually completes: show install U.I. */ + completeDownload(); + mockSuccessCursor(); + Intent installIntent = mockInstallIntent(); + waitCheckDownloadTask(); + waitCheckDownloadTask(); + verify(mContext).startActivity(installIntent); + verifyStatic(); + PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_INSTALLING); + verifyNoMoreInteractions(mNotificationManager); + + /* Showing install U.I. pauses app. */ + Distribute.getInstance().onActivityPaused(mActivity); + + /* We also display mandatory install dialog if user goes back to app. */ + Distribute.getInstance().onActivityResumed(mActivity); + + /* Check dialog shown. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_install), clickListener.capture()); + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + waitCheckDownloadTask(); + verify(mContext, times(2)).startActivity(installIntent); + + /* Showing install U.I. pauses app. */ + Distribute.getInstance().onActivityPaused(mActivity); + + /* Pause/resume leave existing dialog intact. */ + Distribute.getInstance().onActivityResumed(mActivity); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_install), clickListener.capture()); + + /* If we restart the app process, it will display install U.I. again skipping dialog. */ + restartProcessAndSdk(); + Distribute.getInstance().onActivityResumed(mActivity); + waitCheckDownloadTask(); + verify(mContext, times(3)).startActivity(installIntent); + + /* Eventually discard download only if application updated. */ + PackageInfo packageInfo = mock(PackageInfo.class); + packageInfo.lastUpdateTime = Long.MAX_VALUE; + when(mPackageManager.getPackageInfo(mContext.getPackageName(), 0)).thenReturn(packageInfo); + restartProcessAndSdk(); + Distribute.getInstance().onActivityResumed(mActivity); + verify(mDownloadManager).remove(DOWNLOAD_ID); + + /* Check no more dialog displayed. */ + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_install), clickListener.capture()); + + /* And that we don't prompt install anymore. */ + verify(mContext, times(3)).startActivity(installIntent); + } + + @Test + @SuppressWarnings("deprecation") + public void disabledBeforeClickOnDialogInstall() throws Exception { + + /* Unblock download. */ + waitDownloadTask(); + + /* Complete download. */ + completeDownload(); + mockSuccessCursor(); + Intent installIntent = mockInstallIntent(); + waitCheckDownloadTask(); + waitCheckDownloadTask(); + verify(mContext).startActivity(installIntent); + + /* Cancel install to go back to app. */ + Distribute.getInstance().onActivityPaused(mActivity); + Distribute.getInstance().onActivityResumed(mActivity); + + /* Verify install dialog shown. */ + ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mDialogBuilder).setPositiveButton(eq(R.string.mobile_center_distribute_install), clickListener.capture()); + + /* Disable SDK. */ + Distribute.setEnabled(false); + + /* Click. */ + clickListener.getValue().onClick(mDialog, DialogInterface.BUTTON_POSITIVE); + + /* Verify disabled. */ + verify(mDownloadManager).remove(DOWNLOAD_ID); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + } + + @Test + public void startActivityButDisabledAfterCheckpoint() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Process download completion. */ + mockSuccessCursor(); + final Intent installIntent = mockInstallIntent(); + final Semaphore beforeStartingActivityLock = new Semaphore(0); + final Semaphore disabledLock = new Semaphore(0); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + beforeStartingActivityLock.release(); + disabledLock.acquireUninterruptibly(); + return null; + } + }).when(mContext).startActivity(installIntent); + + /* Complete download, unblock the first check progress. */ + completeDownload(); + + /* Disable between check notification and start activity. Also unblock the initial check progress. */ + mCheckDownloadBeforeSemaphore.release(2); + beforeStartingActivityLock.acquireUninterruptibly(); + Distribute.setEnabled(false); + disabledLock.release(); + mCheckDownloadAfterSemaphore.acquireUninterruptibly(2); + + /* Verify start activity and complete workflow skipped, e.g. clean behavior happened only once. */ + verify(mContext).startActivity(installIntent); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + verifyStatic(never()); + PreferencesStorage.putInt(PREFERENCE_KEY_DOWNLOAD_STATE, DOWNLOAD_STATE_INSTALLING); + verifyZeroInteractions(mNotificationManager); + } + + @Test + public void jsonCorruptedWhenRestarting() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Make JSON parsing fail. */ + when(ReleaseDetails.parse(anyString())).thenThrow(new JSONException("mock")); + restartProcessAndSdk(); + Distribute.getInstance().onActivityResumed(mActivity); + mockProgressCursor(-1); + + /* Unblock the mock task before restart sdk (unit test limitation). */ + waitCheckDownloadTask(); + + /* Unblock the task that is scheduled after restart to check sanity. */ + waitCheckDownloadTask(); + + /* Verify JSON corrupted. */ + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_RELEASE_DETAILS); + + /* In that case the SDK will think its not mandatory but anyway this case never happens. */ + mockSuccessCursor(); + Intent intent = mockInstallIntent(); + completeDownload(); + waitCheckDownloadTask(); + verify(mContext).startActivity(intent); + verifyStatic(); + PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + } +} diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeConstantsTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeUtilsTest.java similarity index 72% rename from sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeConstantsTest.java rename to sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeUtilsTest.java index 00090fe062..9f1ccdb214 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeConstantsTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeUtilsTest.java @@ -4,10 +4,11 @@ import static org.junit.Assert.assertNotNull; -public class DistributeConstantsTest { +public class DistributeUtilsTest { @Test public void init() { + assertNotNull(new DistributeUtils()); assertNotNull(new DistributeConstants()); } } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java index 664ded5eec..3740177b18 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java @@ -15,9 +15,10 @@ import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; -import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.Mock; @@ -25,6 +26,9 @@ import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; +import java.util.Arrays; +import java.util.Collection; + import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_DOWNLOAD_STATE; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN; import static com.microsoft.azure.mobile.utils.storage.StorageHelper.PreferencesStorage; @@ -46,14 +50,24 @@ import static org.powermock.api.mockito.PowerMockito.whenNew; @SuppressWarnings("CanBeFinal") +@RunWith(Parameterized.class) public class DistributeWarnUnknownSourcesTest extends AbstractDistributeTest { + @SuppressWarnings("WeakerAccess") + @Parameterized.Parameter + public boolean mMandatoryUpdate; + @Mock private AlertDialog mUnknownSourcesDialog; @Mock private Activity mFirstActivity; + @Parameterized.Parameters(name = "mandatory_update={0}") + public static Collection data() { + return Arrays.asList(false, true); + } + @Before public void setUpDialog() throws Exception { @@ -72,6 +86,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { ReleaseDetails releaseDetails = mock(ReleaseDetails.class); when(releaseDetails.getId()).thenReturn(4); when(releaseDetails.getVersion()).thenReturn(7); + when(releaseDetails.isMandatoryUpdate()).thenReturn(mMandatoryUpdate); when(ReleaseDetails.parse(anyString())).thenReturn(releaseDetails); /* Trigger call. */ @@ -110,6 +125,14 @@ public Void answer(InvocationOnMock invocation) throws Throwable { @Test public void cancelDialogWithBack() { + /* Mandatory update cannot be canceled. */ + if (mMandatoryUpdate) { + + /* 1 for update dialog, 1 for unknown sources dialog. */ + verify(mDialogBuilder, times(2)).setCancelable(false); + return; + } + /* Cancel. */ ArgumentCaptor cancelListener = ArgumentCaptor.forClass(DialogInterface.OnCancelListener.class); verify(mDialogBuilder, times(2)).setOnCancelListener(cancelListener.capture()); @@ -130,6 +153,11 @@ public void cancelDialogWithBack() { @Test public void cancelDialogWithButton() { + /* Mandatory update cannot be canceled. */ + if (mMandatoryUpdate) { + return; + } + /* Cancel. */ ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); verify(mDialogBuilder).setNegativeButton(eq(android.R.string.cancel), clickListener.capture()); @@ -150,6 +178,11 @@ public void cancelDialogWithButton() { @Test public void disableBeforeCancelWithBack() { + /* Mandatory update cannot be canceled. */ + if (mMandatoryUpdate) { + return; + } + /* Disable. */ Distribute.setEnabled(false); verifyStatic(); @@ -175,6 +208,11 @@ public void disableBeforeCancelWithBack() { @Test public void disableBeforeCancelWithButton() { + /* Mandatory update cannot be canceled. */ + if (mMandatoryUpdate) { + return; + } + /* Disable. */ Distribute.setEnabled(false); verifyStatic(); @@ -275,7 +313,7 @@ public void clickSettingsThenEnableThenBack() throws Exception { @Override public boolean matches(Object argument) { - return argument instanceof Distribute.DownloadTask; + return argument instanceof DownloadTask; } }), anyVararg()); } @@ -336,16 +374,4 @@ public void disableThenClickSettingsThenFailsToNavigate() throws Exception { verify(mDialog).show(); verify(mUnknownSourcesDialog).show(); } - - @After - public void restartShowDialog() { - - /* Restart should check release and show update dialog again. */ - when(mDialogBuilder.create()).thenReturn(mDialog); - Distribute.unsetInstance(); - Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Distribute.getInstance().onActivityResumed(mock(Activity.class)); - Distribute.setEnabled(true); - verify(mDialog, times(2)).show(); - } } diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/utils/HandlerUtilsTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/utils/HandlerUtilsTest.java index fc7edb0ae4..060a108dd5 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/utils/HandlerUtilsTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/utils/HandlerUtilsTest.java @@ -12,10 +12,13 @@ public class HandlerUtilsTest { @Test - public void runOnUiThread() { + public void coverageWorkAround() { + assertNotNull(new HandlerUtils()); + assertNotNull(HandlerUtils.getMainHandler()); + } - /* Constructor code coverage is needed... */ - new HandlerUtils(); + @Test + public void runOnUiThread() { final AtomicReference mainThreadFirstRun = new AtomicReference<>(); final AtomicReference mainThreadNestedRun = new AtomicReference<>(); final Semaphore semaphore = new Semaphore(0); diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HandlerUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HandlerUtils.java index bfd68caf2b..de973daeae 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HandlerUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HandlerUtils.java @@ -11,7 +11,7 @@ public class HandlerUtils { /** * Main/UI thread Handler. */ - private static final Handler sHandler = new Handler(Looper.getMainLooper()); + private static final Handler sMainHandler = new Handler(Looper.getMainLooper()); /** * Runs the specified runnable on the UI thread. @@ -19,10 +19,19 @@ public class HandlerUtils { * @param runnable the runnable to run on the UI thread. */ public static void runOnUiThread(Runnable runnable) { - if (Thread.currentThread() == sHandler.getLooper().getThread()) { + if (Thread.currentThread() == sMainHandler.getLooper().getThread()) { runnable.run(); } else { - sHandler.post(runnable); + sMainHandler.post(runnable); } } + + /** + * Main thread handler. + * + * @return main thread handler. + */ + public static Handler getMainHandler() { + return sMainHandler; + } } diff --git a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/UUIDUtilsTest.java b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/UUIDUtilsTest.java index 4420775763..8e538b5ef1 100644 --- a/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/UUIDUtilsTest.java +++ b/sdk/mobile-center/src/test/java/com/microsoft/azure/mobile/utils/UUIDUtilsTest.java @@ -19,7 +19,6 @@ public void utilsCoverage() { @Test public void secureRandom() { UUID uuid = UUIDUtils.randomUUID(); - System.out.println(uuid); assertEquals(4, uuid.version()); assertEquals(2, uuid.variant()); } @@ -30,7 +29,6 @@ public void securityException() { when(UUIDUtils.sImplementation.randomUUID()).thenThrow(new SecurityException("mock")); for (int i = 0; i < 2; i++) { UUID uuid = UUIDUtils.randomUUID(); - System.out.println(uuid); assertEquals(4, uuid.version()); assertEquals(2, uuid.variant()); } From 4d7f1a2f9830420acfc28161ef1ad9265b68658c Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Wed, 15 Mar 2017 16:49:53 -0700 Subject: [PATCH 136/142] Allow verbose logs before SDK started (for distribute) in sasquatch --- apps/sasquatch/src/main/AndroidManifest.xml | 1 + .../mobile/sasquatch/SasquatchApplication.java | 15 +++++++++++++++ .../mobile/sasquatch/activities/MainActivity.java | 1 - 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/SasquatchApplication.java diff --git a/apps/sasquatch/src/main/AndroidManifest.xml b/apps/sasquatch/src/main/AndroidManifest.xml index 88011cf07d..b80040aa74 100644 --- a/apps/sasquatch/src/main/AndroidManifest.xml +++ b/apps/sasquatch/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ package="com.microsoft.azure.mobile.sasquatch"> Date: Wed, 15 Mar 2017 17:04:09 -0700 Subject: [PATCH 137/142] Fix code coverage when cached release details are missing --- .../DistributeMandatoryDownloadTest.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java index 6cf5c186ec..0ee0f086a4 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java @@ -321,6 +321,21 @@ public void jsonCorruptedWhenRestarting() throws Exception { /* Make JSON parsing fail. */ when(ReleaseDetails.parse(anyString())).thenThrow(new JSONException("mock")); + verifyWithInvalidOrMissingCachedJson(); + } + + @Test + public void jsonMissingWhenRestarting() throws Exception { + + /* Simulate async task. */ + waitDownloadTask(); + + /* Make JSON disappear for some reason (should not happen for real). */ + PreferencesStorage.remove(PREFERENCE_KEY_RELEASE_DETAILS); + verifyWithInvalidOrMissingCachedJson(); + } + + private void verifyWithInvalidOrMissingCachedJson() throws Exception { restartProcessAndSdk(); Distribute.getInstance().onActivityResumed(mActivity); mockProgressCursor(-1); @@ -331,7 +346,7 @@ public void jsonCorruptedWhenRestarting() throws Exception { /* Unblock the task that is scheduled after restart to check sanity. */ waitCheckDownloadTask(); - /* Verify JSON corrupted. */ + /* Verify JSON removed. */ verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_RELEASE_DETAILS); From c8abb8ed51a6c1524f4f47d76b10251737249a98 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 16 Mar 2017 17:47:56 -0700 Subject: [PATCH 138/142] Address #359 first batch of comments --- .../azure/mobile/distribute/CheckDownloadTask.java | 3 ++- .../azure/mobile/distribute/Distribute.java | 4 ++-- .../mobile/distribute/DistributeConstants.java | 2 +- .../azure/mobile/distribute/DistributeUtils.java | 2 +- .../distribute/DistributeMandatoryDownloadTest.java | 6 +++--- .../DistributeWarnUnknownSourcesTest.java | 13 ++++--------- .../azure/mobile/utils/HandlerUtilsTest.java | 10 ++++++++-- .../microsoft/azure/mobile/utils/HandlerUtils.java | 4 +++- 8 files changed, 24 insertions(+), 20 deletions(-) diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/CheckDownloadTask.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/CheckDownloadTask.java index 5c587a61d9..277e1e3685 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/CheckDownloadTask.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/CheckDownloadTask.java @@ -34,7 +34,8 @@ class CheckDownloadTask extends AsyncTask { private final long mDownloadId; /** - * Flag just to check progress. + * Flag to only check progress and not notify or show install U.I. if checking progress while + * download completed in the meantime. */ private final boolean mCheckProgress; diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java index 1f965dda29..60309ac6ad 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java @@ -54,7 +54,7 @@ import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE; import static android.util.Log.VERBOSE; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.CHECK_PROGRESS_TIME_INTERVAL; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.CHECK_PROGRESS_TIME_INTERVAL_IN_MILLIS; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DEFAULT_API_URL; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DEFAULT_INSTALL_URL; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_COMPLETED; @@ -1151,7 +1151,7 @@ synchronized void updateProgressDialog(CheckDownloadTask task, DownloadProgress public void run() { checkDownload(mContext, DistributeUtils.getStoredDownloadId(), true); } - }, HANDLER_TOKEN_CHECK_PROGRESS, SystemClock.uptimeMillis() + CHECK_PROGRESS_TIME_INTERVAL); + }, HANDLER_TOKEN_CHECK_PROGRESS, SystemClock.uptimeMillis() + CHECK_PROGRESS_TIME_INTERVAL_IN_MILLIS); } } diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java index c559c52194..776b2800bd 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java @@ -121,7 +121,7 @@ final class DistributeConstants { /** * How often to check download progress in millis. */ - static final long CHECK_PROGRESS_TIME_INTERVAL = 1000; + static final long CHECK_PROGRESS_TIME_INTERVAL_IN_MILLIS = 1000; /** * 1 MiB in bytes (this not a megabyte). diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeUtils.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeUtils.java index f7cbc54b3a..412fc7db6e 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeUtils.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeUtils.java @@ -155,7 +155,7 @@ static ReleaseDetails loadCachedReleaseDetails() { try { return ReleaseDetails.parse(cachedReleaseDetails); } catch (JSONException e) { - MobileCenterLog.error(LOG_TAG, "Invalid cached release details.", e); + MobileCenterLog.error(LOG_TAG, "Invalid release details in cache.", e); StorageHelper.PreferencesStorage.remove(PREFERENCE_KEY_RELEASE_DETAILS); } } diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java index 0ee0f086a4..5beb22b66d 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeMandatoryDownloadTest.java @@ -25,7 +25,7 @@ import java.util.concurrent.Semaphore; -import static com.microsoft.azure.mobile.distribute.DistributeConstants.CHECK_PROGRESS_TIME_INTERVAL; +import static com.microsoft.azure.mobile.distribute.DistributeConstants.CHECK_PROGRESS_TIME_INTERVAL_IN_MILLIS; import static com.microsoft.azure.mobile.distribute.DistributeConstants.DOWNLOAD_STATE_INSTALLING; import static com.microsoft.azure.mobile.distribute.DistributeConstants.HANDLER_TOKEN_CHECK_PROGRESS; import static com.microsoft.azure.mobile.distribute.DistributeConstants.MEBIBYTE_IN_BYTES; @@ -143,7 +143,7 @@ public void longMandatoryDownloadAndInstallAcrossRestarts() throws Exception { waitDownloadTask(); waitCheckDownloadTask(); verify(cursor).close(); - verify(mHandler).postAtTime(any(Runnable.class), eq(HANDLER_TOKEN_CHECK_PROGRESS), eq(CHECK_PROGRESS_TIME_INTERVAL + 1)); + verify(mHandler).postAtTime(any(Runnable.class), eq(HANDLER_TOKEN_CHECK_PROGRESS), eq(CHECK_PROGRESS_TIME_INTERVAL_IN_MILLIS + 1)); verify(mProgressDialog, never()).setProgress(anyInt()); /* Mock some progress. */ @@ -166,7 +166,7 @@ public void longMandatoryDownloadAndInstallAcrossRestarts() throws Exception { /* Check no more timer and progress update while paused. */ verify(mProgressDialog).setProgress(42); - verify(mHandler, times(3)).postAtTime(any(Runnable.class), eq(HANDLER_TOKEN_CHECK_PROGRESS), eq(CHECK_PROGRESS_TIME_INTERVAL + 1)); + verify(mHandler, times(3)).postAtTime(any(Runnable.class), eq(HANDLER_TOKEN_CHECK_PROGRESS), eq(CHECK_PROGRESS_TIME_INTERVAL_IN_MILLIS + 1)); /* Reusing dialog on resume. */ Distribute.getInstance().onActivityResumed(mActivity); diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java index 3740177b18..2e21d932fd 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeWarnUnknownSourcesTest.java @@ -15,6 +15,7 @@ import com.microsoft.azure.mobile.http.ServiceCallback; import com.microsoft.azure.mobile.utils.AsyncTaskUtils; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -154,9 +155,7 @@ public void cancelDialogWithBack() { public void cancelDialogWithButton() { /* Mandatory update cannot be canceled. */ - if (mMandatoryUpdate) { - return; - } + Assume.assumeFalse(mMandatoryUpdate); /* Cancel. */ ArgumentCaptor clickListener = ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); @@ -179,9 +178,7 @@ public void cancelDialogWithButton() { public void disableBeforeCancelWithBack() { /* Mandatory update cannot be canceled. */ - if (mMandatoryUpdate) { - return; - } + Assume.assumeFalse(mMandatoryUpdate); /* Disable. */ Distribute.setEnabled(false); @@ -209,9 +206,7 @@ public void disableBeforeCancelWithBack() { public void disableBeforeCancelWithButton() { /* Mandatory update cannot be canceled. */ - if (mMandatoryUpdate) { - return; - } + Assume.assumeFalse(mMandatoryUpdate); /* Disable. */ Distribute.setEnabled(false); diff --git a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/utils/HandlerUtilsTest.java b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/utils/HandlerUtilsTest.java index 060a108dd5..b06727c85a 100644 --- a/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/utils/HandlerUtilsTest.java +++ b/sdk/mobile-center/src/androidTest/java/com/microsoft/azure/mobile/utils/HandlerUtilsTest.java @@ -8,13 +8,19 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; public class HandlerUtilsTest { @Test - public void coverageWorkAround() { + public void init() { assertNotNull(new HandlerUtils()); - assertNotNull(HandlerUtils.getMainHandler()); + } + + @Test + public void getMainThreadHandler() { + assertSame(HandlerUtils.sMainHandler, HandlerUtils.getMainHandler()); + assertSame(HandlerUtils.getMainHandler(), HandlerUtils.getMainHandler()); } @Test diff --git a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HandlerUtils.java b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HandlerUtils.java index de973daeae..f56b1964e1 100644 --- a/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HandlerUtils.java +++ b/sdk/mobile-center/src/main/java/com/microsoft/azure/mobile/utils/HandlerUtils.java @@ -2,6 +2,7 @@ import android.os.Handler; import android.os.Looper; +import android.support.annotation.VisibleForTesting; /** * Utilities related to Handler class. @@ -11,7 +12,8 @@ public class HandlerUtils { /** * Main/UI thread Handler. */ - private static final Handler sMainHandler = new Handler(Looper.getMainLooper()); + @VisibleForTesting + static final Handler sMainHandler = new Handler(Looper.getMainLooper()); /** * Runs the specified runnable on the UI thread. From 25542c15152bb3838c5ea15389a48c579c9020df Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Thu, 16 Mar 2017 18:40:01 -0700 Subject: [PATCH 139/142] Keep token if error is JSON "no_releases_for_user" --- .../mobile/distribute/ErrorDetailsTest.java | 20 +++++ .../azure/mobile/distribute/Distribute.java | 30 +++++++- .../azure/mobile/distribute/ErrorDetails.java | 48 ++++++++++++ .../DistributeBeforeApiSuccessTest.java | 77 +++++++++++-------- 4 files changed, 139 insertions(+), 36 deletions(-) create mode 100644 sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ErrorDetailsTest.java create mode 100644 sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ErrorDetails.java diff --git a/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ErrorDetailsTest.java b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ErrorDetailsTest.java new file mode 100644 index 0000000000..be1705c9e3 --- /dev/null +++ b/sdk/mobile-center-distribute/src/androidTest/java/com/microsoft/azure/mobile/distribute/ErrorDetailsTest.java @@ -0,0 +1,20 @@ +package com.microsoft.azure.mobile.distribute; + +import org.json.JSONException; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ErrorDetailsTest { + + @Test + public void parseErrorCode() throws JSONException { + ErrorDetails errorDetails = ErrorDetails.parse("{code:'test'}"); + assertEquals("test", errorDetails.getCode()); + } + + @Test(expected = JSONException.class) + public void missingErrorCode() throws JSONException { + ErrorDetails.parse("{}"); + } +} diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java index 6b3698b617..4b525d33bd 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/Distribute.java @@ -33,6 +33,7 @@ import com.microsoft.azure.mobile.http.HttpClient; import com.microsoft.azure.mobile.http.HttpClientNetworkStateHandler; import com.microsoft.azure.mobile.http.HttpClientRetryer; +import com.microsoft.azure.mobile.http.HttpException; import com.microsoft.azure.mobile.http.HttpUtils; import com.microsoft.azure.mobile.http.ServiceCall; import com.microsoft.azure.mobile.http.ServiceCallback; @@ -721,10 +722,35 @@ private synchronized void handleApiCallFailure(Object releaseCallId, Exception e /* Check if state did not change. */ if (mCheckReleaseCallId == releaseCallId) { - MobileCenterLog.error(LOG_TAG, "Failed to check latest release:", e); + + /* Complete workflow in error. */ completeWorkflow(); + + /* Delete token on unrecoverable error. */ if (!HttpUtils.isRecoverableError(e)) { - PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); + + /* + * Unless its a special case: 404 with json code that no release is found. + * Could happen by cleaning releases with remove button. + */ + String code = null; + if (e instanceof HttpException) { + HttpException httpException = (HttpException) e; + try { + + /* We actually don't care of the http code if JSON code is specified. */ + ErrorDetails errorDetails = ErrorDetails.parse(httpException.getPayload()); + code = errorDetails.getCode(); + } catch (JSONException je) { + MobileCenterLog.verbose(LOG_TAG, "Cannot read the error as JSON", je); + } + } + if (ErrorDetails.NO_RELEASES_FOR_USER_CODE.equals(code)) { + MobileCenterLog.info(LOG_TAG, "No release available to the current user."); + } else { + MobileCenterLog.error(LOG_TAG, "Failed to check latest release:", e); + PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); + } } } } diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ErrorDetails.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ErrorDetails.java new file mode 100644 index 0000000000..8d06ccfff5 --- /dev/null +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/ErrorDetails.java @@ -0,0 +1,48 @@ +package com.microsoft.azure.mobile.distribute; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Release details JSON schema. + */ +class ErrorDetails { + + /** + * Error code when all releases have been deleted. + */ + static final String NO_RELEASES_FOR_USER_CODE = "no_releases_for_user"; + + /** + * Code property. + */ + private static final String CODE = "code"; + + /** + * Error code. + */ + private String code; + + /** + * Parse a JSON string describing error details. + * + * @param json a string. + * @return parsed error details. + * @throws JSONException if JSON is invalid. + */ + static ErrorDetails parse(String json) throws JSONException { + JSONObject object = new JSONObject(json); + ErrorDetails errorDetails = new ErrorDetails(); + errorDetails.code = object.getString(CODE); + return errorDetails; + } + + /** + * Get error code. + * + * @return error code + */ + String getCode() { + return code; + } +} diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java index 0d9207723d..9c78cfebaf 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeApiSuccessTest.java @@ -23,11 +23,15 @@ import org.mockito.internal.util.reflection.Whitebox; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.mockito.verification.VerificationMode; +import org.powermock.core.classloader.annotations.PrepareForTest; import java.util.HashMap; import java.util.UUID; import java.util.concurrent.Semaphore; +import javax.net.ssl.SSLPeerUnverifiedException; + import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_PLATFORM; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_PLATFORM_VALUE; import static com.microsoft.azure.mobile.distribute.DistributeConstants.PARAMETER_REDIRECT_ID; @@ -48,14 +52,17 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyStatic; import static org.powermock.api.mockito.PowerMockito.whenNew; /** * Cover scenarios that are happening before we see an API call success for latest release. */ +@PrepareForTest(ErrorDetails.class) public class DistributeBeforeApiSuccessTest extends AbstractDistributeTest { /** @@ -413,8 +420,7 @@ public void disableWhileCheckingRelease() throws Exception { verify(firstCall).cancel(); } - @Test - public void checkReleaseFailsRecoverable() throws Exception { + private void checkReleaseFailure(final Exception exception, VerificationMode deleteTokenVerificationMode) throws Exception { /* Mock we already have token. */ when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); @@ -424,7 +430,7 @@ public void checkReleaseFailsRecoverable() throws Exception { @Override public ServiceCall answer(InvocationOnMock invocation) throws Throwable { - ((ServiceCallback) invocation.getArguments()[4]).onCallFailed(new HttpException(503)); + ((ServiceCallback) invocation.getArguments()[4]).onCallFailed(exception); return mock(ServiceCall.class); } }); @@ -440,8 +446,8 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verifyStatic(); PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); - /* The error was recoverable, keep token. */ - verifyStatic(never()); + /* Check token kept or not depending on the test. */ + verifyStatic(deleteTokenVerificationMode); PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); /* After that if we resume app nothing happens. */ @@ -451,42 +457,45 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { } @Test - public void checkReleaseFailsNotRecoverable() throws Exception { - - /* Mock we already have token. */ - when(PreferencesStorage.getString(PREFERENCE_KEY_UPDATE_TOKEN)).thenReturn("some token"); - HttpClientNetworkStateHandler httpClient = mock(HttpClientNetworkStateHandler.class); - whenNew(HttpClientNetworkStateHandler.class).withAnyArguments().thenReturn(httpClient); - when(httpClient.callAsync(anyString(), anyString(), anyMapOf(String.class, String.class), any(HttpClient.CallTemplate.class), any(ServiceCallback.class))).thenAnswer(new Answer() { + public void checkReleaseFailsRecoverable503() throws Exception { + checkReleaseFailure(new HttpException(503), never()); + } - @Override - public ServiceCall answer(InvocationOnMock invocation) throws Throwable { - ((ServiceCallback) invocation.getArguments()[4]).onCallFailed(new HttpException(403)); - return mock(ServiceCall.class); - } - }); - HashMap headers = new HashMap<>(); - headers.put(DistributeConstants.HEADER_API_TOKEN, "some token"); + @Test + public void checkReleaseFailsWith403() throws Exception { + checkReleaseFailure(new HttpException(403), times(1)); - /* Trigger call. */ - Distribute.getInstance().onStarted(mContext, "a", mock(Channel.class)); - Distribute.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + } - /* Verify on failure we complete workflow. */ - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_DOWNLOAD_STATE); + @Test + public void checkReleaseFailsWithSomeSSL() throws Exception { + checkReleaseFailure(new SSLPeerUnverifiedException("unsecured connection"), times(1)); + } - /* The error was unrecoverable, get rid of token. */ - verifyStatic(); - PreferencesStorage.remove(PREFERENCE_KEY_UPDATE_TOKEN); + @Test + public void checkReleaseFailsWithSome404urlNotFound() throws Exception { + + /* Mock error parsing. */ + mockStatic(ErrorDetails.class); + final String errorPayload = "Not Found"; + when(ErrorDetails.parse(errorPayload)).thenThrow(new JSONException("Expected {")); + final Exception exception = new HttpException(404, errorPayload); + checkReleaseFailure(exception, times(1)); + } - /* After that if we resume app nothing happens. */ - Distribute.getInstance().onActivityPaused(mock(Activity.class)); - Distribute.getInstance().onActivityResumed(mock(Activity.class)); - verify(httpClient).callAsync(anyString(), anyString(), eq(headers), any(HttpClient.CallTemplate.class), any(ServiceCallback.class)); + @Test + public void checkReleaseFailsWithSome404noRelease() throws Exception { + + /* Mock error parsing. */ + ErrorDetails errorDetails = mock(ErrorDetails.class); + when(errorDetails.getCode()).thenReturn(ErrorDetails.NO_RELEASES_FOR_USER_CODE); + mockStatic(ErrorDetails.class); + String errorPayload = "{code: 'no_releases_for_user'}"; + when(ErrorDetails.parse(errorPayload)).thenReturn(errorDetails); + checkReleaseFailure(new HttpException(404, errorPayload), never()); } + @Test public void checkReleaseFailsParsing() throws Exception { From e671a9b0ae29c195c502ce5f0828c8367e79a57b Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Fri, 17 Mar 2017 13:32:38 -0700 Subject: [PATCH 140/142] Check optional dialog is cancelable in some tests --- .../azure/mobile/distribute/DistributeBeforeDownloadTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java index 3f99a683a6..5ba6e5bf23 100644 --- a/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java +++ b/sdk/mobile-center-distribute/src/test/java/com/microsoft/azure/mobile/distribute/DistributeBeforeDownloadTest.java @@ -246,6 +246,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { verify(mDialogBuilder).setTitle(R.string.mobile_center_distribute_update_dialog_title); verify(mDialogBuilder).setMessage(R.string.mobile_center_distribute_update_dialog_message); verify(mDialogBuilder, never()).setMessage(any(CharSequence.class)); + verify(mDialogBuilder, never()).setCancelable(false); verify(mDialogBuilder).create(); verify(mDialog).show(); @@ -306,6 +307,7 @@ public ServiceCall answer(InvocationOnMock invocation) throws Throwable { /* Verify dialog. */ verify(mDialogBuilder).setTitle(R.string.mobile_center_distribute_update_dialog_title); verify(mDialogBuilder).setMessage("mock"); + verify(mDialogBuilder, never()).setCancelable(false); verify(mDialogBuilder).create(); verify(mDialog).show(); } From 698ce78795d25d7a7156cd9f28f61464516916f8 Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 20 Mar 2017 11:41:08 -0700 Subject: [PATCH 141/142] Use pre-release in test app, fix API URL Also remove reflection for distribute in test app. --- apps/sasquatch/build.gradle | 9 +++- .../sasquatch/activities/MainActivity.java | 45 ++++++++---------- .../activities/SettingsActivity.java | 46 +++++++------------ apps/sasquatch/src/main/res/values/env.xml | 2 + .../src/projectDependency/res/values/env.xml | 2 + .../distribute/DistributeConstants.java | 2 +- versions.gradle | 2 +- 7 files changed, 48 insertions(+), 60 deletions(-) diff --git a/apps/sasquatch/build.gradle b/apps/sasquatch/build.gradle index b3c1c7e0f0..b1cb867f8c 100644 --- a/apps/sasquatch/build.gradle +++ b/apps/sasquatch/build.gradle @@ -21,12 +21,19 @@ android { } } +repositories { + maven { + url "http://dl.bintray.com/mobile-center/mobile-center-snapshot" + } +} + dependencies { - def version = "0.5.0" + def version = "0.6.0-3" compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" projectDependencyCompile project(':sdk:mobile-center-analytics') projectDependencyCompile project(':sdk:mobile-center-crashes') projectDependencyCompile project(':sdk:mobile-center-distribute') jcenterDependencyCompile "com.microsoft.azure.mobile:mobile-center-analytics:${version}" jcenterDependencyCompile "com.microsoft.azure.mobile:mobile-center-crashes:${version}" + jcenterDependencyCompile "com.microsoft.azure.mobile:mobile-center-distribute:${version}" } \ No newline at end of file diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java index 970af1cdd3..763dbdd32e 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/MainActivity.java @@ -18,18 +18,15 @@ import android.widget.Toast; import com.microsoft.azure.mobile.MobileCenter; -import com.microsoft.azure.mobile.MobileCenterService; import com.microsoft.azure.mobile.ResultCallback; import com.microsoft.azure.mobile.analytics.Analytics; import com.microsoft.azure.mobile.crashes.AbstractCrashesListener; import com.microsoft.azure.mobile.crashes.Crashes; import com.microsoft.azure.mobile.crashes.model.ErrorReport; +import com.microsoft.azure.mobile.distribute.Distribute; import com.microsoft.azure.mobile.sasquatch.R; import com.microsoft.azure.mobile.sasquatch.features.TestFeatures; import com.microsoft.azure.mobile.sasquatch.features.TestFeaturesListAdapter; -import com.microsoft.azure.mobile.utils.MobileCenterLog; - -import java.lang.reflect.Method; public class MainActivity extends AppCompatActivity { @@ -49,33 +46,26 @@ protected void onCreate(Bundle savedInstanceState) { /* Set custom log URL if one was configured in settings. */ String logUrl = sSharedPreferences.getString(LOG_URL_KEY, getString(R.string.log_url)); if (!TextUtils.isEmpty(logUrl)) { - try { - - /* Method name changed and jCenter not yet updated so need to use reflection. */ - Method setLogUrl; - try { - setLogUrl = MobileCenter.class.getMethod("setLogUrl", String.class); - } catch (NoSuchMethodException e) { - setLogUrl = MobileCenter.class.getMethod("setServerUrl", String.class); - } - setLogUrl.invoke(null, logUrl); - } catch (Exception e) { - throw new RuntimeException(e); - } + MobileCenter.setLogUrl(logUrl); } + + /* Set crash listener. */ Crashes.setListener(getCrashesListener()); - MobileCenter.start(getApplication(), sSharedPreferences.getString(APP_SECRET_KEY, getString(R.string.app_secret)), Analytics.class, Crashes.class); - try { - - @SuppressWarnings("unchecked") - Class distribute = (Class) Class.forName("com.microsoft.azure.mobile.distribute.Distribute"); - distribute.getMethod("setInstallUrl", String.class).invoke(null, "http://install.asgard-int.trafficmanager.net"); - distribute.getMethod("setApiUrl", String.class).invoke(null, "https://asgard-int.trafficmanager.net/api/v0.1"); - MobileCenter.start(distribute); - } catch (Exception e) { - MobileCenterLog.info(LOG_TAG, "Distribute class not yet available in this flavor."); + + /* Set distribute urls. */ + String installUrl = getString(R.string.install_url); + if (!TextUtils.isEmpty(installUrl)) { + Distribute.setInstallUrl(installUrl); + } + String apiUrl = getString(R.string.api_url); + if (!TextUtils.isEmpty(apiUrl)) { + Distribute.setApiUrl(apiUrl); } + /* Start Mobile center. */ + MobileCenter.start(getApplication(), sSharedPreferences.getString(APP_SECRET_KEY, getString(R.string.app_secret)), Analytics.class, Crashes.class, Distribute.class); + + /* Print last crash. */ Log.i(LOG_TAG, "Crashes.hasCrashedInLastSession=" + Crashes.hasCrashedInLastSession()); Crashes.getLastSessionCrashReport(new ResultCallback() { @@ -87,6 +77,7 @@ public void onResult(@Nullable ErrorReport data) { } }); + /* Populate UI. */ ((TextView) findViewById(R.id.package_name)).setText(String.format(getString(R.string.sdk_source_format), getPackageName().substring(getPackageName().lastIndexOf(".") + 1))); TestFeatures.initialize(this); ListView listView = (ListView) findViewById(R.id.list); diff --git a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java index 41899dd6e6..a99157f6a8 100644 --- a/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java +++ b/apps/sasquatch/src/main/java/com/microsoft/azure/mobile/sasquatch/activities/SettingsActivity.java @@ -16,15 +16,14 @@ import android.widget.Toast; import com.microsoft.azure.mobile.MobileCenter; -import com.microsoft.azure.mobile.MobileCenterService; import com.microsoft.azure.mobile.analytics.Analytics; import com.microsoft.azure.mobile.analytics.AnalyticsPrivateHelper; import com.microsoft.azure.mobile.crashes.Crashes; +import com.microsoft.azure.mobile.distribute.Distribute; import com.microsoft.azure.mobile.sasquatch.R; import com.microsoft.azure.mobile.utils.PrefStorageConstants; import com.microsoft.azure.mobile.utils.storage.StorageHelper; -import java.lang.reflect.Method; import java.util.UUID; import static com.microsoft.azure.mobile.sasquatch.activities.MainActivity.APP_SECRET_KEY; @@ -91,36 +90,23 @@ public boolean isEnabled() { return Crashes.isEnabled(); } }); - try { - - @SuppressWarnings("unchecked") - Class distribute = (Class) Class.forName("com.microsoft.azure.mobile.distribute.Distribute"); - final Method isEnabled = distribute.getMethod("isEnabled"); - final Method setEnabled = distribute.getMethod("setEnabled", boolean.class); - initCheckBoxSetting(R.string.mobile_center_distribute_state_key, (boolean) isEnabled.invoke(null), R.string.mobile_center_distribute_state_summary_enabled, R.string.mobile_center_distribute_state_summary_disabled, new HasEnabled() { - - @Override - public void setEnabled(boolean enabled) { - try { - setEnabled.invoke(null, enabled); - distributeEnabledPreference.setChecked((boolean) isEnabled.invoke(null)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } + initCheckBoxSetting(R.string.mobile_center_distribute_state_key, Distribute.isEnabled(), R.string.mobile_center_distribute_state_summary_enabled, R.string.mobile_center_distribute_state_summary_disabled, new HasEnabled() { - @Override - public boolean isEnabled() { - try { - return (boolean) isEnabled.invoke(null); - } catch (Exception e) { - throw new RuntimeException(e); - } + @Override + public void setEnabled(boolean enabled) { + try { + Distribute.setEnabled(enabled); + distributeEnabledPreference.setChecked(Distribute.isEnabled()); + } catch (Exception e) { + throw new RuntimeException(e); } - }); - } catch (Exception e) { - getPreferenceScreen().removePreference(findPreference(getString(R.string.distribute_key))); - } + } + + @Override + public boolean isEnabled() { + return Distribute.isEnabled(); + } + }); initCheckBoxSetting(R.string.mobile_center_auto_page_tracking_key, AnalyticsPrivateHelper.isAutoPageTrackingEnabled(), R.string.mobile_center_auto_page_tracking_enabled, R.string.mobile_center_auto_page_tracking_disabled, new HasEnabled() { @Override diff --git a/apps/sasquatch/src/main/res/values/env.xml b/apps/sasquatch/src/main/res/values/env.xml index 8880b6e11d..b794b62ff3 100644 --- a/apps/sasquatch/src/main/res/values/env.xml +++ b/apps/sasquatch/src/main/res/values/env.xml @@ -2,4 +2,6 @@ 45d1d9f6-2492-4e68-bd44-7190351eb5f3 + + https://api.mobile.azure.com/v0.1 diff --git a/apps/sasquatch/src/projectDependency/res/values/env.xml b/apps/sasquatch/src/projectDependency/res/values/env.xml index 5ff88c0269..fe1744341b 100644 --- a/apps/sasquatch/src/projectDependency/res/values/env.xml +++ b/apps/sasquatch/src/projectDependency/res/values/env.xml @@ -2,4 +2,6 @@ 9e0d97c1-7838-46d0-9dab-1a0ef66aec6e https://in-integration.dev.avalanch.es + http://install.asgard-int.trafficmanager.net + https://asgard-int.trafficmanager.net/api/v0.1 diff --git a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java index 776b2800bd..c44217fefb 100644 --- a/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java +++ b/sdk/mobile-center-distribute/src/main/java/com/microsoft/azure/mobile/distribute/DistributeConstants.java @@ -38,7 +38,7 @@ final class DistributeConstants { /** * Base URL to call server to check latest release. */ - static final String DEFAULT_API_URL = "https://api.mobile.azure.com"; + static final String DEFAULT_API_URL = "https://api.mobile.azure.com/v0.1"; /** * Update setup URL path. Contains the app secret variable to replace. diff --git a/versions.gradle b/versions.gradle index d4a534ebde..c6d92f8244 100644 --- a/versions.gradle +++ b/versions.gradle @@ -7,5 +7,5 @@ ext { targetSdkVersion = 25 compileSdkVersion = 25 buildToolsVersion = '25.0.2' - supportLibVersion = '25.2.0' + supportLibVersion = '25.3.0' } From 1fa23b64ed323e6e8f8af47ef07d393e6f06ba2e Mon Sep 17 00:00:00 2001 From: Guillaume Perrot Date: Mon, 20 Mar 2017 12:36:49 -0700 Subject: [PATCH 142/142] Use pre-release that fixes the default API url in distribute --- apps/sasquatch/build.gradle | 2 +- apps/sasquatch/src/main/res/values/env.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sasquatch/build.gradle b/apps/sasquatch/build.gradle index b1cb867f8c..64e46c3b4b 100644 --- a/apps/sasquatch/build.gradle +++ b/apps/sasquatch/build.gradle @@ -28,7 +28,7 @@ repositories { } dependencies { - def version = "0.6.0-3" + def version = "0.6.0-4" compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" projectDependencyCompile project(':sdk:mobile-center-analytics') projectDependencyCompile project(':sdk:mobile-center-crashes') diff --git a/apps/sasquatch/src/main/res/values/env.xml b/apps/sasquatch/src/main/res/values/env.xml index b794b62ff3..42f64bb29e 100644 --- a/apps/sasquatch/src/main/res/values/env.xml +++ b/apps/sasquatch/src/main/res/values/env.xml @@ -3,5 +3,5 @@ 45d1d9f6-2492-4e68-bd44-7190351eb5f3 - https://api.mobile.azure.com/v0.1 +