aboutsummaryrefslogtreecommitdiff
path: root/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityInfo.java
blob: 49291484b8ec80dc551633067af3b421c3d07e2f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ims.rcs.uce.presence.publish;

import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;

import android.content.Context;
import android.net.Uri;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
import android.telephony.AccessNetworkConstants;
import android.telephony.ims.ImsRegistrationAttributes;
import android.telephony.ims.RcsContactPresenceTuple;
import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
import android.telephony.ims.RcsContactUceCapability;
import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
import android.telephony.ims.feature.MmTelFeature;
import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
import android.util.IndentingPrintWriter;
import android.util.ArraySet;
import android.util.LocalLog;
import android.util.Log;

import com.android.ims.rcs.uce.util.FeatureTags;
import com.android.ims.rcs.uce.util.UceUtils;
import com.android.internal.annotations.VisibleForTesting;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Stores the device's capabilities information.
 */
public class DeviceCapabilityInfo {
    private static final String LOG_TAG = UceUtils.getLogPrefix() + "DeviceCapabilityInfo";

    private final int mSubId;

    private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);

    // FT overrides to add to the IMS registration, which will be added to the existing
    // capabilities.
    private final Set<String> mOverrideAddFeatureTags = new ArraySet<>();

    // FT overrides to remove from the existing IMS registration, which will remove the related
    // capabilities.
    private final Set<String> mOverrideRemoveFeatureTags = new ArraySet<>();

    // Tracks capability status based on the IMS registration.
    private PublishServiceDescTracker mServiceCapRegTracker;

    // The feature tags associated with the last IMS registration update.
    private Set<String> mLastRegistrationFeatureTags = Collections.emptySet();
    // The feature tags associated with the last IMS registration update, which also include
    // overrides
    private Set<String> mLastRegistrationOverrideFeatureTags = Collections.emptySet();

    // The mmtel feature is registered or not
    private boolean mMmtelRegistered;

    // The network type which ims mmtel registers on.
    private int mMmtelNetworkRegType;

    // The list of the mmtel associated uris
    private List<Uri> mMmtelAssociatedUris = Collections.emptyList();

    // The rcs feature is registered or not
    private boolean mRcsRegistered;

    // The list of the rcs associated uris
    private List<Uri> mRcsAssociatedUris = Collections.emptyList();

    // Whether or not presence is reported as capable
    private boolean mPresenceCapable;

    // The network type which ims rcs registers on.
    private int mRcsNetworkRegType;

    // The MMTel capabilities of this subscription Id
    private MmTelFeature.MmTelCapabilities mMmTelCapabilities;

    // Whether the settings are changed or not
    private int mTtyPreferredMode;
    private boolean mMobileData;
    private boolean mVtSetting;

    // The service description associated with the last publication update.
    private final Set<ServiceDescription> mLastSuccessfulCapabilities = new ArraySet<>();
    // The service description to temporarily store the presence capability being sent.
    private Set<ServiceDescription> mPendingPublishCapabilities;

    public DeviceCapabilityInfo(int subId, String[] capToRegistrationMap) {
        mSubId = subId;
        mServiceCapRegTracker = PublishServiceDescTracker.fromCarrierConfig(capToRegistrationMap);
        reset();
    }

    /**
     * Reset all the status.
     */
    public synchronized void reset() {
        logd("reset");
        mMmtelRegistered = false;
        mMmtelNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
        mRcsRegistered = false;
        mRcsNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
        mTtyPreferredMode = TelecomManager.TTY_MODE_OFF;
        mMobileData = true;
        mVtSetting = true;
        mMmTelCapabilities = new MmTelCapabilities();
        mMmtelAssociatedUris = Collections.EMPTY_LIST;
        mRcsAssociatedUris = Collections.EMPTY_LIST;
        mLastSuccessfulCapabilities.clear();
        mPendingPublishCapabilities = null;
    }

    /**
     * Update the capability registration tracker feature tag override mapping.
     * @return if true, this has caused a change in the Feature Tags associated with the device
     * and a new PUBLISH should be generated.
     */
    public synchronized boolean updateCapabilityRegistrationTrackerMap(String[] newMap) {
        Set<String> oldTags = mServiceCapRegTracker.copyRegistrationFeatureTags();
        mServiceCapRegTracker = PublishServiceDescTracker.fromCarrierConfig(newMap);
        mServiceCapRegTracker.updateImsRegistration(mLastRegistrationOverrideFeatureTags);
        boolean changed = !oldTags.equals(mServiceCapRegTracker.copyRegistrationFeatureTags());
        if (changed) logi("Carrier Config Change resulted in associated FT list change");
        return changed;
    }

    public synchronized boolean isImsRegistered() {
        return mMmtelRegistered || mRcsRegistered;
    }

    /**
     * Update the status that IMS MMTEL is registered.
     */
    public synchronized void updateImsMmtelRegistered(int type) {
        StringBuilder builder = new StringBuilder();
        builder.append("IMS MMTEL registered: original state=").append(mMmtelRegistered)
                .append(", changes type from ").append(mMmtelNetworkRegType)
                .append(" to ").append(type);
        logi(builder.toString());

        if (!mMmtelRegistered) {
            mMmtelRegistered = true;
        }

        if (mMmtelNetworkRegType != type) {
            mMmtelNetworkRegType = type;
        }
    }

    /**
     * Update the status that IMS MMTEL is unregistered.
     */
    public synchronized boolean updateImsMmtelUnregistered() {
        logi("IMS MMTEL unregistered: original state=" + mMmtelRegistered);
        boolean changed = false;
        if (mMmtelRegistered) {
            mMmtelRegistered = false;
            changed = true;
        }
        mMmtelNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
        mLastSuccessfulCapabilities.clear();
        mPendingPublishCapabilities = null;
        return changed;
    }

    /**
     * Update the MMTel associated URIs which are provided by the IMS service.
     */
    public synchronized void updateMmTelAssociatedUri(Uri[] uris) {
        int originalSize = mMmtelAssociatedUris.size();
        if (uris != null) {
            mMmtelAssociatedUris = Arrays.stream(uris)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
        } else {
            mMmtelAssociatedUris.clear();
        }
        int currentSize = mMmtelAssociatedUris.size();
        logd("updateMmTelAssociatedUri: size from " + originalSize + " to " + currentSize);
    }

    /**
     * Get the MMTEL associated URI. When there are multiple uris in the list, take the first uri.
     * Return null if the list of the MMTEL associated uri is empty.
     */
    public synchronized Uri getMmtelAssociatedUri() {
        if (!mMmtelAssociatedUris.isEmpty()) {
            return mMmtelAssociatedUris.get(0);
        }
        return null;
    }

    /**
     * Update the status that IMS RCS is registered.
     * @return true if the IMS registration status changed, false if it did not.
     */
    public synchronized boolean updateImsRcsRegistered(ImsRegistrationAttributes attr) {
        StringBuilder builder = new StringBuilder();
        builder.append("IMS RCS registered: original state=").append(mRcsRegistered)
                .append(", changes type from ").append(mRcsNetworkRegType)
                .append(" to ").append(attr.getTransportType());
        logi(builder.toString());

        boolean changed = false;
        if (!mRcsRegistered) {
            mRcsRegistered = true;
            changed = true;
        }

        if (mRcsNetworkRegType != attr.getTransportType()) {
            mRcsNetworkRegType = attr.getTransportType();
            changed = true;
        }

        mLastRegistrationFeatureTags = attr.getFeatureTags();
        changed |= updateRegistration(mLastRegistrationFeatureTags);

        return changed;
    }

    /**
     * Update the status that IMS RCS is unregistered.
     */
    public synchronized boolean updateImsRcsUnregistered() {
        logi("IMS RCS unregistered: original state=" + mRcsRegistered);
        boolean changed = false;
        if (mRcsRegistered) {
            mRcsRegistered = false;
            changed = true;
        }

        mLastRegistrationFeatureTags = Collections.emptySet();
        updateRegistration(mLastRegistrationFeatureTags);
        mRcsNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
        mLastSuccessfulCapabilities.clear();
        mPendingPublishCapabilities = null;
        return changed;
    }

    /**
     * Update the RCS associated URIs which is provided by the IMS service.
     */
    public synchronized void updateRcsAssociatedUri(Uri[] uris) {
        int originalSize = mRcsAssociatedUris.size();
        if (uris != null) {
            mRcsAssociatedUris = Arrays.stream(uris)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
        } else {
            mRcsAssociatedUris.clear();
        }
        int currentSize = mRcsAssociatedUris.size();
        logd("updateRcsAssociatedUri: size from " + originalSize + " to " + currentSize);
    }

    /**
     * Get the RCS associated URI. When there are multiple uris in the list, take the first uri.
     * Return null if the list of the RCS associated uri is empty.
     */
    public synchronized Uri getRcsAssociatedUri() {
        if (!mRcsAssociatedUris.isEmpty()) {
            return mRcsAssociatedUris.get(0);
        }
        return null;
    }

    /**
     * Get the first URI from the "p-associated-uri" header included in the IMS registration
     * response.
     * @param preferTelUri If {@code true}, prefer returning the first TEL URI. If no TEL
     *                     URIs exist, this method will still return the preferred (first) SIP URI
     *                     in the header. If {@code false}, we will return the first URI
     *                     in the "p-associated-uri" header, independent of the URI scheme.
     */
    public synchronized Uri getImsAssociatedUri(boolean preferTelUri) {
        if (preferTelUri) {
            if (!mRcsAssociatedUris.isEmpty()) {
                for (Uri rcsAssociatedUri : mRcsAssociatedUris) {
                    if (PhoneAccount.SCHEME_TEL.equalsIgnoreCase(rcsAssociatedUri.getScheme())) {
                        return rcsAssociatedUri;
                    }
                }
            }
            if (!mMmtelAssociatedUris.isEmpty()) {
                for (Uri mmtelAssociatedUri : mMmtelAssociatedUris) {
                    if (PhoneAccount.SCHEME_TEL.equalsIgnoreCase(mmtelAssociatedUri.getScheme())) {
                        return mmtelAssociatedUri;
                    }
                }
            }
        }

        // Either we have not found a TEL URI or we do not prefer TEL URIs. Get the first URI from
        // p-associated-uri list.
        if (!mRcsAssociatedUris.isEmpty()) {
            return mRcsAssociatedUris.get(0);
        } else if (!mMmtelAssociatedUris.isEmpty()) {
            return mMmtelAssociatedUris.get(0);
        } else {
            return null;
        }
    }

    public synchronized boolean addRegistrationOverrideCapabilities(Set<String> featureTags) {
        logd("override - add: " + featureTags);
        mOverrideRemoveFeatureTags.removeAll(featureTags);
        mOverrideAddFeatureTags.addAll(featureTags);
        // Call with the last feature tags so that the new ones will be potentially picked up.
        return updateRegistration(mLastRegistrationFeatureTags);
    };

    public synchronized boolean removeRegistrationOverrideCapabilities(Set<String> featureTags) {
        logd("override - remove: " + featureTags);
        mOverrideAddFeatureTags.removeAll(featureTags);
        mOverrideRemoveFeatureTags.addAll(featureTags);
        // Call with the last feature tags so that the new ones will be potentially picked up.
        return updateRegistration(mLastRegistrationFeatureTags);
    };

    public synchronized boolean clearRegistrationOverrideCapabilities() {
        logd("override - clear");
        mOverrideAddFeatureTags.clear();
        mOverrideRemoveFeatureTags.clear();
        // Call with the last feature tags so that base tags will be restored
        return updateRegistration(mLastRegistrationFeatureTags);
    };

    /**
     * Update the IMS registration tracked by the PublishServiceDescTracker if needed.
     * @return true if the registration changed, else otherwise.
     */
    private boolean updateRegistration(Set<String> baseTags) {
        Set<String> updatedTags = updateImsRegistrationFeatureTags(baseTags);
        if (!mLastRegistrationOverrideFeatureTags.equals(updatedTags)) {
            mLastRegistrationOverrideFeatureTags = updatedTags;
            mServiceCapRegTracker.updateImsRegistration(updatedTags);
            return true;
        }
        return false;
    }

    /**
     * Combine IMS registration with overrides to produce a new feature tag Set.
     * @return true if the IMS registration changed, false otherwise.
     */
    private synchronized Set<String> updateImsRegistrationFeatureTags(Set<String> featureTags) {
        Set<String> tags = new ArraySet<>(featureTags);
        tags.addAll(mOverrideAddFeatureTags);
        tags.removeAll(mOverrideRemoveFeatureTags);
        return tags;
    }

    /**
     * Update the TTY preferred mode.
     * @return {@code true} if tty preferred mode is changed, {@code false} otherwise.
     */
    public synchronized boolean updateTtyPreferredMode(int ttyMode) {
        if (mTtyPreferredMode != ttyMode) {
            logd("TTY preferred mode changes from " + mTtyPreferredMode + " to " + ttyMode);
            mTtyPreferredMode = ttyMode;
            return true;
        }
        return false;
    }

    /**
     * Update mobile data setting.
     * @return {@code true} if the mobile data setting is changed, {@code false} otherwise.
     */
    public synchronized boolean updateMobileData(boolean mobileData) {
        if (mMobileData != mobileData) {
            logd("Mobile data changes from " + mMobileData + " to " + mobileData);
            mMobileData = mobileData;
            return true;
        }
        return false;
    }

    /**
     * Update VT setting.
     * @return {@code true} if vt setting is changed, {@code false}.otherwise.
     */
    public synchronized boolean updateVtSetting(boolean vtSetting) {
        if (mVtSetting != vtSetting) {
            logd("VT setting changes from " + mVtSetting + " to " + vtSetting);
            mVtSetting = vtSetting;
            return true;
        }
        return false;
    }

    /**
     * Update the MMTEL capabilities if the capabilities is changed.
     * @return {@code true} if the mmtel capabilities are changed, {@code false} otherwise.
     */
    public synchronized boolean updateMmtelCapabilitiesChanged(MmTelCapabilities capabilities) {
        if (capabilities == null) {
            return false;
        }
        boolean oldVolteAvailable = isVolteAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
        boolean oldVoWifiAvailable = isVoWifiAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
        boolean oldVtAvailable = isVtAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
        boolean oldViWifiAvailable = isViWifiAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
        boolean oldCallComposerAvailable = isCallComposerAvailable(mMmTelCapabilities);

        boolean volteAvailable = isVolteAvailable(mMmtelNetworkRegType, capabilities);
        boolean voWifiAvailable = isVoWifiAvailable(mMmtelNetworkRegType, capabilities);
        boolean vtAvailable = isVtAvailable(mMmtelNetworkRegType, capabilities);
        boolean viWifiAvailable = isViWifiAvailable(mMmtelNetworkRegType, capabilities);
        boolean callComposerAvailable = isCallComposerAvailable(capabilities);

        logd("updateMmtelCapabilitiesChanged: from " + mMmTelCapabilities + " to " + capabilities);

        // Update to the new mmtel capabilities
        mMmTelCapabilities = deepCopyCapabilities(capabilities);

        if (oldVolteAvailable != volteAvailable
                || oldVoWifiAvailable != voWifiAvailable
                || oldVtAvailable != vtAvailable
                || oldViWifiAvailable != viWifiAvailable
                || oldCallComposerAvailable != callComposerAvailable) {
            return true;
        }
        return false;
    }

    public synchronized void updatePresenceCapable(boolean isCapable) {
        mPresenceCapable = isCapable;
    }

    public synchronized boolean isPresenceCapable() {
        return mPresenceCapable;
    }

    // Get the device's capabilities with the PRESENCE mechanism.
    public RcsContactUceCapability getChangedPresenceCapability(Context context) {
        if (context == null) {
            return null;
        }
        Set<ServiceDescription> capableFromReg =
                mServiceCapRegTracker.copyRegistrationCapabilities();
        if (isPresenceCapabilityChanged(capableFromReg)) {
            RcsContactUceCapability rcsContactUceCapability = getPresenceCapabilities(context);
            if (rcsContactUceCapability != null) {
                mPendingPublishCapabilities = mServiceCapRegTracker.copyRegistrationCapabilities();
            }
            return rcsContactUceCapability;
        }
        return null;
    }

    public void setPresencePublishResult(boolean isSuccess) {
        if (isSuccess) {
            mLastSuccessfulCapabilities.clear();
            if (mPendingPublishCapabilities != null) {
                mLastSuccessfulCapabilities.addAll(mPendingPublishCapabilities);
            }
        }
        mPendingPublishCapabilities = null;
    }

    public void resetPresenceCapability() {
        mLastSuccessfulCapabilities.clear();
        mPendingPublishCapabilities = null;
    }

    public List<RcsContactPresenceTuple> getLastSuccessfulPresenceTuplesWithoutContactUri() {
        List<RcsContactPresenceTuple> presenceTuples = new ArrayList<>();
        if (mLastSuccessfulCapabilities.isEmpty()) {
            return presenceTuples;
        }

        for (ServiceDescription capability : mLastSuccessfulCapabilities) {
            presenceTuples.add(capability.getTupleBuilder().build());
        }
        return presenceTuples;
    }

    @VisibleForTesting
    public void addLastSuccessfulServiceDescription(ServiceDescription capability) {
        mLastSuccessfulCapabilities.add(capability);
    }

    @VisibleForTesting
    public boolean isPresenceCapabilityChanged(Set<ServiceDescription> capableFromReg) {
        if (mLastSuccessfulCapabilities.isEmpty()) {
            return true;
        }

        if (capableFromReg.equals(mLastSuccessfulCapabilities)) {
            return false;
        }
        return true;
    }

    private boolean isVolteAvailable(int networkRegType, MmTelCapabilities capabilities) {
        return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
                && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
    }

    private boolean isVoWifiAvailable(int networkRegType, MmTelCapabilities capabilities) {
        return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN)
                && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
    }

    private boolean isVtAvailable(int networkRegType, MmTelCapabilities capabilities) {
        return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
                && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
    }

    private boolean isViWifiAvailable(int networkRegType, MmTelCapabilities capabilities) {
        return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN)
                && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
    }

    private boolean isCallComposerAvailable(MmTelCapabilities capabilities) {
        return capabilities.isCapable(
                MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER);
    }

    /**
     * Get the device's capabilities.
     */
    public synchronized RcsContactUceCapability getDeviceCapabilities(
            @CapabilityMechanism int mechanism, Context context) {
        switch (mechanism) {
            case RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE:
                RcsContactUceCapability rcsContactUceCapability = getPresenceCapabilities(context);
                if (rcsContactUceCapability != null) {
                    mPendingPublishCapabilities =
                            mServiceCapRegTracker.copyRegistrationCapabilities();
                }
                return rcsContactUceCapability;
            case RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS:
                return getOptionsCapabilities(context);
            default:
                logw("getDeviceCapabilities: invalid mechanism " + mechanism);
                return null;
        }
    }

    // Get the device's capabilities with the PRESENCE mechanism.
    private RcsContactUceCapability getPresenceCapabilities(Context context) {
        Uri uri = PublishUtils.getDeviceContactUri(context, mSubId, this, true);
        if (uri == null) {
            logw("getPresenceCapabilities: uri is empty");
            return null;
        }
        Set<ServiceDescription> capableFromReg =
                mServiceCapRegTracker.copyRegistrationCapabilities();

        PresenceBuilder presenceBuilder = new PresenceBuilder(uri,
                RcsContactUceCapability.SOURCE_TYPE_CACHED,
                RcsContactUceCapability.REQUEST_RESULT_FOUND);
        // RCS presence tag (added to all presence documents)
        ServiceDescription presDescription = getCustomizedDescription(
                ServiceDescription.SERVICE_DESCRIPTION_PRESENCE, capableFromReg);
        addCapability(presenceBuilder, presDescription.getTupleBuilder(), uri);
        capableFromReg.remove(presDescription);

        // mmtel
        ServiceDescription voiceDescription = getCustomizedDescription(
                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE, capableFromReg);
        ServiceDescription vtDescription = getCustomizedDescription(
                ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO, capableFromReg);
        ServiceDescription descToUse = (hasVolteCapability() && hasVtCapability()) ?
                vtDescription : voiceDescription;
        ServiceCapabilities servCaps = new ServiceCapabilities.Builder(
                hasVolteCapability(), hasVtCapability())
                .addSupportedDuplexMode(ServiceCapabilities.DUPLEX_MODE_FULL).build();
        addCapability(presenceBuilder, descToUse.getTupleBuilder()
                .setServiceCapabilities(servCaps), uri);
        capableFromReg.remove(voiceDescription);
        capableFromReg.remove(vtDescription);

        // call composer via mmtel
        ServiceDescription composerDescription = getCustomizedDescription(
                ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL, capableFromReg);
        if (hasCallComposerCapability()) {
            addCapability(presenceBuilder, composerDescription.getTupleBuilder(), uri);
        }
        capableFromReg.remove(composerDescription);

        // External features can only be found using registration states from other components.
        // Count these features as capable and include in PIDF XML if they are registered.
        for (ServiceDescription capability : capableFromReg) {
            addCapability(presenceBuilder, capability.getTupleBuilder(), uri);
        }

        return presenceBuilder.build();
    }

    /**
     * Search the refSet for the ServiceDescription that matches the service-id && version and
     * return that or return the reference if there is no match.
     */
    private ServiceDescription getCustomizedDescription(ServiceDescription reference,
            Set<ServiceDescription> refSet) {
        return refSet.stream().filter(s -> s.serviceId.equals(reference.serviceId)
                && s.version.equals(reference.version)).findFirst().orElse(reference);
    }

    // Get the device's capabilities with the OPTIONS mechanism.
    private RcsContactUceCapability getOptionsCapabilities(Context context) {
        Uri uri = PublishUtils.getDeviceContactUri(context, mSubId, this, false);
        if (uri == null) {
            logw("getOptionsCapabilities: uri is empty");
            return null;
        }

        Set<String> capableFromReg = mServiceCapRegTracker.copyRegistrationFeatureTags();

        OptionsBuilder optionsBuilder = new OptionsBuilder(uri, SOURCE_TYPE_CACHED);
        optionsBuilder.setRequestResult(RcsContactUceCapability.REQUEST_RESULT_FOUND);
        FeatureTags.addFeatureTags(optionsBuilder, hasVolteCapability(), hasVtCapability(),
                isPresenceCapable(), hasCallComposerCapability(), capableFromReg);
        return optionsBuilder.build();
    }

    private void addCapability(RcsContactUceCapability.PresenceBuilder presenceBuilder,
            RcsContactPresenceTuple.Builder tupleBuilder, Uri contactUri) {
        presenceBuilder.addCapabilityTuple(tupleBuilder.setContactUri(contactUri).build());
    }

    // Check if the device has the VoLTE capability
    private synchronized boolean hasVolteCapability() {
        return overrideCapability(FeatureTags.FEATURE_TAG_MMTEL, mMmTelCapabilities != null
                && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE));
    }

    // Check if the device has the VT capability
    private synchronized boolean hasVtCapability() {
        return overrideCapability(FeatureTags.FEATURE_TAG_VIDEO, mMmTelCapabilities != null
                && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO));
    }

    // Check if the device has the Call Composer capability
    private synchronized boolean hasCallComposerCapability() {
        return overrideCapability(FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY,
                mMmTelCapabilities != null && mMmTelCapabilities.isCapable(
                        MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER));
    }

    /**
     * @return the overridden value for the provided feature tag or the original capability if there
     * is no override.
     */
    private synchronized boolean overrideCapability(String featureTag, boolean originalCap) {
        if (mOverrideRemoveFeatureTags.contains(featureTag)) {
            return false;
        }

        if (mOverrideAddFeatureTags.contains(featureTag)) {
            return true;
        }

        return originalCap;
    }

    private synchronized MmTelCapabilities deepCopyCapabilities(MmTelCapabilities capabilities) {
        MmTelCapabilities mmTelCapabilities = new MmTelCapabilities();
        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE)) {
            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_VOICE);
        }
        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO)) {
            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
        }
        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_UT)) {
            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_UT);
        }
        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_SMS)) {
            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_SMS);
        }
        if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER)) {
            mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER);
        }
        return mmTelCapabilities;
    }

    private void logd(String log) {
        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
        mLocalLog.log("[D] " + log);
    }

    private void logi(String log) {
        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
        mLocalLog.log("[I] " + log);
    }

    private void logw(String log) {
        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
        mLocalLog.log("[W] " + log);
    }

    private StringBuilder getLogPrefix() {
        StringBuilder builder = new StringBuilder("[");
        builder.append(mSubId);
        builder.append("] ");
        return builder;
    }

    public void dump(PrintWriter printWriter) {
        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
        pw.println("DeviceCapabilityInfo :");
        pw.increaseIndent();

        mServiceCapRegTracker.dump(pw);

        pw.println("Log:");
        pw.increaseIndent();
        mLocalLog.dump(pw);
        pw.decreaseIndent();

        pw.decreaseIndent();
    }
}