1 /*
2  * Copyright (C) 2020 Google LLC
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.carrier;
17 
18 import com.google.common.base.Preconditions;
19 import com.google.carrier.CarrierConfig;
20 import com.google.carrier.CarrierId;
21 import com.google.carrier.CarrierMap;
22 import com.google.carrier.CarrierSettings;
23 import com.google.carrier.MultiCarrierSettings;
24 import com.google.carrier.VendorConfigClient;
25 import com.google.carrier.VendorConfigs;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.Comparator;
29 import java.util.List;
30 
31 /** Utility methods */
32 public class CarrierProtoUtils {
33 
34   /**
35    * The base of release version.
36    *
37    * A valid release version must be a multiple of the base.
38    * A file version must be smaller than the base - otherwise the version schema is broken.
39    */
40   public static final long RELEASE_VERSION_BASE = 1000000000L;
41 
42   /**
43    * Bump the version by an offset, to differentiate release/branch.
44    *
45    * The input version must be smaller than RELEASE_VERSION_BASE, and the offset must be a
46    * multiple of RELEASE_VERSION_BASE.
47    */
addVersionOffset(long version, long offset)48   public static long addVersionOffset(long version, long offset) {
49 
50     Preconditions.checkArgument(version < RELEASE_VERSION_BASE,
51         "OMG, by design the file version should be smaller than %s, but is %s.",
52         RELEASE_VERSION_BASE, version);
53     Preconditions.checkArgument((offset % RELEASE_VERSION_BASE) == 0,
54         "The offset %s is not a multiple of %s",
55         offset, RELEASE_VERSION_BASE);
56 
57     return version + offset;
58   }
59 
60   /**
61    * Merge configs fields in {@code patch} into {@code base}; configs sorted by key.
62    * See {@link mergeCarrierConfig(CarrierConfig, CarrierConfig.Builder)}.
63    */
64   public static CarrierConfig mergeCarrierConfig(CarrierConfig base, CarrierConfig patch) {
65     Preconditions.checkNotNull(patch);
66 
67     return mergeCarrierConfig(base, patch.toBuilder());
68   }
69 
70   /**
71    * Merge configs fields in {@code patch} into {@code base}; configs sorted by key.
72    *
73    * <p>Sorting is desired because:
74    *
75    * <ul>
76    *   <li>1. The order doesn't matter for the consumer so sorting will not cause behavior change;
77    *   <li>2. The output proto is serilized to text for human review; undeterministic order can
78    *       confuse reviewers as they cannot easily tell if it's re-ordering or actual data change.
79    * </ul>
80    */
81   public static CarrierConfig mergeCarrierConfig(CarrierConfig base, CarrierConfig.Builder patch) {
82     Preconditions.checkNotNull(base);
83     Preconditions.checkNotNull(patch);
84 
85     CarrierConfig.Builder baseBuilder = base.toBuilder();
86 
87     // Traverse each config in patch
88     for (int i = 0; i < patch.getConfigCount(); i++) {
89       CarrierConfig.Config patchConfig = patch.getConfig(i);
90 
91       // Try to find an config in base with the same key as the config from patch
92       int j = 0;
93       for (j = 0; j < baseBuilder.getConfigCount(); j++) {
94         if (baseBuilder.getConfig(j).getKey().equals(patchConfig.getKey())) {
95           break;
96         }
97       }
98 
99       // If match found, replace base with patch; otherwise append patch into base.
100       if (j < baseBuilder.getConfigCount()) {
101         baseBuilder.setConfig(j, patchConfig);
102       } else {
103         baseBuilder.addConfig(patchConfig);
104       }
105     }
106 
107     // Sort configs in baseBuilder by key
108     List<CarrierConfig.Config> configs = new ArrayList<>(baseBuilder.getConfigList());
109     Collections.sort(configs, Comparator.comparing(CarrierConfig.Config::getKey));
110     baseBuilder.clearConfig();
111     baseBuilder.addAllConfig(configs);
112 
113     return baseBuilder.build();
114   }
115 
116   /**
117    * Find a carrier's CarrierSettings by canonical_name from a MultiCarrierSettings.
118    *
119    * <p>Return null if not found.
120    */
121   public static CarrierSettings findCarrierSettingsByCanonicalName(
122       MultiCarrierSettings mcs, String name) {
123 
124     Preconditions.checkNotNull(mcs);
125     Preconditions.checkNotNull(name);
126 
127     for (int i = 0; i < mcs.getSettingCount(); i++) {
128       CarrierSettings cs = mcs.getSetting(i);
129       if (cs.getCanonicalName().equals(name)) {
130         return cs;
131       }
132     }
133 
134     return null;
135   }
136 
137   /** Apply device overly to a carrier setting */
138   public static CarrierSettings.Builder applyDeviceOverlayToCarrierSettings(
139       MultiCarrierSettings allOverlay, CarrierSettings.Builder base) {
140     return applyDeviceOverlayToCarrierSettings(allOverlay, base.build());
141   }
142 
143   /** Apply device overly to a carrier setting */
144   public static CarrierSettings.Builder applyDeviceOverlayToCarrierSettings(
145       MultiCarrierSettings allOverlay, CarrierSettings base) {
146 
147     Preconditions.checkNotNull(allOverlay);
148     Preconditions.checkNotNull(base);
149 
150     // Find overlay of the base carrier. If not found, just return base.
151     CarrierSettings overlay =
152         findCarrierSettingsByCanonicalName(allOverlay, base.getCanonicalName());
153     if (overlay == null) {
154       return base.toBuilder();
155     }
156 
157     CarrierSettings.Builder resultBuilder = base.toBuilder();
158     // Add version number of base settings and overlay, so the result version number
159     // monotonically increases.
160     resultBuilder.setVersion(base.getVersion() + overlay.getVersion());
161     // Merge overlay settings into base settings
162     resultBuilder.setConfigs(mergeCarrierConfig(resultBuilder.getConfigs(), overlay.getConfigs()));
163     // Replace base apns with overlay apns (if not empty)
164     if (overlay.getApns().getApnCount() > 0) {
165       resultBuilder.setApns(overlay.getApns());
166     }
167     // Merge the overlay vendor configuration into base vendor configuration
168     // Can be cutomized
169     return resultBuilder;
170   }
171 
172   /** Apply device overly to multiple carriers setting */
173   public static MultiCarrierSettings applyDeviceOverlayToMultiCarrierSettings(
174       MultiCarrierSettings overlay, MultiCarrierSettings base) {
175 
176     Preconditions.checkNotNull(overlay);
177     Preconditions.checkNotNull(base);
178 
179     MultiCarrierSettings.Builder resultBuilder = base.toBuilder().clearSetting();
180     long version = 0L;
181 
182     for (CarrierSettings cs : base.getSettingList()) {
183       // Apply overlay and put overlayed carrier setting back
184       CarrierSettings.Builder merged = applyDeviceOverlayToCarrierSettings(overlay, cs);
185       resultBuilder.addSetting(merged);
186       // The top-level version number is the sum of all version numbers of settings
187       version += merged.getVersion();
188     }
189     resultBuilder.setVersion(version);
190 
191     return resultBuilder.build();
192   }
193 
194   /**
195    * Sort a list of CarrierMap with single CarrierId.
196    *
197    * <p>Precondition: no duplication in input list
198    *
199    * <p>Order by:
200    *
201    * <ul>
202    *   <li>mcc_mnc
203    *   <li>(for the same mcc_mnc) any mvno_data comes before MVNODATA_NOT_SET (Preconditon: there is
204    *       only one entry with MVNODATA_NOT_SET per mcc_mnc otherwise they're duplicated)
205    *   <li>(for MVNOs of the same mcc_mnc) mvno_data case + value as string
206    * </ul>
207    */
208   public static void sortCarrierMapEntries(List<CarrierMap> list) {
209     final Comparator<CarrierMap> byMccMnc =
210         Comparator.comparing(
211             (cm) -> {
212               return cm.getCarrierId(0).getMccMnc();
213             });
214     final Comparator<CarrierMap> mvnoFirst =
215         Comparator.comparingInt(
216             (cm) -> {
217               switch (cm.getCarrierId(0).getMvnoDataCase()) {
218                 case MVNODATA_NOT_SET:
219                   return 1;
220                 default:
221                   return 0;
222               }
223             });
224     final Comparator<CarrierMap> byMvnoDataCaseValue =
225         Comparator.comparing(
226             (cm) -> {
227               final CarrierId cid = cm.getCarrierId(0);
228               switch (cid.getMvnoDataCase()) {
229                 case GID1:
230                   return "GID1=" + cid.getGid1();
231                 case IMSI:
232                   return "IMSI=" + cid.getImsi();
233                 case SPN:
234                   return "SPN=" + cid.getSpn();
235                 case MVNODATA_NOT_SET:
236                   throw new AssertionError("MNO should not be compared here but in `mvnoFirst`");
237               }
238               throw new AssertionError("uncaught case " + cid.getMvnoDataCase());
239             });
240     Collections.sort(list, byMccMnc.thenComparing(mvnoFirst).thenComparing(byMvnoDataCaseValue));
241   }
242 }
243