• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.email.mail.store;
18 
19 import android.content.Context;
20 import android.os.Build;
21 import android.os.Bundle;
22 import android.telephony.TelephonyManager;
23 import android.text.TextUtils;
24 import android.util.Base64;
25 
26 import com.android.email.LegacyConversions;
27 import com.android.email.Preferences;
28 import com.android.email.mail.Store;
29 import com.android.email.mail.store.imap.ImapConstants;
30 import com.android.email.mail.store.imap.ImapResponse;
31 import com.android.email.mail.store.imap.ImapString;
32 import com.android.email.mail.transport.MailTransport;
33 import com.android.emailcommon.Logging;
34 import com.android.emailcommon.VendorPolicyLoader;
35 import com.android.emailcommon.internet.MimeMessage;
36 import com.android.emailcommon.mail.AuthenticationFailedException;
37 import com.android.emailcommon.mail.Flag;
38 import com.android.emailcommon.mail.Folder;
39 import com.android.emailcommon.mail.Message;
40 import com.android.emailcommon.mail.MessagingException;
41 import com.android.emailcommon.provider.Account;
42 import com.android.emailcommon.provider.HostAuth;
43 import com.android.emailcommon.provider.Mailbox;
44 import com.android.emailcommon.service.EmailServiceProxy;
45 import com.android.emailcommon.utility.Utility;
46 import com.android.mail.utils.LogUtils;
47 import com.beetstra.jutf7.CharsetProvider;
48 import com.google.common.annotations.VisibleForTesting;
49 
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.nio.ByteBuffer;
53 import java.nio.charset.Charset;
54 import java.security.MessageDigest;
55 import java.security.NoSuchAlgorithmException;
56 import java.util.Collection;
57 import java.util.HashMap;
58 import java.util.List;
59 import java.util.Set;
60 import java.util.concurrent.ConcurrentLinkedQueue;
61 import java.util.regex.Pattern;
62 
63 
64 /**
65  * <pre>
66  * TODO Need to start keeping track of UIDVALIDITY
67  * TODO Need a default response handler for things like folder updates
68  * TODO In fetch(), if we need a ImapMessage and were given
69  *      something else we can try to do a pre-fetch first.
70  * TODO Collect ALERT messages and show them to users.
71  *
72  * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
73  * certain information in a FETCH command, the server may return the requested
74  * information in any order, not necessarily in the order that it was requested.
75  * Further, the server may return the information in separate FETCH responses
76  * and may also return information that was not explicitly requested (to reflect
77  * to the client changes in the state of the subject message).
78  * </pre>
79  */
80 public class ImapStore extends Store {
81     /** Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. */
82     private static final Charset MODIFIED_UTF_7_CHARSET =
83             new CharsetProvider().charsetForName("X-RFC-3501");
84 
85     @VisibleForTesting static String sImapId = null;
86     @VisibleForTesting String mPathPrefix;
87     @VisibleForTesting String mPathSeparator;
88 
89     private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool =
90             new ConcurrentLinkedQueue<ImapConnection>();
91 
92     /**
93      * Static named constructor.
94      */
newInstance(Account account, Context context)95     public static Store newInstance(Account account, Context context) throws MessagingException {
96         return new ImapStore(context, account);
97     }
98 
99     /**
100      * Creates a new store for the given account. Always use
101      * {@link #newInstance(Account, Context)} to create an IMAP store.
102      */
ImapStore(Context context, Account account)103     private ImapStore(Context context, Account account) throws MessagingException {
104         mContext = context;
105         mAccount = account;
106 
107         HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
108         if (recvAuth == null) {
109             throw new MessagingException("No HostAuth in ImapStore?");
110         }
111         mTransport = new MailTransport(context, "IMAP", recvAuth);
112 
113         String[] userInfo = recvAuth.getLogin();
114         if (userInfo != null) {
115             mUsername = userInfo[0];
116             mPassword = userInfo[1];
117         } else {
118             mUsername = null;
119             mPassword = null;
120         }
121         mPathPrefix = recvAuth.mDomain;
122     }
123 
124     @VisibleForTesting
getConnectionPoolForTest()125     Collection<ImapConnection> getConnectionPoolForTest() {
126         return mConnectionPool;
127     }
128 
129     /**
130      * For testing only.  Injects a different root transport (it will be copied using
131      * newInstanceWithConfiguration() each time IMAP sets up a new channel).  The transport
132      * should already be set up and ready to use.  Do not use for real code.
133      * @param testTransport The Transport to inject and use for all future communication.
134      */
135     @VisibleForTesting
setTransportForTest(MailTransport testTransport)136     void setTransportForTest(MailTransport testTransport) {
137         mTransport = testTransport;
138     }
139 
140     /**
141      * Return, or create and return, an string suitable for use in an IMAP ID message.
142      * This is constructed similarly to the way the browser sets up its user-agent strings.
143      * See RFC 2971 for more details.  The output of this command will be a series of key-value
144      * pairs delimited by spaces (there is no point in returning a structured result because
145      * this will be sent as-is to the IMAP server).  No tokens, parenthesis or "ID" are included,
146      * because some connections may append additional values.
147      *
148      * The following IMAP ID keys may be included:
149      *   name                   Android package name of the program
150      *   os                     "android"
151      *   os-version             "version; model; build-id"
152      *   vendor                 Vendor of the client/server
153      *   x-android-device-model Model (only revealed if release build)
154      *   x-android-net-operator Mobile network operator (if known)
155      *   AGUID                  A device+account UID
156      *
157      * In addition, a vendor policy .apk can append key/value pairs.
158      *
159      * @param userName the username of the account
160      * @param host the host (server) of the account
161      * @param capabilities a list of the capabilities from the server
162      * @return a String for use in an IMAP ID message.
163      */
getImapId(Context context, String userName, String host, String capabilities)164     public static String getImapId(Context context, String userName, String host,
165             String capabilities) {
166         // The first section is global to all IMAP connections, and generates the fixed
167         // values in any IMAP ID message
168         synchronized (ImapStore.class) {
169             if (sImapId == null) {
170                 TelephonyManager tm =
171                         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
172                 String networkOperator = tm.getNetworkOperatorName();
173                 if (networkOperator == null) networkOperator = "";
174 
175                 sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE,
176                         Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER,
177                         networkOperator);
178             }
179         }
180 
181         // This section is per Store, and adds in a dynamic elements like UID's.
182         // We don't cache the result of this work, because the caller does anyway.
183         StringBuilder id = new StringBuilder(sImapId);
184 
185         // Optionally add any vendor-supplied id keys
186         String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host,
187                 capabilities);
188         if (vendorId != null) {
189             id.append(' ');
190             id.append(vendorId);
191         }
192 
193         // Generate a UID that mixes a "stable" device UID with the email address
194         try {
195             String devUID = Preferences.getPreferences(context).getDeviceUID();
196             MessageDigest messageDigest;
197             messageDigest = MessageDigest.getInstance("SHA-1");
198             messageDigest.update(userName.getBytes());
199             messageDigest.update(devUID.getBytes());
200             byte[] uid = messageDigest.digest();
201             String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP);
202             id.append(" \"AGUID\" \"");
203             id.append(hexUid);
204             id.append('\"');
205         } catch (NoSuchAlgorithmException e) {
206             LogUtils.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID");
207         }
208         return id.toString();
209     }
210 
211     /**
212      * Helper function that actually builds the static part of the IMAP ID string.  This is
213      * separated from getImapId for testability.  There is no escaping or encoding in IMAP ID so
214      * any rogue chars must be filtered here.
215      *
216      * @param packageName context.getPackageName()
217      * @param version Build.VERSION.RELEASE
218      * @param codeName Build.VERSION.CODENAME
219      * @param model Build.MODEL
220      * @param id Build.ID
221      * @param vendor Build.MANUFACTURER
222      * @param networkOperator TelephonyManager.getNetworkOperatorName()
223      * @return the static (never changes) portion of the IMAP ID
224      */
225     @VisibleForTesting
makeCommonImapId(String packageName, String version, String codeName, String model, String id, String vendor, String networkOperator)226     static String makeCommonImapId(String packageName, String version,
227             String codeName, String model, String id, String vendor, String networkOperator) {
228 
229         // Before building up IMAP ID string, pre-filter the input strings for "legal" chars
230         // This is using a fairly arbitrary char set intended to pass through most reasonable
231         // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space>
232         // The most important thing is *not* to pass parens, quotes, or CRLF, which would break
233         // the format of the IMAP ID list.
234         Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]");
235         packageName = p.matcher(packageName).replaceAll("");
236         version = p.matcher(version).replaceAll("");
237         codeName = p.matcher(codeName).replaceAll("");
238         model = p.matcher(model).replaceAll("");
239         id = p.matcher(id).replaceAll("");
240         vendor = p.matcher(vendor).replaceAll("");
241         networkOperator = p.matcher(networkOperator).replaceAll("");
242 
243         // "name" "com.android.email"
244         StringBuffer sb = new StringBuffer("\"name\" \"");
245         sb.append(packageName);
246         sb.append("\"");
247 
248         // "os" "android"
249         sb.append(" \"os\" \"android\"");
250 
251         // "os-version" "version; build-id"
252         sb.append(" \"os-version\" \"");
253         if (version.length() > 0) {
254             sb.append(version);
255         } else {
256             // default to "1.0"
257             sb.append("1.0");
258         }
259         // add the build ID or build #
260         if (id.length() > 0) {
261             sb.append("; ");
262             sb.append(id);
263         }
264         sb.append("\"");
265 
266         // "vendor" "the vendor"
267         if (vendor.length() > 0) {
268             sb.append(" \"vendor\" \"");
269             sb.append(vendor);
270             sb.append("\"");
271         }
272 
273         // "x-android-device-model" the device model (on release builds only)
274         if ("REL".equals(codeName)) {
275             if (model.length() > 0) {
276                 sb.append(" \"x-android-device-model\" \"");
277                 sb.append(model);
278                 sb.append("\"");
279             }
280         }
281 
282         // "x-android-mobile-net-operator" "name of network operator"
283         if (networkOperator.length() > 0) {
284             sb.append(" \"x-android-mobile-net-operator\" \"");
285             sb.append(networkOperator);
286             sb.append("\"");
287         }
288 
289         return sb.toString();
290     }
291 
292 
293     @Override
getFolder(String name)294     public Folder getFolder(String name) {
295         return new ImapFolder(this, name);
296     }
297 
298     /**
299      * Creates a mailbox hierarchy out of the flat data provided by the server.
300      */
301     @VisibleForTesting
createHierarchy(HashMap<String, ImapFolder> mailboxes)302     static void createHierarchy(HashMap<String, ImapFolder> mailboxes) {
303         Set<String> pathnames = mailboxes.keySet();
304         for (String path : pathnames) {
305             final ImapFolder folder = mailboxes.get(path);
306             final Mailbox mailbox = folder.mMailbox;
307             int delimiterIdx = mailbox.mServerId.lastIndexOf(mailbox.mDelimiter);
308             long parentKey = Mailbox.NO_MAILBOX;
309             String parentPath = null;
310             if (delimiterIdx != -1) {
311                 parentPath = path.substring(0, delimiterIdx);
312                 final ImapFolder parentFolder = mailboxes.get(parentPath);
313                 final Mailbox parentMailbox = (parentFolder == null) ? null : parentFolder.mMailbox;
314                 if (parentMailbox != null) {
315                     parentKey = parentMailbox.mId;
316                     parentMailbox.mFlags
317                             |= (Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE);
318                 }
319             }
320             mailbox.mParentKey = parentKey;
321             mailbox.mParentServerId = parentPath;
322         }
323     }
324 
325     /**
326      * Creates a {@link Folder} and associated {@link Mailbox}. If the folder does not already
327      * exist in the local database, a new row will immediately be created in the mailbox table.
328      * Otherwise, the existing row will be used. Any changes to existing rows, will not be stored
329      * to the database immediately.
330      * @param accountId The ID of the account the mailbox is to be associated with
331      * @param mailboxPath The path of the mailbox to add
332      * @param delimiter A path delimiter. May be {@code null} if there is no delimiter.
333      * @param selectable If {@code true}, the mailbox can be selected and used to store messages.
334      * @param mailbox If not null, mailbox is used instead of querying for the Mailbox.
335      */
addMailbox(Context context, long accountId, String mailboxPath, char delimiter, boolean selectable, Mailbox mailbox)336     private ImapFolder addMailbox(Context context, long accountId, String mailboxPath,
337             char delimiter, boolean selectable, Mailbox mailbox) {
338         ImapFolder folder = (ImapFolder) getFolder(mailboxPath);
339         if (mailbox == null) {
340             mailbox = Mailbox.getMailboxForPath(context, accountId, mailboxPath);
341         }
342         if (mailbox.isSaved()) {
343             // existing mailbox
344             // mailbox retrieved from database; save hash _before_ updating fields
345             folder.mHash = mailbox.getHashes();
346         }
347         updateMailbox(mailbox, accountId, mailboxPath, delimiter, selectable,
348                 LegacyConversions.inferMailboxTypeFromName(context, mailboxPath));
349         if (folder.mHash == null) {
350             // new mailbox
351             // save hash after updating. allows tracking changes if the mailbox is saved
352             // outside of #saveMailboxList()
353             folder.mHash = mailbox.getHashes();
354             // We must save this here to make sure we have a valid ID for later
355             mailbox.save(mContext);
356         }
357         folder.mMailbox = mailbox;
358         return folder;
359     }
360 
361     /**
362      * Persists the folders in the given list.
363      */
saveMailboxList(Context context, HashMap<String, ImapFolder> folderMap)364     private static void saveMailboxList(Context context, HashMap<String, ImapFolder> folderMap) {
365         for (ImapFolder imapFolder : folderMap.values()) {
366             imapFolder.save(context);
367         }
368     }
369 
370     @Override
updateFolders()371     public Folder[] updateFolders() throws MessagingException {
372         // TODO: There is nothing that ever closes this connection. Trouble is, it's not exactly
373         // clear when we should close it, we'd like to keep it open until we're really done
374         // using it.
375         ImapConnection connection = getConnection();
376         try {
377             HashMap<String, ImapFolder> mailboxes = new HashMap<String, ImapFolder>();
378             // Establish a connection to the IMAP server; if necessary
379             // This ensures a valid prefix if the prefix is automatically set by the server
380             connection.executeSimpleCommand(ImapConstants.NOOP);
381             String imapCommand = ImapConstants.LIST + " \"\" \"*\"";
382             if (mPathPrefix != null) {
383                 imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\"";
384             }
385             List<ImapResponse> responses = connection.executeSimpleCommand(imapCommand);
386             for (ImapResponse response : responses) {
387                 // S: * LIST (\Noselect) "/" ~/Mail/foo
388                 if (response.isDataResponse(0, ImapConstants.LIST)) {
389                     // Get folder name.
390                     ImapString encodedFolder = response.getStringOrEmpty(3);
391                     if (encodedFolder.isEmpty()) continue;
392 
393                     String folderName = decodeFolderName(encodedFolder.getString(), mPathPrefix);
394 
395                     if (ImapConstants.INBOX.equalsIgnoreCase(folderName)) continue;
396 
397                     // Parse attributes.
398                     boolean selectable =
399                         !response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT);
400                     String delimiter = response.getStringOrEmpty(2).getString();
401                     char delimiterChar = '\0';
402                     if (!TextUtils.isEmpty(delimiter)) {
403                         delimiterChar = delimiter.charAt(0);
404                     }
405                     ImapFolder folder = addMailbox(
406                             mContext, mAccount.mId, folderName, delimiterChar, selectable, null);
407                     mailboxes.put(folderName, folder);
408                 }
409             }
410 
411             // In order to properly map INBOX -> Inbox, handle it as a special case.
412             final Mailbox inbox =
413                     Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
414             final ImapFolder newFolder = addMailbox(
415                     mContext, mAccount.mId, inbox.mServerId, '\0', true /*selectable*/, inbox);
416             mailboxes.put(ImapConstants.INBOX, newFolder);
417 
418             createHierarchy(mailboxes);
419             saveMailboxList(mContext, mailboxes);
420             return mailboxes.values().toArray(new Folder[] {});
421         } catch (IOException ioe) {
422             connection.close();
423             throw new MessagingException("Unable to get folder list.", ioe);
424         } catch (AuthenticationFailedException afe) {
425             // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT
426             // commands to the server
427             connection.destroyResponses();
428             connection = null;
429             throw afe;
430         } finally {
431             if (connection != null) {
432                 poolConnection(connection);
433             }
434         }
435     }
436 
437     @Override
checkSettings()438     public Bundle checkSettings() throws MessagingException {
439         int result = MessagingException.NO_ERROR;
440         Bundle bundle = new Bundle();
441         ImapConnection connection = new ImapConnection(this, mUsername, mPassword);
442         try {
443             connection.open();
444             connection.close();
445         } catch (IOException ioe) {
446             bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage());
447             result = MessagingException.IOERROR;
448         } finally {
449             connection.destroyResponses();
450         }
451         bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
452         return bundle;
453     }
454 
455     /**
456      * Returns whether or not the prefix has been set by the user. This can be determined by
457      * the fact that the prefix is set, but, the path separator is not set.
458      */
isUserPrefixSet()459     boolean isUserPrefixSet() {
460         return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix);
461     }
462 
463     /** Sets the path separator */
setPathSeparator(String pathSeparator)464     void setPathSeparator(String pathSeparator) {
465         mPathSeparator = pathSeparator;
466     }
467 
468     /** Sets the prefix */
setPathPrefix(String pathPrefix)469     void setPathPrefix(String pathPrefix) {
470         mPathPrefix = pathPrefix;
471     }
472 
473     /** Gets the context for this store */
getContext()474     Context getContext() {
475         return mContext;
476     }
477 
478     /** Returns a clone of the transport associated with this store. */
cloneTransport()479     MailTransport cloneTransport() {
480         return mTransport.clone();
481     }
482 
483     /**
484      * Fixes the path prefix, if necessary. The path prefix must always end with the
485      * path separator.
486      */
ensurePrefixIsValid()487     void ensurePrefixIsValid() {
488         // Make sure the path prefix ends with the path separator
489         if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) {
490             if (!mPathPrefix.endsWith(mPathSeparator)) {
491                 mPathPrefix = mPathPrefix + mPathSeparator;
492             }
493         }
494     }
495 
496     /**
497      * Gets a connection if one is available from the pool, or creates a new one if not.
498      */
getConnection()499     ImapConnection getConnection() {
500         ImapConnection connection = null;
501         while ((connection = mConnectionPool.poll()) != null) {
502             try {
503                 connection.setStore(this, mUsername, mPassword);
504                 connection.executeSimpleCommand(ImapConstants.NOOP);
505                 break;
506             } catch (MessagingException e) {
507                 // Fall through
508             } catch (IOException e) {
509                 // Fall through
510             }
511             connection.close();
512             connection = null;
513         }
514         if (connection == null) {
515             connection = new ImapConnection(this, mUsername, mPassword);
516         }
517         return connection;
518     }
519 
520     /**
521      * Save a {@link ImapConnection} in the pool for reuse. Any responses associated with the
522      * connection are destroyed before adding the connection to the pool.
523      */
poolConnection(ImapConnection connection)524     void poolConnection(ImapConnection connection) {
525         if (connection != null) {
526             connection.destroyResponses();
527             mConnectionPool.add(connection);
528         }
529     }
530 
531     /**
532      * Prepends the folder name with the given prefix and UTF-7 encodes it.
533      */
encodeFolderName(String name, String prefix)534     static String encodeFolderName(String name, String prefix) {
535         // do NOT add the prefix to the special name "INBOX"
536         if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name;
537 
538         // Prepend prefix
539         if (prefix != null) {
540             name = prefix + name;
541         }
542 
543         // TODO bypass the conversion if name doesn't have special char.
544         ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name);
545         byte[] b = new byte[bb.limit()];
546         bb.get(b);
547 
548         return Utility.fromAscii(b);
549     }
550 
551     /**
552      * UTF-7 decodes the folder name and removes the given path prefix.
553      */
decodeFolderName(String name, String prefix)554     static String decodeFolderName(String name, String prefix) {
555         // TODO bypass the conversion if name doesn't have special char.
556         String folder;
557         folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString();
558         if ((prefix != null) && folder.startsWith(prefix)) {
559             folder = folder.substring(prefix.length());
560         }
561         return folder;
562     }
563 
564     /**
565      * Returns UIDs of Messages joined with "," as the separator.
566      */
joinMessageUids(Message[] messages)567     static String joinMessageUids(Message[] messages) {
568         StringBuilder sb = new StringBuilder();
569         boolean notFirst = false;
570         for (Message m : messages) {
571             if (notFirst) {
572                 sb.append(',');
573             }
574             sb.append(m.getUid());
575             notFirst = true;
576         }
577         return sb.toString();
578     }
579 
580     static class ImapMessage extends MimeMessage {
ImapMessage(String uid, ImapFolder folder)581         ImapMessage(String uid, ImapFolder folder) {
582             mUid = uid;
583             mFolder = folder;
584         }
585 
setSize(int size)586         public void setSize(int size) {
587             mSize = size;
588         }
589 
590         @Override
parse(InputStream in)591         public void parse(InputStream in) throws IOException, MessagingException {
592             super.parse(in);
593         }
594 
setFlagInternal(Flag flag, boolean set)595         public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
596             super.setFlag(flag, set);
597         }
598 
599         @Override
setFlag(Flag flag, boolean set)600         public void setFlag(Flag flag, boolean set) throws MessagingException {
601             super.setFlag(flag, set);
602             mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
603         }
604     }
605 
606     static class ImapException extends MessagingException {
607         private static final long serialVersionUID = 1L;
608 
609         String mAlertText;
610 
ImapException(String message, String alertText, Throwable throwable)611         public ImapException(String message, String alertText, Throwable throwable) {
612             super(message, throwable);
613             mAlertText = alertText;
614         }
615 
ImapException(String message, String alertText)616         public ImapException(String message, String alertText) {
617             super(message);
618             mAlertText = alertText;
619         }
620 
getAlertText()621         public String getAlertText() {
622             return mAlertText;
623         }
624 
setAlertText(String alertText)625         public void setAlertText(String alertText) {
626             mAlertText = alertText;
627         }
628     }
629 }
630