• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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