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