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