summaryrefslogtreecommitdiff
path: root/core/java/android/net/vcn/VcnManager.java
blob: 6246dd77fd6deae983d91185a267d785eff0e92a (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
744
745
746
747
748
749
750
751
/*
 * 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 android.net.vcn;

import static java.util.Objects.requireNonNull;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresFeature;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.LinkProperties;
import android.net.NetworkCapabilities;
import android.os.Binder;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.ServiceSpecificException;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;

/**
 * VcnManager publishes APIs for applications to configure and manage Virtual Carrier Networks.
 *
 * <p>A VCN creates a virtualization layer to allow carriers to aggregate heterogeneous physical
 * networks, unifying them as a single carrier network. This enables infrastructure flexibility on
 * the part of carriers without impacting user connectivity, abstracting the physical network
 * technologies as an implementation detail of their public network.
 *
 * <p>Each VCN virtualizes a carrier's network by building tunnels to a carrier's core network over
 * carrier-managed physical links and supports a IP mobility layer to ensure seamless transitions
 * between the underlying networks. Each VCN is configured based on a Subscription Group (see {@link
 * android.telephony.SubscriptionManager}) and aggregates all networks that are brought up based on
 * a profile or suggestion in the specified Subscription Group.
 *
 * <p>The VCN can be configured to expose one or more {@link android.net.Network}(s), each with
 * different capabilities, allowing for APN virtualization.
 *
 * <p>If a tunnel fails to connect, or otherwise encounters a fatal error, the VCN will attempt to
 * reestablish the connection. If the tunnel still has not reconnected after a system-determined
 * timeout, the VCN Safe Mode (see below) will be entered.
 *
 * <p>The VCN Safe Mode ensures that users (and carriers) have a fallback to restore system
 * connectivity to update profiles, diagnose issues, contact support, or perform other remediation
 * tasks. In Safe Mode, the system will allow underlying cellular networks to be used as default.
 * Additionally, during Safe Mode, the VCN will continue to retry the connections, and will
 * automatically exit Safe Mode if all active tunnels connect successfully.
 *
 * <p>Apps targeting Android 15 or newer should check the existence of {@link
 * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION} before querying the service. If the feature is
 * absent, {@link Context#getSystemService} may return null.
 */
@SystemService(Context.VCN_MANAGEMENT_SERVICE)
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
public class VcnManager {
    @NonNull private static final String TAG = VcnManager.class.getSimpleName();

    /**
     * Key for WiFi entry RSSI thresholds
     *
     * <p>The VCN will only migrate to a Carrier WiFi network that has a signal strength greater
     * than, or equal to this threshold.
     *
     * @hide
     */
    @NonNull
    public static final String VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY =
            "vcn_network_selection_wifi_entry_rssi_threshold";

    /**
     * Key for WiFi entry RSSI thresholds
     *
     * <p>If the VCN's selected Carrier WiFi network has a signal strength less than this threshold,
     * the VCN will attempt to migrate away from the Carrier WiFi network.
     *
     * @hide
     */
    @NonNull
    public static final String VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY =
            "vcn_network_selection_wifi_exit_rssi_threshold";

    /**
     * Key for the interval to poll IpSecTransformState for packet loss monitoring
     *
     * @hide
     */
    @NonNull
    public static final String VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY =
            "vcn_network_selection_poll_ipsec_state_interval_seconds";

    /**
     * Key for the threshold of IPSec packet loss rate
     *
     * @hide
     */
    @NonNull
    public static final String VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY =
            "vcn_network_selection_ipsec_packet_loss_percent_threshold";

    /**
     * Key for the list of timeouts in minute to stop penalizing an underlying network candidate
     *
     * @hide
     */
    @NonNull
    public static final String VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY =
            "vcn_network_selection_penalty_timeout_minutes_list";

    // TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz

    /**
     * Key for transports that need to be marked as restricted by the VCN
     *
     * <p>Defaults to TRANSPORT_WIFI if the config does not exist
     *
     * @hide
     */
    public static final String VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY =
            "vcn_restricted_transports";

    /**
     * Key for number of seconds to wait before entering safe mode
     *
     * <p>A VcnGatewayConnection will enter safe mode when it takes over the configured timeout to
     * enter {@link ConnectedState}.
     *
     * <p>Defaults to 30, unless overridden by carrier config
     *
     * @hide
     */
    @NonNull
    public static final String VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY =
            "vcn_safe_mode_timeout_seconds_key";

    /**
     * Key for maximum number of parallel SAs for tunnel aggregation
     *
     * <p>If set to a value > 1, multiple tunnels will be set up, and inbound traffic will be
     * aggregated over the various tunnels.
     *
     * <p>Defaults to 1, unless overridden by carrier config
     *
     * @hide
     */
    @NonNull
    public static final String VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY =
            "vcn_tunnel_aggregation_sa_count_max";

    /** List of Carrier Config options to extract from Carrier Config bundles. @hide */
    @NonNull
    public static final String[] VCN_RELATED_CARRIER_CONFIG_KEYS =
            new String[] {
                VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
                VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
                VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
                VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
                VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY,
                VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
                VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY,
                VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY,
            };

    private static final Map<
                    VcnNetworkPolicyChangeListener, VcnUnderlyingNetworkPolicyListenerBinder>
            REGISTERED_POLICY_LISTENERS = new ConcurrentHashMap<>();

    @NonNull private final Context mContext;
    @NonNull private final IVcnManagementService mService;

    /**
     * Construct an instance of VcnManager within an application context.
     *
     * @param ctx the application context for this manager
     * @param service the VcnManagementService binder backing this manager
     *
     * @hide
     */
    public VcnManager(@NonNull Context ctx, @NonNull IVcnManagementService service) {
        mContext = requireNonNull(ctx, "missing context");
        mService = requireNonNull(service, "missing service");
    }

    /**
     * Get all currently registered VcnNetworkPolicyChangeListeners for testing purposes.
     *
     * @hide
     */
    @VisibleForTesting(visibility = Visibility.PRIVATE)
    @NonNull
    public static Map<VcnNetworkPolicyChangeListener, VcnUnderlyingNetworkPolicyListenerBinder>
            getAllPolicyListeners() {
        return Collections.unmodifiableMap(REGISTERED_POLICY_LISTENERS);
    }

    /**
     * Sets the VCN configuration for a given subscription group.
     *
     * <p>An app that has carrier privileges for any of the subscriptions in the given group may set
     * a VCN configuration. If a configuration already exists for the given subscription group, it
     * will be overridden. Any active VCN(s) may be forced to restart to use the new configuration.
     *
     * <p>This API is ONLY permitted for callers running as the primary user.
     *
     * @param subscriptionGroup the subscription group that the configuration should be applied to
     * @param config the configuration parameters for the VCN
     * @throws SecurityException if the caller does not have carrier privileges for the provided
     *     subscriptionGroup, or is not running as the primary user
     * @throws IOException if the configuration failed to be saved and persisted to disk. This may
     *     occur due to temporary disk errors, or more permanent conditions such as a full disk.
     */
    @RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant
    public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config)
            throws IOException {
        requireNonNull(subscriptionGroup, "subscriptionGroup was null");
        requireNonNull(config, "config was null");

        try {
            mService.setVcnConfig(subscriptionGroup, config, mContext.getOpPackageName());
        } catch (ServiceSpecificException e) {
            throw new IOException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Clears the VCN configuration for a given subscription group.
     *
     * <p>An app that has carrier privileges for any of the subscriptions in the given group may
     * clear a VCN configuration. This API is ONLY permitted for callers running as the primary
     * user. Any active VCN associated with this configuration will be torn down.
     *
     * @param subscriptionGroup the subscription group that the configuration should be applied to
     * @throws SecurityException if the caller does not have carrier privileges, is not the owner of
     *     the associated configuration, or is not running as the primary user
     * @throws IOException if the configuration failed to be cleared from disk. This may occur due
     *     to temporary disk errors, or more permanent conditions such as a full disk.
     */
    @RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant
    public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) throws IOException {
        requireNonNull(subscriptionGroup, "subscriptionGroup was null");

        try {
            mService.clearVcnConfig(subscriptionGroup, mContext.getOpPackageName());
        } catch (ServiceSpecificException e) {
            throw new IOException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Retrieves the list of Subscription Groups for which a VCN Configuration has been set.
     *
     * <p>The returned list will include only subscription groups for which an associated {@link
     * VcnConfig} exists, and the app is either:
     *
     * <ul>
     *   <li>Carrier privileged for that subscription group, or
     *   <li>Is the provisioning package of the config
     * </ul>
     *
     * @throws SecurityException if the caller is not running as the primary user
     */
    @NonNull
    public List<ParcelUuid> getConfiguredSubscriptionGroups() {
        try {
            return mService.getConfiguredSubscriptionGroups(mContext.getOpPackageName());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    // TODO(b/180537630): remove all VcnUnderlyingNetworkPolicyListener refs once Telephony is using
    // the new VcnNetworkPolicyChangeListener API
    /**
     * VcnUnderlyingNetworkPolicyListener is the interface through which internal system components
     * can register to receive updates for VCN-underlying Network policies from the System Server.
     *
     * @hide
     */
    public interface VcnUnderlyingNetworkPolicyListener extends VcnNetworkPolicyChangeListener {}

    /**
     * Add a listener for VCN-underlying network policy updates.
     *
     * @param executor the Executor that will be used for invoking all calls to the specified
     *     Listener
     * @param listener the VcnUnderlyingNetworkPolicyListener to be added
     * @throws SecurityException if the caller does not have permission NETWORK_FACTORY
     * @throws IllegalStateException if the specified VcnUnderlyingNetworkPolicyListener is already
     *     registered
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
    public void addVcnUnderlyingNetworkPolicyListener(
            @NonNull Executor executor, @NonNull VcnUnderlyingNetworkPolicyListener listener) {
        addVcnNetworkPolicyChangeListener(executor, listener);
    }

    /**
     * Remove the specified VcnUnderlyingNetworkPolicyListener from VcnManager.
     *
     * <p>If the specified listener is not currently registered, this is a no-op.
     *
     * @param listener the VcnUnderlyingNetworkPolicyListener that will be removed
     * @hide
     */
    public void removeVcnUnderlyingNetworkPolicyListener(
            @NonNull VcnUnderlyingNetworkPolicyListener listener) {
        removeVcnNetworkPolicyChangeListener(listener);
    }

    /**
     * Queries the underlying network policy for a network with the given parameters.
     *
     * <p>Prior to a new NetworkAgent being registered, or upon notification that Carrier VCN policy
     * may have changed via {@link VcnUnderlyingNetworkPolicyListener#onPolicyChanged()}, a Network
     * Provider MUST poll for the updated Network policy based on that Network's capabilities and
     * properties.
     *
     * @param networkCapabilities the NetworkCapabilities to be used in determining the Network
     *     policy for this Network.
     * @param linkProperties the LinkProperties to be used in determining the Network policy for
     *     this Network.
     * @throws SecurityException if the caller does not have permission NETWORK_FACTORY
     * @return the VcnUnderlyingNetworkPolicy to be used for this Network.
     * @hide
     */
    @NonNull
    @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
    public VcnUnderlyingNetworkPolicy getUnderlyingNetworkPolicy(
            @NonNull NetworkCapabilities networkCapabilities,
            @NonNull LinkProperties linkProperties) {
        requireNonNull(networkCapabilities, "networkCapabilities must not be null");
        requireNonNull(linkProperties, "linkProperties must not be null");

        try {
            return mService.getUnderlyingNetworkPolicy(networkCapabilities, linkProperties);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * VcnNetworkPolicyChangeListener is the interface through which internal system components
     * (e.g. Network Factories) can register to receive updates for VCN-underlying Network policies
     * from the System Server.
     *
     * <p>Any Network Factory that brings up Networks capable of being VCN-underlying Networks
     * should register a VcnNetworkPolicyChangeListener. VcnManager will then use this listener to
     * notify the registrant when VCN Network policies change. Upon receiving this signal, the
     * listener must check {@link VcnManager} for the current Network policy result for each of its
     * Networks via {@link #applyVcnNetworkPolicy(NetworkCapabilities, LinkProperties)}.
     *
     * @hide
     */
    @SystemApi
    public interface VcnNetworkPolicyChangeListener {
        /**
         * Notifies the implementation that the VCN's underlying Network policy has changed.
         *
         * <p>After receiving this callback, implementations should get the current {@link
         * VcnNetworkPolicyResult} via {@link #applyVcnNetworkPolicy(NetworkCapabilities,
         * LinkProperties)}.
         */
        void onPolicyChanged();
    }

    /**
     * Add a listener for VCN-underlying Network policy updates.
     *
     * <p>A {@link VcnNetworkPolicyChangeListener} is eligible to begin receiving callbacks once it
     * is registered. No callbacks are guaranteed upon registration.
     *
     * @param executor the Executor that will be used for invoking all calls to the specified
     *     Listener
     * @param listener the VcnNetworkPolicyChangeListener to be added
     * @throws SecurityException if the caller does not have permission NETWORK_FACTORY
     * @throws IllegalStateException if the specified VcnNetworkPolicyChangeListener is already
     *     registered
     * @hide
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
    public void addVcnNetworkPolicyChangeListener(
            @NonNull Executor executor, @NonNull VcnNetworkPolicyChangeListener listener) {
        requireNonNull(executor, "executor must not be null");
        requireNonNull(listener, "listener must not be null");

        VcnUnderlyingNetworkPolicyListenerBinder binder =
                new VcnUnderlyingNetworkPolicyListenerBinder(executor, listener);
        if (REGISTERED_POLICY_LISTENERS.putIfAbsent(listener, binder) != null) {
            throw new IllegalStateException("listener is already registered with VcnManager");
        }

        try {
            mService.addVcnUnderlyingNetworkPolicyListener(binder);
        } catch (RemoteException e) {
            REGISTERED_POLICY_LISTENERS.remove(listener);
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Remove the specified VcnNetworkPolicyChangeListener from VcnManager.
     *
     * <p>If the specified listener is not currently registered, this is a no-op.
     *
     * @param listener the VcnNetworkPolicyChangeListener that will be removed
     * @throws SecurityException if the caller does not have permission NETWORK_FACTORY
     * @hide
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
    public void removeVcnNetworkPolicyChangeListener(
            @NonNull VcnNetworkPolicyChangeListener listener) {
        requireNonNull(listener, "listener must not be null");

        VcnUnderlyingNetworkPolicyListenerBinder binder =
                REGISTERED_POLICY_LISTENERS.remove(listener);
        if (binder == null) {
            return;
        }

        try {
            mService.removeVcnUnderlyingNetworkPolicyListener(binder);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Applies the network policy for a {@link android.net.Network} with the given parameters.
     *
     * <p>Prior to a new NetworkAgent being registered, or upon notification that Carrier VCN policy
     * may have changed via {@link VcnNetworkPolicyChangeListener#onPolicyChanged()}, a Network
     * Provider MUST poll for the updated Network policy based on that Network's capabilities and
     * properties.
     *
     * @param networkCapabilities the NetworkCapabilities to be used in determining the Network
     *     policy result for this Network.
     * @param linkProperties the LinkProperties to be used in determining the Network policy result
     *     for this Network.
     * @throws SecurityException if the caller does not have permission NETWORK_FACTORY
     * @return the {@link VcnNetworkPolicyResult} to be used for this Network.
     * @hide
     */
    @NonNull
    @SystemApi
    @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
    public VcnNetworkPolicyResult applyVcnNetworkPolicy(
            @NonNull NetworkCapabilities networkCapabilities,
            @NonNull LinkProperties linkProperties) {
        requireNonNull(networkCapabilities, "networkCapabilities must not be null");
        requireNonNull(linkProperties, "linkProperties must not be null");

        final VcnUnderlyingNetworkPolicy policy =
                getUnderlyingNetworkPolicy(networkCapabilities, linkProperties);
        return new VcnNetworkPolicyResult(
                policy.isTeardownRequested(), policy.getMergedNetworkCapabilities());
    }

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
        VCN_STATUS_CODE_NOT_CONFIGURED,
        VCN_STATUS_CODE_INACTIVE,
        VCN_STATUS_CODE_ACTIVE,
        VCN_STATUS_CODE_SAFE_MODE
    })
    public @interface VcnStatusCode {}

    /**
     * Value indicating that the VCN for the subscription group is not configured, or that the
     * callback is not privileged for the subscription group.
     */
    public static final int VCN_STATUS_CODE_NOT_CONFIGURED = 0;

    /**
     * Value indicating that the VCN for the subscription group is inactive.
     *
     * <p>A VCN is inactive if a {@link VcnConfig} is present for the subscription group, but the
     * provisioning package is not privileged.
     */
    public static final int VCN_STATUS_CODE_INACTIVE = 1;

    /**
     * Value indicating that the VCN for the subscription group is active.
     *
     * <p>A VCN is active if a {@link VcnConfig} is present for the subscription, the provisioning
     * package is privileged, and the VCN is not in Safe Mode. In other words, a VCN is considered
     * active while it is connecting, fully connected, and disconnecting.
     */
    public static final int VCN_STATUS_CODE_ACTIVE = 2;

    /**
     * Value indicating that the VCN for the subscription group is in Safe Mode.
     *
     * <p>A VCN will be put into Safe Mode if any of the gateway connections were unable to
     * establish a connection within a system-determined timeout (while underlying networks were
     * available).
     */
    public static final int VCN_STATUS_CODE_SAFE_MODE = 3;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
        VCN_ERROR_CODE_INTERNAL_ERROR,
        VCN_ERROR_CODE_CONFIG_ERROR,
        VCN_ERROR_CODE_NETWORK_ERROR
    })
    public @interface VcnErrorCode {}

    /**
     * Value indicating that an internal failure occurred in this Gateway Connection.
     */
    public static final int VCN_ERROR_CODE_INTERNAL_ERROR = 0;

    /**
     * Value indicating that an error with this Gateway Connection's configuration occurred.
     *
     * <p>For example, this error code will be returned after authentication failures.
     */
    public static final int VCN_ERROR_CODE_CONFIG_ERROR = 1;

    /**
     * Value indicating that a Network error occurred with this Gateway Connection.
     *
     * <p>For example, this error code will be returned if an underlying {@link android.net.Network}
     * for this Gateway Connection is lost, or if an error occurs while resolving the connection
     * endpoint address.
     */
    public static final int VCN_ERROR_CODE_NETWORK_ERROR = 2;

    /**
     * VcnStatusCallback is the interface for Carrier apps to receive updates for their VCNs.
     *
     * <p>VcnStatusCallbacks may be registered before {@link VcnConfig}s are provided for a
     * subscription group.
     */
    public abstract static class VcnStatusCallback {
        private VcnStatusCallbackBinder mCbBinder;

        /**
         * Invoked when status of the VCN for this callback's subscription group changes.
         *
         * @param statusCode the code for the status change encountered by this {@link
         *     VcnStatusCallback}'s subscription group. This value will be one of VCN_STATUS_CODE_*.
         */
        public abstract void onStatusChanged(@VcnStatusCode int statusCode);

        /**
         * Invoked when a VCN Gateway Connection corresponding to this callback's subscription group
         * encounters an error.
         *
         * @param gatewayConnectionName the String GatewayConnection name for the GatewayConnection
         *     encountering an error. This will match the name for exactly one {@link
         *     VcnGatewayConnectionConfig} for the {@link VcnConfig} configured for this callback's
         *     subscription group
         * @param errorCode the code to indicate the error that occurred. This value will be one of
         *     VCN_ERROR_CODE_*.
         * @param detail Throwable to provide additional information about the error, or {@code
         *     null} if none
         */
        public abstract void onGatewayConnectionError(
                @NonNull String gatewayConnectionName,
                @VcnErrorCode int errorCode,
                @Nullable Throwable detail);
    }

    /**
     * Registers the given callback to receive status updates for the specified subscription.
     *
     * <p>Callbacks can be registered for a subscription before {@link VcnConfig}s are set for it.
     *
     * <p>A {@link VcnStatusCallback} may only be registered for one subscription at a time. {@link
     * VcnStatusCallback}s may be reused once unregistered.
     *
     * <p>A {@link VcnStatusCallback} will only be invoked if the registering package has carrier
     * privileges for the specified subscription at the time of invocation.
     *
     * <p>A {@link VcnStatusCallback} is eligible to begin receiving callbacks once it is registered
     * and there is a VCN active for its specified subscription group (this may happen after the
     * callback is registered).
     *
     * <p>{@link VcnStatusCallback#onStatusChanged(int)} will be invoked on registration with the
     * current status for the specified subscription group's VCN. If the registrant is not
     * privileged for this subscription group, {@link #VCN_STATUS_CODE_NOT_CONFIGURED} will be
     * returned.
     *
     * @param subscriptionGroup The subscription group to match for callbacks
     * @param executor The {@link Executor} to be used for invoking callbacks
     * @param callback The VcnStatusCallback to be registered
     * @throws IllegalStateException if callback is currently registered with VcnManager
     */
    public void registerVcnStatusCallback(
            @NonNull ParcelUuid subscriptionGroup,
            @NonNull Executor executor,
            @NonNull VcnStatusCallback callback) {
        requireNonNull(subscriptionGroup, "subscriptionGroup must not be null");
        requireNonNull(executor, "executor must not be null");
        requireNonNull(callback, "callback must not be null");

        synchronized (callback) {
            if (callback.mCbBinder != null) {
                throw new IllegalStateException("callback is already registered with VcnManager");
            }
            callback.mCbBinder = new VcnStatusCallbackBinder(executor, callback);

            try {
                mService.registerVcnStatusCallback(
                        subscriptionGroup, callback.mCbBinder, mContext.getOpPackageName());
            } catch (RemoteException e) {
                callback.mCbBinder = null;
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /**
     * Unregisters the given callback.
     *
     * <p>Once unregistered, the callback will stop receiving status updates for the subscription it
     * was registered with.
     *
     * @param callback The callback to be unregistered
     */
    public void unregisterVcnStatusCallback(@NonNull VcnStatusCallback callback) {
        requireNonNull(callback, "callback must not be null");

        synchronized (callback) {
            if (callback.mCbBinder == null) {
                // no Binder attached to this callback, so it's not currently registered
                return;
            }

            try {
                mService.unregisterVcnStatusCallback(callback.mCbBinder);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            } finally {
                callback.mCbBinder = null;
            }
        }
    }

    /**
     * Binder wrapper for added VcnNetworkPolicyChangeListeners to receive signals from System
     * Server.
     *
     * @hide
     */
    private static class VcnUnderlyingNetworkPolicyListenerBinder
            extends IVcnUnderlyingNetworkPolicyListener.Stub {
        @NonNull private final Executor mExecutor;
        @NonNull private final VcnNetworkPolicyChangeListener mListener;

        private VcnUnderlyingNetworkPolicyListenerBinder(
                Executor executor, VcnNetworkPolicyChangeListener listener) {
            mExecutor = executor;
            mListener = listener;
        }

        @Override
        public void onPolicyChanged() {
            Binder.withCleanCallingIdentity(
                    () -> mExecutor.execute(() -> mListener.onPolicyChanged()));
        }
    }

    /**
     * Binder wrapper for VcnStatusCallbacks to receive signals from VcnManagementService.
     *
     * @hide
     */
    @VisibleForTesting(visibility = Visibility.PRIVATE)
    public static class VcnStatusCallbackBinder extends IVcnStatusCallback.Stub {
        @NonNull private final Executor mExecutor;
        @NonNull private final VcnStatusCallback mCallback;

        public VcnStatusCallbackBinder(
                @NonNull Executor executor, @NonNull VcnStatusCallback callback) {
            mExecutor = executor;
            mCallback = callback;
        }

        @Override
        public void onVcnStatusChanged(@VcnStatusCode int statusCode) {
            Binder.withCleanCallingIdentity(
                    () -> mExecutor.execute(() -> mCallback.onStatusChanged(statusCode)));
        }

        // TODO(b/180521637): use ServiceSpecificException for safer Exception 'parceling'
        @Override
        public void onGatewayConnectionError(
                @NonNull String gatewayConnectionName,
                @VcnErrorCode int errorCode,
                @Nullable String exceptionClass,
                @Nullable String exceptionMessage) {
            final Throwable cause = createThrowableByClassName(exceptionClass, exceptionMessage);

            Binder.withCleanCallingIdentity(
                    () ->
                            mExecutor.execute(
                                    () ->
                                            mCallback.onGatewayConnectionError(
                                                    gatewayConnectionName, errorCode, cause)));
        }

        private static Throwable createThrowableByClassName(
                @Nullable String className, @Nullable String message) {
            if (className == null) {
                return null;
            }

            try {
                Class<?> c = Class.forName(className);
                return (Throwable) c.getConstructor(String.class).newInstance(message);
            } catch (ReflectiveOperationException | ClassCastException e) {
                return new RuntimeException(className + ": " + message);
            }
        }
    }
}