• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
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 
17 package com.android.textclassifier.downloader;
18 
19 import static com.android.textclassifier.downloader.TextClassifierDownloadLogger.REASON_TO_SCHEDULE_DEVICE_CONFIG_UPDATED;
20 import static com.android.textclassifier.downloader.TextClassifierDownloadLogger.REASON_TO_SCHEDULE_LOCALE_SETTINGS_CHANGED;
21 import static com.android.textclassifier.downloader.TextClassifierDownloadLogger.REASON_TO_SCHEDULE_TCS_STARTED;
22 import static java.util.concurrent.TimeUnit.MILLISECONDS;
23 
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.os.LocaleList;
29 import android.provider.DeviceConfig;
30 import android.text.TextUtils;
31 import androidx.work.BackoffPolicy;
32 import androidx.work.Constraints;
33 import androidx.work.Data;
34 import androidx.work.ExistingWorkPolicy;
35 import androidx.work.ListenableWorker;
36 import androidx.work.NetworkType;
37 import androidx.work.OneTimeWorkRequest;
38 import androidx.work.Operation;
39 import androidx.work.WorkManager;
40 import com.android.textclassifier.common.ModelType.ModelTypeDef;
41 import com.android.textclassifier.common.TextClassifierSettings;
42 import com.android.textclassifier.common.base.TcLog;
43 import com.android.textclassifier.utils.IndentingPrintWriter;
44 import com.google.common.annotations.VisibleForTesting;
45 import com.google.common.base.Enums;
46 import com.google.common.base.Preconditions;
47 import com.google.common.collect.ImmutableList;
48 import com.google.common.hash.Hashing;
49 import com.google.common.util.concurrent.FutureCallback;
50 import com.google.common.util.concurrent.Futures;
51 import com.google.common.util.concurrent.ListenableFuture;
52 import com.google.common.util.concurrent.ListeningExecutorService;
53 import java.io.File;
54 import java.time.Instant;
55 import java.util.List;
56 import java.util.Locale;
57 import java.util.UUID;
58 import java.util.concurrent.Callable;
59 import javax.annotation.Nullable;
60 
61 /** Manager to listen to config update and download latest models. */
62 public final class ModelDownloadManager {
63   private static final String TAG = "ModelDownloadManager";
64 
65   @VisibleForTesting static final String UNIQUE_QUEUE_NAME = "ModelDownloadWorkManagerQueue";
66 
67   private final Context appContext;
68   private final Class<? extends ListenableWorker> modelDownloadWorkerClass;
69   private final Callable<WorkManager> workManagerSupplier;
70   private final DownloadedModelManager downloadedModelManager;
71   private final TextClassifierSettings settings;
72   private final ListeningExecutorService executorService;
73   private final DeviceConfig.OnPropertiesChangedListener deviceConfigListener;
74   private final BroadcastReceiver localeChangedReceiver;
75 
76   /**
77    * Constructor for ModelDownloadManager.
78    *
79    * @param appContext the context of this application
80    * @param settings TextClassifierSettings to access DeviceConfig and other settings
81    * @param executorService background executor service
82    */
ModelDownloadManager( Context appContext, TextClassifierSettings settings, ListeningExecutorService executorService)83   public ModelDownloadManager(
84       Context appContext,
85       TextClassifierSettings settings,
86       ListeningExecutorService executorService) {
87     this(
88         appContext,
89         ModelDownloadWorker.class,
90         () -> WorkManager.getInstance(appContext),
91         DownloadedModelManagerImpl.getInstance(appContext),
92         settings,
93         executorService);
94   }
95 
96   @VisibleForTesting
ModelDownloadManager( Context appContext, Class<? extends ListenableWorker> modelDownloadWorkerClass, Callable<WorkManager> workManagerSupplier, DownloadedModelManager downloadedModelManager, TextClassifierSettings settings, ListeningExecutorService executorService)97   public ModelDownloadManager(
98       Context appContext,
99       Class<? extends ListenableWorker> modelDownloadWorkerClass,
100       Callable<WorkManager> workManagerSupplier,
101       DownloadedModelManager downloadedModelManager,
102       TextClassifierSettings settings,
103       ListeningExecutorService executorService) {
104     this.appContext = Preconditions.checkNotNull(appContext);
105     this.modelDownloadWorkerClass = Preconditions.checkNotNull(modelDownloadWorkerClass);
106     this.workManagerSupplier = Preconditions.checkNotNull(workManagerSupplier);
107     this.downloadedModelManager = Preconditions.checkNotNull(downloadedModelManager);
108     this.settings = Preconditions.checkNotNull(settings);
109     this.executorService = Preconditions.checkNotNull(executorService);
110 
111     this.deviceConfigListener =
112         new DeviceConfig.OnPropertiesChangedListener() {
113           @Override
114           public void onPropertiesChanged(DeviceConfig.Properties unused) {
115             onTextClassifierDeviceConfigChanged();
116           }
117         };
118     this.localeChangedReceiver =
119         new BroadcastReceiver() {
120           @Override
121           public void onReceive(Context context, Intent intent) {
122             onLocaleChanged();
123           }
124         };
125   }
126 
127   /** Returns the downlaoded models for the given modelType. */
128   @Nullable
listDownloadedModels(@odelTypeDef String modelType)129   public List<File> listDownloadedModels(@ModelTypeDef String modelType) {
130     try {
131       return downloadedModelManager.listModels(modelType);
132     } catch (Throwable t) {
133       TcLog.e(TAG, "Failed to list downloaded models", t);
134       return ImmutableList.of();
135     }
136   }
137 
138   /** Notifies the model downlaoder that the text classifier service is created. */
onTextClassifierServiceCreated()139   public void onTextClassifierServiceCreated() {
140     try {
141       DeviceConfig.addOnPropertiesChangedListener(
142           DeviceConfig.NAMESPACE_TEXTCLASSIFIER, executorService, deviceConfigListener);
143       appContext.registerReceiver(
144           localeChangedReceiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
145       TcLog.d(TAG, "DeviceConfig listener and locale change listener are registered.");
146       if (!settings.isModelDownloadManagerEnabled()) {
147         return;
148       }
149       maybeOverrideLocaleListForTesting();
150       TcLog.d(TAG, "Try to schedule model download work because TextClassifierService started.");
151       scheduleDownloadWork(REASON_TO_SCHEDULE_TCS_STARTED);
152     } catch (Throwable t) {
153       TcLog.e(TAG, "Failed inside onTextClassifierServiceCreated", t);
154     }
155   }
156 
157   // TODO(licha): Make this private. Let the constructor accept a receiver to enable testing.
158   /** Notifies the model downlaoder that the system locale setting is changed. */
159   @VisibleForTesting
onLocaleChanged()160   void onLocaleChanged() {
161     if (!settings.isModelDownloadManagerEnabled()) {
162       return;
163     }
164     TcLog.d(TAG, "Try to schedule model download work because of system locale changes.");
165     try {
166       scheduleDownloadWork(REASON_TO_SCHEDULE_LOCALE_SETTINGS_CHANGED);
167     } catch (Throwable t) {
168       TcLog.e(TAG, "Failed inside onLocaleChanged", t);
169     }
170   }
171 
172   // TODO(licha): Make this private. Let the constructor accept a receiver to enable testing.
173   /** Notifies the model downlaoder that the device config for textclassifier is changed. */
174   @VisibleForTesting
onTextClassifierDeviceConfigChanged()175   void onTextClassifierDeviceConfigChanged() {
176     if (!settings.isModelDownloadManagerEnabled()) {
177       return;
178     }
179     TcLog.d(TAG, "Try to schedule model download work because of device config changes.");
180     try {
181       maybeOverrideLocaleListForTesting();
182       scheduleDownloadWork(REASON_TO_SCHEDULE_DEVICE_CONFIG_UPDATED);
183     } catch (Throwable t) {
184       TcLog.e(TAG, "Failed inside onTextClassifierDeviceConfigChanged", t);
185     }
186   }
187 
188   /** Clean up internal states on destroying. */
destroy()189   public void destroy() {
190     try {
191       DeviceConfig.removeOnPropertiesChangedListener(deviceConfigListener);
192       appContext.unregisterReceiver(localeChangedReceiver);
193       TcLog.d(TAG, "DeviceConfig and Locale listener unregistered by ModelDownloadeManager");
194     } catch (Throwable t) {
195       TcLog.e(TAG, "Failed to destroy ModelDownloadManager", t);
196     }
197   }
198 
199   /**
200    * Dumps the internal state for debugging.
201    *
202    * @param printWriter writer to write dumped states
203    */
dump(IndentingPrintWriter printWriter)204   public void dump(IndentingPrintWriter printWriter) {
205     if (!settings.isModelDownloadManagerEnabled()) {
206       return;
207     }
208     try {
209       printWriter.println("ModelDownloadManager:");
210       printWriter.increaseIndent();
211       downloadedModelManager.dump(printWriter);
212       printWriter.decreaseIndent();
213     } catch (Throwable t) {
214       TcLog.e(TAG, "Failed to dump ModelDownloadManager", t);
215     }
216   }
217 
218   /**
219    * Enqueue an idempotent work to check device configs and download model files if necessary.
220    *
221    * <p>At any time there will only be at most one work running. If a work is already pending or
222    * running, the newly scheduled work will be appended as a child of that work.
223    */
scheduleDownloadWork(int reasonToSchedule)224   private void scheduleDownloadWork(int reasonToSchedule) {
225     long workId =
226         Hashing.farmHashFingerprint64().hashUnencodedChars(UUID.randomUUID().toString()).asLong();
227     try {
228       NetworkType networkType =
229           Enums.getIfPresent(NetworkType.class, settings.getManifestDownloadRequiredNetworkType())
230               .or(NetworkType.UNMETERED);
231       OneTimeWorkRequest downloadRequest =
232           new OneTimeWorkRequest.Builder(modelDownloadWorkerClass)
233               .setConstraints(
234                   new Constraints.Builder()
235                       .setRequiredNetworkType(networkType)
236                       .setRequiresBatteryNotLow(true)
237                       .setRequiresStorageNotLow(true)
238                       .setRequiresDeviceIdle(settings.getManifestDownloadRequiresDeviceIdle())
239                       .setRequiresCharging(settings.getManifestDownloadRequiresCharging())
240                       .build())
241               .setBackoffCriteria(
242                   BackoffPolicy.EXPONENTIAL,
243                   settings.getModelDownloadBackoffDelayInMillis(),
244                   MILLISECONDS)
245               .setInputData(
246                   new Data.Builder()
247                       .putLong(ModelDownloadWorker.INPUT_DATA_KEY_WORK_ID, workId)
248                       .putLong(
249                           ModelDownloadWorker.INPUT_DATA_KEY_SCHEDULED_TIMESTAMP,
250                           Instant.now().toEpochMilli())
251                       .build())
252               .build();
253       ListenableFuture<Operation.State.SUCCESS> enqueueResultFuture =
254           workManagerSupplier
255               .call()
256               .enqueueUniqueWork(
257                   UNIQUE_QUEUE_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, downloadRequest)
258               .getResult();
259       Futures.addCallback(
260           enqueueResultFuture,
261           new FutureCallback<Operation.State.SUCCESS>() {
262             @Override
263             public void onSuccess(Operation.State.SUCCESS unused) {
264               TcLog.d(TAG, "Download work scheduled.");
265               TextClassifierDownloadLogger.downloadWorkScheduled(
266                   workId, reasonToSchedule, /* failedToSchedule= */ false);
267             }
268 
269             @Override
270             public void onFailure(Throwable t) {
271               TcLog.e(TAG, "Failed to schedule download work: ", t);
272               TextClassifierDownloadLogger.downloadWorkScheduled(
273                   workId, reasonToSchedule, /* failedToSchedule= */ true);
274             }
275           },
276           executorService);
277     } catch (Throwable t) {
278       // TODO(licha): this is just for temporary fix. Refactor the try-catch in the future.
279       TcLog.e(TAG, "Failed to schedule download work: ", t);
280       TextClassifierDownloadLogger.downloadWorkScheduled(
281           workId, reasonToSchedule, /* failedToSchedule= */ true);
282     }
283   }
284 
maybeOverrideLocaleListForTesting()285   private void maybeOverrideLocaleListForTesting() {
286     String localeList = settings.getTestingLocaleListOverride();
287     if (TextUtils.isEmpty(localeList)) {
288       return;
289     }
290     TcLog.d(
291         TAG,
292         String.format(
293             Locale.US,
294             "Override LocaleList from %s to %s",
295             LocaleList.getAdjustedDefault().toLanguageTags(),
296             localeList));
297     LocaleList.setDefault(LocaleList.forLanguageTags(localeList));
298   }
299 }
300