Skip to content

Commit

Permalink
Merge pull request #496 from Microsoft/develop
Browse files Browse the repository at this point in the history
Version 0.11.2
  • Loading branch information
guperrot authored Aug 10, 2017
2 parents 0fe73cf + 0fc6cd1 commit 7bb60af
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 97 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The Mobile Center SDK uses a modular architecture so you can use any or all of t

3. **Mobile Center Distribute**: Mobile Center Distribute will let your users install a new version of the app when you distribute it via the Mobile Center. With a new version of the app available, the SDK will present an update dialog to the users to either download or postpone the new version. Once they choose to update, the SDK will start to update your application. This feature will NOT work if your app is deployed to the app store.

4. **Mobile Center Push**: Mobile Center Push enables you to send push notifications to users of your app from the Mobile Center portal. To do that the Mobile Center SDK and portal integrate with [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/).
4. **Mobile Center Push**: Mobile Center Push enables you to send push notifications to users of your app from the Mobile Center portal. To do that, the Mobile Center SDK and portal integrate with [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/). You can also segment your user base based on a set of properties and send them targeted notifications.

## 1. Get started
It is super easy to use Mobile Center. Have a look at our [get started documentation](https://docs.microsoft.com/en-us/mobile-center/sdk/getting-started/android) and onboard your app within minutes. Our [detailed documentation](https://docs.microsoft.com/en-us/mobile-center/sdk/) is available as well.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,8 @@ public boolean onOptionsItemSelected(MenuItem item) {
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
try {
Push.class.getMethod("checkLaunchedFromNotification", Activity.class, Intent.class).invoke(null, this, intent);
} catch (Exception ignored) {
}
Log.d(LOG_TAG, "onNewIntent triggered");
Push.checkLaunchedFromNotification(this, intent);
}

private AnalyticsListener getAnalyticsListener() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ public class Analytics extends AbstractMobileCenterService {
*/
private static final String ACTIVITY_SUFFIX = "Activity";

/**
* Max number of properties.
*/
private static final int MAX_PROPERTY_COUNT = 5;

/**
* Max length of event/page name.
*/
@VisibleForTesting
static final int MAX_NAME_LENGTH = 256;

/**
* Max length of properties.
*/
@VisibleForTesting
static final int MAX_PROPERTY_ITEM_LENGTH = 64;

/**
* Shared instance.
*/
Expand Down Expand Up @@ -184,8 +201,9 @@ protected static void trackPage(String name) {
* Track a custom page with name and optional properties.
* The name parameter can not be null or empty. Maximum allowed length = 256.
* The properties parameter maximum item count = 5.
* The properties keys/names can not be null or empty, maximum allowed key length = 64.
* The properties keys can not be null or empty, maximum allowed key length = 64.
* The properties values can not be null, maximum allowed value length = 64.
* Any length of name/keys/values that are longer than each limit will be truncated.
* <p>
* TODO the backend does not support that service yet, will be public method later.
*
Expand All @@ -195,7 +213,8 @@ protected static void trackPage(String name) {
@SuppressWarnings("WeakerAccess")
protected static void trackPage(String name, Map<String, String> properties) {
final String logType = "Page";
if (validateName(name, logType)) {
name = validateName(name, logType);
if (name != null) {
Map<String, String> validatedProperties = validateProperties(properties, name, logType);
getInstance().trackPageAsync(name, validatedProperties);
}
Expand All @@ -215,16 +234,18 @@ public static void trackEvent(String name) {
* Track a custom event with name and optional properties.
* The name parameter can not be null or empty. Maximum allowed length = 256.
* The properties parameter maximum item count = 5.
* The properties keys/names can not be null or empty, maximum allowed key length = 64.
* The properties keys can not be null or empty, maximum allowed key length = 64.
* The properties values can not be null, maximum allowed value length = 64.
* Any length of name/keys/values that are longer than each limit will be truncated.
*
* @param name An event name.
* @param properties Optional properties.
*/
@SuppressWarnings("WeakerAccess")
public static void trackEvent(String name, Map<String, String> properties) {
final String logType = "Event";
if (validateName(name, logType)) {
name = validateName(name, logType);
if (name != null) {
Map<String, String> validatedProperties = validateProperties(properties, name, logType);
getInstance().trackEventAsync(name, validatedProperties);
}
Expand All @@ -250,19 +271,18 @@ private static String generatePageName(Class<?> activityClass) {
*
* @param name Log name to validate.
* @param logType Log type.
* @return <code>true</code> if validation succeeds, otherwise <code>false</code>.
* @return <code>null</code> if validation failed, otherwise a valid name within the length limit will be returned.
*/
private static boolean validateName(String name, String logType) {
final int maxNameLength = 256;
private static String validateName(String name, String logType) {
if (name == null || name.isEmpty()) {
MobileCenterLog.error(Analytics.LOG_TAG, logType + " name cannot be null or empty.");
return false;
return null;
}
if (name.length() > maxNameLength) {
MobileCenterLog.error(Analytics.LOG_TAG, String.format("%s '%s' : name length cannot be longer than %s characters.", logType, name, maxNameLength));
return false;
if (name.length() > MAX_NAME_LENGTH) {
MobileCenterLog.warn(Analytics.LOG_TAG, String.format("%s '%s' : name length cannot be longer than %s characters. Name will be truncated.", logType, name, MAX_NAME_LENGTH));
name = name.substring(0, MAX_NAME_LENGTH);
}
return true;
return name;
}

/**
Expand All @@ -277,36 +297,36 @@ private static Map<String, String> validateProperties(Map<String, String> proper
if (properties == null)
return null;
String message;
final int maxPropertiesCount = 5;
final int maxPropertyItemLength = 64;
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, String> property : properties.entrySet()) {
if (result.size() >= maxPropertiesCount) {
message = String.format("%s '%s' : properties cannot contain more than %s items. Skipping other properties.", logType, logName, maxPropertiesCount);
String key = property.getKey();
String value = property.getValue();
if (result.size() >= MAX_PROPERTY_COUNT) {
message = String.format("%s '%s' : properties cannot contain more than %s items. Skipping other properties.", logType, logName, MAX_PROPERTY_COUNT);
MobileCenterLog.warn(Analytics.LOG_TAG, message);
break;
}
if (property.getKey() == null || property.getKey().isEmpty()) {
if (key == null || key.isEmpty()) {
message = String.format("%s '%s' : a property key cannot be null or empty. Property will be skipped.", logType, logName);
MobileCenterLog.warn(Analytics.LOG_TAG, message);
continue;
}
if (property.getKey().length() > maxPropertyItemLength) {
message = String.format("%s '%s' : property '%s' : property key length cannot be longer than %s characters. Property '%s' will be skipped.", logType, logName, property.getKey(), maxPropertyItemLength, property.getKey());
if (value == null) {
message = String.format("%s '%s' : property '%s' : property value cannot be null. Property '%s' will be skipped.", logType, logName, key, key);
MobileCenterLog.warn(Analytics.LOG_TAG, message);
continue;
}
if (property.getValue() == null) {
message = String.format("%s '%s' : property '%s' : property value cannot be null. Property '%s' will be skipped.", logType, logName, property.getKey(), property.getKey());
if (key.length() > MAX_PROPERTY_ITEM_LENGTH) {
message = String.format("%s '%s' : property '%s' : property key length cannot be longer than %s characters. Property key will be truncated.", logType, logName, key, MAX_PROPERTY_ITEM_LENGTH);
MobileCenterLog.warn(Analytics.LOG_TAG, message);
continue;
key = key.substring(0, MAX_PROPERTY_ITEM_LENGTH);
}
if (property.getValue().length() > maxPropertyItemLength) {
message = String.format("%s '%s' : property '%s' : property value cannot be longer than %s characters. Property '%s' will be skipped.", logType, logName, property.getKey(), maxPropertyItemLength, property.getKey());
if (value.length() > MAX_PROPERTY_ITEM_LENGTH) {
message = String.format("%s '%s' : property '%s' : property value cannot be longer than %s characters. Property value will be truncated.", logType, logName, key, MAX_PROPERTY_ITEM_LENGTH);
MobileCenterLog.warn(Analytics.LOG_TAG, message);
continue;
value = value.substring(0, MAX_PROPERTY_ITEM_LENGTH);
}
result.put(property.getKey(), property.getValue());
result.put(key, value);
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,18 +258,28 @@ public void testTrackEvent() {
Analytics.trackEvent(" ", null);
verify(channel, times(1)).enqueue(any(Log.class), anyString());
reset(channel);
Analytics.trackEvent(generateString(257, '*'), null);
verify(channel, never()).enqueue(any(Log.class), anyString());
final String maxName = generateString(Analytics.MAX_NAME_LENGTH, '*');
Analytics.trackEvent(maxName + "*", null);
verify(channel, times(1)).enqueue(argThat(new ArgumentMatcher<Log>() {

@Override
public boolean matches(Object item) {
if (item instanceof EventLog) {
EventLog eventLog = (EventLog) item;
return eventLog.getName().equals(maxName) && eventLog.getProperties() == null;
}
return false;
}
}), anyString());
reset(channel);
Analytics.trackEvent(generateString(256, '*'), null);
Analytics.trackEvent(maxName, null);
verify(channel, times(1)).enqueue(any(Log.class), anyString());
reset(channel);
Analytics.trackEvent("eventName", new HashMap<String, String>() {{
put(null, null);
put("", null);
put(generateString(65, '*'), null);
put(generateString(Analytics.MAX_PROPERTY_ITEM_LENGTH + 1, '*'), null);
put("1", null);
put("2", generateString(65, '*'));
}});
verify(channel, times(1)).enqueue(argThat(new ArgumentMatcher<Log>() {

Expand Down Expand Up @@ -300,6 +310,26 @@ public boolean matches(Object item) {
return false;
}
}), anyString());
reset(channel);
final String longerMapItem = generateString(Analytics.MAX_PROPERTY_ITEM_LENGTH + 1, '*');
Analytics.trackEvent("eventName", new HashMap<String, String>() {{
put(longerMapItem, longerMapItem);
}});
verify(channel, times(1)).enqueue(argThat(new ArgumentMatcher<Log>() {

@Override
public boolean matches(Object item) {
if (item instanceof EventLog) {
EventLog eventLog = (EventLog) item;
if (eventLog.getProperties().size() == 1) {
Map.Entry<String, String> entry = eventLog.getProperties().entrySet().iterator().next();
String truncatedMapItem = generateString(Analytics.MAX_PROPERTY_ITEM_LENGTH, '*');
return entry.getKey().length() == Analytics.MAX_PROPERTY_ITEM_LENGTH && entry.getValue().length() == Analytics.MAX_PROPERTY_ITEM_LENGTH;
}
}
return false;
}
}), anyString());
}

@Test
Expand All @@ -317,18 +347,28 @@ public void testTrackPage() {
Analytics.trackPage(" ", null);
verify(channel, times(1)).enqueue(any(Log.class), anyString());
reset(channel);
Analytics.trackPage(generateString(257, '*'), null);
verify(channel, never()).enqueue(any(Log.class), anyString());
final String maxName = generateString(Analytics.MAX_NAME_LENGTH, '*');
Analytics.trackPage(maxName + "*", null);
verify(channel, times(1)).enqueue(argThat(new ArgumentMatcher<Log>() {

@Override
public boolean matches(Object item) {
if (item instanceof PageLog) {
PageLog pageLog = (PageLog) item;
return pageLog.getName().equals(maxName) && pageLog.getProperties() == null;
}
return false;
}
}), anyString());
reset(channel);
Analytics.trackPage(generateString(256, '*'), null);
Analytics.trackPage(maxName, null);
verify(channel, times(1)).enqueue(any(Log.class), anyString());
reset(channel);
Analytics.trackPage("pageName", new HashMap<String, String>() {{
put(null, null);
put("", null);
put(generateString(65, '*'), null);
put(generateString(Analytics.MAX_PROPERTY_ITEM_LENGTH + 1, '*'), null);
put("1", null);
put("2", generateString(65, '*'));
}});
verify(channel, times(1)).enqueue(argThat(new ArgumentMatcher<Log>() {

Expand Down Expand Up @@ -359,6 +399,26 @@ public boolean matches(Object item) {
return false;
}
}), anyString());
reset(channel);
final String longerMapItem = generateString(Analytics.MAX_PROPERTY_ITEM_LENGTH + 1, '*');
Analytics.trackPage("pageName", new HashMap<String, String>() {{
put(longerMapItem, longerMapItem);
}});
verify(channel, times(1)).enqueue(argThat(new ArgumentMatcher<Log>() {

@Override
public boolean matches(Object item) {
if (item instanceof PageLog) {
PageLog pageLog = (PageLog) item;
if (pageLog.getProperties().size() == 1) {
Map.Entry<String, String> entry = pageLog.getProperties().entrySet().iterator().next();
String truncatedMapItem = generateString(Analytics.MAX_PROPERTY_ITEM_LENGTH, '*');
return entry.getKey().length() == Analytics.MAX_PROPERTY_ITEM_LENGTH && entry.getValue().length() == Analytics.MAX_PROPERTY_ITEM_LENGTH;
}
}
return false;
}
}), anyString());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
package com.microsoft.azure.mobile.crashes;

import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.support.test.InstrumentationRegistry;

import com.microsoft.azure.mobile.Constants;
import com.microsoft.azure.mobile.MobileCenter;
import com.microsoft.azure.mobile.MobileCenterPrivateHelper;
import com.microsoft.azure.mobile.channel.Channel;
import com.microsoft.azure.mobile.crashes.ingestion.models.Exception;
import com.microsoft.azure.mobile.crashes.utils.ErrorLogHelper;
import com.microsoft.azure.mobile.utils.MobileCenterLog;
import com.microsoft.azure.mobile.utils.storage.StorageHelper;

import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.File;
import java.lang.reflect.Method;
import java.util.UUID;

import static com.microsoft.azure.mobile.test.TestUtils.TAG;
Expand All @@ -27,24 +30,50 @@

public class WrapperSdkExceptionManagerAndroidTest {

@SuppressLint("StaticFieldLeak")
private static Application sApplication;

@BeforeClass
public static void setUpClass() {
MobileCenterLog.setLogLevel(android.util.Log.VERBOSE);
Context context = InstrumentationRegistry.getContext();
MobileCenter.configure((Application) context.getApplicationContext(), "dummy");
sApplication = (Application) InstrumentationRegistry.getContext().getApplicationContext();
StorageHelper.initialize(sApplication);
Constants.loadFromContext(sApplication);
}

@Before
public void cleanup() {
public void setUp() throws java.lang.Exception {
android.util.Log.i(TAG, "Cleanup");
StorageHelper.PreferencesStorage.clear();
for (File logFile : ErrorLogHelper.getErrorStorageDirectory().listFiles()) {
assertTrue(logFile.delete());
}
}

private void startFresh() throws java.lang.Exception {

/* Configure new instance. */
MobileCenterPrivateHelper.unsetInstance();
Crashes.unsetInstance();
MobileCenter.setLogLevel(android.util.Log.VERBOSE);
MobileCenter.configure(sApplication, "a");

/* Replace channel. */
Method method = MobileCenter.class.getDeclaredMethod("getInstance");
method.setAccessible(true);
MobileCenter mobileCenter = (MobileCenter) method.invoke(null);
method = MobileCenter.class.getDeclaredMethod("setChannel", Channel.class);
method.setAccessible(true);
method.invoke(mobileCenter, mock(Channel.class));

/* Start crashes. */
MobileCenter.start(Crashes.class);

/* Wait for start. */
Assert.assertTrue(Crashes.isEnabled().get());
}

@Test
public void saveWrapperException() {
public void saveWrapperException() throws java.lang.Exception {

class ErrorData {
private byte[] data;
Expand All @@ -65,8 +94,7 @@ class ErrorData {
for (ErrorData error : errors) {

/* Reset crash state as only 1 crash is saved per process life time. */
Crashes.unsetInstance();
Crashes.getInstance().onStarted(InstrumentationRegistry.getContext(), "", mock(Channel.class));
startFresh();

/* Save crash. */
error.id = WrapperSdkExceptionManager.saveWrapperException(Thread.currentThread(), new Exception(), error.data);
Expand Down
Loading

0 comments on commit 7bb60af

Please sign in to comment.