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 
17 package com.android.emailcommon.service;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.os.Bundle;
22 import android.os.IBinder;
23 import android.os.RemoteException;
24 
25 import com.android.emailcommon.Device;
26 import com.android.emailcommon.TempDirectory;
27 import com.android.emailcommon.mail.MessagingException;
28 import com.android.emailcommon.provider.HostAuth;
29 import com.android.emailcommon.provider.Policy;
30 import com.android.mail.utils.LogUtils;
31 
32 import java.io.IOException;
33 
34 /**
35  * The EmailServiceProxy class provides a simple interface for the UI to call into the various
36  * EmailService classes (e.g. EasService for EAS).  It wraps the service connect/disconnect
37  * process so that the caller need not be concerned with it.
38  *
39  * Use the class like this:
40  *   new EmailServiceProxy(context, class).loadAttachment(attachmentId, callback)
41  *
42  * Methods without a return value return immediately (i.e. are asynchronous); methods with a
43  * return value wait for a result from the Service (i.e. they should not be called from the UI
44  * thread) with a default timeout of 30 seconds (settable)
45  *
46  * An EmailServiceProxy object cannot be reused (trying to do so generates a RemoteException)
47  */
48 
49 public class EmailServiceProxy extends ServiceProxy implements IEmailService {
50     private static final String TAG = "EmailServiceProxy";
51 
52     public static final String AUTO_DISCOVER_BUNDLE_ERROR_CODE = "autodiscover_error_code";
53     public static final String AUTO_DISCOVER_BUNDLE_HOST_AUTH = "autodiscover_host_auth";
54 
55     public static final String VALIDATE_BUNDLE_RESULT_CODE = "validate_result_code";
56     public static final String VALIDATE_BUNDLE_POLICY_SET = "validate_policy_set";
57     public static final String VALIDATE_BUNDLE_ERROR_MESSAGE = "validate_error_message";
58     public static final String VALIDATE_BUNDLE_UNSUPPORTED_POLICIES =
59         "validate_unsupported_policies";
60     public static final String VALIDATE_BUNDLE_PROTOCOL_VERSION = "validate_protocol_version";
61     public static final String VALIDATE_BUNDLE_REDIRECT_ADDRESS = "validate_redirect_address";
62 
63     private Object mReturn = null;
64     private IEmailService mService;
65     private final boolean isRemote;
66 
67     // Standard debugging
68     public static final int DEBUG_BIT = 0x01;
69     // Verbose (parser) logging
70     public static final int DEBUG_EXCHANGE_BIT = 0x02;
71     // File (SD card) logging
72     public static final int DEBUG_FILE_BIT = 0x04;
73     // Enable strict mode
74     public static final int DEBUG_ENABLE_STRICT_MODE = 0x08;
75 
76     // The first two constructors are used with local services that can be referenced by class
EmailServiceProxy(Context _context, Class<?> _class)77     public EmailServiceProxy(Context _context, Class<?> _class) {
78         super(_context, new Intent(_context, _class));
79         TempDirectory.setTempDirectory(_context);
80         isRemote = false;
81     }
82 
83     // The following two constructors are used with remote services that must be referenced by
84     // a known action or by a prebuilt intent
EmailServiceProxy(Context _context, Intent _intent)85     public EmailServiceProxy(Context _context, Intent _intent) {
86         super(_context, _intent);
87         try {
88             Device.getDeviceId(_context);
89         } catch (IOException e) {
90         }
91         TempDirectory.setTempDirectory(_context);
92         isRemote = true;
93     }
94 
95     @Override
onConnected(IBinder binder)96     public void onConnected(IBinder binder) {
97         mService = IEmailService.Stub.asInterface(binder);
98     }
99 
isRemote()100     public boolean isRemote() {
101         return isRemote;
102     }
103 
104     /**
105      * Request an attachment to be loaded; the service MUST give higher priority to
106      * non-background loading.  The service MUST use the loadAttachmentStatus callback when
107      * loading has started and stopped and SHOULD send callbacks with progress information if
108      * possible.
109      *
110      * @param cb The {@link IEmailServiceCallback} to use for this operation.
111      * @param accountId the id of the account in question
112      * @param attachmentId the id of the attachment record
113      * @param background whether or not this request corresponds to a background action (i.e.
114      * prefetch) vs a foreground action (user request)
115      */
116     @Override
loadAttachment(final IEmailServiceCallback cb, final long accountId, final long attachmentId, final boolean background)117     public void loadAttachment(final IEmailServiceCallback cb, final long accountId,
118             final long attachmentId, final boolean background)
119             throws RemoteException {
120         setTask(new ProxyTask() {
121             @Override
122             public void run() throws RemoteException {
123                 try {
124                     mService.loadAttachment(cb, accountId, attachmentId, background);
125                 } catch (RemoteException e) {
126                     try {
127                         // Try to send a callback (if set)
128                         if (cb != null) {
129                             cb.loadAttachmentStatus(-1, attachmentId,
130                                     EmailServiceStatus.REMOTE_EXCEPTION, 0);
131                         }
132                     } catch (RemoteException e1) {
133                     }
134                 }
135             }
136         }, "loadAttachment");
137     }
138 
139     /**
140      * Validate a user account, given a protocol, host address, port, ssl status, and credentials.
141      * The result of this call is returned in a Bundle which MUST include a result code and MAY
142      * include a PolicySet that is required by the account. A successful validation implies a host
143      * address that serves the specified protocol and credentials sufficient to be authorized
144      * by the server to do so.
145      *
146      * @param hostAuthCom the hostAuthCom object to validate
147      * @return a Bundle as described above
148      */
149     @Override
validate(final HostAuthCompat hostAuthCom)150     public Bundle validate(final HostAuthCompat hostAuthCom) throws RemoteException {
151         setTask(new ProxyTask() {
152             @Override
153             public void run() throws RemoteException{
154                 mReturn = mService.validate(hostAuthCom);
155             }
156         }, "validate");
157         waitForCompletion();
158         if (mReturn == null) {
159             Bundle bundle = new Bundle();
160             bundle.putInt(VALIDATE_BUNDLE_RESULT_CODE, MessagingException.UNSPECIFIED_EXCEPTION);
161             return bundle;
162         } else {
163             Bundle bundle = (Bundle) mReturn;
164             bundle.setClassLoader(Policy.class.getClassLoader());
165             LogUtils.v(TAG, "validate returns " + bundle.getInt(VALIDATE_BUNDLE_RESULT_CODE));
166             return bundle;
167         }
168     }
169 
170     /**
171      * Attempt to determine a user's host address and credentials from an email address and
172      * password. The result is returned in a Bundle which MUST include an error code and MAY (on
173      * success) include a HostAuth record sufficient to enable the service to validate the user's
174      * account.
175      *
176      * @param userName the user's email address
177      * @param password the user's password
178      * @return a Bundle as described above
179      */
180     @Override
autoDiscover(final String userName, final String password)181     public Bundle autoDiscover(final String userName, final String password)
182             throws RemoteException {
183         setTask(new ProxyTask() {
184             @Override
185             public void run() throws RemoteException{
186                 mReturn = mService.autoDiscover(userName, password);
187             }
188         }, "autoDiscover");
189         waitForCompletion();
190         if (mReturn == null) {
191             return null;
192         } else {
193             Bundle bundle = (Bundle) mReturn;
194             bundle.setClassLoader(HostAuth.class.getClassLoader());
195             LogUtils.v(TAG, "autoDiscover returns "
196                     + bundle.getInt(AUTO_DISCOVER_BUNDLE_ERROR_CODE));
197             return bundle;
198         }
199     }
200 
201     /**
202      * Request that the service reload the folder list for the specified account. The service
203      * MUST use the syncMailboxListStatus callback to indicate "starting" and "finished"
204      *
205      * @param accountId the id of the account whose folder list is to be updated
206      */
207     @Override
updateFolderList(final long accountId)208     public void updateFolderList(final long accountId) throws RemoteException {
209         setTask(new ProxyTask() {
210             @Override
211             public void run() throws RemoteException {
212                 mService.updateFolderList(accountId);
213             }
214         }, "updateFolderList");
215     }
216 
217     /**
218      * Specify the debug flags selected by the user.  The service SHOULD log debug information as
219      * requested.
220      *
221      * @param flags an integer whose bits represent logging flags as defined in DEBUG_* flags above
222      */
223     @Override
setLogging(final int flags)224     public void setLogging(final int flags) throws RemoteException {
225         setTask(new ProxyTask() {
226             @Override
227             public void run() throws RemoteException {
228                 mService.setLogging(flags);
229             }
230         }, "setLogging");
231     }
232 
233     /**
234      * Send a meeting response for the specified message
235      *
236      * @param messageId the id of the message containing the meeting request
237      * @param response the response code, as defined in EmailServiceConstants
238      */
239     @Override
sendMeetingResponse(final long messageId, final int response)240     public void sendMeetingResponse(final long messageId, final int response)
241             throws RemoteException {
242         setTask(new ProxyTask() {
243             @Override
244             public void run() throws RemoteException {
245                 mService.sendMeetingResponse(messageId, response);
246             }
247         }, "sendMeetingResponse");
248     }
249 
250     /**
251      * Request the service to delete the account's PIM (personal information management) data. This
252      * data includes any data that is 1) associated with the account and 2) created/stored by the
253      * service or its sync adapters and 3) not stored in the EmailProvider database (e.g. contact
254      * and calendar information).
255      *
256      * @param emailAddress the email address for the account whose data should be deleted
257      */
258     @Override
deleteExternalAccountPIMData(final String emailAddress)259     public void deleteExternalAccountPIMData(final String emailAddress) throws RemoteException {
260         setTask(new ProxyTask() {
261             @Override
262             public void run() throws RemoteException {
263                 mService.deleteExternalAccountPIMData(emailAddress);
264             }
265         }, "deleteAccountPIMData");
266         // This can be called when deleting accounts. After making this call, the caller will
267         // ask for account reconciliation, which will kill the processes. We wait for completion
268         // to avoid the race.
269         waitForCompletion();
270     }
271 
272     /**
273      * Search for messages given a query string.  The string is interpreted as the logical AND of
274      * terms separated by white space.  The search is performed on the specified mailbox in the
275      * specified account (including subfolders, as specified by the includeSubfolders parameter).
276      * At most numResults messages matching the query term(s) will be added to the mailbox specified
277      * as destMailboxId. If mailboxId is -1, the entire account will be searched. If firstResult is
278      * specified and non-zero, results will be added starting with the firstResult'th match (i.e.
279      * for the continuation of a previous search)
280      *
281      * @param accountId the id of the account to be searched
282      * @param searchParams the search specification
283      * @param destMailboxId the id of the mailbox into which search results are appended
284      * @return the total number of matches for this search (regardless of how many were requested)
285      */
286     @Override
searchMessages(final long accountId, final SearchParams searchParams, final long destMailboxId)287     public int searchMessages(final long accountId, final SearchParams searchParams,
288             final long destMailboxId) throws RemoteException {
289         setTask(new ProxyTask() {
290             @Override
291             public void run() throws RemoteException{
292                 mReturn = mService.searchMessages(accountId, searchParams, destMailboxId);
293             }
294         }, "searchMessages");
295         waitForCompletion();
296         if (mReturn == null) {
297             return 0;
298         } else {
299             return (Integer) mReturn;
300         }
301     }
302 
303     /**
304      * Request the service to send mail in the specified account's Outbox
305      *
306      * @param accountId the account whose outgoing mail should be sent.
307      */
308     @Override
sendMail(final long accountId)309     public void sendMail(final long accountId) throws RemoteException {
310         setTask(new ProxyTask() {
311             @Override
312             public void run() throws RemoteException{
313                 mService.sendMail(accountId);
314             }
315         }, "sendMail");
316     }
317 
318     /**
319      * Request the service to refresh its push notification status (e.g. to start or stop receiving
320      * them, or to change which folders we want notifications for).
321      * @param accountId The account whose push settings to modify.
322      */
323     @Override
pushModify(final long accountId)324     public void pushModify(final long accountId) throws RemoteException {
325         setTask(new ProxyTask() {
326             @Override
327             public void run() throws RemoteException{
328                 mService.pushModify(accountId);
329             }
330         }, "pushModify");
331     }
332 
333     @Override
sync(final long accountId, final Bundle syncExtras)334     public int sync(final long accountId, final Bundle syncExtras) {
335         setTask(new ProxyTask() {
336             @Override
337             public void run() throws RemoteException{
338                 mReturn = mService.sync(accountId, syncExtras);
339             }
340         }, "sync");
341         waitForCompletion();
342         if (mReturn == null) {
343             // This occurs if sync times out.
344             // TODO: Sync may take a long time, maybe we should extend the timeout here.
345             return EmailServiceStatus.IO_ERROR;
346         } else {
347             return (Integer)mReturn;
348         }
349     }
350 
351     @Override
asBinder()352     public IBinder asBinder() {
353         return null;
354     }
355 
getApiVersion()356     public int getApiVersion() {
357         setTask(new ProxyTask() {
358             @Override
359             public void run() throws RemoteException{
360                 mReturn = mService.getApiVersion();
361             }
362         }, "getApiVersion");
363         waitForCompletion();
364         if (mReturn == null) {
365             // This occurs if there is a timeout or remote exception. Is not expected to happen.
366             LogUtils.wtf(TAG, "failed to get api version");
367             return -1;
368         } else {
369             return (Integer) mReturn;
370         }
371     }
372 }
373