summaryrefslogtreecommitdiffstats
path: root/emailcommon/src/com/android/emailcommon/VendorPolicyLoader.java
blob: d42aa14c30a5b79e1fb4f451ebd483fe76bc97d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.emailcommon;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;

import com.android.mail.utils.LogUtils;

import java.io.Serializable;
import java.lang.reflect.Method;

/**
 * A bridge class to the email vendor policy apk.
 *
 * <p>Email vendor policy is a system apk named "com.android.email.helper".  When exists, it must
 * contain a class called "com.android.email.policy.EmailPolicy" with a static public method
 * <code>Bundle getPolicy(String, Bundle)</code>, which serves vendor specific configurations.
 *
 * <p>A vendor policy apk is optional.  The email application will operate properly when none is
 * found.
 */
public class VendorPolicyLoader {
    private static final String POLICY_PACKAGE = "com.android.email.policy";
    private static final String POLICY_CLASS = POLICY_PACKAGE + ".EmailPolicy";
    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;

    private final Method mPolicyMethod;

    public static VendorPolicyLoader getInstance(Context context) {
        if (sInstance == null) {
            // It's okay to instantiate VendorPolicyLoader multiple times.  No need to synchronize.
            sInstance = new VendorPolicyLoader(context);
        }
        return sInstance;
    }

    /**
     * For testing only.
     *
     * Replaces the instance with a new instance that loads a specified class.
     */
    public static void injectPolicyForTest(Context context, String apkPackageName, Class<?> clazz) {
        String name = clazz.getName();
        LogUtils.d(Logging.LOG_TAG, String.format("Using policy: package=%s name=%s",
                apkPackageName, name));
        sInstance = new VendorPolicyLoader(context, apkPackageName, name, true);
    }

    /**
     * For testing only.
     *
     * Clear the instance so that the next {@link #getInstance} call will return a regular,
     * non-injected instance.
     */
    public static void clearInstanceForTest() {
        sInstance = null;
    }

    private VendorPolicyLoader(Context context) {
        this(context, POLICY_PACKAGE, POLICY_CLASS, false);
    }

    /**
     * Constructor for testing, where we need to use an alternate package/class name, and skip
     * the system apk check.
     */
    public VendorPolicyLoader(Context context, String apkPackageName, String className,
            boolean allowNonSystemApk) {
        if (!allowNonSystemApk && !isSystemPackage(context, apkPackageName)) {
            mPolicyMethod = null;
            return;
        }

        Class<?> clazz = null;
        Method method = null;
        try {
            final Context policyContext = context.createPackageContext(apkPackageName,
                    Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
            final ClassLoader classLoader = policyContext.getClassLoader();
            clazz = classLoader.loadClass(className);
            method = clazz.getMethod(GET_POLICY_METHOD, ARGS);
        } catch (NameNotFoundException ignore) {
            // Package not found -- it's okay - there's no policy .apk found, which is OK
        } catch (ClassNotFoundException e) {
            // Class not found -- probably not OK, but let's not crash here
            LogUtils.w(Logging.LOG_TAG, "VendorPolicyLoader: " + e);
        } catch (NoSuchMethodException e) {
            // Method not found -- probably not OK, but let's not crash here
            LogUtils.w(Logging.LOG_TAG, "VendorPolicyLoader: " + e);
        }
        mPolicyMethod = method;
    }

    // Not private for testing
    public static boolean isSystemPackage(Context context, String packageName) {
        try {
            ApplicationInfo ai = context.getPackageManager().getApplicationInfo(packageName, 0);
            return (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
        } catch (NameNotFoundException e) {
            return false; // Package not found.
        }
    }

    /**
     * Calls the getPolicy method in the policy apk, if one exists.  This method never returns null;
     * It returns an empty {@link Bundle} when there is no policy apk (or even if the inner
     * getPolicy returns null).
     */
    // Not private for testing
    public Bundle getPolicy(String policy, Bundle args) {
        Bundle ret = null;
        if (mPolicyMethod != null) {
            try {
                ret = (Bundle) mPolicyMethod.invoke(null, policy, args);
            } catch (Exception e) {
                LogUtils.w(Logging.LOG_TAG, "VendorPolicyLoader", e);
            }
        }
        return (ret != null) ? ret : Bundle.EMPTY;
    }

    /**
     * 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 capabilities 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;
    }

    public static class OAuthProvider implements Serializable {
        private static final long serialVersionUID = 8511656164616538990L;

        public String id;
        public String label;
        public String authEndpoint;
        public String tokenEndpoint;
        public String refreshEndpoint;
        public String responseType;
        public String redirectUri;
        public String scope;
        public String clientId;
        public String clientSecret;
        public String state;
    }

    public static class Provider implements Serializable {
        private static final long serialVersionUID = 8511656164616538989L;

        public String id;
        public String label;
        public String domain;
        public String incomingUriTemplate;
        public String incomingUsernameTemplate;
        public String outgoingUriTemplate;
        public String outgoingUsernameTemplate;
        public String incomingUri;
        public String incomingUsername;
        public String outgoingUri;
        public String outgoingUsername;
        public String note;
        public String oauth;

        /**
         * Expands templates in all of the  provider fields that support them. Currently,
         * templates are used in 4 fields -- incoming and outgoing URI and user name.
         * @param email user-specified data used to replace template values
         */
        public void expandTemplates(String email) {
            String[] emailParts = email.split("@");
            String user = emailParts[0];

            incomingUri = expandTemplate(incomingUriTemplate, email, user);
            incomingUsername = expandTemplate(incomingUsernameTemplate, email, user);
            outgoingUri = expandTemplate(outgoingUriTemplate, email, user);
            outgoingUsername = expandTemplate(outgoingUsernameTemplate, email, user);
        }

        /**
         * Replaces all parameterized values in the given template. The values replaced are
         * $domain, $user and $email.
         */
        private String expandTemplate(String template, String email, String user) {
            String returnString = template;
            returnString = returnString.replaceAll("\\$email", email);
            returnString = returnString.replaceAll("\\$user", user);
            returnString = returnString.replaceAll("\\$domain", domain);
            return returnString;
        }
    }

    /**
     * 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 (optional - null is OK)
     *
     * Note, if we get this far, we expect "correct" results from the policy method.  But throwing
     * checked exceptions requires a bunch of upstream changes, so we're going to catch them here
     * and add logging.  Other exceptions may escape here (such as null pointers) to fail fast.
     *
     * @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 && !out.isEmpty()) {
            Provider p = new Provider();
            p.id = null;
            p.label = null;
            p.domain = domain;
            p.incomingUriTemplate = out.getString(FIND_PROVIDER_IN_URI);
            p.incomingUsernameTemplate = out.getString(FIND_PROVIDER_IN_USER);
            p.outgoingUriTemplate = out.getString(FIND_PROVIDER_OUT_URI);
            p.outgoingUsernameTemplate = out.getString(FIND_PROVIDER_OUT_USER);
            p.note = out.getString(FIND_PROVIDER_NOTE);
            return p;
        }
        return null;
    }
}