Store.java revision f65bdbdaf5960951452b148f801c51feca864bfe
1/* 2 * Copyright (C) 2008 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; 18 19import android.content.Context; 20import android.content.res.XmlResourceParser; 21import android.os.Bundle; 22import android.util.Log; 23 24import com.android.email.Email; 25import com.android.email.R; 26import com.android.emailcommon.Logging; 27import com.android.emailcommon.mail.Folder; 28import com.android.emailcommon.mail.MessagingException; 29import com.android.emailcommon.provider.Account; 30import com.android.emailcommon.provider.EmailContent; 31import com.android.emailcommon.provider.HostAuth; 32import com.android.emailcommon.provider.Mailbox; 33import com.google.common.annotations.VisibleForTesting; 34 35import org.xmlpull.v1.XmlPullParserException; 36 37import java.io.IOException; 38import java.util.HashMap; 39 40/** 41 * Store is the access point for an email message store. It's location can be 42 * local or remote and no specific protocol is defined. Store is intended to 43 * loosely model in combination the JavaMail classes javax.mail.Store and 44 * javax.mail.Folder along with some additional functionality to improve 45 * performance on mobile devices. Implementations of this class should focus on 46 * making as few network connections as possible. 47 */ 48public abstract class Store { 49 50 /** 51 * String constants for known store schemes. 52 */ 53 public static final String STORE_SCHEME_IMAP = HostAuth.SCHEME_IMAP; 54 public static final String STORE_SCHEME_POP3 = HostAuth.SCHEME_POP3; 55 public static final String STORE_SCHEME_EAS = HostAuth.SCHEME_EAS; 56 public static final String STORE_SCHEME_LOCAL = "local"; 57 58 public static final String STORE_SECURITY_SSL = "+ssl"; 59 public static final String STORE_SECURITY_TLS = "+tls"; 60 public static final String STORE_SECURITY_TRUST_CERTIFICATES = "+trustallcerts"; 61 62 /** 63 * A global suggestion to Store implementors on how much of the body 64 * should be returned on FetchProfile.Item.BODY_SANE requests. 65 */ 66 public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (50 * 1024); 67 68 @VisibleForTesting 69 static final HashMap<HostAuth, Store> sStores = new HashMap<HostAuth, Store>(); 70 71 protected Context mContext; 72 protected Account mAccount; 73 protected Transport mTransport; 74 protected String mUsername; 75 protected String mPassword; 76 77 /** 78 * Static named constructor. It should be overrode by extending class. 79 * Because this method will be called through reflection, it can not be protected. 80 */ 81 public static Store newInstance(Account account, Context context, 82 PersistentDataCallbacks callbacks) throws MessagingException { 83 throw new MessagingException("Store#newInstance: Unknown scheme in " 84 + account.mDisplayName); 85 } 86 87 private static Store instantiateStore(String className, Account account, Context context, 88 PersistentDataCallbacks callbacks) 89 throws MessagingException { 90 Object o = null; 91 try { 92 Class<?> c = Class.forName(className); 93 // and invoke "newInstance" class method and instantiate store object. 94 java.lang.reflect.Method m = 95 c.getMethod("newInstance", Account.class, Context.class, 96 PersistentDataCallbacks.class); 97 // TODO Do the stores _really need a context? Is there a way to not pass it along? 98 o = m.invoke(null, account, context, callbacks); 99 } catch (Exception e) { 100 Log.d(Logging.LOG_TAG, String.format( 101 "exception %s invoking method %s#newInstance(Account, Context) for %s", 102 e.toString(), className, account.mDisplayName)); 103 throw new MessagingException("can not instantiate Store for " + account.mDisplayName); 104 } 105 if (!(o instanceof Store)) { 106 throw new MessagingException( 107 account.mDisplayName + ": " + className + " create incompatible object"); 108 } 109 return (Store) o; 110 } 111 112 /** 113 * Look up descriptive information about a particular type of store. 114 */ 115 public static class StoreInfo { 116 public String mScheme; 117 public String mClassName; 118 public boolean mPushSupported = false; 119 public int mVisibleLimitDefault; 120 public int mVisibleLimitIncrement; 121 public int mAccountInstanceLimit; 122 123 // TODO cache result for performance - silly to keep reading the XML 124 public static StoreInfo getStoreInfo(String scheme, Context context) { 125 StoreInfo result = getStoreInfo(R.xml.stores_product, scheme, context); 126 if (result == null) { 127 result = getStoreInfo(R.xml.stores, scheme, context); 128 } 129 return result; 130 } 131 132 public static StoreInfo getStoreInfo(int resourceId, String scheme, Context context) { 133 try { 134 XmlResourceParser xml = context.getResources().getXml(resourceId); 135 int xmlEventType; 136 // walk through stores.xml file. 137 while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { 138 if (xmlEventType == XmlResourceParser.START_TAG && 139 "store".equals(xml.getName())) { 140 String xmlScheme = xml.getAttributeValue(null, "scheme"); 141 if (scheme != null && scheme.startsWith(xmlScheme)) { 142 StoreInfo result = new StoreInfo(); 143 result.mScheme = xmlScheme; 144 result.mClassName = xml.getAttributeValue(null, "class"); 145 result.mPushSupported = xml.getAttributeBooleanValue( 146 null, "push", false); 147 result.mVisibleLimitDefault = xml.getAttributeIntValue( 148 null, "visibleLimitDefault", Email.VISIBLE_LIMIT_DEFAULT); 149 result.mVisibleLimitIncrement = xml.getAttributeIntValue( 150 null, "visibleLimitIncrement", Email.VISIBLE_LIMIT_INCREMENT); 151 result.mAccountInstanceLimit = xml.getAttributeIntValue( 152 null, "accountInstanceLimit", -1); 153 return result; 154 } 155 } 156 } 157 } catch (XmlPullParserException e) { 158 // ignore 159 } catch (IOException e) { 160 // ignore 161 } 162 return null; 163 } 164 } 165 166 /** 167 * Get an instance of a mail store for the given account. The account must be valid (i.e. has 168 * at least an incoming server name). 169 * 170 * NOTE: The internal algorithm used to find a cached store depends upon the account's 171 * HostAuth row. If this ever changes (e.g. such as the user updating the 172 * host name or port), we will leak entries. This should not be typical, so, it is not 173 * a critical problem. However, it is something we should consider fixing. 174 * 175 * @param account The account of the store. 176 * @return an initialized store of the appropriate class 177 * @throws MessagingException If the store cannot be obtained or if the account is invalid. 178 */ 179 public synchronized static Store getInstance(Account account, Context context, 180 PersistentDataCallbacks callbacks) throws MessagingException { 181 HostAuth hostAuth = account.getOrCreateHostAuthRecv(context); 182 Store store = sStores.get(hostAuth); 183 if (store == null) { 184 Context appContext = context.getApplicationContext(); 185 StoreInfo info = StoreInfo.getStoreInfo(hostAuth.mProtocol, context); 186 if (info != null) { 187 store = instantiateStore(info.mClassName, account, appContext, callbacks); 188 } 189 // Don't cache this unless it's we've got a saved HostAUth 190 if (store != null && (hostAuth.mId != EmailContent.NOT_SAVED)) { 191 sStores.put(hostAuth, store); 192 } 193 } else { 194 // update the callbacks, which may have been null at creation time. 195 store.setPersistentDataCallbacks(callbacks); 196 } 197 198 if (store == null) { 199 throw new MessagingException("Cannot find store for account " + account.mDisplayName); 200 } 201 202 return store; 203 } 204 205 /** 206 * Delete the mail store associated with the given account. The account must be valid (i.e. has 207 * at least an incoming server name). 208 * 209 * The store should have been notified already by calling delete(), and the caller should 210 * also take responsibility for deleting the matching LocalStore, etc. 211 * 212 * @throws MessagingException If the store cannot be removed or if the account is invalid. 213 */ 214 public synchronized static Store removeInstance(Account account, Context context) 215 throws MessagingException { 216 return sStores.remove(HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv)); 217 } 218 219 /** 220 * Get class of SettingActivity for this Store class. 221 * @return Activity class that has class method actionEditIncomingSettings(). 222 */ 223 public Class<? extends android.app.Activity> getSettingActivityClass() { 224 // default SettingActivity class 225 return com.android.email.activity.setup.AccountSetupIncoming.class; 226 } 227 228 /** 229 * Some protocols require that a sent message be copied (uploaded) into the Sent folder 230 * while others can take care of it automatically (ideally, on the server). This function 231 * allows a given store to indicate which mode(s) it supports. 232 * @return true if the store requires an upload into "sent", false if this happens automatically 233 * for any sent message. 234 */ 235 public boolean requireCopyMessageToSentFolder() { 236 return true; 237 } 238 239 public abstract Folder getFolder(String name) throws MessagingException; 240 241 /** 242 * Updates the local list of mailboxes according to what is located on the remote server. 243 * <em>Note: This does not perform folder synchronization and it will not remove mailboxes 244 * that are stored locally but not remotely.</em> 245 * @return The set of remote folders 246 * @throws MessagingException If there was a problem connecting to the remote server 247 */ 248 public abstract Folder[] updateFolders() throws MessagingException; 249 250 public abstract Bundle checkSettings() throws MessagingException; 251 252 /** 253 * Delete Store and its corresponding resources. 254 * @throws MessagingException 255 */ 256 public void delete() throws MessagingException { 257 } 258 259 /** 260 * If a Store intends to implement callbacks, it should be prepared to update them 261 * via overriding this method. They may not be available at creation time (in which case they 262 * will be passed in as null. 263 * @param callbacks The updated provider of store callbacks 264 */ 265 protected void setPersistentDataCallbacks(PersistentDataCallbacks callbacks) { 266 } 267 268 /** 269 * Callback interface by which a Store can read and write persistent data. 270 * TODO This needs to be made more generic & flexible 271 */ 272 public interface PersistentDataCallbacks { 273 274 /** 275 * Provides a small place for Stores to store persistent data. 276 * @param key identifier for the data (e.g. "sync.key" or "folder.id") 277 * @param value The data to persist. All data must be encoded into a string, 278 * so use base64 or some other encoding if necessary. 279 */ 280 public void setPersistentString(String key, String value); 281 282 /** 283 * @param key identifier for the data (e.g. "sync.key" or "folder.id") 284 * @param defaultValue The data to return if no data was ever saved for this store 285 * @return the data saved by the Store, or null if never set. 286 */ 287 public String getPersistentString(String key, String defaultValue); 288 } 289 290 /** 291 * Handle discovery of account settings using only the user's email address and password 292 * @param context the context of the caller 293 * @param emailAddress the email address of the exchange user 294 * @param password the password of the exchange user 295 * @return a Bundle containing an error code and a HostAuth (if successful) 296 * @throws MessagingException 297 */ 298 public Bundle autoDiscover(Context context, String emailAddress, String password) 299 throws MessagingException { 300 return null; 301 } 302 303 /** 304 * Returns a {@link Mailbox} for the given path. If the path is not in the database, a new 305 * mailbox will be created. 306 */ 307 protected static Mailbox getMailboxForPath(Context context, long accountId, String path) { 308 Mailbox mailbox = Mailbox.restoreMailboxForPath(context, accountId, path); 309 if (mailbox == null) { 310 mailbox = new Mailbox(); 311 } 312 return mailbox; 313 } 314 315 /** 316 * Updates the fields within the given mailbox. Only the fields that are important to 317 * non-EAS accounts are modified. 318 */ 319 protected static void updateMailbox(Mailbox mailbox, long accountId, String mailboxPath, 320 char delimiter, boolean selectable, int type) { 321 mailbox.mAccountKey = accountId; 322 mailbox.mDelimiter = delimiter; 323 String displayPath = mailboxPath; 324 int pathIndex = mailboxPath.lastIndexOf(delimiter); 325 if (pathIndex > 0) { 326 displayPath = mailboxPath.substring(pathIndex + 1); 327 } 328 mailbox.mDisplayName = displayPath; 329 if (selectable) { 330 mailbox.mFlags = Mailbox.FLAG_HOLDS_MAIL | Mailbox.FLAG_ACCEPTS_MOVED_MAIL; 331 } 332 mailbox.mFlagVisible = true; 333 //mailbox.mParentKey; 334 //mailbox.mParentServerId; 335 mailbox.mServerId = mailboxPath; 336 //mailbox.mServerId; 337 //mailbox.mSyncFrequency; 338 //mailbox.mSyncKey; 339 //mailbox.mSyncLookback; 340 //mailbox.mSyncTime; 341 mailbox.mType = type; 342 //box.mUnreadCount; 343 mailbox.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT; 344 } 345} 346