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

Revision 962, 44.7 KB checked in by octorian, 9 months ago (diff)

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

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
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 */
31
32package org.logicprobe.LogicMail.ui;
33
34import java.util.Calendar;
35import java.util.Date;
36import java.util.Enumeration;
37import java.util.Hashtable;
38import java.util.Vector;
39
40import net.rim.device.api.i18n.DateFormat;
41import net.rim.device.api.system.Bitmap;
42import net.rim.device.api.system.KeypadListener;
43import net.rim.device.api.ui.Field;
44import net.rim.device.api.ui.FocusChangeListener;
45import net.rim.device.api.ui.Keypad;
46import net.rim.device.api.ui.Manager;
47import net.rim.device.api.ui.MenuItem;
48import net.rim.device.api.ui.Screen;
49import net.rim.device.api.ui.component.Dialog;
50import net.rim.device.api.ui.component.Menu;
51import net.rim.device.api.ui.container.VerticalFieldManager;
52import net.rim.device.api.util.Comparator;
53import net.rim.device.api.util.DateTimeUtilities;
54
55import org.logicprobe.LogicMail.AnalyticsDataCollector;
56import org.logicprobe.LogicMail.LogicMailResource;
57import org.logicprobe.LogicMail.PlatformInfo;
58import org.logicprobe.LogicMail.conf.GlobalConfig;
59import org.logicprobe.LogicMail.conf.MailSettings;
60import org.logicprobe.LogicMail.conf.MailSettingsEvent;
61import org.logicprobe.LogicMail.conf.MailSettingsListener;
62import org.logicprobe.LogicMail.model.MailboxNode;
63import org.logicprobe.LogicMail.model.MailboxNodeEvent;
64import org.logicprobe.LogicMail.model.MailboxNodeListener;
65import org.logicprobe.LogicMail.model.MessageNode;
66import org.logicprobe.LogicMail.model.MessageNodeEvent;
67import org.logicprobe.LogicMail.model.MessageNodeListener;
68import org.logicprobe.LogicMail.model.NetworkAccountNode;
69import org.logicprobe.LogicMail.util.EventObjectRunnable;
70
71/**
72 * Display the active mailbox listing.
73 * If the supplied folder matches the configured sent folder
74 * for the provided account, then the display fields will be
75 * adjusted accordingly.
76 */
77public class MailboxScreen extends AbstractScreenProvider {
78    private static final boolean hasTouchscreen = PlatformInfo.getInstance().hasTouchscreen();
79        private static final int SHORTCUT_COMPOSE = 0;
80    private static final int SHORTCUT_OPEN    = 1;
81    private static final int SHORTCUT_DELETE  = 2;
82        private static final int SHORTCUT_UP      = 3;
83        private static final int SHORTCUT_DOWN    = 4;
84       
85        private MailboxNode mailboxNode;
86    private Vector knownMessages;
87    private Hashtable messageNodeToFieldMap;
88    private boolean firstDisplay = true;
89    private MailSettings mailSettings;
90    private GlobalConfig globalConfig;
91    private VerticalFieldManager messageFieldManager;
92    private boolean navigationMoved;
93    private boolean displayOrder;
94    private boolean hideDeleted;
95    private MessageActions messageActions;
96    protected boolean composeEnabled;
97    protected Field currentContextField;
98
99    protected MenuItem compositionItem;
100    protected MenuItem markPriorOpenedItem;
101       
102    /**
103     * Initializes a new MailboxScreen to view the provided mailbox.
104     *
105     * @param mailboxNode Mailbox node to view.
106     */
107    public MailboxScreen(MailboxNode mailboxNode) {
108        this.mailboxNode = mailboxNode;
109        this.knownMessages = new Vector();
110        this.messageNodeToFieldMap = new Hashtable();
111        this.mailSettings = MailSettings.getInstance();
112        this.globalConfig = this.mailSettings.getGlobalConfig();
113    }
114
115    public String getTitle() {
116        return mailboxNode.toString();
117    }
118   
119    /**
120     * Gets the mailbox node being displayed by this screen.
121     *
122     * @return The mailbox node
123     */
124    public MailboxNode getMailboxNode() {
125        return mailboxNode;
126    }
127   
128    private MailSettingsListener mailSettingsListener = new MailSettingsListener() {
129        public void mailSettingsSaved(MailSettingsEvent e) {
130            if((e.getGlobalChange() & GlobalConfig.CHANGE_TYPE_OTHER) != 0) {
131                if(globalConfig.getDispOrder() != displayOrder) {
132                    final MessageNode[][] gaps = mailboxNode.findMessageNodeGaps();
133                    invokeLater(new EventObjectRunnable(e) {
134                        public void run() {
135                            displayOrder = !displayOrder;
136                            displayableChanged();
137                            if(gaps == null || gaps.length == 0) { return; }
138                            handleMessageNodeGaps(gaps);
139                        }
140                    });
141                }
142                if(globalConfig.getHideDeletedMsg() != hideDeleted) {
143                    final MessageNode[][] gaps = mailboxNode.findMessageNodeGaps();
144                    invokeLater(new EventObjectRunnable(e) {
145                        public void run() {
146                            hideDeleted = !hideDeleted;
147                            displayableChanged();
148                            if(gaps == null || gaps.length == 0) { return; }
149                            handleMessageNodeGaps(gaps);
150                        }
151                    });
152                }
153            }
154        }
155    };
156   
157    /** The mailbox node listener. */
158    private MailboxNodeListener mailboxNodeListener = new MailboxNodeListener() {
159        public void mailboxStatusChanged(MailboxNodeEvent e) {
160            mailboxNode_MailboxStatusChanged(e);
161        }
162    };
163   
164    /** The message node listener. */
165    private MessageNodeListener messageNodeListener = new MessageNodeListener() {
166                public void messageStatusChanged(MessageNodeEvent e) {
167                        invokeLater(new EventObjectRunnable(e) {
168                                public void run() {
169                                        messageNode_MessageStatusChanged((MessageNodeEvent)getEvent());
170                                }
171                        });
172                }
173    };
174
175    /* (non-Javadoc)
176     * @see org.logicprobe.LogicMail.ui.BaseScreen#onDisplay()
177     */
178    public void onDisplay() {
179        super.onDisplay();
180       
181        String eventType;
182        switch(mailboxNode.getType()) {
183        case MailboxNode.TYPE_INBOX:
184            eventType = "Mailbox_INBOX";
185            break;
186        case MailboxNode.TYPE_OUTBOX:
187            eventType = "Mailbox_OUTBOX";
188            break;
189        case MailboxNode.TYPE_DRAFTS:
190            eventType = "Mailbox_DRAFTS";
191            break;
192        case MailboxNode.TYPE_SENT:
193            eventType = "Mailbox_SENT";
194            break;
195        case MailboxNode.TYPE_TRASH:
196            eventType = "Mailbox_TRASH";
197            break;
198        default:
199            eventType = "Mailbox_NORMAL";
200            break;
201        }
202        String contentGroup = mailboxNode.getParentAccount().getProtocolName();
203        AnalyticsDataCollector.getInstance().onScreenView(getScreenPath(), getScreenName(), eventType, contentGroup);
204       
205        if(this.hideDeleted != globalConfig.getHideDeletedMsg()) {
206            this.hideDeleted = !this.hideDeleted;
207            displayableChanged();
208        }
209       
210        if(this.displayOrder != globalConfig.getDispOrder()) {
211            this.displayOrder = !this.displayOrder;
212            displayableChanged();
213        }
214        this.mailSettings.addMailSettingsListener(mailSettingsListener);
215        this.mailboxNode.addMailboxNodeListener(mailboxNodeListener);
216       
217        if(firstDisplay) {
218            MessageNode[] initialMessages = this.mailboxNode.getMessages();
219            for(int i=0; i<initialMessages.length; i++) {
220                knownMessages.addElement(initialMessages[i]);
221                if(isMessageDisplayable(initialMessages[i])) {
222                        insertDisplayableMessage(initialMessages[i]);
223                }
224            }
225           
226                this.mailboxNode.refreshMessages(true);
227                firstDisplay = false;
228        }
229        int size = knownMessages.size();
230        for(int i=0; i<size; i++) {
231                ((MessageNode)knownMessages.elementAt(i)).addMessageNodeListener(messageNodeListener);
232        }
233       
234        composeEnabled = (mailboxNode.getParentAccount() instanceof NetworkAccountNode)
235            && ((NetworkAccountNode)mailboxNode.getParentAccount()).hasMailSender();
236       
237        if(hasTouchscreen) {
238            ((StandardScreen)screen).setShortcutEnabled(SHORTCUT_COMPOSE, composeEnabled);
239            updateShortcuts();
240        }
241    }
242   
243        /* (non-Javadoc)
244         * @see org.logicprobe.LogicMail.ui.BaseScreen#onUndisplay()
245         */
246        public void onUndisplay() {
247        this.mailSettings.removeMailSettingsListener(mailSettingsListener);
248        this.mailboxNode.removeMailboxNodeListener(mailboxNodeListener);
249        int size = knownMessages.size();
250        for(int i=0; i<size; i++) {
251                ((MessageNode)knownMessages.elementAt(i)).removeMessageNodeListener(messageNodeListener);
252        }
253    }
254   
255        /* (non-Javadoc)
256         * @see org.logicprobe.LogicMail.ui.AbstractScreenProvider#hasShortcuts()
257         */
258        public boolean hasShortcuts() {
259                return true;
260        }
261       
262        /* (non-Javadoc)
263         * @see org.logicprobe.LogicMail.ui.AbstractScreenProvider#getShortcuts()
264         */
265        public ShortcutItem[] getShortcuts() {
266                // Note: This method is only called once, during initialization of the screen,
267                // and only on devices that have touchscreen support.
268                return new ShortcutItem[] {
269                        new ShortcutItem(
270                                        SHORTCUT_COMPOSE,
271                                        resources.getString(LogicMailResource.MENUITEM_COMPOSE_EMAIL),
272                                        "shortcut-compose.png", "shortcut-compose-d.png"),
273                        new ShortcutItem(
274                                SHORTCUT_OPEN,
275                                resources.getString(LogicMailResource.MENUITEM_SELECT),
276                                "shortcut-message-open.png", "shortcut-message-open-d.png"),
277            new ShortcutItem(
278                    SHORTCUT_DELETE,
279                    resources.getString(LogicMailResource.MENUITEM_DELETE),
280                    "shortcut-message-delete.png", "shortcut-message-delete-d.png"),
281                        new ShortcutItem(
282                                        SHORTCUT_UP,
283                                        resources.getString(LogicMailResource.MENUITEM_SCROLL_UP),
284                                        "shortcut-up.png", "shortcut-up-d.png"),
285                        new ShortcutItem(
286                                        SHORTCUT_DOWN,
287                                        resources.getString(LogicMailResource.MENUITEM_SCROLL_DOWN),
288                                        "shortcut-down.png", "shortcut-down-d.png")
289                };
290        }
291   
292        /**
293         * Listener to be added to all fields in the message field manager, so that
294         * focus changes can be detected to update the shortcut bar.  This is
295         * unfortunately the only reliable way to handle this situation.
296         */
297    private FocusChangeListener messageFieldFocusChangeListener = new FocusChangeListener() {
298        public void focusChanged(Field field, int eventType) {
299            if(hasTouchscreen) {
300                updateShortcuts();
301            }
302        }
303    };
304       
305        /**
306         * Initializes the fields.
307         */
308        public void initFields(Screen screen) {
309        super.initFields(screen);
310
311                messageFieldManager = new VerticalFieldManager(Manager.VERTICAL_SCROLL | Manager.VERTICAL_SCROLLBAR) {
312                    protected boolean navigationMovement(int dx, int dy, int status, int time) {
313                        navigationMoved = true;
314                        return super.navigationMovement(dx, dy, status, time);
315                    }
316                    public void add(Field field) {
317                        field.setFocusListener(messageFieldFocusChangeListener);
318                        super.add(field);
319                    }
320                    public void insert(Field field, int index) {
321                field.setFocusListener(messageFieldFocusChangeListener);
322                        super.insert(field, index);
323                    }
324                    public void delete(Field field) {
325                        field.setFocusListener(null);
326                        super.delete(field);
327                        updateShortcuts();
328                    }
329                    public void deleteRange(int start, int count) {
330                for(int i=start; i < start + count; i++) {
331                    getField(start).setFocusListener(null);
332                }
333                        super.deleteRange(start, count);
334                        updateShortcuts();
335                    }
336                };
337               
338        screen.add(messageFieldManager);
339        this.messageActions = navigationController.getMessageActions();
340        initMenuItems();
341        }   
342       
343        private void updateShortcuts() {
344            int fieldCount = messageFieldManager.getFieldCount();
345            StandardScreen standardScreen = (StandardScreen)screen;
346           
347            if(fieldCount <= 1) {
348                standardScreen.setShortcutEnabled(SHORTCUT_UP, false);
349                standardScreen.setShortcutEnabled(SHORTCUT_DOWN, false);
350                standardScreen.setShortcutEnabled(SHORTCUT_OPEN, false);
351                standardScreen.setShortcutEnabled(SHORTCUT_DELETE, false);
352            return;
353            }
354
355            boolean openEnabled = false;
356            boolean deleteEnabled = false;
357        Field fieldWithFocus = messageFieldManager.getFieldWithFocus();
358        if(fieldWithFocus instanceof MailboxMessageField) {
359            MessageNode messageNode = ((MailboxMessageField)fieldWithFocus).getMessageNode();
360            openEnabled = messageNode.existsOnServer() || messageNode.hasCachedContent();
361            deleteEnabled = (messageNode.getFlags() & MessageNode.Flag.DELETED) == 0;
362        }
363        int focusIndex = messageFieldManager.getFieldWithFocusIndex();
364        if(focusIndex == 0) {
365            standardScreen.setShortcutEnabled(SHORTCUT_UP, false);
366            standardScreen.setShortcutEnabled(SHORTCUT_DOWN, true);
367        }
368        else if(focusIndex == fieldCount - 1) {
369            standardScreen.setShortcutEnabled(SHORTCUT_UP, true);
370            standardScreen.setShortcutEnabled(SHORTCUT_DOWN, false);
371        }
372        else {
373            standardScreen.setShortcutEnabled(SHORTCUT_UP, true);
374            standardScreen.setShortcutEnabled(SHORTCUT_DOWN, true);
375        }
376       
377        standardScreen.setShortcutEnabled(SHORTCUT_OPEN, openEnabled);
378        standardScreen.setShortcutEnabled(SHORTCUT_DELETE, deleteEnabled);
379    }
380
381    protected void initMenuItems() {
382            compositionItem = new MenuItem(resources, LogicMailResource.MENUITEM_COMPOSE_EMAIL, 400100, 2000) {
383                public void run() {
384                    AnalyticsDataCollector.getInstance().onButtonClick(getScreenPath(), getScreenName(), "composition");
385                        navigationController.displayComposition((NetworkAccountNode)mailboxNode.getParentAccount());
386                }
387            };
388            markPriorOpenedItem = new MenuItem(resources, LogicMailResource.MENUITEM_MARK_PRIOR_OPENED, 400110, 2000) {
389            public void run() {
390                if(!(currentContextField instanceof MessageSeparatorField)) { return; }
391               
392                int choice = Dialog.ask(
393                        resources.getString(LogicMailResource.MAILBOX_MARK_ALL_PRIOR_ITEMS_OPENED_PROMPT),
394                        new Object[] {
395                                resources.getString(LogicMailResource.MENUITEM_MARK_OPENED),
396                                resources.getString(LogicMailResource.MENUITEM_CANCEL)
397                        },
398                        new int[] { Dialog.OK, Dialog.CANCEL },
399                        Dialog.CANCEL);
400               
401                if(choice == Dialog.OK) {
402                    AnalyticsDataCollector.getInstance().onButtonClick(getScreenPath(), getScreenName(), "markPriorOpened");
403                    Date separatorDate = ((MessageSeparatorField)currentContextField).getDate();
404                   
405                    // The separator date is midnight on the day it shows,
406                    // so we pass a date that is exactly one day past it to
407                    // ensure the desired effect of this request.
408                    mailboxNode.markPriorMessagesOpened(new Date(separatorDate.getTime() + 86400000));
409                }
410            }
411        };
412        }
413
414    /* (non-Javadoc)
415     * @see org.logicprobe.LogicMail.ui.BaseScreen#makeMenu(net.rim.device.api.ui.component.Menu, int)
416     */
417    public void makeMenu(Menu menu, int instance) {
418        currentContextField = messageFieldManager.getFieldWithFocus();
419        if(currentContextField instanceof MailboxMessageField) {
420            makeMessageMenu((MailboxMessageField)currentContextField, menu, instance);
421        }
422        else if(currentContextField instanceof MessageSeparatorField) {
423            makeSeparatorMenu((MessageSeparatorField)currentContextField, menu, instance);
424        }
425    }
426
427    protected void makeMessageMenu(MailboxMessageField messageField, Menu menu, int instance) {
428        MessageNode messageNode = messageField.getMessageNode();
429        if(instance == Menu.INSTANCE_DEFAULT) {
430            messageActions.makeMenu(menu, instance, messageNode, false);
431            if(composeEnabled) {
432                menu.add(compositionItem);
433            }
434        }
435        else {
436            messageActions.makeContextMenu(menu, instance, messageNode, false);
437        }
438    }
439   
440    protected void makeSeparatorMenu(MessageSeparatorField separatorField, Menu menu, int instance) {
441        if(composeEnabled) {
442            menu.add(compositionItem);
443        }
444        menu.add(markPriorOpenedItem);
445    }
446   
447    public boolean onClose() {
448        // Check for deleted messages in the mailbox
449        if(mailboxNode.getParentAccount().hasExpunge()
450                && mailboxNode.hasDeletedMessages()) {
451            MailSettings mailSettings = MailSettings.getInstance();
452            int expungeMode = globalConfig.getExpungeMode();
453            if(expungeMode == GlobalConfig.EXPUNGE_PROMPT) {
454                Dialog dialog = new Dialog(
455                        Dialog.D_YES_NO,
456                        resources.getString(LogicMailResource.MAILBOX_EXPUNGE_PROMPT),
457                        Dialog.NO,
458                        Bitmap.getPredefinedBitmap(Bitmap.QUESTION), 0, true);
459                int choice = dialog.doModal();
460
461                // Request expunge if desired
462                if(choice == Dialog.YES) {
463                    mailboxNode.expungeDeletedMessages();
464                }
465
466                if(dialog.getDontAskAgainValue()) {
467                    if(choice == Dialog.YES) {
468                        globalConfig.setExpungeMode(GlobalConfig.EXPUNGE_ALWAYS);
469                        mailSettings.saveSettings();
470                    }
471                    else if(choice == Dialog.NO) {
472                        globalConfig.setExpungeMode(GlobalConfig.EXPUNGE_NEVER);
473                        mailSettings.saveSettings();
474                    }
475                }
476            }
477            else if(expungeMode == GlobalConfig.EXPUNGE_ALWAYS) {
478                mailboxNode.expungeDeletedMessages();
479            }
480        }
481
482        // Close the screen
483        screen.close();
484        return true;
485    }
486   
487    /**
488     * Handles mailbox status change events.
489     *
490     * @param e Event data.
491     */
492    private void mailboxNode_MailboxStatusChanged(final MailboxNodeEvent e) {
493        int type = e.getType();
494        if(type == MailboxNodeEvent.TYPE_NEW_MESSAGES) {
495            invokeLater(new Runnable() {
496                public void run() {
497                    handleMailboxNewMessages(e.getAffectedMessages());
498                }
499            });
500        }
501        else if(type == MailboxNodeEvent.TYPE_DELETED_MESSAGES) {
502            invokeLater(new Runnable() {
503                public void run() {
504                    handleMailboxDeletedMessages(e.getAffectedMessages());
505                }
506            });
507        }
508        else if(type == MailboxNodeEvent.TYPE_FETCH_COMPLETE) {
509            // Collect the gaps before scheduling the operation for the UI
510            // thread.  This is necessary because the gap-finding operation
511            // could be time consuming.
512            final MessageNode[][] gaps = mailboxNode.findMessageNodeGaps();
513            if(gaps == null || gaps.length == 0) { return; }
514           
515            invokeLater(new Runnable() {
516                public void run() {
517                    handleMessageNodeGaps(gaps);
518                }
519            });
520        }
521    }
522
523    private void handleMailboxNewMessages(MessageNode[] messageNodes) {
524        for(int i=0; i<messageNodes.length; i++) {
525                knownMessages.addElement(messageNodes[i]);
526               
527                if(isMessageDisplayable(messageNodes[i])) {
528                        // Insert the message
529                        insertDisplayableMessage(messageNodes[i]);
530                }
531               
532                if(screen != null && screen.isDisplayed()) {
533                        messageNodes[i].addMessageNodeListener(messageNodeListener);
534                }
535        }
536    }
537
538    private void handleMailboxDeletedMessages(MessageNode[] messageNodes) {
539        for(int i=0; i<messageNodes.length; i++) {
540            if(screen != null && screen.isDisplayed()) {
541                messageNodes[i].removeMessageNodeListener(messageNodeListener);
542            }
543            removeDisplayableMessage(messageNodes[i]);
544            knownMessages.removeElement(messageNodes[i]);
545        }
546    }
547
548    private void handleMessageNodeGaps(MessageNode[][] gaps) {
549        // Build a set of all gap fields currently on the screen
550        Hashtable orphanedGapFieldSet = new Hashtable();
551        int size = messageFieldManager.getFieldCount();
552        for(int i=0; i<size; i++) {
553            Field fieldAtIndex = messageFieldManager.getField(i);
554            if(fieldAtIndex instanceof MailboxActionField) {
555                orphanedGapFieldSet.put(fieldAtIndex, Boolean.TRUE);
556            }
557        }
558
559        for(int i = 0; i < gaps.length; i++) {
560            MailboxActionField gapField = null;
561            int insertIndex;
562            if(displayOrder) {
563                // Ascending order
564
565                // Find the field for message after the gap
566                MailboxMessageField messageFieldAfter = (MailboxMessageField)messageNodeToFieldMap.get(gaps[i][1]);
567                if(messageFieldAfter == null) { continue; }
568                insertIndex = messageFieldAfter.getIndex();
569
570                // See if there is an existing field that can be taken over
571                if(insertIndex - 1 > 0) {
572                    Field fieldAtIndex = messageFieldManager.getField(insertIndex - 1);
573                    if(fieldAtIndex instanceof MailboxActionField) {
574                        // Take over an existing field and remove it from the
575                        // orphaned field set
576                        gapField = (MailboxActionField)fieldAtIndex;
577                        orphanedGapFieldSet.remove(gapField);
578                    }
579                }
580            }
581            else {
582                // Descending order
583
584                // Find the field for message before the gap
585                MailboxMessageField messageFieldBefore = (MailboxMessageField)messageNodeToFieldMap.get(gaps[i][1]);
586                if(messageFieldBefore == null) { continue; }
587                insertIndex = messageFieldBefore.getIndex() + 1;
588
589                // See if there is an existing field that can be taken over
590                if(insertIndex < messageFieldManager.getFieldCount()) {
591                    Field fieldAtIndex = messageFieldManager.getField(insertIndex);
592                    if(fieldAtIndex instanceof MailboxActionField) {
593                        // Take over an existing field and remove it from the
594                        // orphaned field set
595                        gapField = (MailboxActionField)fieldAtIndex;
596                        orphanedGapFieldSet.remove(gapField);
597                    }
598                }
599            }
600
601            // Create a new field, if necessary, and set field properties
602            if(gapField == null) {
603                gapField = new MailboxActionField(
604                        resources.getString(LogicMailResource.MAILBOX_LOAD_MORE_MESSAGES),
605                        Field.USE_ALL_WIDTH | Field.FOCUSABLE);
606                messageFieldManager.insert(gapField, insertIndex);
607            }
608            gapField.setEditable(true);
609            gapField.setTagObject(gaps[i]);
610        }
611
612        // Remove orphaned gap fields
613        Enumeration e = orphanedGapFieldSet.keys();
614        while(e.hasMoreElements()) {
615            messageFieldManager.delete((MailboxActionField)e.nextElement());
616        }
617    }
618   
619    /**
620     * Determines whether a message should be displayed,
621     * per the configuration.
622     *
623     * @param messageNode Message to check.
624     *
625     * @return True if it should be displayed, false otherwise.
626     */
627    private boolean isMessageDisplayable(MessageNode messageNode) {
628        if((messageNode.getFlags() & MessageNode.Flag.DELETED) != 0
629                && hideDeleted) {
630            return false;
631        }
632        else {
633            return true;
634        }
635    }
636   
637    /**
638     * A configuration item affecting the displayable state of messages has
639     * changed.  Since this is infrequent, this operation will take the simple
640     * approach of emptying and repopulating the screen.
641     */
642    private void displayableChanged() {
643        int size = knownMessages.size();
644        if(size == 0) { return; }
645       
646        // Clear out all the existing content from the field manager and map
647        messageNodeToFieldMap.clear();
648        messageFieldManager.deleteAll();
649       
650        // Reset the flag that controls field focus behavior
651        navigationMoved = false;
652       
653        // Get the known messages display them if appropriate
654        for(int i=0; i<size; i++) {
655            MessageNode messageNode = (MessageNode)knownMessages.elementAt(i);
656            if(isMessageDisplayable(messageNode)) {
657                insertDisplayableMessage(messageNode);
658            }
659        }
660    }
661   
662    /**
663     * Insert a message into the list and associated data structures.
664     * This will insert into the correct order, per the configuration.
665     *
666     * @param messageNode Message to insert.
667     */
668    private void insertDisplayableMessage(MessageNode messageNode) {
669        Field selectedField = messageFieldManager.getFieldWithFocus();
670       
671        int fieldCount = messageFieldManager.getFieldCount();
672                if(fieldCount > 0) {
673                        Comparator comparator = MessageNode.getComparator();
674                        int index = messageFieldManager.getFieldCount();
675                       
676                        if(displayOrder) {
677                                // Ascending order
678                                MessageNode lastMessage = getLastDisplayedMessage(index - 1);
679                                while(lastMessage != null && index > 0 && comparator.compare(lastMessage, messageNode) >= 0) {
680                                        index--;
681                                        if(index > 0) { lastMessage = getLastDisplayedMessage(index - 1); }
682                                }
683               
684                // Deal with the case of trying to insert right before an action field
685                if(index < fieldCount - 1 && messageFieldManager.getField(index + 1) instanceof MailboxActionField) {
686                    index++;
687                }
688                        }
689                        else {
690                                // Descending order
691                                MessageNode lastMessage = getLastDisplayedMessage(index - 1);
692                                while(lastMessage != null && index > 0 && comparator.compare(lastMessage, messageNode) <= 0) {
693                                        index--;
694                                        if(index > 0) { lastMessage = getLastDisplayedMessage(index - 1); }
695                                }
696                               
697                                // Deal with the case of trying to insert right after an action field
698                                if(index > 0 && messageFieldManager.getField(index - 1) instanceof MailboxActionField) {
699                                    index--;
700                                }
701                        }
702                        MailboxMessageField mailboxMessageField =
703                                new MailboxMessageField(mailboxNode, messageNode, Field.USE_ALL_WIDTH | Field.FOCUSABLE);
704                        messageNodeToFieldMap.put(messageNode, mailboxMessageField);
705            insertMessageField(mailboxMessageField, index);
706                }
707                else {
708                        MailboxMessageField mailboxMessageField =
709                                new MailboxMessageField(mailboxNode, messageNode, Field.USE_ALL_WIDTH | Field.FOCUSABLE);
710                        messageNodeToFieldMap.put(messageNode, mailboxMessageField);
711                        insertMessageField(mailboxMessageField, 0);
712                }
713               
714                if(!navigationMoved) {
715                    // Select newest message
716            int count = messageFieldManager.getFieldCount();
717            if(count > 0) {
718                if(displayOrder) {
719                    messageFieldManager.getField(count - 1).setFocus();
720                }
721                else {
722                    messageFieldManager.getField(0).setFocus();
723                }
724            }
725                }
726                else {
727                    // Keep previous selection
728            if(selectedField != null) { selectedField.setFocus(); }
729                }
730    }
731
732    /**
733     * Remove a message from the list and associated data structures,
734     * if that message exists in the list.
735     *
736     * @param messageNode Message to insert.
737     */
738    private void removeDisplayableMessage(MessageNode messageNode) {
739        MailboxMessageField mailboxMessageField = (MailboxMessageField)messageNodeToFieldMap.remove(messageNode);
740        if(mailboxMessageField == null) { return; }
741
742        deleteMessageField(mailboxMessageField);
743    }
744   
745    private void insertMessageField(MailboxMessageField messageField, int index) {
746        // It is assumed that the index will only be 0 for the first message.
747       
748        Date messageDate = messageField.getMessageNode().getDate();
749       
750        if(displayOrder) {
751            // Ascending order
752            if(messageFieldManager.getFieldCount() == 0) {
753                messageFieldManager.add(messageField);
754                messageFieldManager.add(new MessageSeparatorField(messageDate));
755            }
756            else {
757                if(index > 0 && messageFieldManager.getField(index - 1) instanceof MessageSeparatorField) {
758                    Date sepDate = ((MessageSeparatorField)messageFieldManager.getField(index - 1)).getDate();
759                    if(DateTimeUtilities.isSameDate(messageDate.getTime(), sepDate.getTime())) {
760                        index--;
761                    }
762                }
763               
764                Field nextField;
765                if(index < messageFieldManager.getFieldCount()) {
766                    nextField = messageFieldManager.getField(index);
767                }
768                else {
769                    nextField = null;
770                }
771               
772                if(nextField instanceof MessageSeparatorField) {
773                    Date sepDate = ((MessageSeparatorField)nextField).getDate();
774                    if(DateTimeUtilities.isSameDate(messageDate.getTime(), sepDate.getTime())) {
775                        messageFieldManager.insert(messageField, index);
776                    }
777                    else {
778                        messageFieldManager.insert(new MessageSeparatorField(messageDate), index + 1);
779                        messageFieldManager.insert(messageField, index + 1);
780                    }
781                }
782                else if(nextField instanceof MailboxMessageField) {
783                    Date nextDate = ((MailboxMessageField)nextField).getMessageNode().getDate();
784                    if(DateTimeUtilities.isSameDate(messageDate.getTime(), nextDate.getTime())) {
785                        messageFieldManager.insert(messageField, index);
786                    }
787                    else {
788                        messageFieldManager.insert(new MessageSeparatorField(messageDate), index);
789                        messageFieldManager.insert(messageField, index);
790                    }
791                }
792                else {
793                    messageFieldManager.insert(new MessageSeparatorField(messageDate), index);
794                    messageFieldManager.insert(messageField, index);
795                }
796            }
797        }
798        else {
799            // Descending order
800            if(messageFieldManager.getFieldCount() == 0) {
801                messageFieldManager.add(new MessageSeparatorField(messageDate));
802                messageFieldManager.add(messageField);
803            }
804            else  {
805                Field prevField = messageFieldManager.getField(index - 1);
806
807                if(prevField instanceof MessageSeparatorField) {
808                    Date sepDate = ((MessageSeparatorField)prevField).getDate();
809                    if(!DateTimeUtilities.isSameDate(messageDate.getTime(), sepDate.getTime())
810                            && index > 1
811                            && messageFieldManager.getField(index - 2) instanceof MailboxMessageField) {
812                        index--;
813                        prevField = messageFieldManager.getField(index - 1);
814                    }
815                }
816               
817                if(prevField instanceof MessageSeparatorField) {
818                    Date sepDate = ((MessageSeparatorField)prevField).getDate();
819                    if(DateTimeUtilities.isSameDate(messageDate.getTime(), sepDate.getTime())) {
820                        messageFieldManager.insert(messageField, index);
821                    }
822                    else {
823                        messageFieldManager.insert(new MessageSeparatorField(messageDate), index - 1);
824                        messageFieldManager.insert(messageField, index);
825                    }
826                }
827                else {
828                    Date prevDate = ((MailboxMessageField)prevField).getMessageNode().getDate();
829                    if(DateTimeUtilities.isSameDate(messageDate.getTime(), prevDate.getTime())) {
830                        messageFieldManager.insert(messageField, index);
831                    }
832                    else {
833                        messageFieldManager.insert(new MessageSeparatorField(messageDate), index);
834                        messageFieldManager.insert(messageField, index + 1);
835                    }
836                }
837            }
838        }
839    }
840   
841    private void deleteMessageField(MailboxMessageField messageField) {
842        int fieldCount = messageFieldManager.getFieldCount();
843        int index = messageField.getIndex();
844       
845        if(displayOrder) {
846            // Ascending order
847            if(messageFieldManager.getField(index + 1) instanceof MessageSeparatorField
848                    && (index == 0 || messageFieldManager.getField(index - 1) instanceof MessageSeparatorField)) {
849                messageFieldManager.deleteRange(index, 2);
850            }
851            else {
852                messageFieldManager.delete(messageField);
853            }
854        }
855        else {
856            // Descending order
857            if(messageFieldManager.getField(index - 1) instanceof MessageSeparatorField
858                    && (index == fieldCount - 1 || messageFieldManager.getField(index + 1) instanceof MessageSeparatorField)) {
859                messageFieldManager.deleteRange(index - 1, 2);
860            }
861            else {
862                messageFieldManager.delete(messageField);
863            }
864        }
865    }
866   
867    /**
868     * Gets the last displayed message.
869     *
870     * @param index the index
871     *
872     * @return the last displayed message
873     */
874    private MessageNode getLastDisplayedMessage(int index) {
875        while(index >= 0) {
876                if(messageFieldManager.getField(index) instanceof MailboxMessageField) {
877                        return ((MailboxMessageField)messageFieldManager.getField(index)).getMessageNode();
878                }
879                index--;
880        }
881        return null;
882    }
883   
884    /**
885     * Handles message status change events.
886     *
887     * @param e Event data.
888     */
889        private void messageNode_MessageStatusChanged(MessageNodeEvent e) {
890                if(e.getType() == MessageNodeEvent.TYPE_FLAGS) {
891                        MessageNode messageNode = (MessageNode)e.getSource();
892                        boolean currentlyDisplayed = messageNodeToFieldMap.containsKey(messageNode);
893                        boolean displayable = isMessageDisplayable(messageNode);
894                       
895                        if(currentlyDisplayed && !displayable) {
896                                // Remove from display
897                            removeDisplayableMessage(messageNode);
898                        }
899                        else if(!currentlyDisplayed && displayable) {
900                                // Add to display
901                                insertDisplayableMessage(messageNode);
902                        }
903                        else if(currentlyDisplayed) {
904                                // Just a visual flag update, so find and invalidate the item
905                                MailboxMessageField mailboxMessageField = (MailboxMessageField)messageNodeToFieldMap.get(messageNode);
906                                mailboxMessageField.invalidate();
907                        }
908                }
909        }
910
911    /**
912     * Gets the selected message.
913     *
914     * @return the selected message
915     */
916    private MessageNode getSelectedMessage() {
917        Field fieldWithFocus = messageFieldManager.getFieldWithFocus();
918        if(fieldWithFocus instanceof MailboxMessageField) {
919                return ((MailboxMessageField)fieldWithFocus).getMessageNode();
920        }
921        else {
922                return null;
923        }
924    }
925   
926    /**
927     * Gets the selected message node gap, if available.
928     *
929     * @return the selected message gap range
930     */
931    private MailboxActionField getSelectedMessageGapField() {
932        Field fieldWithFocus = messageFieldManager.getFieldWithFocus();
933        if(fieldWithFocus instanceof MailboxActionField) {
934            MailboxActionField field = (MailboxActionField)fieldWithFocus;
935            if(field.getTagObject() instanceof MessageNode[]) {
936                return field;
937            }
938        }
939        return null;
940    }
941   
942    /* (non-Javadoc)
943     * @see net.rim.device.api.ui.Screen#navigationClick(int, int)
944     */
945    public boolean navigationClick(int status, int time) {
946        MessageNode messageNode = getSelectedMessage();
947        if(messageNode != null) {
948                messageActions.openMessage(messageNode);
949                return true;
950        }
951        MailboxActionField gapField = getSelectedMessageGapField();
952        if(gapField != null) {
953            handleMessageGapAction(gapField);
954            return true;
955        }
956       
957        return false;
958    }
959
960    /* (non-Javadoc)
961     * @see net.rim.device.api.ui.Screen#keyChar(char, int, int)
962     */
963    public boolean keyChar(char key, int status, int time) {
964        MessageNode messageNode;
965       
966        // First, check and see if any hard-coded shortcuts are applicable
967        switch(key) {
968        case Keypad.KEY_ENTER:
969            messageNode = getSelectedMessage();
970            if(messageNode != null) {
971                messageActions.openMessage(messageNode);
972                return true;
973            }
974            MailboxActionField gapField = getSelectedMessageGapField();
975            if(gapField != null) {
976                handleMessageGapAction(gapField);
977                return true;
978            }
979            break;
980        case Keypad.KEY_BACKSPACE:
981            messageNode = getSelectedMessage();
982            if(messageNode != null) {
983                messageActions.deleteMessage(messageNode);
984                return true;
985            }
986            break;
987        case Keypad.KEY_SPACE:
988            if(status == 0) {
989                screen.scroll(Manager.DOWNWARD);
990                return true;
991            }
992            else if(status == KeypadListener.STATUS_ALT) {
993                screen.scroll(Manager.UPWARD);
994                return true;
995            }
996            break;
997        }
998       
999        // Now check the keypad/locale-specific shortcuts
1000        int shortcut = KeyHandler.keyCharShortcut(key, status);
1001        switch(shortcut) {
1002        case KeyHandler.SCROLL_TOP:
1003            screen.scroll(Manager.TOPMOST);
1004            return true;
1005        case KeyHandler.SCROLL_BOTTOM:
1006            screen.scroll(Manager.BOTTOMMOST);
1007            return true;
1008        case KeyHandler.SCROLL_NEXT_DATE:
1009            scrollNextDate(displayOrder);
1010            return true;
1011        case KeyHandler.SCROLL_PREV_DATE:
1012            scrollNextDate(!displayOrder);
1013            return true;
1014        case KeyHandler.SCROLL_NEXT_UNOPENED:
1015            scrollNextUnopened();
1016            return true;
1017        case KeyHandler.MESSAGE_COMPOSE:
1018            if(composeEnabled) {
1019                compositionItem.run();
1020                return true;
1021            }
1022        default:
1023            messageNode = getSelectedMessage();
1024            if(messageNode != null) {
1025                if(messageActions.keyCharShortcut(messageNode, shortcut)) {
1026                    return true;
1027                }
1028            }
1029        }
1030       
1031        return false;
1032    }
1033   
1034    private void scrollNextDate(boolean direction) {
1035        int index = messageFieldManager.getFieldWithFocusIndex();
1036        int count = messageFieldManager.getFieldCount();
1037       
1038        if(direction) {
1039            // Upward
1040            index--;
1041            while(index >= 0) {
1042                if(messageFieldManager.getField(index) instanceof MessageSeparatorField) {
1043                    messageFieldManager.setFocus();
1044                    messageFieldManager.getField(index).setFocus();
1045                    break;
1046                }
1047                index--;
1048            }
1049        }
1050        else {
1051            // Downward
1052            index++;
1053            while(index < count) {
1054                if(messageFieldManager.getField(index) instanceof MessageSeparatorField) {
1055                    messageFieldManager.setFocus();
1056                    messageFieldManager.getField(index).setFocus();
1057                    break;
1058                }
1059                index++;
1060            }
1061        }
1062    }
1063
1064    private void scrollNextUnopened() {
1065        int index = messageFieldManager.getFieldWithFocusIndex();
1066        int count = messageFieldManager.getFieldCount();
1067       
1068        if(displayOrder) {
1069            // Ascending
1070            index--;
1071            while(index >= 0) {
1072                if(messageFieldManager.getField(index) instanceof MailboxMessageField) {
1073                    MailboxMessageField field = (MailboxMessageField)messageFieldManager.getField(index);
1074                    if((field.getMessageNode().getFlags() & MessageNode.Flag.SEEN) == 0) {
1075                        messageFieldManager.setFocus();
1076                        field.setFocus();
1077                        break;
1078                    }
1079                }
1080                index--;
1081            }
1082        }
1083        else {
1084            // Descending
1085            index++;
1086            while(index < count) {
1087                if(messageFieldManager.getField(index) instanceof MailboxMessageField) {
1088                    MailboxMessageField field = (MailboxMessageField)messageFieldManager.getField(index);
1089                    if((field.getMessageNode().getFlags() & MessageNode.Flag.SEEN) == 0) {
1090                        messageFieldManager.setFocus();
1091                        field.setFocus();
1092                        break;
1093                    }
1094                }
1095                index++;
1096            }
1097        }
1098    }
1099
1100    private void handleMessageGapAction(final MailboxActionField gapField) {
1101        AnalyticsDataCollector.getInstance().onButtonClick(getScreenPath(), getScreenName(), "requestMoreMessages");
1102       
1103        // This method inserts a short delay, where the gap action field is
1104        // disabled, prior to starting the request.  This is done to provide
1105        // feedback that the user has triggered an action before the field
1106        // disappears from the screen.
1107       
1108        final MessageNode[] gap = (MessageNode[])gapField.getTagObject();
1109        gapField.setEditable(false);
1110        (new Thread() { public void run() {
1111            try { Thread.sleep(200); } catch (InterruptedException e) { }
1112            invokeLater(new Runnable() { public void run() {
1113                messageFieldManager.delete(gapField);
1114                mailboxNode.requestMoreMessages(gap[1]);
1115            }});
1116        }}).start();
1117    }
1118
1119    /* (non-Javadoc)
1120     * @see org.logicprobe.LogicMail.ui.AbstractScreenProvider#shortcutAction(org.logicprobe.LogicMail.ui.ScreenProvider.ShortcutItem)
1121     */
1122    public void shortcutAction(ShortcutItem item) {
1123        MessageNode messageNode;
1124       
1125        switch(item.getId()) {
1126        case SHORTCUT_COMPOSE:
1127                compositionItem.run();
1128                break;
1129        case SHORTCUT_OPEN:
1130            messageNode = getSelectedMessage();
1131            if(messageNode != null) {
1132                messageActions.openMessage(messageNode);
1133            }
1134            break;
1135        case SHORTCUT_DELETE:
1136            messageNode = getSelectedMessage();
1137            if(messageNode != null) {
1138                messageActions.deleteMessage(messageNode);
1139            }
1140            break;
1141        case SHORTCUT_UP:
1142                screen.scroll(Manager.UPWARD);
1143                break;
1144        case SHORTCUT_DOWN:
1145                screen.scroll(Manager.DOWNWARD);
1146                break;
1147        }
1148    }
1149   
1150    protected static class MessageSeparatorField extends LabeledSeparatorField {
1151        private final Date date;
1152       
1153        public MessageSeparatorField(Date date) {
1154            super(Field.FOCUSABLE | LabeledSeparatorField.BOTTOM_BORDER);
1155   
1156            Calendar cal = Calendar.getInstance();
1157            cal.setTime(date);
1158            cal.set(Calendar.HOUR_OF_DAY, 0);
1159            cal.set(Calendar.MINUTE, 0);
1160            cal.set(Calendar.SECOND, 0);
1161            cal.set(Calendar.MILLISECOND, 0);
1162           
1163            StringBuffer buffer = new StringBuffer();
1164            DateFormat.getInstance(DateFormat.DATE_LONG).format(cal, buffer, null);
1165            setText(buffer.toString());
1166           
1167            this.date = cal.getTime();
1168        }
1169       
1170        public Date getDate() {
1171            return date;
1172        }
1173    }
1174}
Note: See TracBrowser for help on using the repository browser.