Store.java revision f5418f1f93b02e7fab9f15eb201800b65510998e
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<Long, Store> sStores = new HashMap<Long, 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 id of the 171 * account's 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 recvAuth = account.getOrCreateHostAuthRecv(context); 182 long storeKey = recvAuth.mId; 183 Store store = sStores.get(storeKey); 184 if (store == null) { 185 Context appContext = context.getApplicationContext(); 186 StoreInfo info = StoreInfo.getStoreInfo(recvAuth.mProtocol, context); 187 if (info != null) { 188 store = instantiateStore(info.mClassName, account, appContext, callbacks); 189 } 190 // Don't cache this unless it's we've got a saved HostAUth 191 if (store != null && (storeKey != EmailContent.NOT_SAVED)) { 192 sStores.put(storeKey, store); 193 } 194 } else { 195 // update the callbacks, which may have been null at creation time. 196 store.setPersistentDataCallbacks(callbacks); 197 } 198 199 if (store == null) { 200 throw new MessagingException("Cannot find store for account " + account.mDisplayName); 201 } 202 203 return store; 204 } 205 206 /** 207 * Delete the mail store associated with the given account. The account must be valid (i.e. has 208 * at least an incoming server name). 209 * 210 * The store should have been notified already by calling delete(), and the caller should 211 * also take responsibility for deleting the matching LocalStore, etc. 212 * 213 * @throws MessagingException If the store cannot be removed or if the account is invalid. 214 */ 215 public synchronized static Store removeInstance(Account account, Context context) 216 throws MessagingException { 217 return sStores.remove(account.mId); 218 } 219 220 /** 221 * Get class of SettingActivity for this Store class. 222 * @return Activity class that has class method actionEditIncomingSettings(). 223 */ 224 public Class<? extends android.app.Activity> getSettingActivityClass() { 225 // default SettingActivity class 226 return com.android.email.activity.setup.AccountSetupIncoming.class; 227 } 228 229 /** 230 * Some protocols require that a sent message be copied (uploaded) into the Sent folder 231 * while others can take care of it automatically (ideally, on the server). This function 232 * allows a given store to indicate which mode(s) it supports. 233 * @return true if the store requires an upload into "sent", false if this happens automatically 234 * for any sent message. 235 */ 236 public boolean requireCopyMessageToSentFolder() { 237 return true; 238 } 239 240 public abstract Folder getFolder(String name) throws MessagingException; 241 242 /** 243 * Updates the local list of mailboxes according to what is located on the remote server. 244 * <em>Note: This does not perform folder synchronization and it will not remove mailboxes 245 * that are stored locally but not remotely.</em> 246 * @return The set of remote folders 247 * @throws MessagingException If there was a problem connecting to the remote server 248 */ 249 public abstract Folder[] updateFolders() throws MessagingException; 250 251 public abstract Bundle checkSettings() throws MessagingException; 252 253 /** 254 * Delete Store and its corresponding resources. 255 * @throws MessagingException 256 */ 257 public void delete() throws MessagingException { 258 } 259 260 /** 261 * If a Store intends to implement callbacks, it should be prepared to update them 262 * via overriding this method. They may not be available at creation time (in which case they 263 * will be passed in as null. 264 * @param callbacks The updated provider of store callbacks 265 */ 266 protected void setPersistentDataCallbacks(PersistentDataCallbacks callbacks) { 267 } 268 269 /** 270 * Callback interface by which a Store can read and write persistent data. 271 * TODO This needs to be made more generic & flexible 272 */ 273 public interface PersistentDataCallbacks { 274 275 /** 276 * Provides a small place for Stores to store persistent data. 277 * @param key identifier for the data (e.g. "sync.key" or "folder.id") 278 * @param value The data to persist. All data must be encoded into a string, 279 * so use base64 or some other encoding if necessary. 280 */ 281 public void setPersistentString(String key, String value); 282 283 /** 284 * @param key identifier for the data (e.g. "sync.key" or "folder.id") 285 * @param defaultValue The data to return if no data was ever saved for this store 286 * @return the data saved by the Store, or null if never set. 287 */ 288 public String getPersistentString(String key, String defaultValue); 289 } 290 291 /** 292 * Handle discovery of account settings using only the user's email address and password 293 * @param context the context of the caller 294 * @param emailAddress the email address of the exchange user 295 * @param password the password of the exchange user 296 * @return a Bundle containing an error code and a HostAuth (if successful) 297 * @throws MessagingException 298 */ 299 public Bundle autoDiscover(Context context, String emailAddress, String password) 300 throws MessagingException { 301 return null; 302 } 303 304 /** 305 * Returns a {@link Mailbox} for the given path. If the path is not in the database, a new 306 * mailbox will be created. 307 */ 308 protected static Mailbox getMailboxForPath(Context context, long accountId, String path) { 309 Mailbox mailbox = Mailbox.restoreMailboxForPath(context, accountId, path); 310 if (mailbox == null) { 311 mailbox = new Mailbox(); 312 } 313 return mailbox; 314 } 315 316 /** 317 * Updates the fields within the given mailbox. Only the fields that are important to 318 * non-EAS accounts are modified. 319 */ 320 protected static void updateMailbox(Mailbox mailbox, long accountId, String mailboxPath, 321 char delimiter, boolean selectable, int type) { 322 mailbox.mAccountKey = accountId; 323 mailbox.mDelimiter = delimiter; 324 String displayPath = mailboxPath; 325 int pathIndex = mailboxPath.lastIndexOf(delimiter); 326 if (pathIndex > 0) { 327 displayPath = mailboxPath.substring(pathIndex + 1); 328 } 329 mailbox.mDisplayName = displayPath; 330 if (selectable) { 331 mailbox.mFlags = Mailbox.FLAG_HOLDS_MAIL | Mailbox.FLAG_ACCEPTS_MOVED_MAIL; 332 } 333 mailbox.mFlagVisible = true; 334 //mailbox.mParentKey; 335 //mailbox.mParentServerId; 336 mailbox.mServerId = mailboxPath; 337 //mailbox.mServerId; 338 //mailbox.mSyncFrequency; 339 //mailbox.mSyncKey; 340 //mailbox.mSyncLookback; 341 //mailbox.mSyncTime; 342 mailbox.mType = type; 343 //box.mUnreadCount; 344 mailbox.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT; 345 } 346} 347