ExchangeStore.java revision 7375d950e4c71a3ab2c8c726b7641c825fb92efc
1/*
2 * Copyright (C) 2009 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.email.mail.store;
18
19import com.android.email.Email;
20import com.android.email.mail.AuthenticationFailedException;
21import com.android.email.mail.FetchProfile;
22import com.android.email.mail.Flag;
23import com.android.email.mail.Folder;
24import com.android.email.mail.Message;
25import com.android.email.mail.MessageRetrievalListener;
26import com.android.email.mail.MessagingException;
27import com.android.email.mail.Store;
28import com.android.email.mail.StoreSynchronizer;
29import com.android.email.service.EmailServiceProxy;
30import com.android.exchange.Eas;
31import com.android.exchange.SyncManager;
32
33import android.accounts.AccountManager;
34import android.accounts.AuthenticatorException;
35import android.accounts.Future2;
36import android.accounts.Future2Callback;
37import android.accounts.OperationCanceledException;
38import android.content.Context;
39import android.os.Bundle;
40import android.os.RemoteException;
41import android.text.TextUtils;
42import android.util.Config;
43import android.util.Log;
44
45import java.io.IOException;
46import java.net.URI;
47import java.net.URISyntaxException;
48import java.util.HashMap;
49
50/**
51 * This is a placeholder for use in Exchange implementations.  It is based on the notion of
52 * lightweight adapter classes for Store, Folder, and Sender, and a common facade for the common
53 * Transport code.
54 */
55public class ExchangeStore extends Store {
56    public static final String LOG_TAG = "ExchangeStore";
57
58    private final Context mContext;
59    private URI mUri;
60    private PersistentDataCallbacks mCallbacks;
61
62    private final ExchangeTransport mTransport;
63    private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
64
65    private boolean mPushModeRunning = false;
66
67    /**
68     * Factory method.
69     */
70    public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks)
71    throws MessagingException {
72        return new ExchangeStore(uri, context, callbacks);
73    }
74
75    /**
76     * eas://user:password@server/domain
77     *
78     * @param _uri
79     * @param application
80     * @throws MessagingException
81     */
82    private ExchangeStore(String _uri, Context context, PersistentDataCallbacks callbacks)
83            throws MessagingException {
84        mContext = context;
85        try {
86            mUri = new URI(_uri);
87        } catch (URISyntaxException e) {
88            throw new MessagingException("Invalid uri for ExchangeStore");
89        }
90        mCallbacks = callbacks;
91
92        String scheme = mUri.getScheme();
93        int connectionSecurity;
94        if (scheme.equals("eas")) {
95            connectionSecurity = ExchangeTransport.CONNECTION_SECURITY_NONE;
96        } else if (scheme.equals("eas+ssl+")) {
97            connectionSecurity = ExchangeTransport.CONNECTION_SECURITY_SSL_REQUIRED;
98        } else {
99            throw new MessagingException("Unsupported protocol");
100        }
101
102        mTransport = ExchangeTransport.getInstance(mUri, context);
103    }
104
105    /**
106     * Retrieve the underlying transport.  Used primarily for testing.
107     * @return
108     */
109    /* package */ ExchangeTransport getTransport() {
110        return mTransport;
111    }
112
113    @Override
114    public void checkSettings() throws MessagingException {
115        mTransport.checkSettings(mUri);
116    }
117
118    @Override
119    public Folder getFolder(String name) throws MessagingException {
120        synchronized (mFolders) {
121            Folder folder = mFolders.get(name);
122            if (folder == null) {
123                folder = new ExchangeFolder(this, name);
124                mFolders.put(folder.getName(), folder);
125            }
126            return folder;
127        }
128    }
129
130    @Override
131    public Folder[] getPersonalNamespaces() throws MessagingException {
132        return new Folder[] {
133                getFolder(ExchangeTransport.FOLDER_INBOX),
134        };
135    }
136
137    /**
138     * For a store that supports push mode, this is the API that enables it or disables it.
139     * The store should use this API to start or stop its persistent connection service or thread.
140     *
141     * <p>Note, may be called multiple times, even after push mode has been started or stopped.
142     *
143     * @param enablePushMode start or stop push mode delivery
144     */
145    @Override
146    public void enablePushModeDelivery(boolean enablePushMode) {
147        if (Config.LOGD && Email.DEBUG) {
148            if (enablePushMode && !mPushModeRunning) {
149                Log.d(Email.LOG_TAG, "start push mode");
150            } else if (!enablePushMode && mPushModeRunning) {
151                Log.d(Email.LOG_TAG, "stop push mode");
152            } else {
153                Log.d(Email.LOG_TAG, enablePushMode ?
154                        "push mode already started" : "push mode already stopped");
155            }
156        }
157        mPushModeRunning = enablePushMode;
158    }
159
160    /**
161     * Get class of SettingActivity for this Store class.
162     * @return Activity class that has class method actionEditIncomingSettings()
163     */
164    @Override
165    public Class<? extends android.app.Activity> getSettingActivityClass() {
166        return com.android.email.activity.setup.AccountSetupExchange.class;
167    }
168
169    /**
170     * Get class of sync'er for this Store class.  Because exchange Sync rules are so different
171     * than IMAP or POP3, it's likely that an Exchange implementation will need its own sync
172     * controller.  If so, this function must return a non-null value.
173     *
174     * @return Message Sync controller, or null to use default
175     */
176    @Override
177    public StoreSynchronizer getMessageSynchronizer() {
178        return null;
179    }
180
181    /**
182     * Inform MessagingController that this store requires message structures to be prefetched
183     * before it can fetch message bodies (this is due to EAS protocol restrictions.)
184     * @return always true for EAS
185     */
186    @Override
187    public boolean requireStructurePrefetch() {
188        return true;
189    }
190
191    /**
192     * Inform MessagingController that messages sent via EAS will be placed in the Sent folder
193     * automatically (server-side) and don't need to be uploaded.
194     * @return always false for EAS (assuming server-side copy is supported)
195     */
196    @Override
197    public boolean requireCopyMessageToSentFolder() {
198        return false;
199    }
200
201    public static class ExchangeTransport {
202        public static final int CONNECTION_SECURITY_NONE = 0;
203        public static final int CONNECTION_SECURITY_SSL_REQUIRED = 1;
204
205        public static final String FOLDER_INBOX = Email.INBOX;
206
207        private static final String TAG = "ExchangeTransport";
208        private final Context mContext;
209
210        private String mHost;
211        private String mDomain;
212        private String mUsername;
213        private String mPassword;
214
215        private static HashMap<String, ExchangeTransport> sUriToInstanceMap =
216            new HashMap<String, ExchangeTransport>();
217        private static final HashMap<String, Integer> sFolderMap = new HashMap<String, Integer>();
218
219        /**
220         * Public factory.  The transport should be a singleton (per Uri)
221         */
222        public synchronized static ExchangeTransport getInstance(URI uri, Context context)
223        throws MessagingException {
224            if (!uri.getScheme().equals("eas") && !uri.getScheme().equals("eas+ssl+")) {
225                throw new MessagingException("Invalid scheme");
226            }
227
228            final String key = uri.toString();
229            ExchangeTransport transport = sUriToInstanceMap.get(key);
230            if (transport == null) {
231                transport = new ExchangeTransport(uri, context);
232                sUriToInstanceMap.put(key, transport);
233            }
234            return transport;
235        }
236
237        /**
238         * Private constructor - use public factory.
239         */
240        private ExchangeTransport(URI uri, Context context) throws MessagingException {
241            mContext = context;
242            setUri(uri);
243        }
244
245        /**
246         * Use the Uri to set up a newly-constructed transport
247         * @param uri
248         * @throws MessagingException
249         */
250        private void setUri(final URI uri) throws MessagingException {
251            mHost = uri.getHost();
252            if (mHost == null) {
253                throw new MessagingException("host not specified");
254            }
255
256            mDomain = uri.getPath();
257            if (!TextUtils.isEmpty(mDomain)) {
258                mDomain = mDomain.substring(1);
259            }
260
261            final String userInfo = uri.getUserInfo();
262            if (userInfo == null) {
263                throw new MessagingException("user information not specifed");
264            }
265            final String[] uinfo = userInfo.split(":", 2);
266            if (uinfo.length != 2) {
267                throw new MessagingException("user name and password not specified");
268            }
269            mUsername = uinfo[0];
270            mPassword = uinfo[1];
271        }
272
273        /**
274         * Here's where we check the settings for EAS.
275         * @param uri the URI of the account to create
276         * @throws MessagingException if we can't authenticate the account
277         */
278        public void checkSettings(URI uri) throws MessagingException {
279            setUri(uri);
280            boolean ssl = uri.getScheme().contains("ssl+");
281            try {
282                int result = new EmailServiceProxy(mContext, SyncManager.class)
283                    .validate("eas", mHost, mUsername, mPassword, ssl ? 443 : 80, ssl);
284                if (result != MessagingException.NO_ERROR) {
285                    if (result == MessagingException.AUTHENTICATION_FAILED) {
286                        throw new AuthenticationFailedException("Authentication failed.");
287                    } else {
288                        throw new MessagingException(result);
289                    }
290                } else {
291                    // This code was taken from sample code in AccountsTester
292                    Bundle options = new Bundle();
293                    options.putString("username", mUsername);
294                    options.putString("password", mPassword);
295                    Future2Callback callback = new Future2Callback() {
296                        public void run(Future2 future) {
297                            try {
298                                Bundle bundle = future.getResult();
299                                bundle.keySet();
300                                Log.d(TAG, "account added: " + bundle);
301                            } catch (OperationCanceledException e) {
302                                Log.d(TAG, "addAccount was canceled");
303                            } catch (IOException e) {
304                                Log.d(TAG, "addAccount failed: " + e);
305                            } catch (AuthenticatorException e) {
306                                Log.d(TAG, "addAccount failed: " + e);
307                            }
308
309                        }
310                    };
311                    // Here's where we tell AccountManager about the new account.  The addAccount
312                    // method in AccountManager calls the addAccount method in our authenticator
313                    // service (EasAuthenticatorService)
314                    AccountManager.get(mContext).addAccount(Eas.ACCOUNT_MANAGER_TYPE, null, null,
315                            options, null, callback, null);
316                }
317            } catch (RemoteException e) {
318                throw new MessagingException("Call to validate generated an exception", e);
319            }
320        }
321
322        /**
323         * Typical helper function:  Return existence of a given folder
324         */
325        public boolean isFolderAvailable(final String folder) {
326            return sFolderMap.containsKey(folder);
327        }
328    }
329
330    public static class ExchangeFolder extends Folder {
331
332        private final ExchangeTransport mTransport;
333        @SuppressWarnings("unused")
334        private final ExchangeStore mStore;
335        @SuppressWarnings("unused")
336        private final String mName;
337
338        @SuppressWarnings("unused")
339        private PersistentDataCallbacks mPersistenceCallbacks;
340
341        public ExchangeFolder(ExchangeStore store, String name)
342                throws MessagingException {
343            mStore = store;
344            mTransport = store.getTransport();
345            mName = name;
346            if (!mTransport.isFolderAvailable(name)) {
347                throw new MessagingException("folder not supported: " + name);
348            }
349        }
350
351        @Override
352        public void appendMessages(Message[] messages) throws MessagingException {
353            // TODO Implement this function
354        }
355
356        @Override
357        public void close(boolean expunge) throws MessagingException {
358            mPersistenceCallbacks = null;
359            // TODO Implement this function
360        }
361
362        @Override
363        public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks)
364                throws MessagingException {
365            // TODO Implement this function
366        }
367
368        @Override
369        public boolean create(FolderType type) throws MessagingException {
370            // TODO Implement this function
371            return false;
372        }
373
374        @Override
375        public void delete(boolean recurse) throws MessagingException {
376            // TODO Implement this function
377        }
378
379        @Override
380        public boolean exists() throws MessagingException {
381            // TODO Implement this function
382            return false;
383        }
384
385        @Override
386        public Message[] expunge() throws MessagingException {
387            // TODO Implement this function
388            return null;
389        }
390
391        @Override
392        public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
393                throws MessagingException {
394            // TODO Implement this function
395        }
396
397        @Override
398        public Message getMessage(String uid) throws MessagingException {
399            // TODO Implement this function
400            return null;
401        }
402
403        @Override
404        public int getMessageCount() throws MessagingException {
405            // TODO Implement this function
406            return 0;
407        }
408
409        @Override
410        public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
411                throws MessagingException {
412            // TODO Implement this function
413            return null;
414        }
415
416        @Override
417        public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException {
418            // TODO Implement this function
419            return null;
420        }
421
422        @Override
423        public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
424                throws MessagingException {
425            // TODO Implement this function
426            return null;
427        }
428
429        @Override
430        public OpenMode getMode() throws MessagingException {
431            // TODO Implement this function
432            return null;
433        }
434
435        @Override
436        public String getName() {
437            // TODO Implement this function
438            return null;
439        }
440
441        @Override
442        public Flag[] getPermanentFlags() throws MessagingException {
443            // TODO Implement this function
444            return null;
445        }
446
447        @Override
448        public int getUnreadMessageCount() throws MessagingException {
449            // TODO Implement this function
450            return 0;
451        }
452
453        @Override
454        public boolean isOpen() {
455            // TODO Implement this function
456            return false;
457        }
458
459        @Override
460        public void open(OpenMode mode, PersistentDataCallbacks callbacks)
461                throws MessagingException {
462            mPersistenceCallbacks = callbacks;
463            // TODO Implement this function
464        }
465
466        @Override
467        public void setFlags(Message[] messages, Flag[] flags, boolean value)
468                throws MessagingException {
469            // TODO Implement this function
470        }
471
472
473    }
474
475
476}
477
478