source: trunk/LogicMail/src/org/logicprobe/LogicMail/ui/CompositionScreen.java @ 962

Revision 962, 47.0 KB checked in by octorian, 6 months ago (diff)

Upgraded to the 0.93 beta version of the analytics SDK, and added screen tracking (refs #343)

Line 
1/*-
2 * Copyright (c) 2008, Derek Konigsberg
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the project nor the names of its
15 *    contributors may be used to endorse or promote products derived
16 *    from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29 * OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31package org.logicprobe.LogicMail.ui;
32
33import java.io.DataInputStream;
34import java.io.IOException;
35import java.util.Calendar;
36import java.util.Vector;
37
38import javax.microedition.io.Connector;
39import javax.microedition.io.file.FileConnection;
40
41import net.rim.device.api.io.MIMETypeAssociations;
42import net.rim.device.api.system.Application;
43import net.rim.device.api.system.EventLogger;
44import net.rim.device.api.ui.Field;
45import net.rim.device.api.ui.FieldChangeListener;
46import net.rim.device.api.ui.Font;
47import net.rim.device.api.ui.Keypad;
48import net.rim.device.api.ui.Manager;
49import net.rim.device.api.ui.MenuItem;
50import net.rim.device.api.ui.Screen;
51import net.rim.device.api.ui.component.AutoTextEditField;
52import net.rim.device.api.ui.component.Dialog;
53import net.rim.device.api.ui.component.Menu;
54import net.rim.device.api.ui.component.ObjectChoiceField;
55import net.rim.device.api.util.Arrays;
56import net.rim.device.api.util.DataBuffer;
57
58import org.logicprobe.LogicMail.AnalyticsDataCollector;
59import org.logicprobe.LogicMail.AppInfo;
60import org.logicprobe.LogicMail.LogicMailResource;
61import org.logicprobe.LogicMail.conf.GlobalConfig;
62import org.logicprobe.LogicMail.conf.IdentityConfig;
63import org.logicprobe.LogicMail.conf.MailSettings;
64import org.logicprobe.LogicMail.message.AbstractMimeMessagePartVisitor;
65import org.logicprobe.LogicMail.message.ApplicationPart;
66import org.logicprobe.LogicMail.message.AudioPart;
67import org.logicprobe.LogicMail.message.ContentPart;
68import org.logicprobe.LogicMail.message.ImagePart;
69import org.logicprobe.LogicMail.message.Message;
70import org.logicprobe.LogicMail.message.MessagePart;
71import org.logicprobe.LogicMail.message.MimeMessageContent;
72import org.logicprobe.LogicMail.message.MimeMessageContentFactory;
73import org.logicprobe.LogicMail.message.MessageEnvelope;
74import org.logicprobe.LogicMail.message.MessageFlags;
75import org.logicprobe.LogicMail.message.MimeMessagePart;
76import org.logicprobe.LogicMail.message.MimeMessagePartFactory;
77import org.logicprobe.LogicMail.message.MultiPart;
78import org.logicprobe.LogicMail.message.TextContent;
79import org.logicprobe.LogicMail.message.TextPart;
80import org.logicprobe.LogicMail.message.UnsupportedContentException;
81import org.logicprobe.LogicMail.message.VideoPart;
82import org.logicprobe.LogicMail.model.Address;
83import org.logicprobe.LogicMail.model.MailboxNode;
84import org.logicprobe.LogicMail.model.MessageNode;
85import org.logicprobe.LogicMail.model.MessageNodeEvent;
86import org.logicprobe.LogicMail.model.MessageNodeListener;
87import org.logicprobe.LogicMail.model.NetworkAccountNode;
88import org.logicprobe.LogicMail.util.EventObjectRunnable;
89import org.logicprobe.LogicMail.util.UnicodeNormalizer;
90
91
92/**
93 * This is the message composition screen.
94 */
95public class CompositionScreen extends AbstractScreenProvider {
96    // The edit field may replace "\r\n" with "\n" in some cases, so we only
97    // use "\n" in places where this could occur.
98    private static String SIGNATURE_PREFIX = "\n-- \r\n";
99    public final static int COMPOSE_NORMAL = 0;
100    public final static int COMPOSE_REPLY = 1;
101    public final static int COMPOSE_REPLY_ALL = 2;
102    public final static int COMPOSE_FORWARD = 3;
103   
104    private int composeType = -1;
105    private String initialRecipient;
106    private MessageNode sourceMessageNode;
107    private NetworkAccountNode accountNode;
108    private boolean selectableIdentity;
109    private String signatureBlock;
110    private int signatureStartOffset;
111    private UnicodeNormalizer unicodeNormalizer;
112   
113    private ObjectChoiceField identityChoiceField;
114    private BorderedFieldManager recipientsFieldManager;
115        private BorderedFieldManager subjectFieldManager;
116        private BorderedFieldManager messageFieldManager;
117    private AutoTextEditField subjectEditField;
118    private AutoTextEditField messageEditField;
119    private BorderedFieldManager attachmentsFieldManager;
120   
121    private String inReplyTo;
122    private boolean messageSent;
123    private IdentityConfig identityConfig;
124    private MessageNode replyToMessageNode;
125
126    private Object generateLock = new Object();
127    private Message pendingMessage;
128    private Vector pendingLocalAttachments = new Vector();
129    private Vector pendingRemoteAttachments = new Vector();
130    private Runnable pendingMessageRunnable;
131   
132    private MenuItem sendMenuItem = new MenuItem(resources.getString(LogicMailResource.MENUITEM_SEND), 300100, 10) {
133        public void run() {
134            sendMessage();
135        }
136    };
137    private MenuItem saveDraftMenuItem = new MenuItem(resources.getString(LogicMailResource.MENUITEM_SAVE_DRAFT), 300200, 20) {
138        public void run() {
139            saveAsDraft();
140            screen.close();
141        }
142    };
143    private MenuItem addToMenuItem = new MenuItem(resources.getString(LogicMailResource.MENUITEM_ADD_TO), 400100, 1010) {
144        public void run() {
145            insertRecipientField(EmailAddressBookEditField.ADDRESS_TO);
146        }
147    };
148    private MenuItem addCcMenuItem = new MenuItem(resources.getString(LogicMailResource.MENUITEM_ADD_CC), 400200, 1020) {
149        public void run() {
150            insertRecipientField(EmailAddressBookEditField.ADDRESS_CC);
151        }
152    };
153    private MenuItem addBccMenuItem = new MenuItem(resources.getString(LogicMailResource.MENUITEM_ADD_BCC), 400300, 1030) {
154        public void run() {
155            insertRecipientField(EmailAddressBookEditField.ADDRESS_BCC);
156        }
157    };
158    private MenuItem attachFileMenuItem = new MenuItem(resources.getString(LogicMailResource.MENUITEM_ATTACH_FILE), 400400, 1040) {
159        public void run() {
160            attachFile();
161        }
162    };
163    private MenuItem deleteFieldMenuItem = new MenuItem(resources.getString(LogicMailResource.MENUITEM_DELETE_FIELD), 400900, 1090) {
164        public void run() {
165            deleteField(false);
166        }
167    };
168   
169    private MessageNodeListener messageNodeListener = new MessageNodeListener() {
170        public void messageStatusChanged(MessageNodeEvent e) {
171            messageNodeListener_MessageStatusChanged(e);
172        }
173    };
174
175    /**
176     * Creates a new instance of CompositionScreen.
177     *
178     * @param accountNode Account node
179     */
180    public CompositionScreen(NetworkAccountNode accountNode) {
181        this.accountNode = accountNode;
182        this.identityConfig = accountNode.getIdentityConfig();
183        this.selectableIdentity =
184            accountNode.isSelectableIdentityEnabled()
185            && (MailSettings.getInstance().getNumIdentities() > 1);
186        if(MailSettings.getInstance().getGlobalConfig().getUnicodeNormalization()) {
187            unicodeNormalizer = UnicodeNormalizer.getInstance();
188        }
189    }
190
191    /**
192     * Creates a new instance of CompositionScreen.
193     *
194     * @param accountNode Account node
195     * @param recipient Message recipient address to pre-populate the "To" field with
196     */
197    public CompositionScreen(NetworkAccountNode accountNode, String recipient) {
198        this(accountNode);
199        this.initialRecipient = recipient;
200    }
201
202    /**
203     * Creates a new instance of CompositionScreen.
204     * Used for working with an already created message,
205     * such as a draft, reply, or forward.
206     *
207     * @param accountNode Account node
208     * @param messageNode Message we are composing
209     * @param composeType Type of message we are creating
210     */
211    public CompositionScreen(
212            NetworkAccountNode accountNode,
213                MessageNode messageNode,
214                int composeType) {
215        this.accountNode = accountNode;
216        this.identityConfig = accountNode.getIdentityConfig();
217        this.selectableIdentity =
218            accountNode.isSelectableIdentityEnabled()
219            && (MailSettings.getInstance().getNumIdentities() > 1);
220        if(MailSettings.getInstance().getGlobalConfig().getUnicodeNormalization()) {
221            unicodeNormalizer = UnicodeNormalizer.getInstance();
222        }
223
224        this.composeType = composeType;
225        this.sourceMessageNode = messageNode;
226    }
227
228    public void onDisplay() {
229        super.onDisplay();
230       
231        String eventType;
232        switch(composeType) {
233        case COMPOSE_REPLY:
234            eventType = "ReplyMessage";
235            break;
236        case COMPOSE_REPLY_ALL:
237            eventType = "ReplyAllMessage";
238            break;
239        case COMPOSE_FORWARD:
240            eventType = "ForwardMessage";
241            break;
242        case COMPOSE_NORMAL:
243        default:           
244            eventType = "NormalMessage";
245            break;
246        }
247        String contentGroup = accountNode.getProtocolName();
248        AnalyticsDataCollector.getInstance().onScreenView(getScreenPath(), getScreenName(), eventType, contentGroup);
249    }
250   
251    private void messageNodeListener_MessageStatusChanged(MessageNodeEvent e) {
252        int type = e.getType();
253        if(type == MessageNodeEvent.TYPE_STRUCTURE_LOADED
254                || type == MessageNodeEvent.TYPE_CONTENT_LOADED) {
255            // Immediately remove the listener to avoid redundant calls
256            MessageNode messageNode = (MessageNode)e.getSource();
257            messageNode.removeMessageNodeListener(messageNodeListener);
258
259            // Schedule the UI update
260            invokeLater(new EventObjectRunnable(e) {
261                public void run() {
262                    MessageNode messageNode = (MessageNode)getEvent().getSource();
263                    populateFromMessage(messageNode);
264                    messageEditField.setEditable(true);
265                }
266            });
267        }
268    }
269
270    private void populateFromMessage(MessageNode message) {
271        int i;
272        MimeMessagePart body = message.getMessageStructure();
273
274        PopulatePartsVisitor visitor = new PopulatePartsVisitor();
275        body.accept(visitor);
276       
277        TextPart bodyPart = visitor.getFirstTextPart();
278        if(bodyPart != null) {
279            MimeMessageContent content = message.getMessageContent(bodyPart);
280            if(content instanceof TextContent) {
281                messageEditField.insert(normalize(((TextContent)content).getText()));
282                messageEditField.setCursorPosition(0);
283            }
284        }
285
286        Vector attachmentParts = visitor.getAttachmentParts();
287        if(attachmentParts.size() > 0) {
288            if(attachmentsFieldManager != null) {
289                attachmentsFieldManager.deleteAll();
290            }
291            else {
292                attachmentsFieldManager = FieldFactory.getInstance().getBorderedFieldManager(
293                        BorderedFieldManager.BOTTOM_BORDER_NORMAL
294                        | BorderedFieldManager.OUTER_FILL_NONE);
295                messageFieldManager.add(attachmentsFieldManager);
296            }
297           
298            int size = attachmentParts.size();
299            for(i = 0; i < size; i++) {
300                ContentPart attachmentPart = (ContentPart)attachmentParts.elementAt(i);
301                attachmentsFieldManager.add(new AttachmentField(message, attachmentPart));
302            }
303        }
304
305        // Set the subject
306        subjectEditField.setText(normalize(message.getSubject()));
307
308        // Set the recipients
309        Address[] recipients = message.getTo();
310        if (recipients != null) {
311            for (i = 0; i < recipients.length; i++) {
312                insertRecipientField(EmailAddressBookEditField.ADDRESS_TO).setText(normalize(recipients[i].toString()));
313            }
314        }
315
316        recipients = message.getCc();
317        if (recipients != null) {
318            for (i = 0; i < recipients.length; i++) {
319                insertRecipientField(EmailAddressBookEditField.ADDRESS_CC).setText(normalize(recipients[i].toString()));
320            }
321        }
322
323        recipients = message.getBcc();
324        if (recipients != null) {
325            for (i = 0; i < recipients.length; i++) {
326                insertRecipientField(EmailAddressBookEditField.ADDRESS_BCC).setText(normalize(recipients[i].toString()));
327            }
328        }
329
330        inReplyTo = message.getInReplyTo();
331    }
332   
333    private static class PopulatePartsVisitor extends AbstractMimeMessagePartVisitor {
334        private TextPart firstTextPart;
335        private Vector attachmentParts = new Vector();;
336       
337        public TextPart getFirstTextPart() {
338            return firstTextPart;
339        }
340       
341        public Vector getAttachmentParts() {
342            return attachmentParts;
343        }
344       
345        public void visitTextPart(TextPart part) {
346            if(firstTextPart == null) {
347                firstTextPart = part;
348            }
349            else {
350                attachmentParts.addElement(part);
351            }
352        }
353       
354        public void visitApplicationPart(ApplicationPart part) {
355            attachmentParts.addElement(part);
356        }
357        public void visitAudioPart(AudioPart part) {
358            attachmentParts.addElement(part);
359        }
360        public void visitImagePart(ImagePart part) {
361            attachmentParts.addElement(part);
362        }
363        public void visitMessagePart(MessagePart part) {
364            attachmentParts.addElement(part);
365        }
366        public void visitVideoPart(VideoPart part) {
367            attachmentParts.addElement(part);
368        }
369    };
370
371    private void appendSignature() {
372        // Add the signature if available
373        signatureBlock = null;
374        signatureStartOffset = -1;
375        if (identityConfig != null) {
376            String sig = identityConfig.getMsgSignature();
377
378            if ((sig != null) && (sig.length() > 0)) {
379                boolean shouldAppend;
380               
381                switch(composeType) {
382                case COMPOSE_NORMAL:
383                    shouldAppend = true;
384                    break;
385                case COMPOSE_REPLY:
386                case COMPOSE_REPLY_ALL:
387                    shouldAppend = accountNode.isReplySignatureIncluded();
388                    break;
389                case COMPOSE_FORWARD:
390                    shouldAppend = accountNode.isForwardSignatureIncluded();
391                    break;
392                default:
393                    shouldAppend = true;
394                    break;
395                }
396               
397                if(shouldAppend) {
398                    signatureBlock = SIGNATURE_PREFIX + sig;
399                    if(composeType == COMPOSE_NORMAL || accountNode.isSignatureAbove()) {
400                        int start = messageEditField.getCursorPosition();
401                        messageEditField.insert(signatureBlock);
402                        if(composeType != COMPOSE_NORMAL) {
403                            messageEditField.insert("\r\n");
404                        }
405                        messageEditField.setCursorPosition(0);
406                        int length = messageEditField.getTextLength();
407                        this.signatureStartOffset = length - start;
408                    }
409                    else {
410                        messageEditField.setCursorPosition(messageEditField.getTextLength());
411                        int start = messageEditField.getCursorPosition();
412                        messageEditField.insert(signatureBlock);
413                        messageEditField.setCursorPosition(0);
414                        int length = messageEditField.getTextLength();
415                        this.signatureStartOffset = length - start;
416                    }
417                }
418            }
419        }
420    }
421   
422    protected void identityChoiceFieldChanged(int context) {
423        // Change the identity value, returning immediately if it hasn't changed
424        if(identityChoiceField.getSelectedIndex() != -1) {
425            IdentityConfig selectedIdentity =
426                (IdentityConfig)identityChoiceField.getChoice(identityChoiceField.getSelectedIndex());
427            if(selectedIdentity == identityConfig) { return; }
428            identityConfig = selectedIdentity;
429        }
430     
431        // Determine if the signature can be removed and re-added.
432        // Offsets are from the end of the text, to be more edit-resistant
433        if(signatureBlock != null && signatureStartOffset != -1) {
434            int textLength = messageEditField.getTextLength();
435            int sigLength = signatureBlock.length();
436            int start = textLength - signatureStartOffset;
437            if(start < 0 || sigLength <= 0 || (start + sigLength) > textLength) { return; }
438           
439            String oldText = messageEditField.getText(start, sigLength);
440            if(oldText.equals(signatureBlock)) {
441                // Check if the new identity has a signature
442                String newSignature = identityConfig.getMsgSignature();
443                if(newSignature == null || newSignature.length() == 0) { return; }
444               
445                // Replace the old signature with the new one
446                signatureBlock = SIGNATURE_PREFIX + newSignature;
447                messageEditField.select(false);
448                int oldCursor = messageEditField.getCursorPosition();
449                messageEditField.setCursorPosition(start + sigLength);
450                messageEditField.backspace(sigLength);
451                messageEditField.insert(signatureBlock);
452                messageEditField.setCursorPosition(oldCursor);
453               
454                // Update the start offset
455                signatureStartOffset = messageEditField.getTextLength() - start;
456            }
457        }
458       
459    }
460   
461    public void initFields(Screen screen) {
462        super.initFields(screen);
463
464        FieldFactory fieldFactory = FieldFactory.getInstance();
465        recipientsFieldManager = fieldFactory.getBorderedFieldManager(
466                        Manager.NO_HORIZONTAL_SCROLL
467                        | Manager.NO_VERTICAL_SCROLL
468                        | BorderedFieldManager.BOTTOM_BORDER_NONE);
469       
470        if(selectableIdentity) {
471            IdentityConfig[] identityList = buildIdentityList();
472           
473            identityChoiceField = new ObjectChoiceField(
474                    resources.getString(LogicMailResource.MESSAGEPROPERTIES_FROM) + ' ',
475                    identityList, 0, Field.FIELD_LEFT);
476            identityChoiceField.setChangeListener(new FieldChangeListener() {
477                public void fieldChanged(Field field, int context) {
478                    identityChoiceFieldChanged(context);
479                }
480            });
481            recipientsFieldManager.add(identityChoiceField);
482        }
483       
484        recipientsFieldManager.add(new EmailAddressBookEditField(
485                EmailAddressBookEditField.ADDRESS_TO, ""));
486        recipientsFieldManager.add(new EmailAddressBookEditField(
487                EmailAddressBookEditField.ADDRESS_CC, ""));
488
489        subjectFieldManager = fieldFactory.getBorderedFieldManager(
490                        Manager.NO_HORIZONTAL_SCROLL
491                        | Manager.NO_VERTICAL_SCROLL
492                        | BorderedFieldManager.BOTTOM_BORDER_LINE);
493        subjectEditField = new AutoTextEditField(resources.getString(LogicMailResource.MESSAGEPROPERTIES_SUBJECT) + ' ', "");
494        subjectEditField.setFont(subjectEditField.getFont().derive(Font.BOLD));
495        subjectFieldManager.add(subjectEditField);
496       
497        messageFieldManager = new BorderedFieldManager(
498                BorderedFieldManager.BOTTOM_BORDER_NORMAL
499                | BorderedFieldManager.FILL_NONE
500                | Field.USE_ALL_HEIGHT);
501        messageEditField = new AutoTextEditField();
502                messageEditField.setEditable(false);
503        messageFieldManager.add(messageEditField);
504       
505        screen.add(recipientsFieldManager);
506        screen.add(subjectFieldManager);
507        screen.add(messageFieldManager);
508       
509        if(sourceMessageNode == null) {
510            appendSignature();
511                messageEditField.setEditable(true);
512
513                if(initialRecipient != null) {
514                    EmailAddressBookEditField toAddressField =
515                        (EmailAddressBookEditField)recipientsFieldManager.getField(selectableIdentity ? 1 : 0);
516                    toAddressField.setText(initialRecipient);
517                }
518        }
519        else if(composeType == COMPOSE_NORMAL) {
520                if(sourceMessageNode.getMessageStructure() != null && sourceMessageNode.hasMessageContent()) {
521                        populateFromMessage(sourceMessageNode);
522                        messageEditField.setEditable(true);
523                }
524                else {
525                        sourceMessageNode.addMessageNodeListener(messageNodeListener);
526                        sourceMessageNode.refreshMessage(GlobalConfig.MESSAGE_DISPLAY_PLAIN_TEXT);
527                }
528        }
529        else
530        {
531                this.replyToMessageNode = sourceMessageNode;
532       
533                MessageNode populateMessage;
534       
535                switch (composeType) {
536                case COMPOSE_REPLY:
537                        populateMessage = sourceMessageNode.toReplyMessage();
538                    break;
539                case COMPOSE_REPLY_ALL:
540                        populateMessage = sourceMessageNode.toReplyAllMessage(identityConfig.getEmailAddress());
541                    break;
542                case COMPOSE_FORWARD:
543                    //TODO: Consider bringing along attachments when forwarding
544                        populateMessage = sourceMessageNode.toForwardMessage();
545                    break;
546            default:
547                populateMessage = sourceMessageNode;
548                break;
549                }
550                populateFromMessage(populateMessage);
551                appendSignature();
552                messageEditField.setEditable(true);
553        }
554    }
555
556    private IdentityConfig[] buildIdentityList() {
557        // Populate an array with the available identity configurations
558        MailSettings mailSettings = MailSettings.getInstance();
559        int num = mailSettings.getNumIdentities();
560        IdentityConfig[] identityList = new IdentityConfig[num];
561        for(int i=0; i<num; i++) {
562            identityList[i] = mailSettings.getIdentityConfig(i);
563        }
564        // Make sure this account's identity is first in the array
565        for(int i=1; i<num; i++) {
566            if(identityList[i] == identityConfig) {
567                IdentityConfig temp = identityList[0];
568                identityList[0] = identityConfig;
569                identityList[i] = temp;
570                break;
571            }
572        }
573        return identityList;
574    }
575
576    public boolean onClose() {
577        if (!messageSent &&
578                ((subjectEditField.getText().length() > 0) ||
579                (messageEditField.getText().length() > 0))) {
580
581                boolean shouldClose = false;
582                if(accountNode.getDraftMailbox() != null) {
583                        int choice = Dialog.ask(
584                                        resources.getString(LogicMailResource.COMPOSITION_PROMPT_SAVE_OR_DISCARD),
585                                        new Object[] {
586                                                        resources.getString(LogicMailResource.MENUITEM_SAVE_AS_DRAFT),
587                                                        resources.getString(LogicMailResource.MENUITEM_DISCARD),
588                                                        resources.getString(LogicMailResource.MENUITEM_CANCEL) }, 0);
589                        if(choice == 0) {
590                                // Save as draft, then close
591                                saveAsDraft();
592                                shouldClose = true;
593                        }
594                        else if(choice == 1) {
595                                shouldClose = true;
596                        }
597                }
598                else {
599                        int choice =
600                                Dialog.ask(
601                                                resources.getString(LogicMailResource.COMPOSITION_PROMPT_DISCARD_UNSENT),
602                                                new Object[] {
603                                                                resources.getString(LogicMailResource.MENUITEM_DISCARD),
604                                                                resources.getString(LogicMailResource.MENUITEM_CANCEL)}, 0);
605                        if(choice == 0) { shouldClose = true; }
606                }
607               
608            if (shouldClose) {
609                screen.close();
610                return true;
611            } else {
612                return false;
613            }
614        } else {
615                screen.close();
616            return true;
617        }
618    }
619
620    public void makeMenu(Menu menu, int instance) {
621        if(messageCanBeSent()) {
622            menu.add(sendMenuItem);
623        }
624        MailboxNode draftMailbox = accountNode.getDraftMailbox();
625        if(draftMailbox != null
626                && ((subjectEditField.getText().length() > 0)
627                || (messageEditField.getText().length() > 0))) {
628            menu.add(saveDraftMenuItem);
629        }
630
631        menu.add(addToMenuItem);
632        menu.add(addCcMenuItem);
633        menu.add(addBccMenuItem);
634        menu.add(attachFileMenuItem);
635       
636        // "Delete field" is shown if the focus is on a recipient field that is
637        // not the only recipient field, or if the focus is on an attachment
638        // field.
639        if((recipientsFieldManager.getFieldWithFocus() != null
640                && recipientsFieldManager.getFieldWithFocusIndex() > (selectableIdentity ? 1 : 0))
641                || (attachmentsFieldManager != null
642                        && attachmentsFieldManager.getFieldWithFocus() != null
643                        && attachmentsFieldManager.getFieldCount() > 0)) {
644            menu.add(deleteFieldMenuItem);
645        }
646    }
647   
648    private boolean messageCanBeSent() {
649        int startIndex = selectableIdentity ? 1 : 0;
650        int size = recipientsFieldManager.getFieldCount();
651        for(int i=startIndex; i<size; i++) {
652            Field field = recipientsFieldManager.getField(i);
653            if(field instanceof EmailAddressBookEditField) {
654                EmailAddressBookEditField recipientField = (EmailAddressBookEditField)field;
655                if(recipientField.getAddressType() == EmailAddressBookEditField.ADDRESS_TO
656                        && recipientField.getText().length() > 0) {
657                    return true;
658                }
659            }
660        }
661        return false;
662    }
663   
664    public boolean keyChar(char key, int status, int time) {
665        boolean handled = false;
666       
667        switch (key) {
668        case Keypad.KEY_BACKSPACE:
669            if(deleteField(true)) {
670                handled = true;
671            }
672            break;
673        case Keypad.KEY_ENTER:
674            if(subjectEditField.isFocus()) {
675                messageEditField.setFocus();
676                handled = true;
677            }
678            else if(recipientsFieldManager.isFocus()) {
679                moveRecipientFocusDownward();
680                handled = true;
681            }
682            break;
683        }
684       
685        if(handled) {
686            return true;
687        }
688        else {
689            return super.keyChar(key, status, time);
690        }
691    }
692   
693    private void moveRecipientFocusDownward() {
694        int count = recipientsFieldManager.getFieldCount();
695        int index = recipientsFieldManager.getFieldWithFocusIndex();
696        if(count == 0 && index == -1) { return; }
697       
698        if(index < count - 1) {
699            recipientsFieldManager.getField(index + 1).setFocus();
700        }
701        else {
702            subjectEditField.setFocus();
703        }
704    }
705
706    /**
707     * Delete the current recipient or attachment field.
708     *
709     * @param onlyEmpty only delete a recipient field if its contents are empty
710     * @return true, if successful
711     */
712    private boolean deleteField(boolean onlyEmpty) {
713        if(recipientsFieldManager.getFieldWithFocus() != null
714                && recipientsFieldManager.getFieldWithFocusIndex() > (selectableIdentity ? 1 : 0)) {
715            EmailAddressBookEditField currentField =
716                (EmailAddressBookEditField) recipientsFieldManager.getFieldWithFocus();
717           
718            if(onlyEmpty && currentField.getText().length() > 0) {
719                return false;
720            }
721           
722            int index = currentField.getIndex();
723            recipientsFieldManager.delete(currentField);
724            recipientsFieldManager.getField(index - 1).setFocus();
725            return true;
726        }
727        else if(attachmentsFieldManager != null
728                && attachmentsFieldManager.getFieldWithFocus() != null
729                && attachmentsFieldManager.getFieldCount() > 0) {
730            attachmentsFieldManager.delete(attachmentsFieldManager.getFieldWithFocus());
731           
732            if(attachmentsFieldManager.getFieldCount() == 0) {
733                messageFieldManager.delete(attachmentsFieldManager);
734                attachmentsFieldManager = null;
735            }
736           
737            return true;
738        }
739        else {
740            return false;
741        }
742    }
743   
744    private void saveAsDraft() {
745        final MailboxNode draftMailbox = accountNode.getDraftMailbox();
746        final MessageEnvelope envelope = generateEnvelope();
747        generateMessage(new Runnable() {
748            public void run() {
749                envelope.date = Calendar.getInstance().getTime();
750                MessageFlags messageFlags = new MessageFlags();
751                messageFlags.setDraft(true).setRecent(true);
752                draftMailbox.appendMessage(envelope, pendingMessage, messageFlags);
753                // TODO: Save reply-to information with the draft message
754            }
755        });
756    }
757
758    private MessageEnvelope generateEnvelope() {
759        // Simplest possible implementation for now,
760        // which turns the content of the screen into
761        // a message containing a single text/plain section
762        MessageEnvelope env = new MessageEnvelope();
763
764        env.inReplyTo = inReplyTo;
765
766        // Build the recipients list
767        EmailAddressBookEditField currentField;
768        int size = recipientsFieldManager.getFieldCount();
769
770        for (int i = 0; i < size; i++) {
771            if(!(recipientsFieldManager.getField(i) instanceof EmailAddressBookEditField)) { continue; }
772            currentField = (EmailAddressBookEditField) recipientsFieldManager.getField(i);
773
774            if ((currentField.getAddressType() == EmailAddressBookEditField.ADDRESS_TO) &&
775                    (currentField.getText().length() > 0)) {
776                if (env.to == null) {
777                    env.to = new String[1];
778                    env.to[0] = currentField.getText();
779                } else {
780                    Arrays.add(env.to, currentField.getText());
781                }
782            } else if ((currentField.getAddressType() == EmailAddressBookEditField.ADDRESS_CC) &&
783                    (currentField.getText().length() > 0)) {
784                if (env.cc == null) {
785                    env.cc = new String[1];
786                    env.cc[0] = currentField.getText();
787                } else {
788                    Arrays.add(env.cc, currentField.getText());
789                }
790            } else if ((currentField.getAddressType() == EmailAddressBookEditField.ADDRESS_BCC) &&
791                    (currentField.getText().length() > 0)) {
792                if (env.bcc == null) {
793                    env.bcc = new String[1];
794                    env.bcc[0] = currentField.getText();
795                } else {
796                    Arrays.add(env.bcc, currentField.getText());
797                }
798            }
799        }
800
801        // Set the sender and reply-to addresses
802        // (this comes from identity settings)
803        env.from = new String[1];
804
805        String fullName = identityConfig.getFullName();
806
807        if ((fullName != null) && (fullName.length() > 0)) {
808            env.from[0] = "\"" + fullName + "\"" + " <" +
809            identityConfig.getEmailAddress() + ">";
810        } else {
811            env.from[0] = identityConfig.getEmailAddress();
812        }
813
814        String replyToAddress = identityConfig.getReplyToAddress();
815
816        if ((replyToAddress != null) && (replyToAddress.length() > 0)) {
817            env.replyTo = new String[1];
818            env.replyTo[0] = replyToAddress;
819        }
820
821        // Set the subject
822        env.subject = subjectEditField.getText();
823
824        // Set the date
825        env.date = Calendar.getInstance().getTime();
826       
827        return env;
828    }
829   
830    private void generateMessage(Runnable generatedRunnable) {
831        synchronized(generateLock) {
832                String contentText = messageEditField.getText();
833            MimeMessagePart bodyPart = MimeMessagePartFactory.createMimeMessagePart(
834                        "text", "plain", null, null, null, "", "", contentText.length());
835            MimeMessageContent bodyContent = new TextContent((TextPart)bodyPart, contentText);
836           
837                if(attachmentsFieldManager != null) {
838                    MultiPart multiPart = new MultiPart(MultiPart.MIXED);
839                    pendingMessage = new Message(multiPart);
840                    multiPart.addPart(bodyPart);
841                   
842                    int count = attachmentsFieldManager.getFieldCount();
843                    for(int i=0; i<count; i++) {
844                        AttachmentField attachmentField =
845                            (AttachmentField)attachmentsFieldManager.getField(i);
846                    ContentPart attachmentPart =
847                        attachmentField.getMessagePart();
848                    MimeMessageContent attachmentContent = null;
849                   
850                        MessageNode messageNode = attachmentField.getMessageNode();
851                        if(messageNode != null && messageNode == sourceMessageNode) {
852                            attachmentContent = messageNode.getMessageContent(attachmentPart);
853                            if(attachmentContent == null) {
854                                pendingRemoteAttachments.addElement(attachmentPart);
855                            }
856                        }
857                        else {
858                                String fileUrl = attachmentPart.getTag();
859                                if(fileUrl != null && fileUrl.startsWith("file:///")) {
860                                    pendingLocalAttachments.addElement(attachmentPart);
861                                }
862                        }
863                       
864                        if(attachmentContent != null) {
865                        multiPart.addPart(attachmentPart);
866                        pendingMessage.putContent(attachmentPart, attachmentContent);
867                        }
868                    }
869                }
870                else {
871                    pendingMessage = new Message(bodyPart);
872                }
873               
874                pendingMessage.putContent(bodyPart, bodyContent);
875           
876                boolean hasAllData = true;
877                if(pendingLocalAttachments.size() > 0) {
878                    hasAllData = false;
879                    (new Thread() { public void run() {
880                        handlePendingLocalAttachments();
881                    } }).start();
882                }
883                if(pendingRemoteAttachments.size() > 0) {
884                    hasAllData = false;
885                    sourceMessageNode.addMessageNodeListener(pendingAttachmentListener);
886                    int size = pendingRemoteAttachments.size();
887                    for(int i = 0; i < size; i++) {
888                        sourceMessageNode.requestContentPart((ContentPart)pendingRemoteAttachments.elementAt(i));
889                    }
890                }
891                if(hasAllData) {
892                    generatedRunnable.run();
893                pendingMessageRunnable = null;
894                pendingMessage = null;
895                }
896                else {
897                    pendingMessageRunnable = generatedRunnable;
898                }
899        }
900    }
901   
902    private void handlePendingLocalAttachments() {
903        synchronized(generateLock) {
904            if(pendingLocalAttachments.size() == 0) {
905                return;
906            }
907           
908            int size = pendingLocalAttachments.size();
909            for(int i=0; i<size; i++) {
910                ContentPart attachmentPart = (ContentPart)pendingLocalAttachments.elementAt(i);
911                String fileUrl = attachmentPart.getTag();
912                byte[] data;
913                try {
914                    FileConnection fileConnection = (FileConnection)Connector.open(fileUrl);
915                    DataInputStream input = fileConnection.openDataInputStream();
916   
917                    DataBuffer buf = new DataBuffer(1024, true);
918                    byte[] rawBuf = new byte[1024];
919                    int n;
920                    while((n = input.read(rawBuf, 0, 1024)) != -1) {
921                        buf.write(rawBuf, 0, n);
922                    }
923                    data = buf.toArray();
924                   
925                    input.close();
926                    fileConnection.close();
927                } catch (IOException e) {
928                    EventLogger.logEvent(AppInfo.GUID,
929                            ("Error adding attachment: " + e.getMessage()).getBytes(),
930                            EventLogger.ERROR);
931                    AnalyticsDataCollector.getInstance().onApplicationError(
932                            "Error adding attachment: " + e.getMessage());
933                    data = null;
934                }
935               
936                if(data != null && data.length > 0) {
937                    try {
938                        MimeMessageContent attachmentContent =
939                            MimeMessageContentFactory.createContentRaw(attachmentPart, data, false);
940                        MultiPart multiPart = (MultiPart)pendingMessage.getStructure();
941                        multiPart.addPart(attachmentPart);
942                        pendingMessage.putContent(attachmentPart, attachmentContent);
943                    } catch (UnsupportedContentException e) { }
944                }
945            }
946            pendingLocalAttachments.removeAllElements();
947           
948            if(pendingLocalAttachments.size() == 0 && pendingRemoteAttachments.size() == 0) {
949                pendingMessageRunnable.run();
950                pendingMessageRunnable = null;
951                pendingMessage = null;
952            }
953        }
954    }
955   
956    private MessageNodeListener pendingAttachmentListener = new MessageNodeListener() {
957        public void messageStatusChanged(MessageNodeEvent e) {
958            if(e.getType() == MessageNodeEvent.TYPE_CONTENT_LOADED) {
959                synchronized(generateLock) {
960                    Vector loadedAttachments = new Vector();
961                    int size = pendingRemoteAttachments.size();
962                    for(int i=0; i<size; i++) {
963                        ContentPart attachmentPart = (ContentPart)pendingRemoteAttachments.elementAt(i);
964                        MimeMessageContent attachmentContent =
965                            sourceMessageNode.getMessageContent(attachmentPart);
966                        if(attachmentContent != null) {
967                            MultiPart multiPart = (MultiPart)pendingMessage.getStructure();
968                            multiPart.addPart(attachmentPart);
969                            pendingMessage.putContent(attachmentPart, attachmentContent);
970                            loadedAttachments.addElement(attachmentPart);
971                        }
972                    }
973                    size = loadedAttachments.size();
974                    for(int i=0; i<size; i++) {
975                        pendingRemoteAttachments.removeElement(loadedAttachments.elementAt(i));
976                    }
977                   
978                    if(pendingRemoteAttachments.size() == 0) {
979                        sourceMessageNode.removeMessageNodeListener(pendingAttachmentListener);
980                    }
981                   
982                    if(pendingLocalAttachments.size() == 0 && pendingRemoteAttachments.size() == 0) {
983                        pendingMessageRunnable.run();
984                        pendingMessageRunnable = null;
985                        pendingMessage = null;
986                    }
987                }
988            }
989        }
990    };
991   
992    private void sendMessage() {
993        final MessageEnvelope envelope = generateEnvelope();
994        generateMessage(new Runnable() {
995            public void run() {
996                if (replyToMessageNode != null) {
997                    if(composeType == COMPOSE_FORWARD) {
998                        accountNode.sendMessageForwarded(envelope, pendingMessage, replyToMessageNode);
999                    }
1000                    else {
1001                        accountNode.sendMessageReply(envelope, pendingMessage, replyToMessageNode);
1002                    }
1003                } else {
1004                    accountNode.sendMessage(envelope, pendingMessage);
1005                }
1006               
1007                messageSent = true;
1008                synchronized(Application.getEventLock()) {
1009                    screen.setDirty(false);
1010                    screen.close();
1011                }
1012            }});
1013    }
1014
1015    /**
1016     * Insert a new recipient field.
1017     * @param addressType The type of address this field will hold
1018     * @return The newly added field
1019     */
1020    private EmailAddressBookEditField insertRecipientField(int addressType) {
1021        int size = recipientsFieldManager.getFieldCount();
1022        EmailAddressBookEditField currentField;
1023        int i;
1024
1025        // If a field of this type already exists, and is empty, move
1026        // focus there instead of adding a new field
1027        for (i = 0; i < size; i++) {
1028            if(!(recipientsFieldManager.getField(i) instanceof EmailAddressBookEditField)) { continue; }
1029            currentField = (EmailAddressBookEditField) recipientsFieldManager.getField(i);
1030
1031            if ((currentField.getAddressType() == addressType) &&
1032                    (currentField.getText().length() == 0)) {
1033                currentField.setFocus();
1034
1035                return currentField;
1036            }
1037        }
1038
1039        // Otherwise, find the appropriate insertion point,
1040        // and add a new field, and give it focus
1041        if (addressType == EmailAddressBookEditField.ADDRESS_TO) {
1042            for (i = 0; i < size; i++) {
1043                if(!(recipientsFieldManager.getField(i) instanceof EmailAddressBookEditField)) { continue; }
1044                currentField = (EmailAddressBookEditField) recipientsFieldManager.getField(i);
1045
1046                if (currentField.getAddressType() != EmailAddressBookEditField.ADDRESS_TO) {
1047                    currentField = new EmailAddressBookEditField(EmailAddressBookEditField.ADDRESS_TO,
1048                            "");
1049                    recipientsFieldManager.insert(currentField, i);
1050                    currentField.setFocus();
1051
1052                    return currentField;
1053                }
1054            }
1055        } else if (addressType == EmailAddressBookEditField.ADDRESS_CC) {
1056            i = 0;
1057
1058            while (i < size) {
1059                if(!(recipientsFieldManager.getField(i) instanceof EmailAddressBookEditField)) { i++; continue; }
1060                currentField = (EmailAddressBookEditField) recipientsFieldManager.getField(i);
1061
1062                if ((currentField.getAddressType() == EmailAddressBookEditField.ADDRESS_TO) ||
1063                        (currentField.getAddressType() == EmailAddressBookEditField.ADDRESS_CC)) {
1064                    i++;
1065                } else {
1066                    currentField = new EmailAddressBookEditField(EmailAddressBookEditField.ADDRESS_CC,
1067                            "");
1068                    recipientsFieldManager.insert(currentField, i);
1069                    currentField.setFocus();
1070
1071                    return currentField;
1072                }
1073            }
1074        }
1075
1076        currentField = new EmailAddressBookEditField(addressType, "");
1077        recipientsFieldManager.add(currentField);
1078        currentField.setFocus();
1079
1080        return currentField;
1081    }
1082   
1083    /**
1084     * Attach a file to the current message.
1085     */
1086    private void attachFile() {
1087        String fileUrl = ScreenFactory.getInstance().showFilePicker();
1088        if(fileUrl != null) {
1089            ContentPart attachmentPart = handleSelectedFile(fileUrl);
1090            if(attachmentPart != null) {
1091                if(attachmentsFieldManager == null) {
1092                    attachmentsFieldManager = FieldFactory.getInstance().getBorderedFieldManager(
1093                            BorderedFieldManager.BOTTOM_BORDER_NORMAL
1094                            | BorderedFieldManager.OUTER_FILL_NONE);
1095                    messageFieldManager.add(attachmentsFieldManager);
1096                }
1097                attachmentsFieldManager.add(new AttachmentField(null, attachmentPart));
1098            }
1099        }
1100    }
1101   
1102    private ContentPart handleSelectedFile(String fileUrl) {
1103        ContentPart mimeContentPart = null;
1104        try {
1105            FileConnection fileConnection = (FileConnection)Connector.open(fileUrl);
1106            if(fileConnection.canRead()) {
1107                String mimeType = MIMETypeAssociations.getMIMEType(fileUrl);
1108                if(mimeType == null) {
1109                    mimeType = "application/octet-stream";
1110                }
1111               
1112                int p = mimeType.indexOf('/');
1113                String mimeSubtype = mimeType.substring(p + 1);
1114                mimeType = mimeType.substring(0, p);
1115               
1116                MimeMessagePart part =
1117                    MimeMessagePartFactory.createMimeMessagePart(
1118                            mimeType,
1119                            mimeSubtype,
1120                            fileConnection.getName(),
1121                            null,         // encoding
1122                            null,         // param
1123                            "attachment", // disposition
1124                            null,         // content ID
1125                            (int)fileConnection.fileSize(),
1126                            fileUrl);
1127               
1128                if(part instanceof ContentPart) {
1129                    mimeContentPart = (ContentPart)part;
1130                }
1131            }
1132            fileConnection.close();
1133        } catch (IOException e) {
1134            mimeContentPart = null;
1135        }
1136        return mimeContentPart;
1137    }
1138   
1139    /**
1140     * Run the Unicode normalizer on the provide string,
1141     * only if normalization is enabled in the configuration.
1142     * If normalization is disabled, this method returns
1143     * the input unmodified.
1144     *
1145     * @param input Input string
1146     * @return Normalized string
1147     */
1148    private String normalize(String input) {
1149        if(unicodeNormalizer == null) {
1150            return input;
1151        }
1152        else {
1153            return unicodeNormalizer.normalize(input);
1154        }
1155    }
1156}
Note: See TracBrowser for help on using the repository browser.