1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.emailcommon; 18 19import android.content.Context; 20import android.content.pm.ApplicationInfo; 21import android.content.pm.PackageManager.NameNotFoundException; 22import android.os.Bundle; 23 24import com.android.mail.utils.LogUtils; 25 26import java.io.Serializable; 27import java.lang.reflect.Method; 28 29/** 30 * A bridge class to the email vendor policy apk. 31 * 32 * <p>Email vendor policy is a system apk named "com.android.email.helper". When exists, it must 33 * contain a class called "com.android.email.policy.EmailPolicy" with a static public method 34 * <code>Bundle getPolicy(String, Bundle)</code>, which serves vendor specific configurations. 35 * 36 * <p>A vendor policy apk is optional. The email application will operate properly when none is 37 * found. 38 */ 39public class VendorPolicyLoader { 40 private static final String POLICY_PACKAGE = "com.android.email.policy"; 41 private static final String POLICY_CLASS = POLICY_PACKAGE + ".EmailPolicy"; 42 private static final String GET_POLICY_METHOD = "getPolicy"; 43 private static final Class<?>[] ARGS = new Class<?>[] {String.class, Bundle.class}; 44 45 // call keys and i/o bundle keys 46 // when there is only one parameter or return value, use call key 47 private static final String USE_ALTERNATE_EXCHANGE_STRINGS = "useAlternateExchangeStrings"; 48 private static final String GET_IMAP_ID = "getImapId"; 49 private static final String GET_IMAP_ID_USER = "getImapId.user"; 50 private static final String GET_IMAP_ID_HOST = "getImapId.host"; 51 private static final String GET_IMAP_ID_CAPA = "getImapId.capabilities"; 52 private static final String FIND_PROVIDER = "findProvider"; 53 private static final String FIND_PROVIDER_IN_URI = "findProvider.inUri"; 54 private static final String FIND_PROVIDER_IN_USER = "findProvider.inUser"; 55 private static final String FIND_PROVIDER_OUT_URI = "findProvider.outUri"; 56 private static final String FIND_PROVIDER_OUT_USER = "findProvider.outUser"; 57 private static final String FIND_PROVIDER_NOTE = "findProvider.note"; 58 59 /** Singleton instance */ 60 private static VendorPolicyLoader sInstance; 61 62 private final Method mPolicyMethod; 63 64 public static VendorPolicyLoader getInstance(Context context) { 65 if (sInstance == null) { 66 // It's okay to instantiate VendorPolicyLoader multiple times. No need to synchronize. 67 sInstance = new VendorPolicyLoader(context); 68 } 69 return sInstance; 70 } 71 72 /** 73 * For testing only. 74 * 75 * Replaces the instance with a new instance that loads a specified class. 76 */ 77 public static void injectPolicyForTest(Context context, String apkPackageName, Class<?> clazz) { 78 String name = clazz.getName(); 79 LogUtils.d(Logging.LOG_TAG, String.format("Using policy: package=%s name=%s", 80 apkPackageName, name)); 81 sInstance = new VendorPolicyLoader(context, apkPackageName, name, true); 82 } 83 84 /** 85 * For testing only. 86 * 87 * Clear the instance so that the next {@link #getInstance} call will return a regular, 88 * non-injected instance. 89 */ 90 public static void clearInstanceForTest() { 91 sInstance = null; 92 } 93 94 private VendorPolicyLoader(Context context) { 95 this(context, POLICY_PACKAGE, POLICY_CLASS, false); 96 } 97 98 /** 99 * Constructor for testing, where we need to use an alternate package/class name, and skip 100 * the system apk check. 101 */ 102 public VendorPolicyLoader(Context context, String apkPackageName, String className, 103 boolean allowNonSystemApk) { 104 if (!allowNonSystemApk && !isSystemPackage(context, apkPackageName)) { 105 mPolicyMethod = null; 106 return; 107 } 108 109 Class<?> clazz = null; 110 Method method = null; 111 try { 112 final Context policyContext = context.createPackageContext(apkPackageName, 113 Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE); 114 final ClassLoader classLoader = policyContext.getClassLoader(); 115 clazz = classLoader.loadClass(className); 116 method = clazz.getMethod(GET_POLICY_METHOD, ARGS); 117 } catch (NameNotFoundException ignore) { 118 // Package not found -- it's okay - there's no policy .apk found, which is OK 119 } catch (ClassNotFoundException e) { 120 // Class not found -- probably not OK, but let's not crash here 121 LogUtils.w(Logging.LOG_TAG, "VendorPolicyLoader: " + e); 122 } catch (NoSuchMethodException e) { 123 // Method not found -- probably not OK, but let's not crash here 124 LogUtils.w(Logging.LOG_TAG, "VendorPolicyLoader: " + e); 125 } 126 mPolicyMethod = method; 127 } 128 129 // Not private for testing 130 public static boolean isSystemPackage(Context context, String packageName) { 131 try { 132 ApplicationInfo ai = context.getPackageManager().getApplicationInfo(packageName, 0); 133 return (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 134 } catch (NameNotFoundException e) { 135 return false; // Package not found. 136 } 137 } 138 139 /** 140 * Calls the getPolicy method in the policy apk, if one exists. This method never returns null; 141 * It returns an empty {@link Bundle} when there is no policy apk (or even if the inner 142 * getPolicy returns null). 143 */ 144 // Not private for testing 145 public Bundle getPolicy(String policy, Bundle args) { 146 Bundle ret = null; 147 if (mPolicyMethod != null) { 148 try { 149 ret = (Bundle) mPolicyMethod.invoke(null, policy, args); 150 } catch (Exception e) { 151 LogUtils.w(Logging.LOG_TAG, "VendorPolicyLoader", e); 152 } 153 } 154 return (ret != null) ? ret : Bundle.EMPTY; 155 } 156 157 /** 158 * Returns true if alternate exchange descriptive text is required. 159 * 160 * Vendor function: 161 * Select: USE_ALTERNATE_EXCHANGE_STRINGS 162 * Params: none 163 * Result: USE_ALTERNATE_EXCHANGE_STRINGS (boolean) 164 */ 165 public boolean useAlternateExchangeStrings() { 166 return getPolicy(USE_ALTERNATE_EXCHANGE_STRINGS, null) 167 .getBoolean(USE_ALTERNATE_EXCHANGE_STRINGS, false); 168 } 169 170 /** 171 * Returns additional key/value pairs for the IMAP ID string. 172 * 173 * Vendor function: 174 * Select: GET_IMAP_ID 175 * Params: GET_IMAP_ID_USER (String) 176 * GET_IMAP_ID_HOST (String) 177 * GET_IMAP_ID_CAPABILITIES (String) 178 * Result: GET_IMAP_ID (String) 179 * 180 * @param userName the server that is being contacted (e.g. "imap.server.com") 181 * @param host the server that is being contacted (e.g. "imap.server.com") 182 * @param capabilities reported capabilities, if known. null is OK 183 * @return zero or more key/value pairs, quoted and delimited by spaces. If there is 184 * nothing to add, return null. 185 */ 186 public String getImapIdValues(String userName, String host, String capabilities) { 187 Bundle params = new Bundle(); 188 params.putString(GET_IMAP_ID_USER, userName); 189 params.putString(GET_IMAP_ID_HOST, host); 190 params.putString(GET_IMAP_ID_CAPA, capabilities); 191 String result = getPolicy(GET_IMAP_ID, params).getString(GET_IMAP_ID); 192 return result; 193 } 194 195 public static class OAuthProvider implements Serializable { 196 private static final long serialVersionUID = 8511656164616538990L; 197 198 public String id; 199 public String label; 200 public String authEndpoint; 201 public String tokenEndpoint; 202 public String refreshEndpoint; 203 public String responseType; 204 public String redirectUri; 205 public String scope; 206 public String clientId; 207 public String clientSecret; 208 public String state; 209 } 210 211 public static class Provider implements Serializable { 212 private static final long serialVersionUID = 8511656164616538989L; 213 214 public String id; 215 public String label; 216 public String domain; 217 public String incomingUriTemplate; 218 public String incomingUsernameTemplate; 219 public String outgoingUriTemplate; 220 public String outgoingUsernameTemplate; 221 public String altIncomingUriTemplate; 222 public String altIncomingUsernameTemplate; 223 public String altOutgoingUriTemplate; 224 public String altOutgoingUsernameTemplate; 225 public String incomingUri; 226 public String incomingUsername; 227 public String outgoingUri; 228 public String outgoingUsername; 229 public String note; 230 public String oauth; 231 232 /** 233 * Expands templates in all of the provider fields that support them. Currently, 234 * templates are used in 4 fields -- incoming and outgoing URI and user name. 235 * @param email user-specified data used to replace template values 236 */ 237 public void expandTemplates(String email) { 238 final String[] emailParts = email.split("@"); 239 final String user = emailParts[0]; 240 241 incomingUri = expandTemplate(incomingUriTemplate, email, user); 242 incomingUsername = expandTemplate(incomingUsernameTemplate, email, user); 243 outgoingUri = expandTemplate(outgoingUriTemplate, email, user); 244 outgoingUsername = expandTemplate(outgoingUsernameTemplate, email, user); 245 } 246 247 /** 248 * Like the above, but expands the alternate templates instead 249 * @param email user-specified data used to replace template values 250 */ 251 public void expandAlternateTemplates(String email) { 252 final String[] emailParts = email.split("@"); 253 final String user = emailParts[0]; 254 255 incomingUri = expandTemplate(altIncomingUriTemplate, email, user); 256 incomingUsername = expandTemplate(altIncomingUsernameTemplate, email, user); 257 outgoingUri = expandTemplate(altOutgoingUriTemplate, email, user); 258 outgoingUsername = expandTemplate(altOutgoingUsernameTemplate, email, user); 259 } 260 261 /** 262 * Replaces all parameterized values in the given template. The values replaced are 263 * $domain, $user and $email. 264 */ 265 private String expandTemplate(String template, String email, String user) { 266 String returnString = template; 267 returnString = returnString.replaceAll("\\$email", email); 268 returnString = returnString.replaceAll("\\$user", user); 269 returnString = returnString.replaceAll("\\$domain", domain); 270 return returnString; 271 } 272 } 273 274 /** 275 * Returns provider setup information for a given email address 276 * 277 * Vendor function: 278 * Select: FIND_PROVIDER 279 * Param: FIND_PROVIDER (String) 280 * Result: FIND_PROVIDER_IN_URI 281 * FIND_PROVIDER_IN_USER 282 * FIND_PROVIDER_OUT_URI 283 * FIND_PROVIDER_OUT_USER 284 * FIND_PROVIDER_NOTE (optional - null is OK) 285 * 286 * Note, if we get this far, we expect "correct" results from the policy method. But throwing 287 * checked exceptions requires a bunch of upstream changes, so we're going to catch them here 288 * and add logging. Other exceptions may escape here (such as null pointers) to fail fast. 289 * 290 * @param domain The domain portion of the user's email address 291 * @return suitable Provider definition, or null if no match found 292 */ 293 public Provider findProviderForDomain(String domain) { 294 Bundle params = new Bundle(); 295 params.putString(FIND_PROVIDER, domain); 296 Bundle out = getPolicy(FIND_PROVIDER, params); 297 if (out != null && !out.isEmpty()) { 298 Provider p = new Provider(); 299 p.id = null; 300 p.label = null; 301 p.domain = domain; 302 p.incomingUriTemplate = out.getString(FIND_PROVIDER_IN_URI); 303 p.incomingUsernameTemplate = out.getString(FIND_PROVIDER_IN_USER); 304 p.outgoingUriTemplate = out.getString(FIND_PROVIDER_OUT_URI); 305 p.outgoingUsernameTemplate = out.getString(FIND_PROVIDER_OUT_USER); 306 p.note = out.getString(FIND_PROVIDER_NOTE); 307 return p; 308 } 309 return null; 310 } 311} 312