001/**
002 *
003 * Copyright © 2017-2018 Florian Schmaus, 2016-2017 Fernando Ramirez
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.mam;
018
019import java.text.ParseException;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.UUID;
027import java.util.WeakHashMap;
028
029import org.jivesoftware.smack.ConnectionCreationListener;
030import org.jivesoftware.smack.Manager;
031import org.jivesoftware.smack.SmackException.NoResponseException;
032import org.jivesoftware.smack.SmackException.NotConnectedException;
033import org.jivesoftware.smack.SmackException.NotLoggedInException;
034import org.jivesoftware.smack.StanzaCollector;
035import org.jivesoftware.smack.XMPPConnection;
036import org.jivesoftware.smack.XMPPConnectionRegistry;
037import org.jivesoftware.smack.XMPPException.XMPPErrorException;
038import org.jivesoftware.smack.filter.IQReplyFilter;
039import org.jivesoftware.smack.packet.IQ;
040import org.jivesoftware.smack.packet.Message;
041import org.jivesoftware.smack.packet.Stanza;
042import org.jivesoftware.smack.util.Objects;
043import org.jivesoftware.smack.util.StringUtils;
044
045import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
046import org.jivesoftware.smackx.forward.packet.Forwarded;
047import org.jivesoftware.smackx.mam.element.MamElements;
048import org.jivesoftware.smackx.mam.element.MamElements.MamResultExtension;
049import org.jivesoftware.smackx.mam.element.MamFinIQ;
050import org.jivesoftware.smackx.mam.element.MamPrefsIQ;
051import org.jivesoftware.smackx.mam.element.MamPrefsIQ.DefaultBehavior;
052import org.jivesoftware.smackx.mam.element.MamQueryIQ;
053import org.jivesoftware.smackx.mam.filter.MamResultFilter;
054import org.jivesoftware.smackx.muc.MultiUserChat;
055import org.jivesoftware.smackx.rsm.packet.RSMSet;
056import org.jivesoftware.smackx.xdata.FormField;
057import org.jivesoftware.smackx.xdata.packet.DataForm;
058
059import org.jxmpp.jid.EntityBareJid;
060import org.jxmpp.jid.EntityFullJid;
061import org.jxmpp.jid.Jid;
062
063/**
064 * A Manager for Message Archive Management (MAM, <a href="http://xmpp.org/extensions/xep-0313.html">XEP-0313</a>).
065 *
066 * <h2>Get an instance of a manager for a message archive</h2>
067 *
068 * In order to work with {@link MamManager} you need to obtain an instance for a particular archive.
069 * To get the instance for the default archive on the user's server, use the {@link #getInstanceFor(XMPPConnection)} method.
070 *
071 * <pre>
072 * {@code
073 * XMPPConnection connection = ...
074 * MamManager mamManager = MamManager.getInstanceFor(connection);
075 * }
076 * </pre>
077 *
078 * If you want to retrieve a manager for a different archive use {@link #getInstanceFor(XMPPConnection, Jid)}, which takes the archive's XMPP address as second argument.
079 *
080 * <h2>Check if MAM is supported</h2>
081 *
082 * After you got your manager instance, you probably first want to check if MAM is supported.
083 * Simply use {@link #isSupported()} to check if there is a MAM archive available.
084 *
085 * <pre>
086 * {@code
087 * boolean isSupported = mamManager.isSupported();
088 * }
089 * </pre>
090 *
091 * <h2>Message Archive Preferences</h2>
092 *
093 * After you have verified that the MAM is supported, you probably want to configure the archive first before using it.
094 * One of the most important preference is to enable MAM for your account.
095 * Some servers set up new accounts with MAM disabled by default.
096 * You can do so by calling {@link #enableMamForAllMessages()}.
097 *
098 * <h3>Retrieve current preferences</h3>
099 *
100 * The archive's preferences can be retrieved using {@link #retrieveArchivingPreferences()}.
101 *
102 * <h3>Update preferences</h3>
103 *
104 * Use {@link MamPrefsResult#asMamPrefs()} to get a modifiable {@link MamPrefs} instance.
105 * After performing the desired changes, use {@link #updateArchivingPreferences(MamPrefs)} to update the preferences.
106 *
107 * <h2>Query the message archive</h2>
108 *
109 * Querying a message archive involves a two step process. First you need specify the query's arguments, for example a date range.
110 * The query arguments of a particular query are represented by a {@link MamQueryArgs} instance, which can be build using {@link MamQueryArgs.Builder}.
111 *
112 * After you have build such an instance, use {@link #queryArchive(MamQueryArgs)} to issue the query.
113 *
114 * <pre>
115 * {@code
116 * MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
117 *                                 .withJid(jid)
118 *                                 .setResultPageSize(10)
119 *                                 .queryLastPage()
120 *                                 .build();
121 * MamQuery mamQuery = mamManager.queryArchive(mamQueryArgs);
122 * }
123 * </pre>
124 *
125 * On success {@link #queryArchive(MamQueryArgs)} returns a {@link MamQuery} instance.
126 * The instance will hold one page of the queries result set.
127 * Use {@link MamQuery#getMessages()} to retrieve the messages of the archive belonging to the page.
128 *
129 * You can get the whole page including all metadata using {@link MamQuery#getPage()}.
130 *
131 * <h2>Paging through the results</h2>
132 *
133 * Because the matching result set could be potentially very big, a MAM service will probably not return all matching messages.
134 * Instead the results are possibly send in multiple pages.
135 * To check if the result was complete or if there are further pages, use {@link MamQuery#isComplete()}.
136 * If this method returns {@code false}, then you may want to page through the archive.
137 *
138 * {@link MamQuery} provides convince methods to do so: {@link MamQuery#pageNext(int)} and {@link MamQuery#pagePrevious(int)}.
139 *
140 * <pre>
141 * {@code
142 * MamQuery nextPageMamQuery = mamQuery.pageNext(10);
143 * }
144 * </pre>
145 *
146 * <h2>Get the supported form fields</h2>
147 *
148 * You can use {@link #retrieveFormFields()} to retrieve a list of the supported additional form fields by this archive.
149 * Those fields can be used for further restrict a query.
150 *
151 *
152 * @see <a href="http://xmpp.org/extensions/xep-0313.html">XEP-0313: Message
153 *      Archive Management</a>
154 * @author Florian Schmaus
155 * @author Fernando Ramirez
156 *
157 */
158public final class MamManager extends Manager {
159
160    static {
161        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
162            @Override
163            public void connectionCreated(XMPPConnection connection) {
164                getInstanceFor(connection);
165            }
166        });
167    }
168
169    private static final String FORM_FIELD_WITH = "with";
170    private static final String FORM_FIELD_START = "start";
171    private static final String FORM_FIELD_END = "end";
172
173    private static final Map<XMPPConnection, Map<Jid, MamManager>> INSTANCES = new WeakHashMap<>();
174
175    /**
176     * Get a MamManager for the MAM archive of the local entity (the "user") of the given connection.
177     *
178     * @param connection the XMPP connection to get the archive for.
179     * @return the instance of MamManager.
180     */
181    public static MamManager getInstanceFor(XMPPConnection connection) {
182        return getInstanceFor(connection, (Jid) null);
183    }
184
185    /**
186     * Get a MamManager for the MAM archive of the given {@code MultiUserChat}. Note that not all MUCs support MAM,
187     * hence it is recommended to use {@link #isSupported()} to check if MAM is supported by the MUC.
188     *
189     * @param multiUserChat the MultiUserChat to retrieve the MamManager for.
190     * @return the MamManager for the given MultiUserChat.
191     * @since 4.3.0
192     */
193    public static MamManager getInstanceFor(MultiUserChat multiUserChat) {
194        XMPPConnection connection = multiUserChat.getXmppConnection();
195        Jid archiveAddress = multiUserChat.getRoom();
196        return getInstanceFor(connection, archiveAddress);
197    }
198
199    public static synchronized MamManager getInstanceFor(XMPPConnection connection, Jid archiveAddress) {
200        Map<Jid, MamManager> managers = INSTANCES.get(connection);
201        if (managers == null) {
202            managers = new HashMap<>();
203            INSTANCES.put(connection, managers);
204        }
205        MamManager mamManager = managers.get(archiveAddress);
206        if (mamManager == null) {
207            mamManager = new MamManager(connection, archiveAddress);
208            managers.put(archiveAddress, mamManager);
209        }
210        return mamManager;
211    }
212
213    private final Jid archiveAddress;
214
215    private final ServiceDiscoveryManager serviceDiscoveryManager;
216
217    private MamManager(XMPPConnection connection, Jid archiveAddress) {
218        super(connection);
219        this.archiveAddress = archiveAddress;
220        serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
221    }
222
223    /**
224     * The the XMPP address of this MAM archive. Note that this method may return {@code null} if this MamManager
225     * handles the local entity's archive and if the connection has never been authenticated at least once.
226     *
227     * @return the XMPP address of this MAM archive or {@code null}.
228     * @since 4.3.0
229     */
230    public Jid getArchiveAddress() {
231        if (archiveAddress == null) {
232            EntityFullJid localJid = connection().getUser();
233            if (localJid == null) {
234                return null;
235            }
236            return localJid.asBareJid();
237        }
238        return archiveAddress;
239    }
240
241    public static final class MamQueryArgs {
242        private final String node;
243
244        private final Map<String, FormField> formFields;
245
246        private final Integer maxResults;
247
248        private final String afterUid;
249
250        private final String beforeUid;
251
252        private MamQueryArgs(Builder builder) {
253            node = builder.node;
254            formFields = builder.formFields;
255            if (builder.maxResults > 0) {
256                maxResults = builder.maxResults;
257            } else {
258                maxResults = null;
259            }
260            afterUid = builder.afterUid;
261            beforeUid = builder.beforeUid;
262        }
263
264        private DataForm dataForm;
265
266        DataForm getDataForm() {
267            if (dataForm != null) {
268                return dataForm;
269            }
270            dataForm = getNewMamForm();
271            dataForm.addFields(formFields.values());
272            return dataForm;
273        }
274
275        void maybeAddRsmSet(MamQueryIQ mamQueryIQ) {
276            if (maxResults == null && afterUid == null && beforeUid == null) {
277                return;
278            }
279
280            int max;
281            if (maxResults != null) {
282                max = maxResults;
283            } else {
284                max = -1;
285            }
286
287            RSMSet rsmSet = new RSMSet(afterUid, beforeUid, -1, -1, null, max, null, -1);
288            mamQueryIQ.addExtension(rsmSet);
289        }
290
291        public static Builder builder() {
292            return new Builder();
293        }
294
295        public static final class Builder {
296            private String node;
297
298            private final Map<String, FormField> formFields = new HashMap<>(8);
299
300            private int maxResults = -1;
301
302            private String afterUid;
303
304            private String beforeUid;
305
306            public Builder queryNode(String node) {
307                if (node == null) {
308                    return this;
309                }
310
311                this.node = node;
312
313                return this;
314            }
315
316            public Builder limitResultsToJid(Jid withJid) {
317                if (withJid == null) {
318                    return this;
319                }
320
321                FormField formField = getWithFormField(withJid);
322                formFields.put(formField.getVariable(), formField);
323
324                return this;
325            }
326
327            public Builder limitResultsSince(Date start) {
328                if (start == null) {
329                    return this;
330                }
331
332                FormField formField = new FormField(FORM_FIELD_START);
333                formField.addValue(start);
334                formFields.put(formField.getVariable(), formField);
335
336                FormField endFormField = formFields.get(FORM_FIELD_END);
337                if (endFormField != null) {
338                    Date end;
339                    try {
340                        end = endFormField.getFirstValueAsDate();
341                    }
342                    catch (ParseException e) {
343                        throw new IllegalStateException(e);
344                    }
345                    if (end.getTime() <= start.getTime()) {
346                        throw new IllegalArgumentException("Given start date (" + start
347                                        + ") is after the existing end date (" + end + ')');
348                    }
349                }
350
351                return this;
352            }
353
354            public Builder limitResultsBefore(Date end) {
355                if (end == null) {
356                    return this;
357                }
358
359                FormField formField = new FormField(FORM_FIELD_END);
360                formField.addValue(end);
361                formFields.put(formField.getVariable(), formField);
362
363                FormField startFormField = formFields.get(FORM_FIELD_START);
364                if (startFormField != null) {
365                    Date start;
366                    try {
367                        start = startFormField.getFirstValueAsDate();
368                    } catch (ParseException e) {
369                        throw new IllegalStateException(e);
370                    }
371                    if (end.getTime() <= start.getTime()) {
372                        throw new IllegalArgumentException("Given end date (" + end
373                                        + ") is before the existing start date (" + start + ')');
374                    }
375                }
376
377                return this;
378            }
379
380            public Builder setResultPageSize(Integer max) {
381                if (max == null) {
382                    maxResults = -1;
383                    return this;
384                }
385                return setResultPageSizeTo(max.intValue());
386            }
387
388            public Builder setResultPageSizeTo(int max) {
389                if (max < 0) {
390                    throw new IllegalArgumentException();
391                }
392                this.maxResults = max;
393                return this;
394            }
395
396            /**
397             * Only return the count of messages the query yields, not the actual messages. Note that not all services
398             * return a correct count, some return an approximate count.
399             *
400             * @return an reference to this builder.
401             * @see <a href="https://xmpp.org/extensions/xep-0059.html#count">XEP-0059 § 2.7</a>
402             */
403            public Builder onlyReturnMessageCount() {
404                return setResultPageSizeTo(0);
405            }
406
407            public Builder withAdditionalFormField(FormField formField) {
408                formFields.put(formField.getVariable(), formField);
409                return this;
410            }
411
412            public Builder withAdditionalFormFields(List<FormField> additionalFields) {
413                for (FormField formField : additionalFields) {
414                    withAdditionalFormField(formField);
415                }
416                return this;
417            }
418
419            public Builder afterUid(String afterUid) {
420                this.afterUid = StringUtils.requireNullOrNotEmpty(afterUid, "afterUid must not be empty");
421                return this;
422            }
423
424            /**
425             * Specifies a message UID as 'before' anchor for the query. Note that unlike {@link #afterUid(String)} this
426             * method also accepts the empty String to query the last page of an archive (c.f. XEP-0059 § 2.5).
427             *
428             * @param beforeUid a message UID acting as 'before' query anchor.
429             * @return an instance to this builder.
430             */
431            public Builder beforeUid(String beforeUid) {
432                // We don't perform any argument validation, since every possible argument (null, empty string,
433                // non-empty string) is valid.
434                this.beforeUid = beforeUid;
435                return this;
436            }
437
438            /**
439             * Query from the last, i.e. most recent, page of the archive. This will return the very last page of the
440             * archive holding the most recent matching messages. You usually would page backwards from there on.
441             *
442             * @return a reference to this builder.
443             * @see <a href="https://xmpp.org/extensions/xep-0059.html#last">XEP-0059 § 2.5. Requesting the Last Page in
444             *      a Result Set</a>
445             */
446            public Builder queryLastPage() {
447                return beforeUid("");
448            }
449
450            public MamQueryArgs build() {
451                return new MamQueryArgs(this);
452            }
453        }
454    }
455
456    /**
457     * Query archive with a maximum amount of results.
458     *
459     * @param max
460     * @return the MAM query result
461     * @throws NoResponseException
462     * @throws XMPPErrorException
463     * @throws NotConnectedException
464     * @throws InterruptedException
465     * @throws NotLoggedInException
466     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
467     */
468    @Deprecated
469    // TODO Remove in Smack 4.4
470    public MamQueryResult queryArchive(Integer max) throws NoResponseException, XMPPErrorException,
471            NotConnectedException, InterruptedException, NotLoggedInException {
472        return queryArchive(null, max, null, null, null, null);
473    }
474
475    /**
476     * Query archive with a JID (only messages from/to the JID).
477     *
478     * @param withJid
479     * @return the MAM query result
480     * @throws NoResponseException
481     * @throws XMPPErrorException
482     * @throws NotConnectedException
483     * @throws InterruptedException
484     * @throws NotLoggedInException
485     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
486     */
487    @Deprecated
488    // TODO Remove in Smack 4.4
489    public MamQueryResult queryArchive(Jid withJid) throws NoResponseException, XMPPErrorException,
490            NotConnectedException, InterruptedException, NotLoggedInException {
491        return queryArchive(null, null, null, null, withJid, null);
492    }
493
494    /**
495     * Query archive filtering by start and/or end date. If start == null, the
496     * value of 'start' will be equal to the date/time of the earliest message
497     * stored in the archive. If end == null, the value of 'end' will be equal
498     * to the date/time of the most recent message stored in the archive.
499     *
500     * @param start
501     * @param end
502     * @return the MAM query result
503     * @throws NoResponseException
504     * @throws XMPPErrorException
505     * @throws NotConnectedException
506     * @throws InterruptedException
507     * @throws NotLoggedInException
508     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
509     */
510    @Deprecated
511    // TODO Remove in Smack 4.4
512    public MamQueryResult queryArchive(Date start, Date end) throws NoResponseException, XMPPErrorException,
513            NotConnectedException, InterruptedException, NotLoggedInException {
514        return queryArchive(null, null, start, end, null, null);
515    }
516
517    /**
518     * Query Archive adding filters with additional fields.
519     *
520     * @param additionalFields
521     * @return the MAM query result
522     * @throws NoResponseException
523     * @throws XMPPErrorException
524     * @throws NotConnectedException
525     * @throws InterruptedException
526     * @throws NotLoggedInException
527     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
528     */
529    @Deprecated
530    // TODO Remove in Smack 4.4
531    public MamQueryResult queryArchive(List<FormField> additionalFields) throws NoResponseException, XMPPErrorException,
532            NotConnectedException, InterruptedException, NotLoggedInException {
533        return queryArchive(null, null, null, null, null, additionalFields);
534    }
535
536    /**
537     * Query archive filtering by start date. The value of 'end' will be equal
538     * to the date/time of the most recent message stored in the archive.
539     *
540     * @param start
541     * @return the MAM query result
542     * @throws NoResponseException
543     * @throws XMPPErrorException
544     * @throws NotConnectedException
545     * @throws InterruptedException
546     * @throws NotLoggedInException
547     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
548     */
549    @Deprecated
550    // TODO Remove in Smack 4.4
551    public MamQueryResult queryArchiveWithStartDate(Date start) throws NoResponseException, XMPPErrorException,
552            NotConnectedException, InterruptedException, NotLoggedInException {
553        return queryArchive(null, null, start, null, null, null);
554    }
555
556    /**
557     * Query archive filtering by end date. The value of 'start' will be equal
558     * to the date/time of the earliest message stored in the archive.
559     *
560     * @param end
561     * @return the MAM query result
562     * @throws NoResponseException
563     * @throws XMPPErrorException
564     * @throws NotConnectedException
565     * @throws InterruptedException
566     * @throws NotLoggedInException
567     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
568     */
569    @Deprecated
570    // TODO Remove in Smack 4.4
571    public MamQueryResult queryArchiveWithEndDate(Date end) throws NoResponseException, XMPPErrorException,
572            NotConnectedException, InterruptedException, NotLoggedInException {
573        return queryArchive(null, null, null, end, null, null);
574    }
575
576
577    /**
578     * Query archive applying filters: max count, start date, end date, from/to
579     * JID and with additional fields.
580     *
581     * @param max
582     * @param start
583     * @param end
584     * @param withJid
585     * @param additionalFields
586     * @return the MAM query result
587     * @throws NoResponseException
588     * @throws XMPPErrorException
589     * @throws NotConnectedException
590     * @throws InterruptedException
591     * @throws NotLoggedInException
592     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
593     */
594    @Deprecated
595    // TODO Remove in Smack 4.4
596    public MamQueryResult queryArchive(Integer max, Date start, Date end, Jid withJid, List<FormField> additionalFields)
597            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
598            NotLoggedInException {
599      return queryArchive(null, max, start, end, withJid, additionalFields);
600    }
601
602
603    /**
604     * Query an message archive like a MUC archive or a PubSub node archive, addressed by an archiveAddress, applying
605     * filters: max count, start date, end date, from/to JID and with additional fields. When archiveAddress is null the
606     * default, the server will be requested.
607     *
608     * @param node The PubSub node name, can be null
609     * @param max
610     * @param start
611     * @param end
612     * @param withJid
613     * @param additionalFields
614     * @return the MAM query result
615     * @throws NoResponseException
616     * @throws XMPPErrorException
617     * @throws NotConnectedException
618     * @throws InterruptedException
619     * @throws NotLoggedInException
620     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
621     */
622    @Deprecated
623    // TODO Remove in Smack 4.4
624    public MamQueryResult queryArchive(String node, Integer max, Date start, Date end, Jid withJid,
625                    List<FormField> additionalFields)
626            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
627            NotLoggedInException {
628        MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
629                        .queryNode(node)
630                        .setResultPageSize(max)
631                        .limitResultsSince(start)
632                        .limitResultsBefore(end)
633                        .limitResultsToJid(withJid)
634                        .withAdditionalFormFields(additionalFields)
635                        .build();
636
637        MamQuery mamQuery = queryArchive(mamQueryArgs);
638        return new MamQueryResult(mamQuery);
639    }
640
641    public MamQuery queryArchive(MamQueryArgs mamQueryArgs) throws NoResponseException, XMPPErrorException,
642                    NotConnectedException, NotLoggedInException, InterruptedException {
643        String queryId = UUID.randomUUID().toString();
644        String node = mamQueryArgs.node;
645        DataForm dataForm = mamQueryArgs.getDataForm();
646
647        MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId, node, dataForm);
648        mamQueryIQ.setType(IQ.Type.set);
649        mamQueryIQ.setTo(archiveAddress);
650
651        mamQueryArgs.maybeAddRsmSet(mamQueryIQ);
652
653        return queryArchive(mamQueryIQ);
654    }
655
656    private static FormField getWithFormField(Jid withJid) {
657        FormField formField = new FormField(FORM_FIELD_WITH);
658        formField.addValue(withJid.toString());
659        return formField;
660    }
661
662    private static void addWithJid(Jid withJid, DataForm dataForm) {
663        if (withJid == null) {
664            return;
665        }
666        FormField formField = getWithFormField(withJid);
667        dataForm.addField(formField);
668    }
669
670    /**
671     * Returns a page of the archive.
672     *
673     * @param dataForm
674     * @param rsmSet
675     * @return the MAM query result
676     * @throws NoResponseException
677     * @throws XMPPErrorException
678     * @throws NotConnectedException
679     * @throws InterruptedException
680     * @throws NotLoggedInException
681     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
682     */
683    @Deprecated
684    // TODO Remove in Smack 4.4
685    public MamQueryResult page(DataForm dataForm, RSMSet rsmSet) throws NoResponseException, XMPPErrorException,
686                    NotConnectedException, InterruptedException, NotLoggedInException {
687        return page(null, dataForm, rsmSet);
688    }
689
690    /**
691     * Returns a page of the archive. This is a low-level method, you possibly do not want to use it directly unless you
692     * know what you are doing.
693     *
694     * @param node The PubSub node name, can be null
695     * @param dataForm
696     * @param rsmSet
697     * @return the MAM query result
698     * @throws NoResponseException
699     * @throws XMPPErrorException
700     * @throws NotConnectedException
701     * @throws InterruptedException
702     * @throws NotLoggedInException
703     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
704     */
705    @Deprecated
706    // TODO Remove in Smack 4.4
707    public MamQueryResult page(String node, DataForm dataForm, RSMSet rsmSet)
708                    throws NoResponseException, XMPPErrorException,
709            NotConnectedException, InterruptedException, NotLoggedInException {
710        MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, dataForm);
711        mamQueryIQ.setType(IQ.Type.set);
712        mamQueryIQ.setTo(archiveAddress);
713        mamQueryIQ.addExtension(rsmSet);
714        MamQuery mamQuery = queryArchive(mamQueryIQ);
715        return new MamQueryResult(mamQuery);
716    }
717
718    /**
719     * Returns the next page of the archive.
720     *
721     * @param mamQueryResult
722     *            is the previous query result
723     * @param count
724     *            is the amount of messages that a page contains
725     * @return the MAM query result
726     * @throws NoResponseException
727     * @throws XMPPErrorException
728     * @throws NotConnectedException
729     * @throws InterruptedException
730     * @throws NotLoggedInException
731     * @deprecated use {@link MamQuery#pageNext(int)} instead.
732     */
733    @Deprecated
734    // TODO Remove in Smack 4.4
735    public MamQueryResult pageNext(MamQueryResult mamQueryResult, int count) throws NoResponseException,
736            XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException {
737        RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet();
738        RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after);
739        return page(mamQueryResult, requestRsmSet);
740    }
741
742    /**
743     * Returns the previous page of the archive.
744     *
745     * @param mamQueryResult
746     *            is the previous query result
747     * @param count
748     *            is the amount of messages that a page contains
749     * @return the MAM query result
750     * @throws NoResponseException
751     * @throws XMPPErrorException
752     * @throws NotConnectedException
753     * @throws InterruptedException
754     * @throws NotLoggedInException
755     * @deprecated use {@link MamQuery#pagePrevious(int)} instead.
756     */
757    @Deprecated
758    // TODO Remove in Smack 4.4
759    public MamQueryResult pagePrevious(MamQueryResult mamQueryResult, int count) throws NoResponseException,
760            XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException {
761        RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet();
762        RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getFirst(), RSMSet.PageDirection.before);
763        return page(mamQueryResult, requestRsmSet);
764    }
765
766    private MamQueryResult page(MamQueryResult mamQueryResult, RSMSet requestRsmSet) throws NoResponseException,
767                    XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException {
768        ensureMamQueryResultMatchesThisManager(mamQueryResult);
769
770        return page(mamQueryResult.node, mamQueryResult.form, requestRsmSet);
771    }
772
773    /**
774     * Obtain page before the first message saved (specific chat).
775     * <p>
776     * Note that the messageUid is the XEP-0313 UID and <b>not</b> the stanza ID of the message.
777     * </p>
778     *
779     * @param chatJid
780     * @param messageUid the UID of the message of which messages before should be received.
781     * @param max
782     * @return the MAM query result
783     * @throws XMPPErrorException
784     * @throws NotLoggedInException
785     * @throws NotConnectedException
786     * @throws InterruptedException
787     * @throws NoResponseException
788     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
789     */
790    @Deprecated
791    // TODO Remove in Smack 4.4
792    public MamQueryResult pageBefore(Jid chatJid, String messageUid, int max) throws XMPPErrorException,
793            NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException {
794        RSMSet rsmSet = new RSMSet(null, messageUid, -1, -1, null, max, null, -1);
795        DataForm dataForm = getNewMamForm();
796        addWithJid(chatJid, dataForm);
797        return page(null, dataForm, rsmSet);
798    }
799
800    /**
801     * Obtain page after the last message saved (specific chat).
802     * <p>
803     * Note that the messageUid is the XEP-0313 UID and <b>not</b> the stanza ID of the message.
804     * </p>
805     *
806     * @param chatJid
807     * @param messageUid the UID of the message of which messages after should be received.
808     * @param max
809     * @return the MAM query result
810     * @throws XMPPErrorException
811     * @throws NotLoggedInException
812     * @throws NotConnectedException
813     * @throws InterruptedException
814     * @throws NoResponseException
815     * @deprecated use {@link #queryArchive(MamQueryArgs)} instead.
816     */
817    @Deprecated
818    // TODO Remove in Smack 4.4
819    public MamQueryResult pageAfter(Jid chatJid, String messageUid, int max) throws XMPPErrorException,
820            NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException {
821        RSMSet rsmSet = new RSMSet(messageUid, null, -1, -1, null, max, null, -1);
822        DataForm dataForm = getNewMamForm();
823        addWithJid(chatJid, dataForm);
824        return page(null, dataForm, rsmSet);
825    }
826
827    /**
828     * Obtain the most recent page of a chat.
829     *
830     * @param chatJid
831     * @param max
832     * @return the MAM query result
833     * @throws XMPPErrorException
834     * @throws NotLoggedInException
835     * @throws NotConnectedException
836     * @throws InterruptedException
837     * @throws NoResponseException
838     * @deprecated use {@link #queryMostRecentPage(Jid, int)} instead.
839     */
840    @Deprecated
841    // TODO Remove in Smack 4.4
842    public MamQueryResult mostRecentPage(Jid chatJid, int max) throws XMPPErrorException, NotLoggedInException,
843            NotConnectedException, InterruptedException, NoResponseException {
844        return pageBefore(chatJid, "", max);
845    }
846
847    public MamQuery queryMostRecentPage(Jid jid, int max) throws NoResponseException, XMPPErrorException,
848                    NotConnectedException, NotLoggedInException, InterruptedException {
849        MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
850                        // Produces an empty <before/> element for XEP-0059 § 2.5
851                        .queryLastPage()
852                        .limitResultsToJid(jid)
853                        .setResultPageSize(max)
854                        .build();
855        return queryArchive(mamQueryArgs);
856    }
857
858    /**
859     * Get the form fields supported by the server.
860     *
861     * @return the list of form fields.
862     * @throws NoResponseException
863     * @throws XMPPErrorException
864     * @throws NotConnectedException
865     * @throws InterruptedException
866     * @throws NotLoggedInException
867     */
868    public List<FormField> retrieveFormFields() throws NoResponseException, XMPPErrorException, NotConnectedException,
869                    InterruptedException, NotLoggedInException {
870        return retrieveFormFields(null);
871    }
872
873    /**
874     * Get the form fields supported by the server.
875     *
876     * @param node The PubSub node name, can be null
877     * @return the list of form fields.
878     * @throws NoResponseException
879     * @throws XMPPErrorException
880     * @throws NotConnectedException
881     * @throws InterruptedException
882     * @throws NotLoggedInException
883     */
884    public List<FormField> retrieveFormFields(String node)
885                    throws NoResponseException, XMPPErrorException, NotConnectedException,
886            InterruptedException, NotLoggedInException {
887        String queryId = UUID.randomUUID().toString();
888        MamQueryIQ mamQueryIq = new MamQueryIQ(queryId, node, null);
889        mamQueryIq.setTo(archiveAddress);
890
891        MamQueryIQ mamResponseQueryIq = connection().createStanzaCollectorAndSend(mamQueryIq).nextResultOrThrow();
892
893        return mamResponseQueryIq.getDataForm().getFields();
894    }
895
896    private MamQuery queryArchive(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException,
897                    NotConnectedException, InterruptedException, NotLoggedInException {
898        MamQueryPage mamQueryPage = queryArchivePage(mamQueryIq);
899        return new MamQuery(mamQueryPage, mamQueryIq.getNode(), DataForm.from(mamQueryIq));
900    }
901
902    private MamQueryPage queryArchivePage(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException,
903                    NotConnectedException, InterruptedException, NotLoggedInException {
904        final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
905        MamFinIQ mamFinIQ;
906
907        StanzaCollector mamFinIQCollector = connection.createStanzaCollector(new IQReplyFilter(mamQueryIq, connection));
908
909        StanzaCollector.Configuration resultCollectorConfiguration = StanzaCollector.newConfiguration()
910                .setStanzaFilter(new MamResultFilter(mamQueryIq)).setCollectorToReset(mamFinIQCollector);
911        StanzaCollector resultCollector = connection.createStanzaCollector(resultCollectorConfiguration);
912
913        try {
914            connection.sendStanza(mamQueryIq);
915            mamFinIQ = mamFinIQCollector.nextResultOrThrow();
916        } finally {
917            mamFinIQCollector.cancel();
918            resultCollector.cancel();
919        }
920
921        return new MamQueryPage(resultCollector, mamFinIQ);
922    }
923
924    /**
925     * MAM query result class.
926     *
927     */
928    @Deprecated
929    public static final class MamQueryResult {
930        public final List<Forwarded> forwardedMessages;
931        public final MamFinIQ mamFin;
932        private final String node;
933        private final DataForm form;
934
935        private MamQueryResult(MamQuery mamQuery) {
936            this(mamQuery.mamQueryPage.forwardedMessages, mamQuery.mamQueryPage.mamFin, mamQuery.node, mamQuery.form);
937        }
938
939        private MamQueryResult(List<Forwarded> forwardedMessages, MamFinIQ mamFin, String node, DataForm form) {
940            this.forwardedMessages = forwardedMessages;
941            this.mamFin = mamFin;
942            this.node = node;
943            this.form = form;
944        }
945    }
946
947    public final class MamQuery {
948        private final String node;
949        private final DataForm form;
950
951        private MamQueryPage mamQueryPage;
952
953        private MamQuery(MamQueryPage mamQueryPage, String node, DataForm form) {
954            this.node = node;
955            this.form = form;
956
957            this.mamQueryPage = mamQueryPage;
958        }
959
960        public boolean isComplete() {
961            return mamQueryPage.getMamFinIq().isComplete();
962        }
963
964        public List<Message> getMessages() {
965            return mamQueryPage.messages;
966        }
967
968        public List<MamResultExtension> getMamResultExtensions() {
969            return mamQueryPage.mamResultExtensions;
970        }
971
972        private List<Message> page(RSMSet requestRsmSet) throws NoResponseException, XMPPErrorException,
973                        NotConnectedException, NotLoggedInException, InterruptedException {
974            MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, form);
975            mamQueryIQ.setType(IQ.Type.set);
976            mamQueryIQ.setTo(archiveAddress);
977            mamQueryIQ.addExtension(requestRsmSet);
978
979            mamQueryPage = queryArchivePage(mamQueryIQ);
980
981            return mamQueryPage.messages;
982        }
983
984        private RSMSet getPreviousRsmSet() {
985            return mamQueryPage.getMamFinIq().getRSMSet();
986        }
987
988        public List<Message> pageNext(int count) throws NoResponseException, XMPPErrorException, NotConnectedException,
989                        NotLoggedInException, InterruptedException {
990            RSMSet previousResultRsmSet = getPreviousRsmSet();
991            RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after);
992            return page(requestRsmSet);
993        }
994
995        public List<Message> pagePrevious(int count) throws NoResponseException, XMPPErrorException,
996                        NotConnectedException, NotLoggedInException, InterruptedException {
997            RSMSet previousResultRsmSet = getPreviousRsmSet();
998            RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.before);
999            return page(requestRsmSet);
1000        }
1001
1002        public int getMessageCount() {
1003            return getMessages().size();
1004        }
1005
1006        public MamQueryPage getPage() {
1007            return mamQueryPage;
1008        }
1009    }
1010
1011    public static final class MamQueryPage {
1012        private final MamFinIQ mamFin;
1013        private final List<Message> mamResultCarrierMessages;
1014        private final List<MamResultExtension> mamResultExtensions;
1015        private final List<Forwarded> forwardedMessages;
1016        private final List<Message> messages;
1017
1018        private MamQueryPage(StanzaCollector stanzaCollector, MamFinIQ mamFin) {
1019            this.mamFin = mamFin;
1020
1021            List<Stanza> mamResultCarrierStanzas = stanzaCollector.getCollectedStanzasAfterCancelled();
1022
1023            List<Message> mamResultCarrierMessages = new ArrayList<>(mamResultCarrierStanzas.size());
1024            List<MamResultExtension> mamResultExtensions = new ArrayList<>(mamResultCarrierStanzas.size());
1025            List<Forwarded> forwardedMessages = new ArrayList<>(mamResultCarrierStanzas.size());
1026
1027            for (Stanza mamResultStanza : mamResultCarrierStanzas) {
1028                Message resultMessage = (Message) mamResultStanza;
1029
1030                mamResultCarrierMessages.add(resultMessage);
1031
1032                MamElements.MamResultExtension mamResultExtension = MamElements.MamResultExtension.from(resultMessage);
1033                mamResultExtensions.add(mamResultExtension);
1034
1035                forwardedMessages.add(mamResultExtension.getForwarded());
1036            }
1037
1038            this.mamResultCarrierMessages = Collections.unmodifiableList(mamResultCarrierMessages);
1039            this.mamResultExtensions = Collections.unmodifiableList(mamResultExtensions);
1040            this.forwardedMessages = Collections.unmodifiableList(forwardedMessages);
1041            this.messages = Collections.unmodifiableList(Forwarded.extractMessagesFrom(forwardedMessages));
1042        }
1043
1044        public List<Message> getMessages() {
1045            return messages;
1046        }
1047
1048        public List<Forwarded> getForwarded() {
1049            return forwardedMessages;
1050        }
1051
1052        public List<MamResultExtension> getMamResultExtensions() {
1053            return mamResultExtensions;
1054        }
1055
1056        public List<Message> getMamResultCarrierMessages() {
1057            return mamResultCarrierMessages;
1058        }
1059
1060        public MamFinIQ getMamFinIq() {
1061            return mamFin;
1062        }
1063    }
1064
1065    private void ensureMamQueryResultMatchesThisManager(MamQueryResult mamQueryResult) {
1066        EntityFullJid localAddress = connection().getUser();
1067        EntityBareJid localBareAddress = null;
1068        if (localAddress != null) {
1069            localBareAddress = localAddress.asEntityBareJid();
1070        }
1071        boolean isLocalUserArchive = archiveAddress == null || archiveAddress.equals(localBareAddress);
1072
1073        Jid finIqFrom = mamQueryResult.mamFin.getFrom();
1074
1075        if (finIqFrom != null) {
1076            if (finIqFrom.equals(archiveAddress) || (isLocalUserArchive && finIqFrom.equals(localBareAddress))) {
1077                return;
1078            }
1079            throw new IllegalArgumentException("The given MamQueryResult is from the MAM archive '" + finIqFrom
1080                            + "' whereas this MamManager is responsible for '" + archiveAddress + '\'');
1081        }
1082        else if (!isLocalUserArchive) {
1083            throw new IllegalArgumentException(
1084                            "The given MamQueryResult is from the local entity (user) MAM archive, whereas this MamManager is responsible for '"
1085                                            + archiveAddress + '\'');
1086        }
1087    }
1088
1089    /**
1090     * Check if this MamManager's archive address supports MAM.
1091     *
1092     * @return true if MAM is supported, <code>false</code>otherwise.
1093     *
1094     * @throws NoResponseException
1095     * @throws XMPPErrorException
1096     * @throws NotConnectedException
1097     * @throws InterruptedException
1098     * @since 4.2.1
1099     * @see <a href="https://xmpp.org/extensions/xep-0313.html#support">XEP-0313 § 7. Determining support</a>
1100     */
1101    public boolean isSupported() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1102        // Note that this may return 'null' but SDM's supportsFeature() does the right thing™ then.
1103        Jid archiveAddress = getArchiveAddress();
1104        return serviceDiscoveryManager.supportsFeature(archiveAddress, MamElements.NAMESPACE);
1105    }
1106
1107    private static DataForm getNewMamForm() {
1108        FormField field = new FormField(FormField.FORM_TYPE);
1109        field.setType(FormField.Type.hidden);
1110        field.addValue(MamElements.NAMESPACE);
1111        DataForm form = new DataForm(DataForm.Type.submit);
1112        form.addField(field);
1113        return form;
1114    }
1115
1116    /**
1117     * Lookup the archive's message ID of the latest message in the archive. Returns {@code null} if the archive is
1118     * empty.
1119     *
1120     * @return the ID of the lastest message or {@code null}.
1121     * @throws NoResponseException
1122     * @throws XMPPErrorException
1123     * @throws NotConnectedException
1124     * @throws NotLoggedInException
1125     * @throws InterruptedException
1126     * @since 4.3.0
1127     */
1128    public String getMessageUidOfLatestMessage() throws NoResponseException, XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException {
1129        MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
1130                .setResultPageSize(1)
1131                .queryLastPage()
1132                .build();
1133
1134        MamQuery mamQuery = queryArchive(mamQueryArgs);
1135        if (mamQuery.getMessages().isEmpty()) {
1136            return null;
1137        }
1138
1139        return mamQuery.getMamResultExtensions().get(0).getId();
1140    }
1141
1142    /**
1143     * Get the preferences stored in the server.
1144     *
1145     * @return the MAM preferences result
1146     * @throws NoResponseException
1147     * @throws XMPPErrorException
1148     * @throws NotConnectedException
1149     * @throws InterruptedException
1150     * @throws NotLoggedInException
1151     */
1152    public MamPrefsResult retrieveArchivingPreferences() throws NoResponseException, XMPPErrorException,
1153            NotConnectedException, InterruptedException, NotLoggedInException {
1154        MamPrefsIQ mamPrefIQ = new MamPrefsIQ();
1155        return queryMamPrefs(mamPrefIQ);
1156    }
1157
1158    /**
1159     * Update the preferences in the server.
1160     *
1161     * @param alwaysJids
1162     *            is the list of JIDs that should always have messages to/from
1163     *            archived in the user's store
1164     * @param neverJids
1165     *            is the list of JIDs that should never have messages to/from
1166     *            archived in the user's store
1167     * @param defaultBehavior
1168     *            can be "roster", "always", "never" (see XEP-0313)
1169     * @return the MAM preferences result
1170     * @throws NoResponseException
1171     * @throws XMPPErrorException
1172     * @throws NotConnectedException
1173     * @throws InterruptedException
1174     * @throws NotLoggedInException
1175     * @deprecated use {@link #updateArchivingPreferences(MamPrefs)} instead.
1176     */
1177    @Deprecated
1178    public MamPrefsResult updateArchivingPreferences(List<Jid> alwaysJids, List<Jid> neverJids, DefaultBehavior defaultBehavior)
1179            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
1180            NotLoggedInException {
1181        Objects.requireNonNull(defaultBehavior, "Default behavior must be set");
1182        MamPrefsIQ mamPrefIQ = new MamPrefsIQ(alwaysJids, neverJids, defaultBehavior);
1183        return queryMamPrefs(mamPrefIQ);
1184    }
1185
1186    /**
1187     * Update the preferences in the server.
1188     *
1189     * @param mamPrefs
1190     * @return the currently active preferences after the operation.
1191     * @throws NoResponseException
1192     * @throws XMPPErrorException
1193     * @throws NotConnectedException
1194     * @throws InterruptedException
1195     * @throws NotLoggedInException
1196     * @since 4.3.0
1197     */
1198    public MamPrefsResult updateArchivingPreferences(MamPrefs mamPrefs) throws NoResponseException, XMPPErrorException,
1199                    NotConnectedException, InterruptedException, NotLoggedInException {
1200        MamPrefsIQ mamPrefIQ = mamPrefs.constructMamPrefsIq();
1201        return queryMamPrefs(mamPrefIQ);
1202    }
1203
1204    public MamPrefsResult enableMamForAllMessages() throws NoResponseException, XMPPErrorException,
1205                    NotConnectedException, NotLoggedInException, InterruptedException {
1206        return setDefaultBehavior(DefaultBehavior.always);
1207    }
1208
1209    public MamPrefsResult enableMamForRosterMessages() throws NoResponseException, XMPPErrorException,
1210                    NotConnectedException, NotLoggedInException, InterruptedException {
1211        return setDefaultBehavior(DefaultBehavior.roster);
1212    }
1213
1214    public MamPrefsResult setDefaultBehavior(DefaultBehavior desiredDefaultBehavior) throws NoResponseException,
1215                    XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException {
1216        MamPrefsResult mamPrefsResult = retrieveArchivingPreferences();
1217        if (mamPrefsResult.mamPrefs.getDefault() == desiredDefaultBehavior) {
1218            return mamPrefsResult;
1219        }
1220
1221        MamPrefs mamPrefs = mamPrefsResult.asMamPrefs();
1222        mamPrefs.setDefaultBehavior(desiredDefaultBehavior);
1223        return updateArchivingPreferences(mamPrefs);
1224    }
1225
1226    /**
1227     * MAM preferences result class.
1228     *
1229     */
1230    public static final class MamPrefsResult {
1231        public final MamPrefsIQ mamPrefs;
1232        public final DataForm form;
1233
1234        private MamPrefsResult(MamPrefsIQ mamPrefs, DataForm form) {
1235            this.mamPrefs = mamPrefs;
1236            this.form = form;
1237        }
1238
1239        public MamPrefs asMamPrefs() {
1240            return new MamPrefs(this);
1241        }
1242    }
1243
1244    public static final class MamPrefs {
1245        private final List<Jid> alwaysJids;
1246        private final List<Jid> neverJids;
1247        private DefaultBehavior defaultBehavior;
1248
1249        private MamPrefs(MamPrefsResult mamPrefsResult) {
1250            MamPrefsIQ mamPrefsIq = mamPrefsResult.mamPrefs;
1251            this.alwaysJids = new ArrayList<>(mamPrefsIq.getAlwaysJids());
1252            this.neverJids = new ArrayList<>(mamPrefsIq.getNeverJids());
1253            this.defaultBehavior = mamPrefsIq.getDefault();
1254        }
1255
1256        public void setDefaultBehavior(DefaultBehavior defaultBehavior) {
1257            this.defaultBehavior = Objects.requireNonNull(defaultBehavior, "defaultBehavior must not be null");
1258        }
1259
1260        public DefaultBehavior getDefaultBehavior() {
1261            return defaultBehavior;
1262        }
1263
1264        public List<Jid> getAlwaysJids() {
1265            return alwaysJids;
1266        }
1267
1268        public List<Jid> getNeverJids() {
1269            return neverJids;
1270        }
1271
1272        private MamPrefsIQ constructMamPrefsIq() {
1273            return new MamPrefsIQ(alwaysJids, neverJids, defaultBehavior);
1274        }
1275    }
1276
1277    private MamPrefsResult queryMamPrefs(MamPrefsIQ mamPrefsIQ) throws NoResponseException, XMPPErrorException,
1278            NotConnectedException, InterruptedException, NotLoggedInException {
1279        final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
1280
1281        MamPrefsIQ mamPrefsResultIQ = connection.createStanzaCollectorAndSend(mamPrefsIQ).nextResultOrThrow();
1282
1283        return new MamPrefsResult(mamPrefsResultIQ, DataForm.from(mamPrefsIQ));
1284    }
1285
1286}