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 
17 package com.android.emailcommon;
18 
19 import android.content.Context;
20 import android.content.pm.ApplicationInfo;
21 import android.content.pm.PackageManager.NameNotFoundException;
22 import android.os.Bundle;
23 
24 import com.android.mail.utils.LogUtils;
25 
26 import java.io.Serializable;
27 import 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  */
39 public 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 
getInstance(Context context)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      */
injectPolicyForTest(Context context, String apkPackageName, Class<?> clazz)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      */
clearInstanceForTest()90     public static void clearInstanceForTest() {
91         sInstance = null;
92     }
93 
VendorPolicyLoader(Context context)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      */
VendorPolicyLoader(Context context, String apkPackageName, String className, boolean allowNonSystemApk)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
isSystemPackage(Context context, String packageName)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
getPolicy(String policy, Bundle args)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      */
useAlternateExchangeStrings()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      */
getImapIdValues(String userName, String host, String capabilities)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          */
expandTemplates(String email)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          */
expandAlternateTemplates(String email)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          */
expandTemplate(String template, String email, String user)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      */
findProviderForDomain(String domain)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