1 /* 2 * Copyright 2022 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.android.libraries.mobiledatadownload.populator; 17 18 import static java.util.stream.Collectors.groupingBy; 19 import static java.util.stream.Collectors.mapping; 20 import static java.util.stream.Collectors.toCollection; 21 22 import android.annotation.TargetApi; 23 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 24 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 25 import com.google.common.base.Optional; 26 import com.google.common.base.Preconditions; 27 import com.google.common.base.Supplier; 28 import com.google.common.util.concurrent.Futures; 29 import com.google.common.util.concurrent.ListenableFuture; 30 import com.google.common.util.concurrent.MoreExecutors; 31 import com.google.errorprone.annotations.CanIgnoreReturnValue; 32 import com.google.mobiledatadownload.DownloadConfigProto.DataFileGroup; 33 import com.google.mobiledatadownload.DownloadConfigProto.ManifestConfig; 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Locale; 38 import java.util.Map; 39 import java.util.Set; 40 import java.util.concurrent.Executor; 41 import java.util.function.BiFunction; 42 43 /** 44 * An Overrider that finds matching {@link DataFileGroup} within {@link ManifestConfig} based on 45 * supplied locale. 46 * 47 * <p>We will first group the {@link DataFileGroup} by {@code group_name}, then apply {@code 48 * matchStrategy} to get 0 or 1 {@link DataFileGroup} for each of the group, then combine them and 49 * returns a list. 50 * 51 * <p>{@code localeSupplier} supplies the locale to get matches 52 * 53 * <p>NOTE: By default we use {@link LOCALE_MATCHER_STRATEGY}, which could fallback to a different 54 * locale to supplied one. 55 * 56 * <p>WARNING: It's UNDEFINED behavior if more than one {locale, group_name} pair exists. 57 */ 58 @TargetApi(24) 59 @SuppressWarnings("AndroidJdkLibsChecker") 60 public final class LocaleOverrider implements ManifestConfigOverrider { 61 62 private static final String TAG = "LocaleOverrider"; 63 64 /** Builder for {@link FilteringPopulator}. */ 65 public static final class Builder { 66 67 private Supplier<ListenableFuture<Locale>> localeSupplier; 68 private BiFunction<Locale, Set<Locale>, Optional<Locale>> matchStrategy; 69 private Executor lightweightExecutor; 70 71 /** only one of setLocaleSupplier or setLocaleFutureSupplier is required */ 72 @CanIgnoreReturnValue setLocaleSupplier(Supplier<Locale> localeSupplier)73 public Builder setLocaleSupplier(Supplier<Locale> localeSupplier) { 74 this.localeSupplier = () -> Futures.immediateFuture(localeSupplier.get()); 75 this.lightweightExecutor = 76 MoreExecutors.directExecutor(); // use directExecutor if locale is provided sync. 77 return this; 78 } 79 80 @CanIgnoreReturnValue setLocaleFutureSupplier( Supplier<ListenableFuture<Locale>> localeSupplier, Executor lightweightExecutor)81 public Builder setLocaleFutureSupplier( 82 Supplier<ListenableFuture<Locale>> localeSupplier, Executor lightweightExecutor) { 83 this.localeSupplier = localeSupplier; 84 this.lightweightExecutor = lightweightExecutor; 85 return this; 86 } 87 88 /** 89 * A function that decides a match based on provided Locale and a set of available Locales in 90 * the config. The set of Locale should be related to ONE {@code group_name} of {@link 91 * DataFilegroup}. 92 */ 93 @CanIgnoreReturnValue setMatchStrategy( BiFunction<Locale, Set<Locale>, Optional<Locale>> matchStrategy)94 public Builder setMatchStrategy( 95 BiFunction<Locale, Set<Locale>, Optional<Locale>> matchStrategy) { 96 this.matchStrategy = matchStrategy; 97 return this; 98 } 99 build()100 public LocaleOverrider build() { 101 Preconditions.checkState( 102 localeSupplier != null, 103 "Must call setLocaleSupplier() or setLocaleFutureSupplier() before build()."); 104 if (matchStrategy == null) { 105 LogUtil.d("%s: Applying LANG_FALLBACK_STRATEGY", TAG); 106 matchStrategy = LANG_FALLBACK_STRATEGY; 107 } 108 109 return new LocaleOverrider(this); 110 } 111 } 112 113 private final Supplier<ListenableFuture<Locale>> localeSupplier; 114 private final BiFunction<Locale, Set<Locale>, Optional<Locale>> matchStrategy; 115 private final Executor lightweightExecutor; 116 117 /** Returns a Builder for {@link LocaleOverrider}. */ builder()118 public static Builder builder() { 119 return new Builder(); 120 } 121 LocaleOverrider(Builder builder)122 private LocaleOverrider(Builder builder) { 123 this.localeSupplier = builder.localeSupplier; 124 this.matchStrategy = builder.matchStrategy; 125 this.lightweightExecutor = builder.lightweightExecutor; 126 } 127 128 /** 129 * Returns the {@link Locale} if it's present in {@link Set<Locale>}, or {@link Optional#absent} 130 */ 131 public static final BiFunction<Locale, Set<Locale>, Optional<Locale>> EQUAL_STRATEGY = 132 (locale, localeSet) -> localeSet.contains(locale) ? Optional.of(locale) : Optional.absent(); 133 134 /** 135 * Returns an exact matching {@link Locale}, or fallback to only matching lang when not available. 136 */ 137 public static final BiFunction<Locale, Set<Locale>, Optional<Locale>> LANG_FALLBACK_STRATEGY = 138 (locale, localeSet) -> { 139 Optional<Locale> exactMatch = EQUAL_STRATEGY.apply(locale, localeSet); 140 if (exactMatch.isPresent()) { 141 return exactMatch; 142 } else { 143 // Match on lang part. 144 return EQUAL_STRATEGY.apply(new Locale(locale.getLanguage()), localeSet); 145 } 146 }; 147 148 @Override override(ManifestConfig manifestConfig)149 public ListenableFuture<List<DataFileGroup>> override(ManifestConfig manifestConfig) { 150 // Groups Entries by GroupName. 151 Map<String, List<ManifestConfig.Entry>> groupToEntries = 152 manifestConfig.getEntryList().stream() 153 .collect( 154 groupingBy( 155 entry -> entry.getDataFileGroup().getGroupName(), 156 HashMap<String, List<ManifestConfig.Entry>>::new, 157 mapping(entry -> entry, toCollection(ArrayList<ManifestConfig.Entry>::new)))); 158 159 // Finds a DataFileGroup for every GroupName. 160 List<ListenableFuture<Optional<DataFileGroup>>> matchedFileGroupsFuture = new ArrayList<>(); 161 for (List<ManifestConfig.Entry> entries : groupToEntries.values()) { 162 matchedFileGroupsFuture.add(getFileGroupWithMatchStrategy(entries)); 163 } 164 165 return PropagatedFutures.transform( 166 Futures.successfulAsList(matchedFileGroupsFuture), 167 fileGroups -> { 168 List<DataFileGroup> matchedFileGroups = new ArrayList<>(); 169 for (Optional<DataFileGroup> fileGroup : fileGroups) { 170 if (fileGroup != null && fileGroup.isPresent()) { 171 matchedFileGroups.add(fileGroup.get()); 172 } 173 } 174 return matchedFileGroups; 175 }, 176 lightweightExecutor); 177 } 178 179 /** Returns an optional {@link DataFileGroup} by applying {@code matchStrategy}. */ getFileGroupWithMatchStrategy( List<ManifestConfig.Entry> entries)180 private ListenableFuture<Optional<DataFileGroup>> getFileGroupWithMatchStrategy( 181 List<ManifestConfig.Entry> entries) { 182 Map<Locale, DataFileGroup> localeToFileGroup = new HashMap<>(); 183 184 for (ManifestConfig.Entry entry : entries) { 185 for (String localeString : entry.getModifier().getLocaleList()) { 186 DataFileGroup dataFileGroup; 187 if (entry.getDataFileGroup().getLocaleList().contains(localeString)) { 188 dataFileGroup = entry.getDataFileGroup(); 189 } else { 190 dataFileGroup = entry.getDataFileGroup().toBuilder().addLocale(localeString).build(); 191 } 192 localeToFileGroup.put(Locale.forLanguageTag(localeString), dataFileGroup); 193 } 194 } 195 196 return PropagatedFutures.transform( 197 localeSupplier.get(), 198 locale -> { 199 Optional<Locale> chosenLocaleOptional = 200 matchStrategy.apply(locale, localeToFileGroup.keySet()); 201 if (chosenLocaleOptional.isPresent()) { 202 Locale chosenLocale = chosenLocaleOptional.get(); 203 LogUtil.d("%s: chosenLocale: %s", TAG, chosenLocale); 204 if (localeToFileGroup.containsKey(chosenLocale)) { 205 LogUtil.v("%s: matched groups %s", TAG, localeToFileGroup.get(chosenLocale)); 206 return Optional.of(localeToFileGroup.get(chosenLocale)); 207 } else { 208 LogUtil.e("%s: Strategy applied retured invalid locale: : %s", TAG, chosenLocale); 209 } 210 } 211 return Optional.absent(); 212 }, 213 lightweightExecutor); 214 } 215 } 216