From ecb1af804144689d4ead96a247b565f9b4eb8160 Mon Sep 17 00:00:00 2001 From: Andrew Stadler Date: Mon, 1 Feb 2010 15:53:46 -0800 Subject: [PATCH] Finish up IMAP ID implementation * scrub all external strings to keep them compliant for IMAP protocol * move Build.MODEL to x-android-device-model * send x-android-mobile-net-operator * send AGUID * unit tests for above * retrieve providers from VendorPolicyLoader Bug: 2332183 --- src/com/android/email/Preferences.java | 17 ++ src/com/android/email/VendorPolicyLoader.java | 84 ++++++++ .../activity/setup/AccountSetupBasics.java | 19 +- .../android/email/mail/store/ImapStore.java | 186 +++++++++++++----- .../android/email/VendorPolicyLoaderTest.java | 40 +++- .../email/mail/store/ImapStoreUnitTests.java | 126 +++++++++--- 6 files changed, 388 insertions(+), 84 deletions(-) diff --git a/src/com/android/email/Preferences.java b/src/com/android/email/Preferences.java index c9e706915..773baef7b 100644 --- a/src/com/android/email/Preferences.java +++ b/src/com/android/email/Preferences.java @@ -21,6 +21,8 @@ import android.content.SharedPreferences; import android.net.Uri; import android.util.Log; +import java.util.UUID; + public class Preferences { // Preferences file @@ -33,6 +35,7 @@ public class Preferences { private static final String ENABLE_SENSITIVE_LOGGING = "enableSensitiveLogging"; private static final String ENABLE_EXCHANGE_LOGGING = "enableExchangeLogging"; private static final String ENABLE_EXCHANGE_FILE_LOGGING = "enableExchangeFileLogging"; + private static final String DEVICE_UID = "deviceUID"; private static Preferences preferences; @@ -161,6 +164,20 @@ public class Preferences { return mSharedPreferences.getBoolean(ENABLE_EXCHANGE_FILE_LOGGING, false); } + /** + * Generate a new "device UID". This is local to Email app only, to prevent possibility + * of correlation with any other user activities in any other apps. + * @return a persistent, unique ID + */ + public synchronized String getDeviceUID() { + String result = mSharedPreferences.getString(DEVICE_UID, null); + if (result == null) { + result = UUID.randomUUID().toString(); + mSharedPreferences.edit().putString(DEVICE_UID, result).commit(); + } + return result; + } + public void save() { } diff --git a/src/com/android/email/VendorPolicyLoader.java b/src/com/android/email/VendorPolicyLoader.java index 34228c28f..497c4707b 100644 --- a/src/com/android/email/VendorPolicyLoader.java +++ b/src/com/android/email/VendorPolicyLoader.java @@ -16,6 +16,8 @@ package com.android.email; +import com.android.email.activity.setup.AccountSetupBasics.Provider; + import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; @@ -23,6 +25,8 @@ import android.os.Bundle; import android.util.Log; import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; /** * A bridge class to the email vendor policy apk. @@ -40,7 +44,19 @@ public class VendorPolicyLoader { private static final String GET_POLICY_METHOD = "getPolicy"; private static final Class[] ARGS = new Class[] {String.class, Bundle.class}; + // call keys and i/o bundle keys + // when there is only one parameter or return value, use call key private static final String USE_ALTERNATE_EXCHANGE_STRINGS = "useAlternateExchangeStrings"; + private static final String GET_IMAP_ID = "getImapId"; + private static final String GET_IMAP_ID_USER = "getImapId.user"; + private static final String GET_IMAP_ID_HOST = "getImapId.host"; + private static final String GET_IMAP_ID_CAPA = "getImapId.capabilities"; + private static final String FIND_PROVIDER = "findProvider"; + private static final String FIND_PROVIDER_IN_URI = "findProvider.inUri"; + private static final String FIND_PROVIDER_IN_USER = "findProvider.inUser"; + private static final String FIND_PROVIDER_OUT_URI = "findProvider.outUri"; + private static final String FIND_PROVIDER_OUT_USER = "findProvider.outUser"; + private static final String FIND_PROVIDER_NOTE = "findProvider.note"; /** Singleton instance */ private static VendorPolicyLoader sInstance; @@ -120,9 +136,77 @@ public class VendorPolicyLoader { /** * Returns true if alternate exchange descriptive text is required. + * + * Vendor function: + * Select: USE_ALTERNATE_EXCHANGE_STRINGS + * Params: none + * Result: USE_ALTERNATE_EXCHANGE_STRINGS (boolean) */ public boolean useAlternateExchangeStrings() { return getPolicy(USE_ALTERNATE_EXCHANGE_STRINGS, null) .getBoolean(USE_ALTERNATE_EXCHANGE_STRINGS, false); } + + /** + * Returns additional key/value pairs for the IMAP ID string. + * + * Vendor function: + * Select: GET_IMAP_ID + * Params: GET_IMAP_ID_USER (String) + * GET_IMAP_ID_HOST (String) + * GET_IMAP_ID_CAPABILITIES (String) + * Result: GET_IMAP_ID (String) + * + * @param userName the server that is being contacted (e.g. "imap.server.com") + * @param host the server that is being contacted (e.g. "imap.server.com") + * @param reported capabilities, if known. null is OK + * @return zero or more key/value pairs, quoted and delimited by spaces. If there is + * nothing to add, return null. + */ + public String getImapIdValues(String userName, String host, String capabilities) { + Bundle params = new Bundle(); + params.putString(GET_IMAP_ID_USER, userName); + params.putString(GET_IMAP_ID_HOST, host); + params.putString(GET_IMAP_ID_CAPA, capabilities); + String result = getPolicy(GET_IMAP_ID, params).getString(GET_IMAP_ID); + return result; + } + + /** + * Returns provider setup information for a given email address + * + * Vendor function: + * Select: FIND_PROVIDER + * Param: FIND_PROVIDER (String) + * Result: FIND_PROVIDER_IN_URI + * FIND_PROVIDER_IN_USER + * FIND_PROVIDER_OUT_URI + * FIND_PROVIDER_OUT_USER + * FIND_PROVIDER_NOTE + * + * @param domain The domain portion of the user's email address + * @return suitable Provider definition, or null if no match found + */ + public Provider findProviderForDomain(String domain) { + Bundle params = new Bundle(); + params.putString(FIND_PROVIDER, domain); + Bundle out = getPolicy(FIND_PROVIDER, params); + if (out != null) { + try { + Provider p = new Provider(); + p.id = null; + p.label = null; + p.domain = domain; + p.incomingUriTemplate = new URI(out.getString(FIND_PROVIDER_IN_URI)); + p.incomingUsernameTemplate = out.getString(FIND_PROVIDER_IN_USER); + p.outgoingUriTemplate = new URI(out.getString(FIND_PROVIDER_OUT_URI)); + p.outgoingUsernameTemplate = out.getString(FIND_PROVIDER_OUT_USER); + p.note = out.getString(FIND_PROVIDER_NOTE); + return p; + } catch (URISyntaxException e) { + Log.d(Email.LOG_TAG, "uri exception while vendor policy loads " + domain); + } + } + return null; + } } diff --git a/src/com/android/email/activity/setup/AccountSetupBasics.java b/src/com/android/email/activity/setup/AccountSetupBasics.java index d1d5540b7..a0dc67bd0 100644 --- a/src/com/android/email/activity/setup/AccountSetupBasics.java +++ b/src/com/android/email/activity/setup/AccountSetupBasics.java @@ -534,8 +534,9 @@ public class AccountSetupBasics extends Activity /** * Search the list of known Email providers looking for one that matches the user's email - * domain. We look in providers_product.xml first, followed by the entries in - * platform providers.xml. This provides a nominal override capability. + * domain. We check for vendor supplied values first, then we look in providers_product.xml + * first, finally by the entries in platform providers.xml. This provides a nominal override + * capability. * * A match is defined as any provider entry for which the "domain" attribute matches. * @@ -543,7 +544,10 @@ public class AccountSetupBasics extends Activity * @return suitable Provider definition, or null if no match found */ private Provider findProviderForDomain(String domain) { - Provider p = findProviderForDomain(domain, R.xml.providers_product); + Provider p = VendorPolicyLoader.getInstance(this).findProviderForDomain(domain); + if (p == null) { + p = findProviderForDomain(domain, R.xml.providers_product); + } if (p == null) { p = findProviderForDomain(domain, R.xml.providers); } @@ -597,23 +601,16 @@ public class AccountSetupBasics extends Activity return null; } - static class Provider implements Serializable { + public static class Provider implements Serializable { private static final long serialVersionUID = 8511656164616538989L; public String id; - public String label; - public String domain; - public URI incomingUriTemplate; - public String incomingUsernameTemplate; - public URI outgoingUriTemplate; - public String outgoingUsernameTemplate; - public String note; } } diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java index 479d58769..2c3778e93 100644 --- a/src/com/android/email/mail/store/ImapStore.java +++ b/src/com/android/email/mail/store/ImapStore.java @@ -17,7 +17,10 @@ package com.android.email.mail.store; import com.android.email.Email; +import com.android.email.Preferences; import com.android.email.Utility; +import com.android.email.VendorPolicyLoader; +import com.android.email.codec.binary.Base64; import com.android.email.mail.AuthenticationFailedException; import com.android.email.mail.CertificateValidationException; import com.android.email.mail.FetchProfile; @@ -43,6 +46,7 @@ import com.beetstra.jutf7.CharsetProvider; import android.content.Context; import android.os.Build; +import android.telephony.TelephonyManager; import android.util.Config; import android.util.Log; @@ -54,12 +58,15 @@ import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.regex.Pattern; import javax.net.ssl.SSLException; @@ -174,7 +181,7 @@ public class ImapStore extends Store { mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501"); // Assign user-agent string (for RFC2971 ID command) - String mUserAgent = getImapId(context); + String mUserAgent = getImapId(context, mUsername, mRootTransport.getHost()); if (mUserAgent != null) { mIdPhrase = "ID (" + mUserAgent + ")"; } else if (DEBUG_FORCE_SEND_ID) { @@ -202,61 +209,142 @@ public class ImapStore extends Store { * because some connections may append additional values. * * The following IMAP ID keys may be included: - * name Android package name of the program - * os "android" - * os-version "version; model; build-id" - * vendor Vendor of the client/server + * name Android package name of the program + * os "android" + * os-version "version; model; build-id" + * vendor Vendor of the client/server + * x-android-device-model Model (only revealed if release build) + * x-android-net-operator Mobile network operator (if known) + * AGUID A device+account UID * + * In addition, a vendor policy .apk can append key/value pairs. + * + * @param userName the username of the account + * @param host the host (server) of the account * @return a String for use in an IMAP ID message. */ - public String getImapId(Context context) { - synchronized (Email.class) { + public String getImapId(Context context, String userName, String host) { + // The first section is global to all IMAP connections, and generates the fixed + // values in any IMAP ID message + synchronized (ImapStore.class) { if (sImapId == null) { - // "name" "com.android.email" - StringBuffer sb = new StringBuffer("\"name\" \""); - sb.append(context.getPackageName()); - sb.append("\""); + String networkOperator = TelephonyManager.getDefault().getNetworkOperatorName(); + if (networkOperator == null) networkOperator = ""; - // "os" "android" - sb.append(" \"os\" \"android\""); - - // "os-version" "version; model; build-id" - sb.append(" \"os-version\" \""); - final String version = Build.VERSION.RELEASE; - if (version.length() > 0) { - sb.append(version); - } else { - // default to "1.0" - sb.append("1.0"); - } - // add the model (on release builds only) - if ("REL".equals(Build.VERSION.CODENAME)) { - final String model = Build.MODEL; - if (model.length() > 0) { - sb.append("; "); - sb.append(model); - } - } - // add the build ID or build # - final String id = Build.ID; - if (id.length() > 0) { - sb.append("; "); - sb.append(id); - } - sb.append("\""); - - // "vendor" "the vendor" - final String vendor = Build.MANUFACTURER; - if (vendor.length() > 0) { - sb.append(" \"vendor\" \""); - sb.append(vendor); - sb.append("\""); - } - - sImapId = sb.toString(); + sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE, + Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER, + networkOperator); } } - return sImapId; + + // This section is per Store, and adds in a dynamic elements like UID's. + // We don't cache the result of this work, because the caller does anyway. + StringBuilder id = new StringBuilder(sImapId); + + // Optionally add any vendor-supplied id keys + String vendorId = + VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, null); + if (vendorId != null) { + id.append(' '); + id.append(vendorId); + } + + // Generate a UID that mixes a "stable" device UID with the email address + try { + String devUID = Preferences.getPreferences(context).getDeviceUID(); + MessageDigest messageDigest; + messageDigest = MessageDigest.getInstance("SHA-1"); + messageDigest.update(userName.getBytes()); + messageDigest.update(devUID.getBytes()); + byte[] uid = messageDigest.digest(); + String hexUid = new String(new Base64().encode(uid)); + id.append(" \"AGUID\" \""); + id.append(hexUid); + id.append('\"'); + } catch (NoSuchAlgorithmException e) { + Log.d(Email.LOG_TAG, "couldn't obtain SHA-1 hash for device UID"); + } + return id.toString(); + } + + /** + * Helper function that actually builds the static part of the IMAP ID string. This is + * separated from getImapId for testability. There is no escaping or encoding in IMAP ID so + * any rogue chars must be filtered here. + * + * @param packageName context.getPackageName() + * @param version Build.VERSION.RELEASE + * @param codeName Build.VERSION.CODENAME + * @param model Build.MODEL + * @param id Build.ID + * @param vendor Build.MANUFACTURER + * @param networkOperator TelephonyManager.getNetworkOperatorName() + * @return the static (never changes) portion of the IMAP ID + */ + /* package */ String makeCommonImapId(String packageName, String version, + String codeName, String model, String id, String vendor, String networkOperator) { + + // Before building up IMAP ID string, pre-filter the input strings for "legal" chars + // This is using a fairly arbitrary char set intended to pass through most reasonable + // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / + // The most important thing is *not* to pass parens, quotes, or CRLF, which would break + // the format of the IMAP ID list. + Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]"); + packageName = p.matcher(packageName).replaceAll(""); + version = p.matcher(version).replaceAll(""); + codeName = p.matcher(codeName).replaceAll(""); + model = p.matcher(model).replaceAll(""); + id = p.matcher(id).replaceAll(""); + vendor = p.matcher(vendor).replaceAll(""); + networkOperator = p.matcher(networkOperator).replaceAll(""); + + // "name" "com.android.email" + StringBuffer sb = new StringBuffer("\"name\" \""); + sb.append(packageName); + sb.append("\""); + + // "os" "android" + sb.append(" \"os\" \"android\""); + + // "os-version" "version; build-id" + sb.append(" \"os-version\" \""); + if (version.length() > 0) { + sb.append(version); + } else { + // default to "1.0" + sb.append("1.0"); + } + // add the build ID or build # + if (id.length() > 0) { + sb.append("; "); + sb.append(id); + } + sb.append("\""); + + // "vendor" "the vendor" + if (vendor.length() > 0) { + sb.append(" \"vendor\" \""); + sb.append(vendor); + sb.append("\""); + } + + // "x-android-device-model" the device model (on release builds only) + if ("REL".equals(codeName)) { + if (model.length() > 0) { + sb.append(" \"x-android-device-model\" \""); + sb.append(model); + sb.append("\""); + } + } + + // "x-android-mobile-net-operator" "name of network operator" + if (networkOperator.length() > 0) { + sb.append(" \"x-android-mobile-net-operator\" \""); + sb.append(networkOperator); + sb.append("\""); + } + + return sb.toString(); } diff --git a/tests/src/com/android/email/VendorPolicyLoaderTest.java b/tests/src/com/android/email/VendorPolicyLoaderTest.java index 847ba5dc6..b237786fd 100644 --- a/tests/src/com/android/email/VendorPolicyLoaderTest.java +++ b/tests/src/com/android/email/VendorPolicyLoaderTest.java @@ -20,6 +20,8 @@ import android.content.Context; import android.os.Bundle; import android.test.AndroidTestCase; +import java.util.HashMap; + public class VendorPolicyLoaderTest extends AndroidTestCase { /** * Test for the case where the helper package doesn't exist. @@ -86,7 +88,7 @@ public class VendorPolicyLoaderTest extends AndroidTestCase { assertNull(MockVendorPolicy.passedPolicy); } - public static class MockVendorPolicy { + private static class MockVendorPolicy { public static String passedPolicy; public static Bundle passedBundle; public static Bundle mockResult; @@ -97,4 +99,40 @@ public class VendorPolicyLoaderTest extends AndroidTestCase { return mockResult; } } + + /** + * Test that any vendor policy that happens to be installed returns legal values + * for getImapIdValues() per its API. + * + * Note, in most cases very little will happen in this test, because there is + * no vendor policy package. Most of this test exists to test a vendor policy + * package itself, to make sure that its API returns reasonable values. + */ + public void testGetImapIdValues() { + VendorPolicyLoader pl = VendorPolicyLoader.getInstance(getContext()); + String id = pl.getImapIdValues("user-name", "server.yahoo.com", + "IMAP4rev1 STARTTLS AUTH=GSSAPI"); + // null is a reasonable result + if (id == null) return; + + // if non-null, basic sanity checks on format + assertEquals("\"", id.charAt(0)); + assertEquals("\"", id.charAt(id.length()-1)); + // see if we can break it up properly + String[] elements = id.split("\""); + assertEquals(0, elements.length % 4); + for (int i = 0; i < elements.length; ) { + // Because we split at quotes, we expect to find: + // [i] = null or one or more spaces + // [i+1] = key + // [i+2] = one or more spaces + // [i+3] = value + // Here are some incomplete checks of the above + assertTrue(elements[i] == null || elements[i].startsWith(" ")); + assertTrue(elements[i+1].charAt(0) != ' '); + assertTrue(elements[i+2].startsWith(" ")); + assertTrue(elements[i+3].charAt(0) != ' '); + i += 4; + } + } } diff --git a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java index 47b4416ac..4163938d1 100644 --- a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java +++ b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java @@ -16,7 +16,6 @@ package com.android.email.mail.store; -import com.android.email.Email; import com.android.email.mail.FetchProfile; import com.android.email.mail.Flag; import com.android.email.mail.Folder; @@ -31,8 +30,8 @@ import com.android.email.mail.internet.MimeUtility; import com.android.email.mail.transport.MockTransport; import android.test.AndroidTestCase; +import android.test.MoreAsserts; import android.test.suitebuilder.annotation.SmallTest; -import android.util.Log; import java.util.ArrayList; import java.util.Date; @@ -93,40 +92,121 @@ public class ImapStoreUnitTests extends AndroidTestCase { /** * Test the generation of the IMAP ID keys - * - * Since this is build-specific, we mostly just ensure that the correct strings - * are being generated, and non-empty, and (if possible) look for expected formatting. */ - public void testImapId() { - String id = mStore.getImapId(getContext()); + public void testImapIdBasic() { + // First test looks at operation of the outer API - we don't control any of the + // values; Just look for basic results. + + // Strings we'll expect to find: + // name Android package name of the program + // os "android" + // os-version "version; build-id" + // vendor Vendor of the client/server + // x-android-device-model Model (Optional, so not tested here) + // x-android-net-operator Carrier (Unreliable, so not tested here) + // AGUID A device+account UID + String id = mStore.getImapId(getContext(), "user-name", "host-name"); + HashMap map = tokenizeImapId(id); + assertEquals(getContext().getPackageName(), map.get("name")); + assertEquals("android", map.get("os")); + assertNotNull(map.get("os-version")); + assertNotNull(map.get("vendor")); + assertNotNull(map.get("AGUID")); + + // Next, use the inner API to confirm operation of a couple of + // variants for release and non-release devices. + + // simple API check - non-REL codename, non-empty version + id = mStore.makeCommonImapId("packageName", "version", "codeName", + "model", "id", "vendor", "network-operator"); + map = tokenizeImapId(id); + assertEquals("packageName", map.get("name")); + assertEquals("android", map.get("os")); + assertEquals("version; id", map.get("os-version")); + assertEquals("vendor", map.get("vendor")); + assertEquals(null, map.get("x-android-device-model")); + assertEquals("network-operator", map.get("x-android-mobile-net-operator")); + assertEquals(null, map.get("AGUID")); + + // simple API check - codename is REL, so use model name. + // also test empty version => 1.0 and empty network operator + id = mStore.makeCommonImapId("packageName", "", "REL", + "model", "id", "vendor", ""); + map = tokenizeImapId(id); + assertEquals("packageName", map.get("name")); + assertEquals("android", map.get("os")); + assertEquals("1.0; id", map.get("os-version")); + assertEquals("vendor", map.get("vendor")); + assertEquals("model", map.get("x-android-device-model")); + assertEquals(null, map.get("x-android-mobile-net-operator")); + assertEquals(null, map.get("AGUID")); + } + + /** + * Test of the internal generator for IMAP ID strings, specifically looking for proper + * filtering of illegal values. This is required because we cannot necessarily trust + * the external sources of some of this data (e.g. release labels). + * + * The (somewhat arbitrary) legal values are: a-z A-Z 0-9 - _ + = ; : . , / + * The most important goal of the filters is to keep out control chars, (, ), and " + */ + public void testImapIdFiltering() { + String id = mStore.makeCommonImapId("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + "0123456789", "codeName", + "model", "-_+=;:.,// ", + "v(e)n\"d\ro\nr", // look for bad chars stripped out, leaving OK chars + "()\""); // look for bad chars stripped out, leaving nothing + HashMap map = tokenizeImapId(id); + + assertEquals("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", map.get("name")); + assertEquals("0123456789; -_+=;:.,// ", map.get("os-version")); + assertEquals("vendor", map.get("vendor")); + assertNull(map.get("x-android-mobile-net-operator")); + } + + /** + * Test that IMAP ID uid's are per-username + */ + public void testImapIdDeviceId() throws MessagingException { + ImapStore store1a = (ImapStore) ImapStore.newInstance("imap://user1:password@server:999", + getContext(), null); + ImapStore store1b = (ImapStore) ImapStore.newInstance("imap://user1:password@server:999", + getContext(), null); + ImapStore store2 = (ImapStore) ImapStore.newInstance("imap://user2:password@server:999", + getContext(), null); + + String id1a = mStore.getImapId(getContext(), "user1", "host-name"); + String id1b = mStore.getImapId(getContext(), "user1", "host-name"); + String id2 = mStore.getImapId(getContext(), "user2", "host-name"); + + String uid1a = tokenizeImapId(id1a).get("AGUID"); + String uid1b = tokenizeImapId(id1b).get("AGUID"); + String uid2 = tokenizeImapId(id2).get("AGUID"); + + assertEquals(uid1a, uid1b); + MoreAsserts.assertNotEqual(uid1a, uid2); + } + + /** + * Helper to break an IMAP ID string into keys & values + * @param id the IMAP Id string (the part inside the parens) + * @return a map of key/value pairs + */ + private HashMap tokenizeImapId(String id) { // Instead of a true tokenizer, we'll use double-quote as the split. // We can's use " " because there may be spaces inside the values. String[] elements = id.split("\""); HashMap map = new HashMap(); for (int i = 0; i < elements.length; ) { // Because we split at quotes, we expect to find: - // [i] = null + // [i] = null or one or more spaces // [i+1] = key // [i+2] = one or more spaces // [i+3] = value map.put(elements[i+1], elements[i+3]); i += 4; } - - // Strings we'll expect to find: - // name Android package name of the program - // os "android" - // os-version "version; model; build-id" - // vendor Vendor of the client/server - - String name = map.get("name"); - assertEquals(getContext().getPackageName(), name); - String os = map.get("os"); - assertEquals("android", os); - String osversion = map.get("os-version"); - assertNotNull(osversion); - String vendor = map.get("vendor"); - assertNotNull(vendor); + return map; } /**