Export to AOSP TextClassifier. am: 1cc5c21b6e

Original change: https://googleplex-android-review.googlesource.com/c/platform/external/libtextclassifier/+/16264920

Change-Id: I421ede974fd7383426f436a8d3343f4df036a9e7
diff --git a/java/src/com/android/textclassifier/ModelFileManagerImpl.java b/java/src/com/android/textclassifier/ModelFileManagerImpl.java
index 68f77f0..45426d0 100644
--- a/java/src/com/android/textclassifier/ModelFileManagerImpl.java
+++ b/java/src/com/android/textclassifier/ModelFileManagerImpl.java
@@ -378,13 +378,17 @@
       @Nullable LocaleList localePreferences,
       @Nullable LocaleList detectedLocales) {
     Locale targetLocale = findBestModelLocale(localePreferences, detectedLocales);
-    if (!isEmptyLocaleList(localePreferences) && !targetLocale.equals(localePreferences.get(0))) {
+    // detectedLocales usually only contains 2-char language (e.g. en), while locale in
+    // localePreferences is usually complete (e.g. en_US). Log only if targetLocale is not a prefix.
+    if (!isEmptyLocaleList(localePreferences)
+        && !localePreferences.get(0).toString().startsWith(targetLocale.toString())) {
       TcLog.d(
           TAG,
-          "locale preference and target locale mismatch. \n Target locale: "
-              + targetLocale
-              + " \n Preference locale: "
-              + localePreferences.get(0));
+          String.format(
+              Locale.US,
+              "localePreference and targetLocale mismatch: preference: %s, target: %s",
+              localePreferences.get(0),
+              targetLocale));
     }
     return findBestModelFile(modelType, targetLocale);
   }
diff --git a/java/src/com/android/textclassifier/downloader/ModelDownloadException.java b/java/src/com/android/textclassifier/downloader/ModelDownloadException.java
index cd7f09b..99d91b8 100644
--- a/java/src/com/android/textclassifier/downloader/ModelDownloadException.java
+++ b/java/src/com/android/textclassifier/downloader/ModelDownloadException.java
@@ -25,48 +25,61 @@
 /** Exception thrown when downloading a model. */
 final class ModelDownloadException extends RuntimeException {
 
-  // Consistent with TextClassifierDownloadReported.failure_reason
+  // Consistent with TextClassifierDownloadReported.failure_reason. [1, 8, 9] reserved
   public static final int UNKNOWN_FAILURE_REASON = 0;
-  public static final int FAILED_TO_SCHEDULE = 1;
   public static final int FAILED_TO_DOWNLOAD_SERVICE_CONN_BROKEN = 2;
   public static final int FAILED_TO_DOWNLOAD_404_ERROR = 3;
   public static final int FAILED_TO_DOWNLOAD_OTHER = 4;
   public static final int DOWNLOADED_FILE_MISSING = 5;
   public static final int FAILED_TO_PARSE_MANIFEST = 6;
   public static final int FAILED_TO_VALIDATE_MODEL = 7;
-  public static final int FAILED_TO_MOVE_MODEL = 8;
-  public static final int WORKER_STOPPED = 9;
 
   /** Error code for a failed download task. */
   @Retention(SOURCE)
   @IntDef({
     UNKNOWN_FAILURE_REASON,
-    FAILED_TO_SCHEDULE,
     FAILED_TO_DOWNLOAD_SERVICE_CONN_BROKEN,
     FAILED_TO_DOWNLOAD_404_ERROR,
     FAILED_TO_DOWNLOAD_OTHER,
     DOWNLOADED_FILE_MISSING,
     FAILED_TO_PARSE_MANIFEST,
-    FAILED_TO_VALIDATE_MODEL,
-    FAILED_TO_MOVE_MODEL,
-    WORKER_STOPPED
+    FAILED_TO_VALIDATE_MODEL
   })
   public @interface ErrorCode {}
 
+  public static final int DEFAULT_DOWNLOADER_LIB_ERROR_CODE = -1;
+
   private final int errorCode;
 
+  private final int downloaderLibErrorCode;
+
   public ModelDownloadException(@ErrorCode int errorCode, Throwable cause) {
     super(cause);
     this.errorCode = errorCode;
+    this.downloaderLibErrorCode = DEFAULT_DOWNLOADER_LIB_ERROR_CODE;
   }
 
   public ModelDownloadException(@ErrorCode int errorCode, String message) {
     super(message);
     this.errorCode = errorCode;
+    this.downloaderLibErrorCode = DEFAULT_DOWNLOADER_LIB_ERROR_CODE;
   }
 
+  public ModelDownloadException(
+      @ErrorCode int errorCode, int downloaderLibErrorCode, String message) {
+    super(message);
+    this.errorCode = errorCode;
+    this.downloaderLibErrorCode = downloaderLibErrorCode;
+  }
+
+  /** Returns the error code from Model Downloader itself. */
   @ErrorCode
   public int getErrorCode() {
     return errorCode;
   }
+
+  /** Returns the error code from internal HTTP stack. */
+  public int getDownloaderLibErrorCode() {
+    return downloaderLibErrorCode;
+  }
 }
diff --git a/java/src/com/android/textclassifier/downloader/ModelDownloadManager.java b/java/src/com/android/textclassifier/downloader/ModelDownloadManager.java
index 3cfc349..b125f13 100644
--- a/java/src/com/android/textclassifier/downloader/ModelDownloadManager.java
+++ b/java/src/com/android/textclassifier/downloader/ModelDownloadManager.java
@@ -16,6 +16,9 @@
 
 package com.android.textclassifier.downloader;
 
+import static com.android.textclassifier.downloader.TextClassifierDownloadLogger.REASON_TO_SCHEDULE_DEVICE_CONFIG_UPDATED;
+import static com.android.textclassifier.downloader.TextClassifierDownloadLogger.REASON_TO_SCHEDULE_LOCALE_SETTINGS_CHANGED;
+import static com.android.textclassifier.downloader.TextClassifierDownloadLogger.REASON_TO_SCHEDULE_TCS_STARTED;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import android.content.BroadcastReceiver;
@@ -27,6 +30,7 @@
 import android.text.TextUtils;
 import androidx.work.BackoffPolicy;
 import androidx.work.Constraints;
+import androidx.work.Data;
 import androidx.work.ExistingWorkPolicy;
 import androidx.work.ListenableWorker;
 import androidx.work.NetworkType;
@@ -40,12 +44,16 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Enums;
 import com.google.common.base.Preconditions;
+import com.google.common.hash.Hashing;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import java.io.File;
+import java.time.Instant;
 import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
 import javax.annotation.Nullable;
 
 /** Manager to listen to config update and download latest models. */
@@ -128,7 +136,7 @@
     }
     maybeOverrideLocaleListForTesting();
     TcLog.v(TAG, "Try to schedule model download work because TextClassifierService started.");
-    scheduleDownloadWork();
+    scheduleDownloadWork(REASON_TO_SCHEDULE_TCS_STARTED);
   }
 
   // TODO(licha): Make this private. Let the constructor accept a receiver to enable testing.
@@ -139,7 +147,7 @@
       return;
     }
     TcLog.v(TAG, "Try to schedule model download work because of system locale changes.");
-    scheduleDownloadWork();
+    scheduleDownloadWork(REASON_TO_SCHEDULE_LOCALE_SETTINGS_CHANGED);
   }
 
   // TODO(licha): Make this private. Let the constructor accept a receiver to enable testing.
@@ -151,7 +159,7 @@
     }
     maybeOverrideLocaleListForTesting();
     TcLog.v(TAG, "Try to schedule model download work because of device config changes.");
-    scheduleDownloadWork();
+    scheduleDownloadWork(REASON_TO_SCHEDULE_DEVICE_CONFIG_UPDATED);
   }
 
   /** Clean up internal states on destroying. */
@@ -182,7 +190,9 @@
    * <p>At any time there will only be at most one work running. If a work is already pending or
    * running, the newly scheduled work will be appended as a child of that work.
    */
-  private void scheduleDownloadWork() {
+  private void scheduleDownloadWork(int reasonToSchedule) {
+    long workId =
+        Hashing.farmHashFingerprint64().hashUnencodedChars(UUID.randomUUID().toString()).asLong();
     NetworkType networkType =
         Enums.getIfPresent(NetworkType.class, settings.getManifestDownloadRequiredNetworkType())
             .or(NetworkType.UNMETERED);
@@ -200,6 +210,13 @@
                 BackoffPolicy.EXPONENTIAL,
                 settings.getModelDownloadBackoffDelayInMillis(),
                 MILLISECONDS)
+            .setInputData(
+                new Data.Builder()
+                    .putLong(ModelDownloadWorker.INPUT_DATA_KEY_WORK_ID, workId)
+                    .putLong(
+                        ModelDownloadWorker.INPUT_DATA_KEY_SCHEDULED_TIMESTAMP,
+                        Instant.now().toEpochMilli())
+                    .build())
             .build();
     ListenableFuture<Operation.State.SUCCESS> enqueueResultFuture =
         WorkManager.getInstance(appContext)
@@ -212,11 +229,15 @@
           @Override
           public void onSuccess(Operation.State.SUCCESS unused) {
             TcLog.v(TAG, "Download work scheduled.");
+            TextClassifierDownloadLogger.downloadWorkScheduled(
+                workId, reasonToSchedule, /* failedToSchedule= */ false);
           }
 
           @Override
           public void onFailure(Throwable t) {
             TcLog.e(TAG, "Failed to schedule download work: ", t);
+            TextClassifierDownloadLogger.downloadWorkScheduled(
+                workId, reasonToSchedule, /* failedToSchedule= */ true);
           }
         },
         executorService);
@@ -230,8 +251,10 @@
     TcLog.d(
         TAG,
         String.format(
+            Locale.US,
             "Override LocaleList from %s to %s",
-            LocaleList.getAdjustedDefault().toLanguageTags(), localeList));
+            LocaleList.getAdjustedDefault().toLanguageTags(),
+            localeList));
     LocaleList.setDefault(LocaleList.forLanguageTags(localeList));
   }
 }
diff --git a/java/src/com/android/textclassifier/downloader/ModelDownloadWorker.java b/java/src/com/android/textclassifier/downloader/ModelDownloadWorker.java
index ebee3c2..6e04e16 100644
--- a/java/src/com/android/textclassifier/downloader/ModelDownloadWorker.java
+++ b/java/src/com/android/textclassifier/downloader/ModelDownloadWorker.java
@@ -32,6 +32,7 @@
 import com.android.textclassifier.downloader.DownloadedModelDatabase.Manifest;
 import com.android.textclassifier.downloader.DownloadedModelDatabase.ManifestEnrollment;
 import com.android.textclassifier.downloader.DownloadedModelDatabase.Model;
+import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -41,6 +42,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.errorprone.annotations.concurrent.GuardedBy;
+import java.time.Clock;
 import java.util.ArrayList;
 import java.util.Locale;
 
@@ -48,13 +50,24 @@
 public final class ModelDownloadWorker extends ListenableWorker {
   private static final String TAG = "ModelDownloadWorker";
 
+  public static final String INPUT_DATA_KEY_WORK_ID = "ModelDownloadWorker_workId";
+  public static final String INPUT_DATA_KEY_SCHEDULED_TIMESTAMP =
+      "ModelDownloadWorker_scheduledTimestamp";
+
   private final ListeningExecutorService executorService;
   private final ModelDownloader downloader;
   private final DownloadedModelManager downloadedModelManager;
   private final TextClassifierSettings settings;
 
+  private final long workId;
+
+  private final Clock clock;
+  private final long workScheduledTimeMillis;
+
   private final Object lock = new Object();
 
+  private long workStartedTimeMillis = 0;
+
   @GuardedBy("lock")
   private final ArrayMap<String, ListenableFuture<Void>> pendingDownloads;
 
@@ -68,6 +81,11 @@
     this.settings = new TextClassifierSettings();
     this.pendingDownloads = new ArrayMap<>();
     this.manifestsToDownload = null;
+
+    this.workId = workerParams.getInputData().getLong(INPUT_DATA_KEY_WORK_ID, 0);
+    this.workScheduledTimeMillis =
+        workerParams.getInputData().getLong(INPUT_DATA_KEY_SCHEDULED_TIMESTAMP, 0);
+    this.clock = Clock.systemUTC();
   }
 
   @VisibleForTesting
@@ -77,7 +95,10 @@
       ListeningExecutorService executorService,
       ModelDownloader modelDownloader,
       DownloadedModelManager downloadedModelManager,
-      TextClassifierSettings settings) {
+      TextClassifierSettings settings,
+      long workId,
+      Clock clock,
+      long workScheduledTimeMillis) {
     super(context, workerParams);
     this.executorService = executorService;
     this.downloader = modelDownloader;
@@ -85,36 +106,54 @@
     this.settings = settings;
     this.pendingDownloads = new ArrayMap<>();
     this.manifestsToDownload = null;
+    this.workId = workId;
+    this.clock = clock;
+    this.workScheduledTimeMillis = workScheduledTimeMillis;
   }
 
   @Override
   public final ListenableFuture<ListenableWorker.Result> startWork() {
+    workStartedTimeMillis = getCurrentTimeMillis();
     // Notice: startWork() is invoked on the main thread
     if (!settings.isModelDownloadManagerEnabled()) {
       TcLog.e(TAG, "Model Downloader is disabled. Abort the work.");
+      logDownloadWorkCompleted(
+          TextClassifierDownloadLogger.WORK_RESULT_FAILURE_MODEL_DOWNLOADER_DISABLED);
       return Futures.immediateFuture(ListenableWorker.Result.failure());
     }
     TcLog.v(TAG, "Start download work...");
     if (getRunAttemptCount() >= settings.getModelDownloadWorkerMaxAttempts()) {
       TcLog.d(TAG, "Max attempt reached. Abort download work.");
+      logDownloadWorkCompleted(
+          TextClassifierDownloadLogger.WORK_RESULT_FAILURE_MAX_RUN_ATTEMPT_REACHED);
       return Futures.immediateFuture(ListenableWorker.Result.failure());
     }
 
     return FluentFuture.from(Futures.submitAsync(this::checkAndDownloadModels, executorService))
         .transform(
-            allSucceeded -> {
+            downloadResult -> {
               Preconditions.checkNotNull(manifestsToDownload);
               downloadedModelManager.onDownloadCompleted(manifestsToDownload);
-              TcLog.v(TAG, "Download work completed. Succeeded: " + allSucceeded);
-              return allSucceeded
-                  ? ListenableWorker.Result.success()
-                  : ListenableWorker.Result.retry();
+              TcLog.v(TAG, "Download work completed: " + downloadResult);
+              if (downloadResult.failureCount() == 0) {
+                logDownloadWorkCompleted(
+                    downloadResult.successCount() > 0
+                        ? TextClassifierDownloadLogger.WORK_RESULT_SUCCESS_MODEL_DOWNLOADED
+                        : TextClassifierDownloadLogger.WORK_RESULT_SUCCESS_NO_UPDATE_AVAILABLE);
+                return ListenableWorker.Result.success();
+              } else {
+                logDownloadWorkCompleted(
+                    TextClassifierDownloadLogger.WORK_RESULT_RETRY_MODEL_DOWNLOAD_FAILED);
+                return ListenableWorker.Result.retry();
+              }
             },
             executorService)
         .catching(
             Throwable.class,
             t -> {
               TcLog.e(TAG, "Unexpected Exception during downloading: ", t);
+              logDownloadWorkCompleted(
+                  TextClassifierDownloadLogger.WORK_RESULT_RETRY_RUNTIME_EXCEPTION);
               return ListenableWorker.Result.retry();
             },
             executorService);
@@ -155,7 +194,7 @@
    * <p>Download tasks will be combined and logged after completion. Return true if all tasks
    * succeeded
    */
-  private ListenableFuture<Boolean> checkAndDownloadModels() {
+  private ListenableFuture<DownloadResult> checkAndDownloadModels() {
     ImmutableList<Locale> localesToDownload = getLocalesToDownload();
     ArrayList<ListenableFuture<Boolean>> downloadResultFutures = new ArrayList<>();
     ImmutableMap.Builder<String, ManifestsToDownloadByType> manifestsToDownloadBuilder =
@@ -170,7 +209,8 @@
         if (bestLocaleTagAndManifestUrl == null) {
           TcLog.w(
               TAG,
-              String.format("No suitable manifest for %s, %s", modelType, locale.toLanguageTag()));
+              String.format(
+                  Locale.US, "No suitable manifest for %s, %s", modelType, locale.toLanguageTag()));
           continue;
         }
         String bestLocaleTag = bestLocaleTagAndManifestUrl.first;
@@ -179,8 +219,12 @@
         TcLog.d(
             TAG,
             String.format(
+                Locale.US,
                 "model type: %s, current locale tag: %s, best locale tag: %s, manifest url: %s",
-                modelType, locale.toLanguageTag(), bestLocaleTag, manifestUrl));
+                modelType,
+                locale.toLanguageTag(),
+                bestLocaleTag,
+                manifestUrl));
         if (!shouldDownloadManifest(modelType, bestLocaleTag, manifestUrl)) {
           continue;
         }
@@ -196,11 +240,16 @@
         .call(
             () -> {
               TcLog.v(TAG, "All Download Tasks Completed");
-              boolean allSucceeded = true;
+              int successCount = 0;
+              int failureCount = 0;
               for (ListenableFuture<Boolean> downloadResultFuture : downloadResultFutures) {
-                allSucceeded &= Futures.getDone(downloadResultFuture);
+                if (Futures.getDone(downloadResultFuture)) {
+                  successCount += 1;
+                } else {
+                  failureCount += 1;
+                }
               }
-              return allSucceeded;
+              return DownloadResult.create(successCount, failureCount);
             },
             executorService);
   }
@@ -216,8 +265,10 @@
         TcLog.w(
             TAG,
             String.format(
+                Locale.US,
                 "Manifest failed too many times, stop retrying: %s %d",
-                manifestUrl, downloadedManifest.getFailureCounts()));
+                manifestUrl,
+                downloadedManifest.getFailureCounts()));
         return false;
       } else {
         return true;
@@ -236,12 +287,17 @@
    */
   private ListenableFuture<Boolean> downloadManifestAndRegister(
       @ModelTypeDef String modelType, String localeTag, String manifestUrl) {
+    long downloadStartTimestamp = getCurrentTimeMillis();
     return FluentFuture.from(downloadManifest(manifestUrl))
         .transform(
             unused -> {
               downloadedModelManager.registerManifestEnrollment(modelType, localeTag, manifestUrl);
               TextClassifierDownloadLogger.downloadSucceeded(
-                  modelType, manifestUrl, getRunAttemptCount());
+                  workId,
+                  modelType,
+                  manifestUrl,
+                  getRunAttemptCount(),
+                  getCurrentTimeMillis() - downloadStartTimestamp);
               TcLog.d(TAG, "Manifest downloaded and registered: " + manifestUrl);
               return true;
             },
@@ -251,12 +307,21 @@
             t -> {
               downloadedModelManager.registerManifestDownloadFailure(manifestUrl);
               int errorCode = ModelDownloadException.UNKNOWN_FAILURE_REASON;
+              int downloaderLibErrorCode = 0;
               if (t instanceof ModelDownloadException) {
-                errorCode = ((ModelDownloadException) t).getErrorCode();
+                ModelDownloadException mde = (ModelDownloadException) t;
+                errorCode = mde.getErrorCode();
+                downloaderLibErrorCode = mde.getDownloaderLibErrorCode();
               }
               TcLog.e(TAG, "Failed to download manfiest: " + manifestUrl, t);
-              TextClassifierDownloadLogger.downloadFailedAndRetry(
-                  modelType, manifestUrl, errorCode, getRunAttemptCount());
+              TextClassifierDownloadLogger.downloadFailed(
+                  workId,
+                  modelType,
+                  manifestUrl,
+                  errorCode,
+                  getRunAttemptCount(),
+                  downloaderLibErrorCode,
+                  getCurrentTimeMillis() - downloadStartTimestamp);
               return false;
             },
             executorService);
@@ -328,6 +393,41 @@
    */
   @Override
   public final void onStopped() {
-    TcLog.d(TAG, String.format("Stop download. Attempt:%d", getRunAttemptCount()));
+    TcLog.d(TAG, String.format(Locale.US, "Stop download. Attempt:%d", getRunAttemptCount()));
+    logDownloadWorkCompleted(TextClassifierDownloadLogger.WORK_RESULT_RETRY_STOPPED_BY_OS);
+  }
+
+  private long getCurrentTimeMillis() {
+    return clock.instant().toEpochMilli();
+  }
+
+  private void logDownloadWorkCompleted(int workResult) {
+    if (workStartedTimeMillis < workScheduledTimeMillis) {
+      TcLog.w(
+          TAG,
+          String.format(
+              Locale.US,
+              "Bad workStartedTimeMillis: %d, workScheduledTimeMillis: %d",
+              workStartedTimeMillis,
+              workScheduledTimeMillis));
+      workStartedTimeMillis = workScheduledTimeMillis;
+    }
+    TextClassifierDownloadLogger.downloadWorkCompleted(
+        workId,
+        workResult,
+        getRunAttemptCount(),
+        workStartedTimeMillis - workScheduledTimeMillis,
+        getCurrentTimeMillis() - workStartedTimeMillis);
+  }
+
+  @AutoValue
+  abstract static class DownloadResult {
+    public abstract int successCount();
+
+    public abstract int failureCount();
+
+    public static DownloadResult create(int successCount, int failureCount) {
+      return new AutoValue_ModelDownloadWorker_DownloadResult(successCount, failureCount);
+    }
   }
 }
diff --git a/java/src/com/android/textclassifier/downloader/ModelDownloaderImpl.java b/java/src/com/android/textclassifier/downloader/ModelDownloaderImpl.java
index 32edfd2..2244e9a 100644
--- a/java/src/com/android/textclassifier/downloader/ModelDownloaderImpl.java
+++ b/java/src/com/android/textclassifier/downloader/ModelDownloaderImpl.java
@@ -210,9 +210,12 @@
                 }
 
                 @Override
-                public void onFailure(
-                    @ModelDownloadException.ErrorCode int errorCode, String errorMsg) {
-                  completer.setException(new ModelDownloadException(errorCode, errorMsg));
+                public void onFailure(int downloaderLibErrorCode, String errorMsg) {
+                  completer.setException(
+                      new ModelDownloadException(
+                          ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER,
+                          downloaderLibErrorCode,
+                          errorMsg));
                 }
               });
           return "downlaoderService.download";
diff --git a/java/src/com/android/textclassifier/downloader/ModelDownloaderServiceImpl.java b/java/src/com/android/textclassifier/downloader/ModelDownloaderServiceImpl.java
index e9f75c1..439588b 100644
--- a/java/src/com/android/textclassifier/downloader/ModelDownloaderServiceImpl.java
+++ b/java/src/com/android/textclassifier/downloader/ModelDownloaderServiceImpl.java
@@ -16,6 +16,7 @@
 
 package com.android.textclassifier.downloader;
 
+import static com.android.textclassifier.downloader.ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE;
 import static com.google.common.base.Predicates.instanceOf;
 import static com.google.common.base.Throwables.getCausalChain;
 
@@ -123,19 +124,17 @@
                               getCausalChain(t),
                               instanceOf(RequestException.class),
                               /* defaultValue= */ null);
-                  // TODO(licha): might be better to pass back the raw error code (instead of
-                  // using the ErrorCode defined inside the ModelDownloadException).
-                  int errorCode =
-                      (requestException != null
-                              && requestException.getErrorDetails().getHttpStatusCode() == 404)
-                          ? ModelDownloadException.FAILED_TO_DOWNLOAD_404_ERROR
-                          : ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER;
-                  dispatchOnFailureSafely(callback, errorCode, t);
+                  // TODO(b/181805039): Use error code once downloader lib supports it.
+                  int downloaderLibErrorCode =
+                      requestException != null
+                          ? requestException.getErrorDetails().getHttpStatusCode()
+                          : DEFAULT_DOWNLOADER_LIB_ERROR_CODE;
+                  dispatchOnFailureSafely(callback, downloaderLibErrorCode, t);
                 }
               },
               bgExecutorService);
     } catch (Throwable t) {
-      dispatchOnFailureSafely(callback, ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER, t);
+      dispatchOnFailureSafely(callback, DEFAULT_DOWNLOADER_LIB_ERROR_CODE, t);
     }
   }
 
@@ -154,11 +153,9 @@
   }
 
   private static void dispatchOnFailureSafely(
-      IModelDownloaderCallback callback,
-      @ModelDownloadException.ErrorCode int errorCode,
-      Throwable throwable) {
+      IModelDownloaderCallback callback, int downloaderLibErrorCode, Throwable throwable) {
     try {
-      callback.onFailure(errorCode, throwable.getMessage());
+      callback.onFailure(downloaderLibErrorCode, throwable.getMessage());
     } catch (RemoteException e) {
       TcLog.e(TAG, "Unable to notify failures in download", e);
     }
diff --git a/java/src/com/android/textclassifier/downloader/TextClassifierDownloadLogger.java b/java/src/com/android/textclassifier/downloader/TextClassifierDownloadLogger.java
index 048ba6a..7416b00 100644
--- a/java/src/com/android/textclassifier/downloader/TextClassifierDownloadLogger.java
+++ b/java/src/com/android/textclassifier/downloader/TextClassifierDownloadLogger.java
@@ -16,6 +16,8 @@
 
 package com.android.textclassifier.downloader;
 
+import static com.android.textclassifier.downloader.ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE;
+
 import android.text.TextUtils;
 import com.android.textclassifier.common.ModelType;
 import com.android.textclassifier.common.ModelType.ModelTypeDef;
@@ -24,20 +26,17 @@
 import com.android.textclassifier.downloader.ModelDownloadException.ErrorCode;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
+import java.util.Locale;
 
 /** Logs TextClassifier download event. */
 final class TextClassifierDownloadLogger {
   private static final String TAG = "TextClassifierDownloadLogger";
 
   // Values for TextClassifierDownloadReported.download_status
-  private static final int DOWNLOAD_STATUS_SCHEDULED =
-      TextClassifierStatsLog.TEXT_CLASSIFIER_DOWNLOAD_REPORTED__DOWNLOAD_STATUS__SCHEDULED;
   private static final int DOWNLOAD_STATUS_SUCCEEDED =
       TextClassifierStatsLog.TEXT_CLASSIFIER_DOWNLOAD_REPORTED__DOWNLOAD_STATUS__SUCCEEDED;
   private static final int DOWNLOAD_STATUS_FAILED_AND_RETRY =
       TextClassifierStatsLog.TEXT_CLASSIFIER_DOWNLOAD_REPORTED__DOWNLOAD_STATUS__FAILED_AND_RETRY;
-  private static final int DOWNLOAD_STATUS_FAILED_AND_ABORT =
-      TextClassifierStatsLog.TEXT_CLASSIFIER_DOWNLOAD_REPORTED__DOWNLOAD_STATUS__FAILED_AND_ABORT;
 
   private static final int DEFAULT_MODEL_TYPE =
       TextClassifierStatsLog.TEXT_CLASSIFIER_DOWNLOAD_REPORTED__MODEL_TYPE__UNKNOWN_MODEL_TYPE;
@@ -89,9 +88,50 @@
                   .TEXT_CLASSIFIER_DOWNLOAD_REPORTED__FAILURE_REASON__FAILED_TO_VALIDATE_MODEL)
           .build();
 
+  // Reasons to schedule
+  public static final int REASON_TO_SCHEDULE_TCS_STARTED =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_SCHEDULED__REASON_TO_SCHEDULE__TCS_STARTED;
+  public static final int REASON_TO_SCHEDULE_LOCALE_SETTINGS_CHANGED =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_SCHEDULED__REASON_TO_SCHEDULE__LOCALE_SETTINGS_CHANGED;
+  public static final int REASON_TO_SCHEDULE_DEVICE_CONFIG_UPDATED =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_SCHEDULED__REASON_TO_SCHEDULE__DEVICE_CONFIG_UPDATED;
+
+  // Work results
+  public static final int WORK_RESULT_UNKNOWN_WORK_RESULT =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED__WORK_RESULT__UNKNOWN_WORK_RESULT;
+  public static final int WORK_RESULT_SUCCESS_MODEL_DOWNLOADED =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED__WORK_RESULT__SUCCESS_MODEL_DOWNLOADED;
+  public static final int WORK_RESULT_SUCCESS_NO_UPDATE_AVAILABLE =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED__WORK_RESULT__SUCCESS_NO_UPDATE_AVAILABLE;
+  public static final int WORK_RESULT_FAILURE_MODEL_DOWNLOADER_DISABLED =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED__WORK_RESULT__FAILURE_MODEL_DOWNLOADER_DISABLED;
+  public static final int WORK_RESULT_FAILURE_MAX_RUN_ATTEMPT_REACHED =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED__WORK_RESULT__FAILURE_MAX_RUN_ATTEMPT_REACHED;
+  public static final int WORK_RESULT_RETRY_MODEL_DOWNLOAD_FAILED =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED__WORK_RESULT__RETRY_MODEL_DOWNLOAD_FAILED;
+  public static final int WORK_RESULT_RETRY_RUNTIME_EXCEPTION =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED__WORK_RESULT__RETRY_RUNTIME_EXCEPTION;
+  public static final int WORK_RESULT_RETRY_STOPPED_BY_OS =
+      TextClassifierStatsLog
+          .TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED__WORK_RESULT__RETRY_STOPPED_BY_OS;
+
   /** Logs a succeeded download task. */
   public static void downloadSucceeded(
-      @ModelTypeDef String modelType, String url, int runAttemptCount) {
+      long workId,
+      @ModelTypeDef String modelType,
+      String url,
+      int runAttemptCount,
+      long downloadDurationMillis) {
     Preconditions.checkArgument(!TextUtils.isEmpty(url), "url cannot be null/empty");
     TextClassifierStatsLog.write(
         TextClassifierStatsLog.TEXT_CLASSIFIER_DOWNLOAD_REPORTED,
@@ -101,27 +141,38 @@
         url,
         DEFAULT_FAILURE_REASON,
         runAttemptCount,
-        /* downloaderLibErrorCode */ 0,
-        /* downloadDurationMillis */ 0,
-        /* workId */ 0L);
+        DEFAULT_DOWNLOADER_LIB_ERROR_CODE,
+        downloadDurationMillis,
+        workId);
     if (TcLog.ENABLE_FULL_LOGGING) {
       TcLog.v(
           TAG,
           String.format(
+              Locale.US,
               "Download Reported: modelType=%s, fileType=%d, status=%d, url=%s, "
-                  + "failureReason=%d, runAttemptCount=%d",
+                  + "failureReason=%d, runAttemptCount=%d, downloaderLibErrorCode=%d, "
+                  + "downloadDurationMillis=%d, workId=%d",
               MODEL_TYPE_MAP.getOrDefault(modelType, DEFAULT_MODEL_TYPE),
               DEFAULT_FILE_TYPE,
               DOWNLOAD_STATUS_SUCCEEDED,
               url,
               DEFAULT_FAILURE_REASON,
-              runAttemptCount));
+              runAttemptCount,
+              DEFAULT_DOWNLOADER_LIB_ERROR_CODE,
+              downloadDurationMillis,
+              workId));
     }
   }
 
   /** Logs a failed download task which will be retried later. */
-  public static void downloadFailedAndRetry(
-      @ModelTypeDef String modelType, String url, @ErrorCode int errorCode, int runAttemptCount) {
+  public static void downloadFailed(
+      long workId,
+      @ModelTypeDef String modelType,
+      String url,
+      @ErrorCode int errorCode,
+      int runAttemptCount,
+      int downloaderLibErrorCode,
+      long downloadDurationMillis) {
     Preconditions.checkArgument(!TextUtils.isEmpty(url), "url cannot be null/empty");
     TextClassifierStatsLog.write(
         TextClassifierStatsLog.TEXT_CLASSIFIER_DOWNLOAD_REPORTED,
@@ -131,21 +182,74 @@
         url,
         FAILURE_REASON_MAP.getOrDefault(errorCode, DEFAULT_FAILURE_REASON),
         runAttemptCount,
-        /* downloaderLibErrorCode */ 0,
-        /* downloadDurationMillis */ 0,
-        /* workId */ 0L);
+        downloaderLibErrorCode,
+        downloadDurationMillis,
+        workId);
     if (TcLog.ENABLE_FULL_LOGGING) {
       TcLog.v(
           TAG,
           String.format(
+              Locale.US,
               "Download Reported: modelType=%s, fileType=%d, status=%d, url=%s, "
-                  + "failureReason=%d, runAttemptCount=%d",
+                  + "failureReason=%d, runAttemptCount=%d, downloaderLibErrorCode=%d, "
+                  + "downloadDurationMillis=%d, workId=%d",
               MODEL_TYPE_MAP.getOrDefault(modelType, DEFAULT_MODEL_TYPE),
               DEFAULT_FILE_TYPE,
               DOWNLOAD_STATUS_FAILED_AND_RETRY,
               url,
               FAILURE_REASON_MAP.getOrDefault(errorCode, DEFAULT_FAILURE_REASON),
-              runAttemptCount));
+              runAttemptCount,
+              downloaderLibErrorCode,
+              downloadDurationMillis,
+              workId));
+    }
+  }
+
+  public static void downloadWorkScheduled(
+      long workId, int reasonToSchedule, boolean failedToSchedule) {
+    TextClassifierStatsLog.write(
+        TextClassifierStatsLog.TEXT_CLASSIFIER_DOWNLOAD_WORK_SCHEDULED,
+        workId,
+        reasonToSchedule,
+        failedToSchedule);
+    if (TcLog.ENABLE_FULL_LOGGING) {
+      TcLog.v(
+          TAG,
+          String.format(
+              Locale.US,
+              "Download Work Scheduled: workId=%d, reasonToSchedule=%d, failedToSchedule=%b",
+              workId,
+              reasonToSchedule,
+              failedToSchedule));
+    }
+  }
+
+  public static void downloadWorkCompleted(
+      long workId,
+      int workResult,
+      int runAttemptCount,
+      long workScheduledToStartedDurationMillis,
+      long workStartedToEndedDurationMillis) {
+    TextClassifierStatsLog.write(
+        TextClassifierStatsLog.TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED,
+        workId,
+        workResult,
+        runAttemptCount,
+        workScheduledToStartedDurationMillis,
+        workStartedToEndedDurationMillis);
+    if (TcLog.ENABLE_FULL_LOGGING) {
+      TcLog.v(
+          TAG,
+          String.format(
+              Locale.US,
+              "Download Work Completed: workId=%d, result=%d, runAttemptCount=%d, "
+                  + "workScheduledToStartedDurationMillis=%d, "
+                  + "workStartedToEndedDurationMillis=%d",
+              workId,
+              workResult,
+              runAttemptCount,
+              workScheduledToStartedDurationMillis,
+              workStartedToEndedDurationMillis));
     }
   }
 
diff --git a/java/tests/instrumentation/src/com/android/textclassifier/common/statsd/TextClassifierDownloadLoggerTestRule.java b/java/tests/instrumentation/src/com/android/textclassifier/common/statsd/TextClassifierDownloadLoggerTestRule.java
index 6d29815..9c49cb1 100644
--- a/java/tests/instrumentation/src/com/android/textclassifier/common/statsd/TextClassifierDownloadLoggerTestRule.java
+++ b/java/tests/instrumentation/src/com/android/textclassifier/common/statsd/TextClassifierDownloadLoggerTestRule.java
@@ -21,6 +21,8 @@
 import com.android.internal.os.StatsdConfigProto.StatsdConfig;
 import com.android.os.AtomsProto.Atom;
 import com.android.os.AtomsProto.TextClassifierDownloadReported;
+import com.android.os.AtomsProto.TextClassifierDownloadWorkCompleted;
+import com.android.os.AtomsProto.TextClassifierDownloadWorkScheduled;
 import com.google.common.collect.ImmutableList;
 import java.util.stream.Collectors;
 import org.junit.rules.ExternalResource;
@@ -30,38 +32,96 @@
 public final class TextClassifierDownloadLoggerTestRule extends ExternalResource {
   private static final String TAG = "DownloadLoggerTestRule";
 
-  /** A statsd config ID, which is arbitrary. */
-  private static final long CONFIG_ID = 423779;
+  // Statsd config IDs, which are arbitrary.
+  private static final long CONFIG_ID_DOWNLOAD_REPORTED = 423779;
+  private static final long CONFIG_ID_DOWNLOAD_WORK_SCHEDULED = 42;
+  private static final long CONFIG_ID_DOWNLOAD_WORK_COMPLETED = 2021;
 
   private static final long SHORT_TIMEOUT_MS = 1000;
 
   @Override
   public void before() throws Exception {
-    StatsdTestUtils.cleanup(CONFIG_ID);
+    StatsdTestUtils.cleanup(CONFIG_ID_DOWNLOAD_REPORTED);
+    StatsdTestUtils.cleanup(CONFIG_ID_DOWNLOAD_WORK_SCHEDULED);
+    StatsdTestUtils.cleanup(CONFIG_ID_DOWNLOAD_WORK_COMPLETED);
 
-    StatsdConfig.Builder builder =
+    StatsdConfig.Builder builder1 =
         StatsdConfig.newBuilder()
-            .setId(CONFIG_ID)
+            .setId(CONFIG_ID_DOWNLOAD_REPORTED)
             .addAllowedLogSource(ApplicationProvider.getApplicationContext().getPackageName());
-    StatsdTestUtils.addAtomMatcher(builder, Atom.TEXT_CLASSIFIER_DOWNLOAD_REPORTED_FIELD_NUMBER);
-    StatsdTestUtils.pushConfig(builder.build());
+    StatsdTestUtils.addAtomMatcher(builder1, Atom.TEXT_CLASSIFIER_DOWNLOAD_REPORTED_FIELD_NUMBER);
+    StatsdTestUtils.pushConfig(builder1.build());
+
+    StatsdConfig.Builder builder2 =
+        StatsdConfig.newBuilder()
+            .setId(CONFIG_ID_DOWNLOAD_WORK_SCHEDULED)
+            .addAllowedLogSource(ApplicationProvider.getApplicationContext().getPackageName());
+    StatsdTestUtils.addAtomMatcher(
+        builder2, Atom.TEXT_CLASSIFIER_DOWNLOAD_WORK_SCHEDULED_FIELD_NUMBER);
+    StatsdTestUtils.pushConfig(builder2.build());
+
+    StatsdConfig.Builder builder3 =
+        StatsdConfig.newBuilder()
+            .setId(CONFIG_ID_DOWNLOAD_WORK_COMPLETED)
+            .addAllowedLogSource(ApplicationProvider.getApplicationContext().getPackageName());
+    StatsdTestUtils.addAtomMatcher(
+        builder3, Atom.TEXT_CLASSIFIER_DOWNLOAD_WORK_COMPLETED_FIELD_NUMBER);
+    StatsdTestUtils.pushConfig(builder3.build());
   }
 
   @Override
   public void after() {
     try {
-      StatsdTestUtils.cleanup(CONFIG_ID);
+      StatsdTestUtils.cleanup(CONFIG_ID_DOWNLOAD_REPORTED);
+      StatsdTestUtils.cleanup(CONFIG_ID_DOWNLOAD_WORK_SCHEDULED);
+      StatsdTestUtils.cleanup(CONFIG_ID_DOWNLOAD_WORK_COMPLETED);
     } catch (Exception e) {
       Log.e(TAG, "Failed to clean up statsd after tests.");
     }
   }
 
-  /** Gets a list of download atoms written into statsd, sorted by increasing timestamp. */
-  public ImmutableList<TextClassifierDownloadReported> getLoggedAtoms() throws Exception {
-    ImmutableList<Atom> loggedAtoms = StatsdTestUtils.getLoggedAtoms(CONFIG_ID, SHORT_TIMEOUT_MS);
+  /**
+   * Gets a list of TextClassifierDownloadReported atoms written into statsd, sorted by increasing
+   * timestamp.
+   */
+  public ImmutableList<TextClassifierDownloadReported> getLoggedDownloadReportedAtoms()
+      throws Exception {
+    ImmutableList<Atom> loggedAtoms =
+        StatsdTestUtils.getLoggedAtoms(CONFIG_ID_DOWNLOAD_REPORTED, SHORT_TIMEOUT_MS);
     return ImmutableList.copyOf(
         loggedAtoms.stream()
+            .filter(Atom::hasTextClassifierDownloadReported)
             .map(Atom::getTextClassifierDownloadReported)
             .collect(Collectors.toList()));
   }
+
+  /**
+   * Gets a list of TextClassifierDownloadWorkScheduled atoms written into statsd, sorted by
+   * increasing timestamp.
+   */
+  public ImmutableList<TextClassifierDownloadWorkScheduled> getLoggedDownloadWorkScheduledAtoms()
+      throws Exception {
+    ImmutableList<Atom> loggedAtoms =
+        StatsdTestUtils.getLoggedAtoms(CONFIG_ID_DOWNLOAD_WORK_SCHEDULED, SHORT_TIMEOUT_MS);
+    return ImmutableList.copyOf(
+        loggedAtoms.stream()
+            .filter(Atom::hasTextClassifierDownloadWorkScheduled)
+            .map(Atom::getTextClassifierDownloadWorkScheduled)
+            .collect(Collectors.toList()));
+  }
+
+  /**
+   * Gets a list of TextClassifierDownloadWorkCompleted atoms written into statsd, sorted by
+   * increasing timestamp.
+   */
+  public ImmutableList<TextClassifierDownloadWorkCompleted> getLoggedDownloadWorkCompletedAtoms()
+      throws Exception {
+    ImmutableList<Atom> loggedAtoms =
+        StatsdTestUtils.getLoggedAtoms(CONFIG_ID_DOWNLOAD_WORK_COMPLETED, SHORT_TIMEOUT_MS);
+    return ImmutableList.copyOf(
+        loggedAtoms.stream()
+            .filter(Atom::hasTextClassifierDownloadWorkCompleted)
+            .map(Atom::getTextClassifierDownloadWorkCompleted)
+            .collect(Collectors.toList()));
+  }
 }
diff --git a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadExceptionTest.java b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadExceptionTest.java
index 794ead9..1e878b2 100644
--- a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadExceptionTest.java
+++ b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadExceptionTest.java
@@ -24,12 +24,30 @@
 
 @RunWith(JUnit4.class)
 public final class ModelDownloadExceptionTest {
-  private static final int ERROR_CODE = ModelDownloadException.WORKER_STOPPED;
+  private static final int ERROR_CODE = ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER;
+  private static final int DOWNLOADER_LIB_ERROR_CODE = 500;
 
   @Test
-  public void getErrorCode() {
-    assertThat(new ModelDownloadException(ERROR_CODE, new Exception()).getErrorCode())
-        .isEqualTo(ERROR_CODE);
-    assertThat(new ModelDownloadException(ERROR_CODE, "").getErrorCode()).isEqualTo(ERROR_CODE);
+  public void getErrorCode_constructor1() {
+    ModelDownloadException e = new ModelDownloadException(ERROR_CODE, new Exception());
+    assertThat(e.getErrorCode()).isEqualTo(ERROR_CODE);
+    assertThat(e.getDownloaderLibErrorCode())
+        .isEqualTo(ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE);
+  }
+
+  @Test
+  public void getErrorCode_constructor2() {
+    ModelDownloadException e = new ModelDownloadException(ERROR_CODE, "error_msg");
+    assertThat(e.getErrorCode()).isEqualTo(ERROR_CODE);
+    assertThat(e.getDownloaderLibErrorCode())
+        .isEqualTo(ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE);
+  }
+
+  @Test
+  public void getErrorCode_constructor3() {
+    ModelDownloadException e =
+        new ModelDownloadException(ERROR_CODE, DOWNLOADER_LIB_ERROR_CODE, "error_msg");
+    assertThat(e.getErrorCode()).isEqualTo(ERROR_CODE);
+    assertThat(e.getDownloaderLibErrorCode()).isEqualTo(DOWNLOADER_LIB_ERROR_CODE);
   }
 }
diff --git a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadManagerTest.java b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadManagerTest.java
index dc86628..c626ed7 100644
--- a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadManagerTest.java
+++ b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadManagerTest.java
@@ -26,6 +26,8 @@
 import androidx.work.WorkInfo;
 import androidx.work.WorkManager;
 import androidx.work.testing.WorkManagerTestInitHelper;
+import com.android.os.AtomsProto.TextClassifierDownloadWorkScheduled;
+import com.android.os.AtomsProto.TextClassifierDownloadWorkScheduled.ReasonToSchedule;
 import com.android.textclassifier.common.ModelType;
 import com.android.textclassifier.common.TextClassifierSettings;
 import com.android.textclassifier.common.statsd.TextClassifierDownloadLoggerTestRule;
@@ -59,7 +61,6 @@
   public final TextClassifierDownloadLoggerTestRule loggerTestRule =
       new TextClassifierDownloadLoggerTestRule();
 
-  // TODO(licha): Maybe we can just use the real TextClassifierSettings
   private TestingDeviceConfig deviceConfig;
   private WorkManager workManager;
   private ModelDownloadManager downloadManager;
@@ -101,6 +102,7 @@
             DownloaderTestUtils.queryWorkInfos(
                 workManager, ModelDownloadManager.UNIQUE_QUEUE_NAME));
     assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.ENQUEUED);
+    verifyWorkScheduledLogging(ReasonToSchedule.TCS_STARTED);
   }
 
   @Test
@@ -111,6 +113,7 @@
     assertThat(Locale.getDefault()).isEqualTo(Locale.forLanguageTag("zh"));
     assertThat(LocaleList.getDefault()).isEqualTo(LocaleList.forLanguageTags("zh,fr"));
     assertThat(LocaleList.getAdjustedDefault()).isEqualTo(LocaleList.forLanguageTags("zh,fr"));
+    verifyWorkScheduledLogging(ReasonToSchedule.TCS_STARTED);
   }
 
   @Test
@@ -122,6 +125,7 @@
             DownloaderTestUtils.queryWorkInfos(
                 workManager, ModelDownloadManager.UNIQUE_QUEUE_NAME));
     assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.ENQUEUED);
+    verifyWorkScheduledLogging(ReasonToSchedule.LOCALE_SETTINGS_CHANGED);
   }
 
   @Test
@@ -133,6 +137,7 @@
             DownloaderTestUtils.queryWorkInfos(
                 workManager, ModelDownloadManager.UNIQUE_QUEUE_NAME));
     assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.ENQUEUED);
+    verifyWorkScheduledLogging(ReasonToSchedule.DEVICE_CONFIG_UPDATED);
   }
 
   @Test
@@ -143,6 +148,7 @@
     assertThat(
             DownloaderTestUtils.queryWorkInfos(workManager, ModelDownloadManager.UNIQUE_QUEUE_NAME))
         .isEmpty();
+    assertThat(loggerTestRule.getLoggedDownloadWorkScheduledAtoms()).isEmpty();
   }
 
   @Test
@@ -154,6 +160,11 @@
 
     assertThat(workInfos.stream().map(WorkInfo::getState).collect(Collectors.toList()))
         .containsExactly(WorkInfo.State.ENQUEUED, WorkInfo.State.BLOCKED);
+    List<TextClassifierDownloadWorkScheduled> atoms =
+        loggerTestRule.getLoggedDownloadWorkScheduledAtoms();
+    assertThat(atoms).hasSize(2);
+    verifyWorkScheduledAtom(atoms.get(0), ReasonToSchedule.DEVICE_CONFIG_UPDATED);
+    verifyWorkScheduledAtom(atoms.get(1), ReasonToSchedule.DEVICE_CONFIG_UPDATED);
   }
 
   @Test
@@ -164,6 +175,7 @@
     assertThat(Locale.getDefault()).isEqualTo(Locale.forLanguageTag("zh"));
     assertThat(LocaleList.getDefault()).isEqualTo(LocaleList.forLanguageTags("zh,fr"));
     assertThat(LocaleList.getAdjustedDefault()).isEqualTo(LocaleList.forLanguageTags("zh,fr"));
+    verifyWorkScheduledLogging(ReasonToSchedule.DEVICE_CONFIG_UPDATED);
   }
 
   @Test
@@ -173,4 +185,16 @@
 
     assertThat(downloadManager.listDownloadedModels(MODEL_TYPE)).containsExactly(modelFile);
   }
+
+  private void verifyWorkScheduledLogging(ReasonToSchedule reasonToSchedule) throws Exception {
+    TextClassifierDownloadWorkScheduled atom =
+        Iterables.getOnlyElement(loggerTestRule.getLoggedDownloadWorkScheduledAtoms());
+    verifyWorkScheduledAtom(atom, reasonToSchedule);
+  }
+
+  private void verifyWorkScheduledAtom(
+      TextClassifierDownloadWorkScheduled atom, ReasonToSchedule reasonToSchedule) {
+    assertThat(atom.getReasonToSchedule()).isEqualTo(reasonToSchedule);
+    assertThat(atom.getFailedToSchedule()).isFalse();
+  }
 }
diff --git a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadWorkerTest.java b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadWorkerTest.java
index 7831697..3646934 100644
--- a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadWorkerTest.java
+++ b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloadWorkerTest.java
@@ -32,6 +32,8 @@
 import com.android.os.AtomsProto.TextClassifierDownloadReported;
 import com.android.os.AtomsProto.TextClassifierDownloadReported.DownloadStatus;
 import com.android.os.AtomsProto.TextClassifierDownloadReported.FailureReason;
+import com.android.os.AtomsProto.TextClassifierDownloadWorkCompleted;
+import com.android.os.AtomsProto.TextClassifierDownloadWorkCompleted.WorkResult;
 import com.android.textclassifier.common.ModelType;
 import com.android.textclassifier.common.TextClassifierSettings;
 import com.android.textclassifier.common.statsd.TextClassifierDownloadLoggerTestRule;
@@ -41,6 +43,8 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.MoreExecutors;
 import java.io.File;
+import java.time.Clock;
+import java.time.Instant;
 import java.util.List;
 import java.util.Locale;
 import java.util.stream.Collectors;
@@ -55,6 +59,7 @@
 
 @RunWith(JUnit4.class)
 public final class ModelDownloadWorkerTest {
+  private static final long WORK_ID = 123456789L;
   private static final String MODEL_TYPE = ModelType.ANNOTATOR;
   private static final String MODEL_TYPE_2 = ModelType.ACTIONS_SUGGESTIONS;
   private static final TextClassifierDownloadReported.ModelType MODEL_TYPE_ATOM =
@@ -112,7 +117,24 @@
       new LocaleList(new Locale(LOCALE_TAG), new Locale(LOCALE_TAG_2));
   private static final LocaleList LOCALE_LIST_3 =
       new LocaleList(new Locale(LOCALE_TAG), new Locale(LOCALE_TAG_2), new Locale(LOCALE_TAG_3));
+  private static final Instant WORK_SCHEDULED_TIME = Instant.now();
+  private static final Instant WORK_STARTED_TIME = WORK_SCHEDULED_TIME.plusSeconds(100);
+  // Make sure any combination has a different diff
+  private static final Instant DOWNLOAD_STARTED_TIME = WORK_STARTED_TIME.plusSeconds(1);
+  private static final Instant DOWNLOAD_ENDED_TIME = WORK_STARTED_TIME.plusSeconds(1 + 2);
+  private static final Instant DOWNLOAD_STARTED_TIME_2 = WORK_STARTED_TIME.plusSeconds(1 + 2 + 3);
+  private static final Instant DOWNLOAD_ENDED_TIME_2 = WORK_STARTED_TIME.plusSeconds(1 + 2 + 3 + 4);
+  private static final Instant WORK_ENDED_TIME = WORK_STARTED_TIME.plusSeconds(1 + 2 + 3 + 4 + 5);
+  private static final long DOWNLOAD_STARTED_TO_ENDED_MILLIS =
+      DOWNLOAD_ENDED_TIME.toEpochMilli() - DOWNLOAD_STARTED_TIME.toEpochMilli();
+  private static final long DOWNLOAD_STARTED_TO_ENDED_2_MILLIS =
+      DOWNLOAD_ENDED_TIME_2.toEpochMilli() - DOWNLOAD_STARTED_TIME_2.toEpochMilli();
+  private static final long WORK_SCHEDULED_TO_STARTED_MILLIS =
+      WORK_STARTED_TIME.toEpochMilli() - WORK_SCHEDULED_TIME.toEpochMilli();
+  private static final long WORK_STARTED_TO_ENDED_MILLIS =
+      WORK_ENDED_TIME.toEpochMilli() - WORK_STARTED_TIME.toEpochMilli();
 
+  @Mock private Clock clock;
   @Mock private ModelDownloader modelDownloader;
   private File modelDownloaderDir;
   private File modelFile;
@@ -163,12 +185,15 @@
     modelFile.createNewFile();
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO))
         .thenReturn(Futures.immediateFuture(modelFile));
+    when(clock.instant())
+        .thenReturn(WORK_STARTED_TIME, DOWNLOAD_STARTED_TIME, DOWNLOAD_ENDED_TIME, WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
     assertThat(modelFile.exists()).isTrue();
     assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
-    verifyLoggedAtom(DownloadStatus.SUCCEEDED, RUN_ATTEMPT_COUNT, /* failureReason= */ null);
+    verifySucceededDownloadLogging();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -178,12 +203,15 @@
         .thenReturn(Futures.immediateFuture(MODEL_MANIFEST_PROTO));
     modelFile.createNewFile();
     downloadedModelManager.registerModel(MODEL_URL, modelFile.getAbsolutePath());
+    when(clock.instant())
+        .thenReturn(WORK_STARTED_TIME, DOWNLOAD_STARTED_TIME, DOWNLOAD_ENDED_TIME, WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
     assertThat(modelFile.exists()).isTrue();
     assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
-    verifyLoggedAtom(DownloadStatus.SUCCEEDED, RUN_ATTEMPT_COUNT, /* failureReason= */ null);
+    verifySucceededDownloadLogging();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -192,12 +220,15 @@
     modelFile.createNewFile();
     downloadedModelManager.registerModel(MODEL_URL, modelFile.getAbsolutePath());
     downloadedModelManager.registerManifest(MANIFEST_URL, MODEL_URL);
+    when(clock.instant())
+        .thenReturn(WORK_STARTED_TIME, DOWNLOAD_STARTED_TIME, DOWNLOAD_ENDED_TIME, WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
     assertThat(modelFile.exists()).isTrue();
     assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
-    verifyLoggedAtom(DownloadStatus.SUCCEEDED, RUN_ATTEMPT_COUNT, /* failureReason= */ null);
+    verifySucceededDownloadLogging();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -214,6 +245,15 @@
         .thenReturn(Futures.immediateFuture(modelFile));
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO_2))
         .thenReturn(Futures.immediateFuture(modelFile2));
+    // We assume we always download MODEL_TYPE first and then MODEL_TYPE_2, o/w this will be flaky
+    when(clock.instant())
+        .thenReturn(
+            WORK_STARTED_TIME,
+            DOWNLOAD_STARTED_TIME,
+            DOWNLOAD_ENDED_TIME,
+            DOWNLOAD_STARTED_TIME_2,
+            DOWNLOAD_ENDED_TIME_2,
+            WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
@@ -221,7 +261,7 @@
     assertThat(modelFile2.exists()).isTrue();
     assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
     assertThat(downloadedModelManager.listModels(MODEL_TYPE_2)).containsExactly(modelFile2);
-    List<TextClassifierDownloadReported> atoms = loggerTestRule.getLoggedAtoms();
+    List<TextClassifierDownloadReported> atoms = loggerTestRule.getLoggedDownloadReportedAtoms();
     assertThat(atoms).hasSize(2);
     assertThat(
             atoms.stream()
@@ -230,6 +270,12 @@
         .containsExactly(MANIFEST_URL, MANIFEST_URL_2);
     assertThat(atoms.get(0).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
     assertThat(atoms.get(1).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
+    assertThat(
+            atoms.stream()
+                .map(TextClassifierDownloadReported::getDownloadDurationMillis)
+                .collect(Collectors.toList()))
+        .containsExactly(DOWNLOAD_STARTED_TO_ENDED_MILLIS, DOWNLOAD_STARTED_TO_ENDED_2_MILLIS);
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -243,6 +289,15 @@
     modelFile.createNewFile();
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO))
         .thenReturn(Futures.immediateFuture(modelFile));
+    // We assume we always download MODEL_TYPE first and then MODEL_TYPE_2, o/w this will be flaky
+    when(clock.instant())
+        .thenReturn(
+            WORK_STARTED_TIME,
+            DOWNLOAD_STARTED_TIME,
+            DOWNLOAD_ENDED_TIME,
+            DOWNLOAD_STARTED_TIME_2,
+            DOWNLOAD_ENDED_TIME_2,
+            WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
@@ -250,7 +305,7 @@
     assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
     assertThat(downloadedModelManager.listModels(MODEL_TYPE_2)).containsExactly(modelFile);
     verify(modelDownloader, times(1)).downloadModel(modelDownloaderDir, MODEL_PROTO);
-    List<TextClassifierDownloadReported> atoms = loggerTestRule.getLoggedAtoms();
+    List<TextClassifierDownloadReported> atoms = loggerTestRule.getLoggedDownloadReportedAtoms();
     assertThat(atoms).hasSize(2);
     assertThat(
             atoms.stream()
@@ -259,6 +314,12 @@
         .containsExactly(MANIFEST_URL, MANIFEST_URL_2);
     assertThat(atoms.get(0).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
     assertThat(atoms.get(1).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
+    assertThat(
+            atoms.stream()
+                .map(TextClassifierDownloadReported::getDownloadDurationMillis)
+                .collect(Collectors.toList()))
+        .containsExactly(DOWNLOAD_STARTED_TO_ENDED_MILLIS, DOWNLOAD_STARTED_TO_ENDED_2_MILLIS);
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -270,6 +331,15 @@
     modelFile.createNewFile();
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO))
         .thenReturn(Futures.immediateFuture(modelFile));
+    // We assume we always download MODEL_TYPE first and then MODEL_TYPE_2, o/w this will be flaky
+    when(clock.instant())
+        .thenReturn(
+            WORK_STARTED_TIME,
+            DOWNLOAD_STARTED_TIME,
+            DOWNLOAD_ENDED_TIME,
+            DOWNLOAD_STARTED_TIME_2,
+            DOWNLOAD_ENDED_TIME_2,
+            WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
@@ -277,7 +347,7 @@
     assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
     assertThat(downloadedModelManager.listModels(MODEL_TYPE_2)).containsExactly(modelFile);
     verify(modelDownloader, times(1)).downloadManifest(MANIFEST_URL);
-    List<TextClassifierDownloadReported> atoms = loggerTestRule.getLoggedAtoms();
+    List<TextClassifierDownloadReported> atoms = loggerTestRule.getLoggedDownloadReportedAtoms();
     assertThat(atoms).hasSize(2);
     assertThat(
             atoms.stream()
@@ -286,6 +356,12 @@
         .containsExactly(MANIFEST_URL, MANIFEST_URL);
     assertThat(atoms.get(0).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
     assertThat(atoms.get(1).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
+    assertThat(
+            atoms.stream()
+                .map(TextClassifierDownloadReported::getDownloadDurationMillis)
+                .collect(Collectors.toList()))
+        .containsExactly(DOWNLOAD_STARTED_TO_ENDED_MILLIS, DOWNLOAD_STARTED_TO_ENDED_2_MILLIS);
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -293,11 +369,13 @@
     setUpManifestUrl(MODEL_TYPE, LOCALE_TAG, MANIFEST_URL);
     when(modelDownloader.downloadManifest(MANIFEST_URL))
         .thenReturn(Futures.immediateFailedFuture(FAILED_TO_DOWNLOAD_EXCEPTION));
+    when(clock.instant())
+        .thenReturn(WORK_STARTED_TIME, DOWNLOAD_STARTED_TIME, DOWNLOAD_ENDED_TIME, WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.retry());
-    verifyLoggedAtom(
-        DownloadStatus.FAILED_AND_RETRY, RUN_ATTEMPT_COUNT, FAILED_TO_DOWNLOAD_FAILURE_REASON);
+    verifyFailedDownloadLogging();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.RETRY_MODEL_DOWNLOAD_FAILED);
   }
 
   @Test
@@ -307,21 +385,39 @@
         .thenReturn(Futures.immediateFuture(MODEL_MANIFEST_PROTO));
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO))
         .thenReturn(Futures.immediateFailedFuture(FAILED_TO_DOWNLOAD_EXCEPTION));
+    when(clock.instant())
+        .thenReturn(WORK_STARTED_TIME, DOWNLOAD_STARTED_TIME, DOWNLOAD_ENDED_TIME, WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.retry());
-    verifyLoggedAtom(
-        DownloadStatus.FAILED_AND_RETRY, RUN_ATTEMPT_COUNT, FAILED_TO_DOWNLOAD_FAILURE_REASON);
+    verifyFailedDownloadLogging();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.RETRY_MODEL_DOWNLOAD_FAILED);
+  }
+
+  @Test
+  public void downloadFailed_modelDownloadManagerDisabled() throws Exception {
+    deviceConfig.setConfig(TextClassifierSettings.MODEL_DOWNLOAD_MANAGER_ENABLED, false);
+    when(clock.instant()).thenReturn(WORK_STARTED_TIME, WORK_ENDED_TIME);
+
+    ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
+    assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.failure());
+    assertThat(loggerTestRule.getLoggedDownloadReportedAtoms()).isEmpty();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.FAILURE_MODEL_DOWNLOADER_DISABLED);
   }
 
   @Test
   public void downloadFailed_reachWorkerMaxRunAttempts() throws Exception {
+    when(clock.instant()).thenReturn(WORK_STARTED_TIME, WORK_ENDED_TIME);
+
     ModelDownloadWorker worker = createWorker(WORKER_MAX_RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.failure());
+    assertThat(loggerTestRule.getLoggedDownloadReportedAtoms()).isEmpty();
+    verifyWorkLogging(WORKER_MAX_RUN_ATTEMPT_COUNT, WorkResult.FAILURE_MAX_RUN_ATTEMPT_REACHED);
   }
 
   @Test
   public void downloadSkipped_reachManifestMaxAttempts() throws Exception {
+    when(clock.instant()).thenReturn(WORK_STARTED_TIME, WORK_ENDED_TIME);
     setUpManifestUrl(MODEL_TYPE, LOCALE_TAG, MANIFEST_URL);
     deviceConfig.setConfig(
         TextClassifierSettings.MANIFEST_DOWNLOAD_MAX_ATTEMPTS, MANIFEST_MAX_ATTEMPT_COUNT);
@@ -332,20 +428,23 @@
     ModelDownloadWorker worker = createWorker(MANIFEST_MAX_ATTEMPT_COUNT);
 
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
-    assertThat(loggerTestRule.getLoggedAtoms()).isEmpty();
+    assertThat(loggerTestRule.getLoggedDownloadReportedAtoms()).isEmpty();
+    verifyWorkLogging(MANIFEST_MAX_ATTEMPT_COUNT, WorkResult.SUCCESS_NO_UPDATE_AVAILABLE);
   }
 
   @Test
   public void downloadSkipped_manifestAlreadyProcessed() throws Exception {
+    when(clock.instant()).thenReturn(WORK_STARTED_TIME, WORK_ENDED_TIME);
     setUpManifestUrl(MODEL_TYPE, LOCALE_TAG, MANIFEST_URL);
     modelFile.createNewFile();
     downloadedModelManager.registerModel(MODEL_URL, modelFile.getAbsolutePath());
     downloadedModelManager.registerManifest(MANIFEST_URL, MODEL_URL);
     downloadedModelManager.registerManifestEnrollment(MODEL_TYPE, LOCALE_TAG, MANIFEST_URL);
 
-    ModelDownloadWorker worker = createWorker(MANIFEST_MAX_ATTEMPT_COUNT);
+    ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
-    assertThat(loggerTestRule.getLoggedAtoms()).isEmpty();
+    assertThat(loggerTestRule.getLoggedDownloadReportedAtoms()).isEmpty();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_NO_UPDATE_AVAILABLE);
   }
 
   @Test
@@ -366,6 +465,15 @@
         .thenReturn(Futures.immediateFuture(modelFile));
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO_2))
         .thenReturn(Futures.immediateFuture(modelFile2));
+    // We assume we always download MODEL_TYPE first and then MODEL_TYPE_2, o/w this will be flaky
+    when(clock.instant())
+        .thenReturn(
+            WORK_STARTED_TIME,
+            DOWNLOAD_STARTED_TIME,
+            DOWNLOAD_ENDED_TIME,
+            DOWNLOAD_STARTED_TIME_2,
+            DOWNLOAD_ENDED_TIME_2,
+            WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
@@ -374,6 +482,21 @@
 
     assertThat(downloadedModelManager.listModels(MODEL_TYPE))
         .containsExactly(modelFile, modelFile2);
+    List<TextClassifierDownloadReported> atoms = loggerTestRule.getLoggedDownloadReportedAtoms();
+    assertThat(atoms).hasSize(2);
+    assertThat(
+            atoms.stream()
+                .map(TextClassifierDownloadReported::getUrlSuffix)
+                .collect(Collectors.toList()))
+        .containsExactly(MANIFEST_URL, MANIFEST_URL_2);
+    assertThat(atoms.get(0).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
+    assertThat(atoms.get(1).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
+    assertThat(
+            atoms.stream()
+                .map(TextClassifierDownloadReported::getDownloadDurationMillis)
+                .collect(Collectors.toList()))
+        .containsExactly(DOWNLOAD_STARTED_TO_ENDED_MILLIS, DOWNLOAD_STARTED_TO_ENDED_2_MILLIS);
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -388,11 +511,15 @@
     modelFile.createNewFile();
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO))
         .thenReturn(Futures.immediateFuture(modelFile));
+    when(clock.instant())
+        .thenReturn(WORK_STARTED_TIME, DOWNLOAD_STARTED_TIME, DOWNLOAD_ENDED_TIME, WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
     assertThat(modelFile.exists()).isTrue();
     assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
+    verifySucceededDownloadLogging();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -404,15 +531,18 @@
     setUpManifestUrl(MODEL_TYPE, LOCALE_TAG_2, MANIFEST_URL_2);
 
     modelFile.createNewFile();
-    downloadedModelManager.registerModel(MODEL_URL, modelFile.getAbsolutePath());
-    downloadedModelManager.registerManifest(MANIFEST_URL, MODEL_URL);
-    downloadedModelManager.registerManifestEnrollment(MODEL_TYPE, LOCALE_TAG, MANIFEST_URL);
+    when(modelDownloader.downloadManifest(MANIFEST_URL))
+        .thenReturn(Futures.immediateFuture(MODEL_MANIFEST_PROTO));
+    when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO))
+        .thenReturn(Futures.immediateFuture(modelFile));
 
-    when(modelDownloader.downloadManifest(MANIFEST_URL_2))
-        .thenReturn(Futures.immediateFuture(MODEL_MANIFEST_PROTO_2));
     modelFile2.createNewFile();
-    when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO_2))
-        .thenReturn(Futures.immediateFuture(modelFile2));
+    downloadedModelManager.registerModel(MODEL_URL_2, modelFile2.getAbsolutePath());
+    downloadedModelManager.registerManifest(MANIFEST_URL_2, MODEL_URL_2);
+    downloadedModelManager.registerManifestEnrollment(MODEL_TYPE, LOCALE_TAG_2, MANIFEST_URL_2);
+
+    when(clock.instant())
+        .thenReturn(WORK_STARTED_TIME, DOWNLOAD_STARTED_TIME, DOWNLOAD_ENDED_TIME, WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
@@ -420,6 +550,8 @@
     assertThat(modelFile2.exists()).isTrue();
     assertThat(downloadedModelManager.listModels(MODEL_TYPE))
         .containsExactly(modelFile, modelFile2);
+    verifySucceededDownloadLogging();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -440,11 +572,15 @@
         .thenReturn(Futures.immediateFuture(modelFile));
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO_2))
         .thenReturn(Futures.immediateFuture(modelFile2));
+    when(clock.instant())
+        .thenReturn(WORK_STARTED_TIME, DOWNLOAD_STARTED_TIME, DOWNLOAD_ENDED_TIME, WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
     assertThat(modelFile.exists()).isTrue();
     assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
+    verifySucceededDownloadLogging();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
@@ -462,11 +598,35 @@
     modelFile.createNewFile();
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO))
         .thenReturn(Futures.immediateFuture(modelFile));
+    // We assume we always download MODEL_TYPE first and then MODEL_TYPE_2, o/w this will be flaky
+    when(clock.instant())
+        .thenReturn(
+            WORK_STARTED_TIME,
+            DOWNLOAD_STARTED_TIME,
+            DOWNLOAD_ENDED_TIME,
+            DOWNLOAD_STARTED_TIME_2,
+            DOWNLOAD_ENDED_TIME_2,
+            WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.retry());
     assertThat(modelFile.exists()).isTrue();
     assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
+    List<TextClassifierDownloadReported> atoms = loggerTestRule.getLoggedDownloadReportedAtoms();
+    assertThat(atoms).hasSize(2);
+    assertThat(
+            atoms.stream()
+                .map(TextClassifierDownloadReported::getUrlSuffix)
+                .collect(Collectors.toList()))
+        .containsExactly(MANIFEST_URL, MANIFEST_URL_2);
+    assertThat(atoms.get(0).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
+    assertThat(atoms.get(1).getDownloadStatus()).isEqualTo(DownloadStatus.FAILED_AND_RETRY);
+    assertThat(
+            atoms.stream()
+                .map(TextClassifierDownloadReported::getDownloadDurationMillis)
+                .collect(Collectors.toList()))
+        .containsExactly(DOWNLOAD_STARTED_TO_ENDED_MILLIS, DOWNLOAD_STARTED_TO_ENDED_2_MILLIS);
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.RETRY_MODEL_DOWNLOAD_FAILED);
   }
 
   @Test
@@ -494,6 +654,15 @@
         .thenReturn(Futures.immediateFuture(modelFile2));
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO_3))
         .thenReturn(Futures.immediateFuture(modelFile3));
+    // We assume we always download MODEL_TYPE first and then MODEL_TYPE_2, o/w this will be flaky
+    when(clock.instant())
+        .thenReturn(
+            WORK_STARTED_TIME,
+            DOWNLOAD_STARTED_TIME,
+            DOWNLOAD_ENDED_TIME,
+            DOWNLOAD_STARTED_TIME_2,
+            DOWNLOAD_ENDED_TIME_2,
+            WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
@@ -501,17 +670,32 @@
     assertThat(modelFile2.exists()).isTrue();
     assertThat(downloadedModelManager.listModels(MODEL_TYPE))
         .containsExactly(modelFile, modelFile2);
+    List<TextClassifierDownloadReported> atoms = loggerTestRule.getLoggedDownloadReportedAtoms();
+    assertThat(atoms).hasSize(2);
+    assertThat(
+            atoms.stream()
+                .map(TextClassifierDownloadReported::getUrlSuffix)
+                .collect(Collectors.toList()))
+        .containsExactly(MANIFEST_URL, MANIFEST_URL_2);
+    assertThat(atoms.get(0).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
+    assertThat(atoms.get(1).getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
+    assertThat(
+            atoms.stream()
+                .map(TextClassifierDownloadReported::getDownloadDurationMillis)
+                .collect(Collectors.toList()))
+        .containsExactly(DOWNLOAD_STARTED_TO_ENDED_MILLIS, DOWNLOAD_STARTED_TO_ENDED_2_MILLIS);
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   @Test
-  public void downloadSucceed_multiLanguageSupportEnabled_checkDownloadedModelTypes()
+  public void downloadSucceed_multiLanguageSupportEnabled_onlyDownloadMultipleForEnabledModelType()
       throws Exception {
     setDefaultLocalesRule.set(LOCALE_LIST_2);
     deviceConfig.setConfig(TextClassifierSettings.MULTI_LANGUAGE_SUPPORT_ENABLED, true);
     deviceConfig.setConfig(
-        TextClassifierSettings.ENABLED_MODEL_TYPES_FOR_MULTI_LANGUAGE_SUPPORT, MODEL_TYPE);
-    setUpManifestUrl(MODEL_TYPE_2, LOCALE_TAG, MANIFEST_URL);
-    setUpManifestUrl(MODEL_TYPE_2, LOCALE_TAG_2, MANIFEST_URL_2);
+        TextClassifierSettings.ENABLED_MODEL_TYPES_FOR_MULTI_LANGUAGE_SUPPORT, MODEL_TYPE_2);
+    setUpManifestUrl(MODEL_TYPE, LOCALE_TAG, MANIFEST_URL);
+    setUpManifestUrl(MODEL_TYPE, LOCALE_TAG_2, MANIFEST_URL_2);
 
     when(modelDownloader.downloadManifest(MANIFEST_URL))
         .thenReturn(Futures.immediateFuture(MODEL_MANIFEST_PROTO));
@@ -525,11 +709,15 @@
         .thenReturn(Futures.immediateFuture(modelFile));
     when(modelDownloader.downloadModel(modelDownloaderDir, MODEL_PROTO_2))
         .thenReturn(Futures.immediateFuture(modelFile2));
+    when(clock.instant())
+        .thenReturn(WORK_STARTED_TIME, DOWNLOAD_STARTED_TIME, DOWNLOAD_ENDED_TIME, WORK_ENDED_TIME);
 
     ModelDownloadWorker worker = createWorker(RUN_ATTEMPT_COUNT);
     assertThat(worker.startWork().get()).isEqualTo(ListenableWorker.Result.success());
     assertThat(modelFile.exists()).isTrue();
-    assertThat(downloadedModelManager.listModels(MODEL_TYPE_2)).containsExactly(modelFile);
+    assertThat(downloadedModelManager.listModels(MODEL_TYPE)).containsExactly(modelFile);
+    verifySucceededDownloadLogging();
+    verifyWorkLogging(RUN_ATTEMPT_COUNT, WorkResult.SUCCESS_MODEL_DOWNLOADED);
   }
 
   private ModelDownloadWorker createWorker(int runAttemptCount) {
@@ -547,23 +735,50 @@
                     MoreExecutors.newDirectExecutorService(),
                     modelDownloader,
                     downloadedModelManager,
-                    settings);
+                    settings,
+                    WORK_ID,
+                    clock,
+                    WORK_SCHEDULED_TIME.toEpochMilli());
               }
             })
         .build();
   }
 
-  private void verifyLoggedAtom(
-      DownloadStatus downloadStatus, long runAttemptCount, FailureReason failureReason)
-      throws Exception {
-    TextClassifierDownloadReported atom = Iterables.getOnlyElement(loggerTestRule.getLoggedAtoms());
-    assertThat(atom.getDownloadStatus()).isEqualTo(downloadStatus);
+  private void verifySucceededDownloadLogging() throws Exception {
+    TextClassifierDownloadReported atom =
+        Iterables.getOnlyElement(loggerTestRule.getLoggedDownloadReportedAtoms());
+    assertThat(atom.getWorkId()).isEqualTo(WORK_ID);
+    assertThat(atom.getDownloadStatus()).isEqualTo(DownloadStatus.SUCCEEDED);
     assertThat(atom.getModelType()).isEqualTo(MODEL_TYPE_ATOM);
     assertThat(atom.getUrlSuffix()).isEqualTo(MANIFEST_URL);
-    assertThat(atom.getRunAttemptCount()).isEqualTo(runAttemptCount);
-    if (failureReason != null) {
-      assertThat(atom.getFailureReason()).isEqualTo(failureReason);
-    }
+    assertThat(atom.getRunAttemptCount()).isEqualTo(RUN_ATTEMPT_COUNT);
+    assertThat(atom.getFailureReason()).isEqualTo(FailureReason.UNKNOWN_FAILURE_REASON);
+    assertThat(atom.getDownloadDurationMillis()).isEqualTo(DOWNLOAD_STARTED_TO_ENDED_MILLIS);
+  }
+
+  private void verifyFailedDownloadLogging() throws Exception {
+    TextClassifierDownloadReported atom =
+        Iterables.getOnlyElement(loggerTestRule.getLoggedDownloadReportedAtoms());
+    assertThat(atom.getWorkId()).isEqualTo(WORK_ID);
+    assertThat(atom.getDownloadStatus()).isEqualTo(DownloadStatus.FAILED_AND_RETRY);
+    assertThat(atom.getModelType()).isEqualTo(MODEL_TYPE_ATOM);
+    assertThat(atom.getUrlSuffix()).isEqualTo(MANIFEST_URL);
+    assertThat(atom.getRunAttemptCount()).isEqualTo(RUN_ATTEMPT_COUNT);
+    assertThat(atom.getFailureReason()).isEqualTo(FAILED_TO_DOWNLOAD_FAILURE_REASON);
+    assertThat(atom.getDownloaderLibFailureCode())
+        .isEqualTo(ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE);
+    assertThat(atom.getDownloadDurationMillis()).isEqualTo(DOWNLOAD_STARTED_TO_ENDED_MILLIS);
+  }
+
+  private void verifyWorkLogging(int runTimeAttempt, WorkResult workResult) throws Exception {
+    TextClassifierDownloadWorkCompleted atom =
+        Iterables.getOnlyElement(loggerTestRule.getLoggedDownloadWorkCompletedAtoms());
+    assertThat(atom.getWorkId()).isEqualTo(WORK_ID);
+    assertThat(atom.getWorkResult()).isEqualTo(workResult);
+    assertThat(atom.getRunAttemptCount()).isEqualTo(runTimeAttempt);
+    assertThat(atom.getWorkScheduledToStartedDurationMillis())
+        .isEqualTo(WORK_SCHEDULED_TO_STARTED_MILLIS);
+    assertThat(atom.getWorkStartedToEndedDurationMillis()).isEqualTo(WORK_STARTED_TO_ENDED_MILLIS);
   }
 
   private void setUpManifestUrl(
diff --git a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloaderImplTest.java b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloaderImplTest.java
index 47e7fb6..0818057 100644
--- a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloaderImplTest.java
+++ b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloaderImplTest.java
@@ -86,6 +86,8 @@
     ModelDownloadException e = (ModelDownloadException) t.getCause();
     assertThat(e.getErrorCode())
         .isEqualTo(ModelDownloadException.FAILED_TO_DOWNLOAD_SERVICE_CONN_BROKEN);
+    assertThat(e.getDownloaderLibErrorCode())
+        .isEqualTo(ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE);
     assertThat(TestModelDownloaderService.isBound()).isFalse();
     assertThat(TestModelDownloaderService.hasEverBeenBound()).isFalse();
   }
@@ -121,6 +123,8 @@
     assertThat(t).hasCauseThat().isInstanceOf(ModelDownloadException.class);
     ModelDownloadException e = (ModelDownloadException) t.getCause();
     assertThat(e.getErrorCode()).isEqualTo(ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER);
+    assertThat(e.getDownloaderLibErrorCode())
+        .isEqualTo(TestModelDownloaderService.DOWNLOADER_LIB_ERROR_CODE);
     assertThat(e).hasMessageThat().contains(TestModelDownloaderService.ERROR_MSG);
     assertThat(TestModelDownloaderService.getOnUnbindInvokedLatch().await(1L, SECONDS)).isTrue();
     assertThat(TestModelDownloaderService.isBound()).isFalse();
@@ -142,6 +146,8 @@
     assertThat(t).hasCauseThat().isInstanceOf(ModelDownloadException.class);
     ModelDownloadException e = (ModelDownloadException) t.getCause();
     assertThat(e.getErrorCode()).isEqualTo(ModelDownloadException.FAILED_TO_PARSE_MANIFEST);
+    assertThat(e.getDownloaderLibErrorCode())
+        .isEqualTo(ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE);
     assertThat(TestModelDownloaderService.getOnUnbindInvokedLatch().await(1L, SECONDS)).isTrue();
     assertThat(TestModelDownloaderService.isBound()).isFalse();
     assertThat(TestModelDownloaderService.hasEverBeenBound()).isTrue();
@@ -181,6 +187,8 @@
     ModelDownloadException e = (ModelDownloadException) t.getCause();
     assertThat(e.getErrorCode())
         .isEqualTo(ModelDownloadException.FAILED_TO_DOWNLOAD_SERVICE_CONN_BROKEN);
+    assertThat(e.getDownloaderLibErrorCode())
+        .isEqualTo(ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE);
     assertThat(TestModelDownloaderService.isBound()).isFalse();
     assertThat(TestModelDownloaderService.hasEverBeenBound()).isFalse();
   }
@@ -218,6 +226,8 @@
     assertThat(t).hasCauseThat().isInstanceOf(ModelDownloadException.class);
     ModelDownloadException e = (ModelDownloadException) t.getCause();
     assertThat(e.getErrorCode()).isEqualTo(ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER);
+    assertThat(e.getDownloaderLibErrorCode())
+        .isEqualTo(TestModelDownloaderService.DOWNLOADER_LIB_ERROR_CODE);
     assertThat(e).hasMessageThat().contains(TestModelDownloaderService.ERROR_MSG);
     assertThat(TestModelDownloaderService.getOnUnbindInvokedLatch().await(1L, SECONDS)).isTrue();
     assertThat(TestModelDownloaderService.isBound()).isFalse();
@@ -239,6 +249,8 @@
     assertThat(t).hasCauseThat().isInstanceOf(ModelDownloadException.class);
     ModelDownloadException e = (ModelDownloadException) t.getCause();
     assertThat(e.getErrorCode()).isEqualTo(ModelDownloadException.FAILED_TO_VALIDATE_MODEL);
+    assertThat(e.getDownloaderLibErrorCode())
+        .isEqualTo(ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE);
     assertThat(TestModelDownloaderService.getOnUnbindInvokedLatch().await(1L, SECONDS)).isTrue();
     assertThat(TestModelDownloaderService.isBound()).isFalse();
     assertThat(TestModelDownloaderService.hasEverBeenBound()).isTrue();
diff --git a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloaderServiceImplTest.java b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloaderServiceImplTest.java
index fd16943..eac2af3 100644
--- a/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloaderServiceImplTest.java
+++ b/java/tests/instrumentation/src/com/android/textclassifier/downloader/ModelDownloaderServiceImplTest.java
@@ -22,11 +22,12 @@
 import static org.testng.Assert.expectThrows;
 
 import androidx.test.core.app.ApplicationProvider;
-import com.android.textclassifier.downloader.ModelDownloadException.ErrorCode;
 import com.google.android.downloader.DownloadConstraints;
 import com.google.android.downloader.DownloadRequest;
 import com.google.android.downloader.DownloadResult;
 import com.google.android.downloader.Downloader;
+import com.google.android.downloader.ErrorDetails;
+import com.google.android.downloader.RequestException;
 import com.google.android.downloader.SimpleFileDownloadDestination;
 import com.google.common.util.concurrent.FluentFuture;
 import com.google.common.util.concurrent.Futures;
@@ -47,6 +48,14 @@
   private static final long BYTES_WRITTEN = 1L;
   private static final String DOWNLOAD_URI =
       "https://www.gstatic.com/android/text_classifier/r/v999/en.fb";
+  private static final int DOWNLOADER_LIB_ERROR_CODE = 500;
+  private static final String ERROR_MESSAGE = "err_msg";
+  private static final Exception DOWNLOADER_LIB_EXCEPTION =
+      new RequestException(
+          ErrorDetails.builder()
+              .setErrorMessage(ERROR_MESSAGE)
+              .setHttpStatusCode(DOWNLOADER_LIB_ERROR_CODE)
+              .build());
 
   @Mock private Downloader downloader;
   private File targetModelFile;
@@ -98,7 +107,7 @@
     targetModelFile.createNewFile();
     targetMetadataFile.createNewFile();
     when(downloader.execute(any()))
-        .thenReturn(FluentFuture.from(Futures.immediateFailedFuture(new Exception("err_msg"))));
+        .thenReturn(FluentFuture.from(Futures.immediateFailedFuture(DOWNLOADER_LIB_EXCEPTION)));
     modelDownloaderServiceImpl.download(
         DOWNLOAD_URI, targetModelFile.getAbsolutePath(), successCallback);
 
@@ -107,7 +116,8 @@
     assertThat(t).hasCauseThat().isInstanceOf(ModelDownloadException.class);
     ModelDownloadException e = (ModelDownloadException) t.getCause();
     assertThat(e.getErrorCode()).isEqualTo(ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER);
-    assertThat(e).hasMessageThat().contains("err_msg");
+    assertThat(e.getDownloaderLibErrorCode()).isEqualTo(DOWNLOADER_LIB_ERROR_CODE);
+    assertThat(e).hasMessageThat().contains(ERROR_MESSAGE);
     assertThat(targetModelFile.exists()).isFalse();
     assertThat(targetMetadataFile.exists()).isFalse();
   }
@@ -132,7 +142,7 @@
     targetModelFile.createNewFile();
     targetMetadataFile.createNewFile();
     when(downloader.execute(any()))
-        .thenReturn(FluentFuture.from(Futures.immediateFailedFuture(new Exception("err_msg"))));
+        .thenReturn(FluentFuture.from(Futures.immediateFailedFuture(DOWNLOADER_LIB_EXCEPTION)));
     modelDownloaderServiceImpl.download(
         DOWNLOAD_URI, targetModelFile.getAbsolutePath(), failureCallback);
 
@@ -155,8 +165,10 @@
     }
 
     @Override
-    public void onFailure(@ErrorCode int errorCode, String errorMsg) {
-      bytesWrittenFuture.setException(new ModelDownloadException(errorCode, errorMsg));
+    public void onFailure(int downloaderLibErrorCode, String errorMsg) {
+      bytesWrittenFuture.setException(
+          new ModelDownloadException(
+              ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER, downloaderLibErrorCode, errorMsg));
     }
   }
 
@@ -171,7 +183,7 @@
     }
 
     @Override
-    public void onFailure(@ErrorCode int errorCode, String errorMsg) {
+    public void onFailure(int downloaderLibErrorCode, String errorMsg) {
       onFailureCalled = true;
       throw new RuntimeException();
     }
diff --git a/java/tests/instrumentation/src/com/android/textclassifier/downloader/TestModelDownloaderService.java b/java/tests/instrumentation/src/com/android/textclassifier/downloader/TestModelDownloaderService.java
index 0527845..a782b4c 100644
--- a/java/tests/instrumentation/src/com/android/textclassifier/downloader/TestModelDownloaderService.java
+++ b/java/tests/instrumentation/src/com/android/textclassifier/downloader/TestModelDownloaderService.java
@@ -33,7 +33,7 @@
   public static final String GOOD_URI = "good_uri";
   public static final String BAD_URI = "bad_uri";
   public static final long BYTES_WRITTEN = 1L;
-  public static final int ERROR_CODE = ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER;
+  public static final int DOWNLOADER_LIB_ERROR_CODE = 500;
   public static final String ERROR_MSG = "not good uri";
 
   public enum DownloadResult {
@@ -129,7 +129,7 @@
             callback.onSuccess(BYTES_WRITTEN);
             break;
           case FAILED:
-            callback.onFailure(ERROR_CODE, ERROR_MSG);
+            callback.onFailure(DOWNLOADER_LIB_ERROR_CODE, ERROR_MSG);
             break;
           case DO_NOTHING:
             // Do nothing
diff --git a/java/tests/instrumentation/src/com/android/textclassifier/downloader/TextClassifierDownloadLoggerTest.java b/java/tests/instrumentation/src/com/android/textclassifier/downloader/TextClassifierDownloadLoggerTest.java
index f979021..ed76fa8 100644
--- a/java/tests/instrumentation/src/com/android/textclassifier/downloader/TextClassifierDownloadLoggerTest.java
+++ b/java/tests/instrumentation/src/com/android/textclassifier/downloader/TextClassifierDownloadLoggerTest.java
@@ -20,6 +20,8 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import com.android.os.AtomsProto.TextClassifierDownloadReported;
+import com.android.os.AtomsProto.TextClassifierDownloadWorkCompleted;
+import com.android.os.AtomsProto.TextClassifierDownloadWorkScheduled;
 import com.android.textclassifier.common.ModelType;
 import com.android.textclassifier.common.statsd.TextClassifierDownloadLoggerTestRule;
 import com.google.common.collect.Iterables;
@@ -38,6 +40,19 @@
   private static final TextClassifierDownloadReported.FailureReason FAILURE_REASON_ATOM =
       TextClassifierDownloadReported.FailureReason.FAILED_TO_DOWNLOAD_404_ERROR;
   private static final int RUN_ATTEMPT_COUNT = 1;
+  private static final long WORK_ID = 123456789L;
+  private static final long DOWNLOAD_DURATION_MILLIS = 666L;
+  private static final int DOWNLOADER_LIB_ERROR_CODE = 500;
+  private static final int REASON_TO_SCHEDULE =
+      TextClassifierDownloadLogger.REASON_TO_SCHEDULE_TCS_STARTED;
+  private static final TextClassifierDownloadWorkScheduled.ReasonToSchedule
+      REASON_TO_SCHEDULE_ATOM = TextClassifierDownloadWorkScheduled.ReasonToSchedule.TCS_STARTED;
+  private static final int WORK_RESULT =
+      TextClassifierDownloadLogger.WORK_RESULT_SUCCESS_MODEL_DOWNLOADED;
+  private static final TextClassifierDownloadWorkCompleted.WorkResult WORK_RESULT_ATOM =
+      TextClassifierDownloadWorkCompleted.WorkResult.SUCCESS_MODEL_DOWNLOADED;
+  private static final long SCHEDULED_TO_START_DURATION_MILLIS = 777L;
+  private static final long STARTED_TO_FINISHED_DURATION_MILLIS = 888L;
 
   @Rule
   public final TextClassifierDownloadLoggerTestRule loggerTestRule =
@@ -45,27 +60,85 @@
 
   @Test
   public void downloadSucceeded() throws Exception {
-    TextClassifierDownloadLogger.downloadSucceeded(MODEL_TYPE, URL, RUN_ATTEMPT_COUNT);
+    TextClassifierDownloadLogger.downloadSucceeded(
+        WORK_ID, MODEL_TYPE, URL, RUN_ATTEMPT_COUNT, DOWNLOAD_DURATION_MILLIS);
 
-    TextClassifierDownloadReported atom = Iterables.getOnlyElement(loggerTestRule.getLoggedAtoms());
+    TextClassifierDownloadReported atom =
+        Iterables.getOnlyElement(loggerTestRule.getLoggedDownloadReportedAtoms());
+    assertThat(atom.getWorkId()).isEqualTo(WORK_ID);
     assertThat(atom.getDownloadStatus())
         .isEqualTo(TextClassifierDownloadReported.DownloadStatus.SUCCEEDED);
     assertThat(atom.getModelType()).isEqualTo(MODEL_TYPE_ATOM);
     assertThat(atom.getUrlSuffix()).isEqualTo(URL);
     assertThat(atom.getRunAttemptCount()).isEqualTo(RUN_ATTEMPT_COUNT);
+    assertThat(atom.getDownloadDurationMillis()).isEqualTo(DOWNLOAD_DURATION_MILLIS);
   }
 
   @Test
-  public void downloadFailedAndRetry() throws Exception {
-    TextClassifierDownloadLogger.downloadFailedAndRetry(
-        MODEL_TYPE, URL, ERROR_CODE, RUN_ATTEMPT_COUNT);
+  public void downloadFailed() throws Exception {
+    TextClassifierDownloadLogger.downloadFailed(
+        WORK_ID,
+        MODEL_TYPE,
+        URL,
+        ERROR_CODE,
+        RUN_ATTEMPT_COUNT,
+        DOWNLOADER_LIB_ERROR_CODE,
+        DOWNLOAD_DURATION_MILLIS);
 
-    TextClassifierDownloadReported atom = Iterables.getOnlyElement(loggerTestRule.getLoggedAtoms());
+    TextClassifierDownloadReported atom =
+        Iterables.getOnlyElement(loggerTestRule.getLoggedDownloadReportedAtoms());
+    assertThat(atom.getWorkId()).isEqualTo(WORK_ID);
     assertThat(atom.getDownloadStatus())
         .isEqualTo(TextClassifierDownloadReported.DownloadStatus.FAILED_AND_RETRY);
     assertThat(atom.getModelType()).isEqualTo(MODEL_TYPE_ATOM);
     assertThat(atom.getUrlSuffix()).isEqualTo(URL);
     assertThat(atom.getRunAttemptCount()).isEqualTo(RUN_ATTEMPT_COUNT);
     assertThat(atom.getFailureReason()).isEqualTo(FAILURE_REASON_ATOM);
+    assertThat(atom.getDownloaderLibFailureCode()).isEqualTo(DOWNLOADER_LIB_ERROR_CODE);
+    assertThat(atom.getDownloadDurationMillis()).isEqualTo(DOWNLOAD_DURATION_MILLIS);
+  }
+
+  @Test
+  public void downloadWorkScheduled_succeeded() throws Exception {
+    TextClassifierDownloadLogger.downloadWorkScheduled(
+        WORK_ID, REASON_TO_SCHEDULE, /* failedToSchedule= */ false);
+
+    TextClassifierDownloadWorkScheduled atom =
+        Iterables.getOnlyElement(loggerTestRule.getLoggedDownloadWorkScheduledAtoms());
+    assertThat(atom.getWorkId()).isEqualTo(WORK_ID);
+    assertThat(atom.getReasonToSchedule()).isEqualTo(REASON_TO_SCHEDULE_ATOM);
+    assertThat(atom.getFailedToSchedule()).isFalse();
+  }
+
+  @Test
+  public void downloadWorkScheduled_failed() throws Exception {
+    TextClassifierDownloadLogger.downloadWorkScheduled(
+        WORK_ID, REASON_TO_SCHEDULE, /* failedToSchedule= */ true);
+
+    TextClassifierDownloadWorkScheduled atom =
+        Iterables.getOnlyElement(loggerTestRule.getLoggedDownloadWorkScheduledAtoms());
+    assertThat(atom.getWorkId()).isEqualTo(WORK_ID);
+    assertThat(atom.getReasonToSchedule()).isEqualTo(REASON_TO_SCHEDULE_ATOM);
+    assertThat(atom.getFailedToSchedule()).isTrue();
+  }
+
+  @Test
+  public void downloadWorkCompleted() throws Exception {
+    TextClassifierDownloadLogger.downloadWorkCompleted(
+        WORK_ID,
+        WORK_RESULT,
+        RUN_ATTEMPT_COUNT,
+        SCHEDULED_TO_START_DURATION_MILLIS,
+        STARTED_TO_FINISHED_DURATION_MILLIS);
+
+    TextClassifierDownloadWorkCompleted atom =
+        Iterables.getOnlyElement(loggerTestRule.getLoggedDownloadWorkCompletedAtoms());
+    assertThat(atom.getWorkId()).isEqualTo(WORK_ID);
+    assertThat(atom.getWorkResult()).isEqualTo(WORK_RESULT_ATOM);
+    assertThat(atom.getRunAttemptCount()).isEqualTo(RUN_ATTEMPT_COUNT);
+    assertThat(atom.getWorkScheduledToStartedDurationMillis())
+        .isEqualTo(SCHEDULED_TO_START_DURATION_MILLIS);
+    assertThat(atom.getWorkStartedToEndedDurationMillis())
+        .isEqualTo(STARTED_TO_FINISHED_DURATION_MILLIS);
   }
 }
diff --git a/native/actions/test_data/actions_suggestions_grammar_test.model b/native/actions/test_data/actions_suggestions_grammar_test.model
index d96eef4..77e556c 100644
--- a/native/actions/test_data/actions_suggestions_grammar_test.model
+++ b/native/actions/test_data/actions_suggestions_grammar_test.model
Binary files differ
diff --git a/native/actions/test_data/actions_suggestions_test.model b/native/actions/test_data/actions_suggestions_test.model
index deb284e..c468bd5 100644
--- a/native/actions/test_data/actions_suggestions_test.model
+++ b/native/actions/test_data/actions_suggestions_test.model
Binary files differ
diff --git a/native/actions/test_data/actions_suggestions_test.multi_task_9heads.model b/native/actions/test_data/actions_suggestions_test.multi_task_9heads.model
index 766d5b0..ec421a1 100644
--- a/native/actions/test_data/actions_suggestions_test.multi_task_9heads.model
+++ b/native/actions/test_data/actions_suggestions_test.multi_task_9heads.model
Binary files differ
diff --git a/native/actions/test_data/actions_suggestions_test.multi_task_sr_emoji.model b/native/actions/test_data/actions_suggestions_test.multi_task_sr_emoji.model
index bde92c2..24be6c6 100644
--- a/native/actions/test_data/actions_suggestions_test.multi_task_sr_emoji.model
+++ b/native/actions/test_data/actions_suggestions_test.multi_task_sr_emoji.model
Binary files differ
diff --git a/native/actions/test_data/actions_suggestions_test.multi_task_sr_nudge_signal_v0.model b/native/actions/test_data/actions_suggestions_test.multi_task_sr_nudge_signal_v0.model
index 1ea71b4..fd7ddf2 100644
--- a/native/actions/test_data/actions_suggestions_test.multi_task_sr_nudge_signal_v0.model
+++ b/native/actions/test_data/actions_suggestions_test.multi_task_sr_nudge_signal_v0.model
Binary files differ
diff --git a/native/actions/test_data/actions_suggestions_test.multi_task_sr_p13n.model b/native/actions/test_data/actions_suggestions_test.multi_task_sr_p13n.model
index 336a0c3..c969c56 100644
--- a/native/actions/test_data/actions_suggestions_test.multi_task_sr_p13n.model
+++ b/native/actions/test_data/actions_suggestions_test.multi_task_sr_p13n.model
Binary files differ
diff --git a/native/actions/test_data/actions_suggestions_test.multi_task_tf2_test.model b/native/actions/test_data/actions_suggestions_test.multi_task_tf2_test.model
index 9ba5574..d171898 100644
--- a/native/actions/test_data/actions_suggestions_test.multi_task_tf2_test.model
+++ b/native/actions/test_data/actions_suggestions_test.multi_task_tf2_test.model
Binary files differ
diff --git a/native/actions/test_data/actions_suggestions_test.sensitive_tflite.model b/native/actions/test_data/actions_suggestions_test.sensitive_tflite.model
index bc9cd04..937552b 100644
--- a/native/actions/test_data/actions_suggestions_test.sensitive_tflite.model
+++ b/native/actions/test_data/actions_suggestions_test.sensitive_tflite.model
Binary files differ
diff --git a/native/lang_id/script/approx-script-data.cc b/native/lang_id/script/approx-script-data.cc
index 233653f..678a1e9 100755
--- a/native/lang_id/script/approx-script-data.cc
+++ b/native/lang_id/script/approx-script-data.cc
@@ -27,7 +27,7 @@
 namespace mobile {
 namespace approx_script_internal {
 
-const int kNumRanges = 376;
+const int kNumRanges = 389;
 
 const uint32 kRangeFirst[] = {
   65,  // Range #0: [65, 90, Latin]
@@ -67,8 +67,8 @@
   2048,  // Range #34: [2048, 2110, Samaritan]
   2112,  // Range #35: [2112, 2142, Mandaic]
   2144,  // Range #36: [2144, 2154, Syriac]
-  2208,  // Range #37: [2208, 2247, Arabic]
-  2259,  // Range #38: [2259, 2273, Arabic]
+  2160,  // Range #37: [2160, 2193, Arabic]
+  2200,  // Range #38: [2200, 2273, Arabic]
   2275,  // Range #39: [2275, 2303, Arabic]
   2304,  // Range #40: [2304, 2384, Devanagari]
   2389,  // Range #41: [2389, 2403, Devanagari]
@@ -87,32 +87,32 @@
   3031,  // Range #54: [3031, 3031, Tamil]
   3046,  // Range #55: [3046, 3066, Tamil]
   3072,  // Range #56: [3072, 3149, Telugu]
-  3157,  // Range #57: [3157, 3162, Telugu]
-  3168,  // Range #58: [3168, 3183, Telugu]
-  3191,  // Range #59: [3191, 3199, Telugu]
-  3200,  // Range #60: [3200, 3277, Kannada]
-  3285,  // Range #61: [3285, 3286, Kannada]
-  3294,  // Range #62: [3294, 3314, Kannada]
-  3328,  // Range #63: [3328, 3455, Malayalam]
-  3457,  // Range #64: [3457, 3551, Sinhala]
-  3558,  // Range #65: [3558, 3572, Sinhala]
-  3585,  // Range #66: [3585, 3642, Thai]
-  3648,  // Range #67: [3648, 3675, Thai]
-  3713,  // Range #68: [3713, 3807, Lao]
-  3840,  // Range #69: [3840, 4052, Tibetan]
-  4057,  // Range #70: [4057, 4058, Tibetan]
-  4096,  // Range #71: [4096, 4255, Myanmar]
-  4256,  // Range #72: [4256, 4295, Georgian]
-  4301,  // Range #73: [4301, 4346, Georgian]
-  4348,  // Range #74: [4348, 4351, Georgian]
-  4352,  // Range #75: [4352, 4607, Hangul]
-  4608,  // Range #76: [4608, 5017, Ethiopic]
-  5024,  // Range #77: [5024, 5117, Cherokee]
-  5120,  // Range #78: [5120, 5759, Canadian_Aboriginal]
-  5760,  // Range #79: [5760, 5788, Ogham]
-  5792,  // Range #80: [5792, 5866, Runic]
-  5870,  // Range #81: [5870, 5880, Runic]
-  5888,  // Range #82: [5888, 5908, Tagalog]
+  3157,  // Range #57: [3157, 3183, Telugu]
+  3191,  // Range #58: [3191, 3199, Telugu]
+  3200,  // Range #59: [3200, 3277, Kannada]
+  3285,  // Range #60: [3285, 3286, Kannada]
+  3293,  // Range #61: [3293, 3314, Kannada]
+  3328,  // Range #62: [3328, 3455, Malayalam]
+  3457,  // Range #63: [3457, 3551, Sinhala]
+  3558,  // Range #64: [3558, 3572, Sinhala]
+  3585,  // Range #65: [3585, 3642, Thai]
+  3648,  // Range #66: [3648, 3675, Thai]
+  3713,  // Range #67: [3713, 3807, Lao]
+  3840,  // Range #68: [3840, 4052, Tibetan]
+  4057,  // Range #69: [4057, 4058, Tibetan]
+  4096,  // Range #70: [4096, 4255, Myanmar]
+  4256,  // Range #71: [4256, 4295, Georgian]
+  4301,  // Range #72: [4301, 4346, Georgian]
+  4348,  // Range #73: [4348, 4351, Georgian]
+  4352,  // Range #74: [4352, 4607, Hangul]
+  4608,  // Range #75: [4608, 5017, Ethiopic]
+  5024,  // Range #76: [5024, 5117, Cherokee]
+  5120,  // Range #77: [5120, 5759, Canadian_Aboriginal]
+  5760,  // Range #78: [5760, 5788, Ogham]
+  5792,  // Range #79: [5792, 5866, Runic]
+  5870,  // Range #80: [5870, 5880, Runic]
+  5888,  // Range #81: [5888, 5909, Tagalog]
+  5919,  // Range #82: [5919, 5919, Tagalog]
   5920,  // Range #83: [5920, 5940, Hanunoo]
   5952,  // Range #84: [5952, 5971, Buhid]
   5984,  // Range #85: [5984, 6003, Tagbanwa]
@@ -133,7 +133,7 @@
   6688,  // Range #100: [6688, 6793, Tai_Tham]
   6800,  // Range #101: [6800, 6809, Tai_Tham]
   6816,  // Range #102: [6816, 6829, Tai_Tham]
-  6912,  // Range #103: [6912, 7036, Balinese]
+  6912,  // Range #103: [6912, 7038, Balinese]
   7040,  // Range #104: [7040, 7103, Sundanese]
   7104,  // Range #105: [7104, 7155, Batak]
   7164,  // Range #106: [7164, 7167, Batak]
@@ -164,7 +164,7 @@
   8526,  // Range #131: [8526, 8526, Latin]
   8544,  // Range #132: [8544, 8584, Latin]
   10240,  // Range #133: [10240, 10495, Braille]
-  11264,  // Range #134: [11264, 11358, Glagolitic]
+  11264,  // Range #134: [11264, 11359, Glagolitic]
   11360,  // Range #135: [11360, 11391, Latin]
   11392,  // Range #136: [11392, 11507, Coptic]
   11513,  // Range #137: [11513, 11519, Coptic]
@@ -196,7 +196,7 @@
   13008,  // Range #163: [13008, 13054, Katakana]
   13056,  // Range #164: [13056, 13143, Katakana]
   13312,  // Range #165: [13312, 19903, Han]
-  19968,  // Range #166: [19968, 40956, Han]
+  19968,  // Range #166: [19968, 40959, Han]
   40960,  // Range #167: [40960, 42182, Yi]
   42192,  // Range #168: [42192, 42239, Lisu]
   42240,  // Range #169: [42240, 42539, Vai]
@@ -204,208 +204,221 @@
   42656,  // Range #171: [42656, 42743, Bamum]
   42786,  // Range #172: [42786, 42887, Latin]
   42891,  // Range #173: [42891, 42954, Latin]
-  42997,  // Range #174: [42997, 43007, Latin]
-  43008,  // Range #175: [43008, 43052, Syloti_Nagri]
-  43072,  // Range #176: [43072, 43127, Phags_Pa]
-  43136,  // Range #177: [43136, 43205, Saurashtra]
-  43214,  // Range #178: [43214, 43225, Saurashtra]
-  43232,  // Range #179: [43232, 43263, Devanagari]
-  43264,  // Range #180: [43264, 43309, Kayah_Li]
-  43311,  // Range #181: [43311, 43311, Kayah_Li]
-  43312,  // Range #182: [43312, 43347, Rejang]
-  43359,  // Range #183: [43359, 43359, Rejang]
-  43360,  // Range #184: [43360, 43388, Hangul]
-  43392,  // Range #185: [43392, 43469, Javanese]
-  43472,  // Range #186: [43472, 43487, Javanese]
-  43488,  // Range #187: [43488, 43518, Myanmar]
-  43520,  // Range #188: [43520, 43574, Cham]
-  43584,  // Range #189: [43584, 43615, Cham]
-  43616,  // Range #190: [43616, 43647, Myanmar]
-  43648,  // Range #191: [43648, 43714, Tai_Viet]
-  43739,  // Range #192: [43739, 43743, Tai_Viet]
-  43744,  // Range #193: [43744, 43766, Meetei_Mayek]
-  43777,  // Range #194: [43777, 43798, Ethiopic]
-  43808,  // Range #195: [43808, 43822, Ethiopic]
-  43824,  // Range #196: [43824, 43866, Latin]
-  43868,  // Range #197: [43868, 43876, Latin]
-  43877,  // Range #198: [43877, 43877, Greek]
-  43878,  // Range #199: [43878, 43881, Latin]
-  43888,  // Range #200: [43888, 43967, Cherokee]
-  43968,  // Range #201: [43968, 44025, Meetei_Mayek]
-  44032,  // Range #202: [44032, 55203, Hangul]
-  55216,  // Range #203: [55216, 55291, Hangul]
-  63744,  // Range #204: [63744, 64217, Han]
-  64256,  // Range #205: [64256, 64262, Latin]
-  64275,  // Range #206: [64275, 64279, Armenian]
-  64285,  // Range #207: [64285, 64335, Hebrew]
-  64336,  // Range #208: [64336, 64449, Arabic]
-  64467,  // Range #209: [64467, 64829, Arabic]
-  64848,  // Range #210: [64848, 64967, Arabic]
-  65008,  // Range #211: [65008, 65021, Arabic]
-  65070,  // Range #212: [65070, 65071, Cyrillic]
-  65136,  // Range #213: [65136, 65276, Arabic]
-  65313,  // Range #214: [65313, 65338, Latin]
-  65345,  // Range #215: [65345, 65370, Latin]
-  65382,  // Range #216: [65382, 65391, Katakana]
-  65393,  // Range #217: [65393, 65437, Katakana]
-  65440,  // Range #218: [65440, 65500, Hangul]
-  65536,  // Range #219: [65536, 65629, Linear_B]
-  65664,  // Range #220: [65664, 65786, Linear_B]
-  65856,  // Range #221: [65856, 65934, Greek]
-  65952,  // Range #222: [65952, 65952, Greek]
-  66176,  // Range #223: [66176, 66204, Lycian]
-  66208,  // Range #224: [66208, 66256, Carian]
-  66304,  // Range #225: [66304, 66339, Old_Italic]
-  66349,  // Range #226: [66349, 66351, Old_Italic]
-  66352,  // Range #227: [66352, 66378, Gothic]
-  66384,  // Range #228: [66384, 66426, Old_Permic]
-  66432,  // Range #229: [66432, 66463, Ugaritic]
-  66464,  // Range #230: [66464, 66517, Old_Persian]
-  66560,  // Range #231: [66560, 66639, Deseret]
-  66640,  // Range #232: [66640, 66687, Shavian]
-  66688,  // Range #233: [66688, 66729, Osmanya]
-  66736,  // Range #234: [66736, 66811, Osage]
-  66816,  // Range #235: [66816, 66855, Elbasan]
-  66864,  // Range #236: [66864, 66915, Caucasian_Albanian]
-  66927,  // Range #237: [66927, 66927, Caucasian_Albanian]
-  67072,  // Range #238: [67072, 67382, Linear_A]
-  67392,  // Range #239: [67392, 67413, Linear_A]
-  67424,  // Range #240: [67424, 67431, Linear_A]
-  67584,  // Range #241: [67584, 67647, Cypriot]
-  67648,  // Range #242: [67648, 67679, Imperial_Aramaic]
-  67680,  // Range #243: [67680, 67711, Palmyrene]
-  67712,  // Range #244: [67712, 67742, Nabataean]
-  67751,  // Range #245: [67751, 67759, Nabataean]
-  67808,  // Range #246: [67808, 67829, Hatran]
-  67835,  // Range #247: [67835, 67839, Hatran]
-  67840,  // Range #248: [67840, 67871, Phoenician]
-  67872,  // Range #249: [67872, 67897, Lydian]
-  67903,  // Range #250: [67903, 67903, Lydian]
-  67968,  // Range #251: [67968, 67999, Meroitic_Hieroglyphs]
-  68000,  // Range #252: [68000, 68095, Meroitic_Cursive]
-  68096,  // Range #253: [68096, 68102, Kharoshthi]
-  68108,  // Range #254: [68108, 68168, Kharoshthi]
-  68176,  // Range #255: [68176, 68184, Kharoshthi]
-  68192,  // Range #256: [68192, 68223, Old_South_Arabian]
-  68224,  // Range #257: [68224, 68255, Old_North_Arabian]
-  68288,  // Range #258: [68288, 68342, Manichaean]
-  68352,  // Range #259: [68352, 68415, Avestan]
-  68416,  // Range #260: [68416, 68447, Inscriptional_Parthian]
-  68448,  // Range #261: [68448, 68466, Inscriptional_Pahlavi]
-  68472,  // Range #262: [68472, 68479, Inscriptional_Pahlavi]
-  68480,  // Range #263: [68480, 68497, Psalter_Pahlavi]
-  68505,  // Range #264: [68505, 68508, Psalter_Pahlavi]
-  68521,  // Range #265: [68521, 68527, Psalter_Pahlavi]
-  68608,  // Range #266: [68608, 68680, Old_Turkic]
-  68736,  // Range #267: [68736, 68786, Old_Hungarian]
-  68800,  // Range #268: [68800, 68850, Old_Hungarian]
-  68858,  // Range #269: [68858, 68863, Old_Hungarian]
-  68864,  // Range #270: [68864, 68903, Hanifi_Rohingya]
-  68912,  // Range #271: [68912, 68921, Hanifi_Rohingya]
-  69216,  // Range #272: [69216, 69246, Arabic]
-  69248,  // Range #273: [69248, 69297, Yezidi]
-  69376,  // Range #274: [69376, 69415, Old_Sogdian]
-  69424,  // Range #275: [69424, 69465, Sogdian]
-  69552,  // Range #276: [69552, 69579, Chorasmian]
-  69600,  // Range #277: [69600, 69622, Elymaic]
-  69632,  // Range #278: [69632, 69743, Brahmi]
-  69759,  // Range #279: [69759, 69759, Brahmi]
-  69760,  // Range #280: [69760, 69825, Kaithi]
-  69837,  // Range #281: [69837, 69837, Kaithi]
-  69840,  // Range #282: [69840, 69864, Sora_Sompeng]
-  69872,  // Range #283: [69872, 69881, Sora_Sompeng]
-  69888,  // Range #284: [69888, 69959, Chakma]
-  69968,  // Range #285: [69968, 70006, Mahajani]
-  70016,  // Range #286: [70016, 70111, Sharada]
-  70113,  // Range #287: [70113, 70132, Sinhala]
-  70144,  // Range #288: [70144, 70206, Khojki]
-  70272,  // Range #289: [70272, 70313, Multani]
-  70320,  // Range #290: [70320, 70378, Khudawadi]
-  70384,  // Range #291: [70384, 70393, Khudawadi]
-  70400,  // Range #292: [70400, 70457, Grantha]
-  70460,  // Range #293: [70460, 70480, Grantha]
-  70487,  // Range #294: [70487, 70487, Grantha]
-  70493,  // Range #295: [70493, 70516, Grantha]
-  70656,  // Range #296: [70656, 70753, Newa]
-  70784,  // Range #297: [70784, 70855, Tirhuta]
-  70864,  // Range #298: [70864, 70873, Tirhuta]
-  71040,  // Range #299: [71040, 71133, Siddham]
-  71168,  // Range #300: [71168, 71236, Modi]
-  71248,  // Range #301: [71248, 71257, Modi]
-  71264,  // Range #302: [71264, 71276, Mongolian]
-  71296,  // Range #303: [71296, 71352, Takri]
-  71360,  // Range #304: [71360, 71369, Takri]
-  71424,  // Range #305: [71424, 71487, Ahom]
-  71680,  // Range #306: [71680, 71739, Dogra]
-  71840,  // Range #307: [71840, 71922, Warang_Citi]
-  71935,  // Range #308: [71935, 71935, Warang_Citi]
-  71936,  // Range #309: [71936, 72006, Dives_Akuru]
-  72016,  // Range #310: [72016, 72025, Dives_Akuru]
-  72096,  // Range #311: [72096, 72164, Nandinagari]
-  72192,  // Range #312: [72192, 72263, Zanabazar_Square]
-  72272,  // Range #313: [72272, 72354, Soyombo]
-  72384,  // Range #314: [72384, 72440, Pau_Cin_Hau]
-  72704,  // Range #315: [72704, 72773, Bhaiksuki]
-  72784,  // Range #316: [72784, 72812, Bhaiksuki]
-  72816,  // Range #317: [72816, 72886, Marchen]
-  72960,  // Range #318: [72960, 73031, Masaram_Gondi]
-  73040,  // Range #319: [73040, 73049, Masaram_Gondi]
-  73056,  // Range #320: [73056, 73112, Gunjala_Gondi]
-  73120,  // Range #321: [73120, 73129, Gunjala_Gondi]
-  73440,  // Range #322: [73440, 73464, Makasar]
-  73648,  // Range #323: [73648, 73648, Lisu]
-  73664,  // Range #324: [73664, 73713, Tamil]
-  73727,  // Range #325: [73727, 73727, Tamil]
-  73728,  // Range #326: [73728, 74649, Cuneiform]
-  74752,  // Range #327: [74752, 74868, Cuneiform]
-  74880,  // Range #328: [74880, 75075, Cuneiform]
-  77824,  // Range #329: [77824, 78904, Egyptian_Hieroglyphs]
-  82944,  // Range #330: [82944, 83526, Anatolian_Hieroglyphs]
-  92160,  // Range #331: [92160, 92728, Bamum]
-  92736,  // Range #332: [92736, 92783, Mro]
-  92880,  // Range #333: [92880, 92917, Bassa_Vah]
-  92928,  // Range #334: [92928, 92997, Pahawh_Hmong]
-  93008,  // Range #335: [93008, 93047, Pahawh_Hmong]
-  93053,  // Range #336: [93053, 93071, Pahawh_Hmong]
-  93760,  // Range #337: [93760, 93850, Medefaidrin]
-  93952,  // Range #338: [93952, 94087, Miao]
-  94095,  // Range #339: [94095, 94111, Miao]
-  94176,  // Range #340: [94176, 94176, Tangut]
-  94177,  // Range #341: [94177, 94177, Nushu]
-  94180,  // Range #342: [94180, 94180, Khitan_Small_Script]
-  94192,  // Range #343: [94192, 94193, Han]
-  94208,  // Range #344: [94208, 100343, Tangut]
-  100352,  // Range #345: [100352, 101119, Tangut]
-  101120,  // Range #346: [101120, 101589, Khitan_Small_Script]
-  101632,  // Range #347: [101632, 101640, Tangut]
-  110592,  // Range #348: [110592, 110592, Katakana]
-  110593,  // Range #349: [110593, 110878, Hiragana]
-  110928,  // Range #350: [110928, 110930, Hiragana]
-  110948,  // Range #351: [110948, 110951, Katakana]
-  110960,  // Range #352: [110960, 111355, Nushu]
-  113664,  // Range #353: [113664, 113770, Duployan]
-  113776,  // Range #354: [113776, 113800, Duployan]
-  113808,  // Range #355: [113808, 113823, Duployan]
-  119296,  // Range #356: [119296, 119365, Greek]
-  120832,  // Range #357: [120832, 121483, SignWriting]
-  121499,  // Range #358: [121499, 121519, SignWriting]
-  122880,  // Range #359: [122880, 122922, Glagolitic]
-  123136,  // Range #360: [123136, 123215, Nyiakeng_Puachue_Hmong]
-  123584,  // Range #361: [123584, 123641, Wancho]
-  123647,  // Range #362: [123647, 123647, Wancho]
-  124928,  // Range #363: [124928, 125142, Mende_Kikakui]
-  125184,  // Range #364: [125184, 125279, Adlam]
-  126464,  // Range #365: [126464, 126523, Arabic]
-  126530,  // Range #366: [126530, 126619, Arabic]
-  126625,  // Range #367: [126625, 126651, Arabic]
-  126704,  // Range #368: [126704, 126705, Arabic]
-  127488,  // Range #369: [127488, 127488, Hiragana]
-  131072,  // Range #370: [131072, 173789, Han]
-  173824,  // Range #371: [173824, 177972, Han]
-  177984,  // Range #372: [177984, 183969, Han]
-  183984,  // Range #373: [183984, 191456, Han]
-  194560,  // Range #374: [194560, 195101, Han]
-  196608,  // Range #375: [196608, 201546, Han]
+  42960,  // Range #174: [42960, 42969, Latin]
+  42994,  // Range #175: [42994, 43007, Latin]
+  43008,  // Range #176: [43008, 43052, Syloti_Nagri]
+  43072,  // Range #177: [43072, 43127, Phags_Pa]
+  43136,  // Range #178: [43136, 43205, Saurashtra]
+  43214,  // Range #179: [43214, 43225, Saurashtra]
+  43232,  // Range #180: [43232, 43263, Devanagari]
+  43264,  // Range #181: [43264, 43309, Kayah_Li]
+  43311,  // Range #182: [43311, 43311, Kayah_Li]
+  43312,  // Range #183: [43312, 43347, Rejang]
+  43359,  // Range #184: [43359, 43359, Rejang]
+  43360,  // Range #185: [43360, 43388, Hangul]
+  43392,  // Range #186: [43392, 43469, Javanese]
+  43472,  // Range #187: [43472, 43487, Javanese]
+  43488,  // Range #188: [43488, 43518, Myanmar]
+  43520,  // Range #189: [43520, 43574, Cham]
+  43584,  // Range #190: [43584, 43615, Cham]
+  43616,  // Range #191: [43616, 43647, Myanmar]
+  43648,  // Range #192: [43648, 43714, Tai_Viet]
+  43739,  // Range #193: [43739, 43743, Tai_Viet]
+  43744,  // Range #194: [43744, 43766, Meetei_Mayek]
+  43777,  // Range #195: [43777, 43798, Ethiopic]
+  43808,  // Range #196: [43808, 43822, Ethiopic]
+  43824,  // Range #197: [43824, 43866, Latin]
+  43868,  // Range #198: [43868, 43876, Latin]
+  43877,  // Range #199: [43877, 43877, Greek]
+  43878,  // Range #200: [43878, 43881, Latin]
+  43888,  // Range #201: [43888, 43967, Cherokee]
+  43968,  // Range #202: [43968, 44025, Meetei_Mayek]
+  44032,  // Range #203: [44032, 55203, Hangul]
+  55216,  // Range #204: [55216, 55291, Hangul]
+  63744,  // Range #205: [63744, 64217, Han]
+  64256,  // Range #206: [64256, 64262, Latin]
+  64275,  // Range #207: [64275, 64279, Armenian]
+  64285,  // Range #208: [64285, 64335, Hebrew]
+  64336,  // Range #209: [64336, 64450, Arabic]
+  64467,  // Range #210: [64467, 64829, Arabic]
+  64832,  // Range #211: [64832, 64967, Arabic]
+  64975,  // Range #212: [64975, 64975, Arabic]
+  65008,  // Range #213: [65008, 65023, Arabic]
+  65070,  // Range #214: [65070, 65071, Cyrillic]
+  65136,  // Range #215: [65136, 65276, Arabic]
+  65313,  // Range #216: [65313, 65338, Latin]
+  65345,  // Range #217: [65345, 65370, Latin]
+  65382,  // Range #218: [65382, 65391, Katakana]
+  65393,  // Range #219: [65393, 65437, Katakana]
+  65440,  // Range #220: [65440, 65500, Hangul]
+  65536,  // Range #221: [65536, 65629, Linear_B]
+  65664,  // Range #222: [65664, 65786, Linear_B]
+  65856,  // Range #223: [65856, 65934, Greek]
+  65952,  // Range #224: [65952, 65952, Greek]
+  66176,  // Range #225: [66176, 66204, Lycian]
+  66208,  // Range #226: [66208, 66256, Carian]
+  66304,  // Range #227: [66304, 66339, Old_Italic]
+  66349,  // Range #228: [66349, 66351, Old_Italic]
+  66352,  // Range #229: [66352, 66378, Gothic]
+  66384,  // Range #230: [66384, 66426, Old_Permic]
+  66432,  // Range #231: [66432, 66463, Ugaritic]
+  66464,  // Range #232: [66464, 66517, Old_Persian]
+  66560,  // Range #233: [66560, 66639, Deseret]
+  66640,  // Range #234: [66640, 66687, Shavian]
+  66688,  // Range #235: [66688, 66729, Osmanya]
+  66736,  // Range #236: [66736, 66811, Osage]
+  66816,  // Range #237: [66816, 66855, Elbasan]
+  66864,  // Range #238: [66864, 66915, Caucasian_Albanian]
+  66927,  // Range #239: [66927, 66927, Caucasian_Albanian]
+  66928,  // Range #240: [66928, 67004, Vithkuqi]
+  67072,  // Range #241: [67072, 67382, Linear_A]
+  67392,  // Range #242: [67392, 67413, Linear_A]
+  67424,  // Range #243: [67424, 67431, Linear_A]
+  67456,  // Range #244: [67456, 67514, Latin]
+  67584,  // Range #245: [67584, 67647, Cypriot]
+  67648,  // Range #246: [67648, 67679, Imperial_Aramaic]
+  67680,  // Range #247: [67680, 67711, Palmyrene]
+  67712,  // Range #248: [67712, 67742, Nabataean]
+  67751,  // Range #249: [67751, 67759, Nabataean]
+  67808,  // Range #250: [67808, 67829, Hatran]
+  67835,  // Range #251: [67835, 67839, Hatran]
+  67840,  // Range #252: [67840, 67871, Phoenician]
+  67872,  // Range #253: [67872, 67897, Lydian]
+  67903,  // Range #254: [67903, 67903, Lydian]
+  67968,  // Range #255: [67968, 67999, Meroitic_Hieroglyphs]
+  68000,  // Range #256: [68000, 68095, Meroitic_Cursive]
+  68096,  // Range #257: [68096, 68102, Kharoshthi]
+  68108,  // Range #258: [68108, 68168, Kharoshthi]
+  68176,  // Range #259: [68176, 68184, Kharoshthi]
+  68192,  // Range #260: [68192, 68223, Old_South_Arabian]
+  68224,  // Range #261: [68224, 68255, Old_North_Arabian]
+  68288,  // Range #262: [68288, 68342, Manichaean]
+  68352,  // Range #263: [68352, 68415, Avestan]
+  68416,  // Range #264: [68416, 68447, Inscriptional_Parthian]
+  68448,  // Range #265: [68448, 68466, Inscriptional_Pahlavi]
+  68472,  // Range #266: [68472, 68479, Inscriptional_Pahlavi]
+  68480,  // Range #267: [68480, 68497, Psalter_Pahlavi]
+  68505,  // Range #268: [68505, 68508, Psalter_Pahlavi]
+  68521,  // Range #269: [68521, 68527, Psalter_Pahlavi]
+  68608,  // Range #270: [68608, 68680, Old_Turkic]
+  68736,  // Range #271: [68736, 68786, Old_Hungarian]
+  68800,  // Range #272: [68800, 68850, Old_Hungarian]
+  68858,  // Range #273: [68858, 68863, Old_Hungarian]
+  68864,  // Range #274: [68864, 68903, Hanifi_Rohingya]
+  68912,  // Range #275: [68912, 68921, Hanifi_Rohingya]
+  69216,  // Range #276: [69216, 69246, Arabic]
+  69248,  // Range #277: [69248, 69297, Yezidi]
+  69376,  // Range #278: [69376, 69415, Old_Sogdian]
+  69424,  // Range #279: [69424, 69465, Sogdian]
+  69488,  // Range #280: [69488, 69513, Old_Uyghur]
+  69552,  // Range #281: [69552, 69579, Chorasmian]
+  69600,  // Range #282: [69600, 69622, Elymaic]
+  69632,  // Range #283: [69632, 69749, Brahmi]
+  69759,  // Range #284: [69759, 69759, Brahmi]
+  69760,  // Range #285: [69760, 69826, Kaithi]
+  69837,  // Range #286: [69837, 69837, Kaithi]
+  69840,  // Range #287: [69840, 69864, Sora_Sompeng]
+  69872,  // Range #288: [69872, 69881, Sora_Sompeng]
+  69888,  // Range #289: [69888, 69959, Chakma]
+  69968,  // Range #290: [69968, 70006, Mahajani]
+  70016,  // Range #291: [70016, 70111, Sharada]
+  70113,  // Range #292: [70113, 70132, Sinhala]
+  70144,  // Range #293: [70144, 70206, Khojki]
+  70272,  // Range #294: [70272, 70313, Multani]
+  70320,  // Range #295: [70320, 70378, Khudawadi]
+  70384,  // Range #296: [70384, 70393, Khudawadi]
+  70400,  // Range #297: [70400, 70457, Grantha]
+  70460,  // Range #298: [70460, 70480, Grantha]
+  70487,  // Range #299: [70487, 70487, Grantha]
+  70493,  // Range #300: [70493, 70516, Grantha]
+  70656,  // Range #301: [70656, 70753, Newa]
+  70784,  // Range #302: [70784, 70855, Tirhuta]
+  70864,  // Range #303: [70864, 70873, Tirhuta]
+  71040,  // Range #304: [71040, 71133, Siddham]
+  71168,  // Range #305: [71168, 71236, Modi]
+  71248,  // Range #306: [71248, 71257, Modi]
+  71264,  // Range #307: [71264, 71276, Mongolian]
+  71296,  // Range #308: [71296, 71353, Takri]
+  71360,  // Range #309: [71360, 71369, Takri]
+  71424,  // Range #310: [71424, 71494, Ahom]
+  71680,  // Range #311: [71680, 71739, Dogra]
+  71840,  // Range #312: [71840, 71922, Warang_Citi]
+  71935,  // Range #313: [71935, 71935, Warang_Citi]
+  71936,  // Range #314: [71936, 72006, Dives_Akuru]
+  72016,  // Range #315: [72016, 72025, Dives_Akuru]
+  72096,  // Range #316: [72096, 72164, Nandinagari]
+  72192,  // Range #317: [72192, 72263, Zanabazar_Square]
+  72272,  // Range #318: [72272, 72354, Soyombo]
+  72368,  // Range #319: [72368, 72383, Canadian_Aboriginal]
+  72384,  // Range #320: [72384, 72440, Pau_Cin_Hau]
+  72704,  // Range #321: [72704, 72773, Bhaiksuki]
+  72784,  // Range #322: [72784, 72812, Bhaiksuki]
+  72816,  // Range #323: [72816, 72886, Marchen]
+  72960,  // Range #324: [72960, 73031, Masaram_Gondi]
+  73040,  // Range #325: [73040, 73049, Masaram_Gondi]
+  73056,  // Range #326: [73056, 73112, Gunjala_Gondi]
+  73120,  // Range #327: [73120, 73129, Gunjala_Gondi]
+  73440,  // Range #328: [73440, 73464, Makasar]
+  73648,  // Range #329: [73648, 73648, Lisu]
+  73664,  // Range #330: [73664, 73713, Tamil]
+  73727,  // Range #331: [73727, 73727, Tamil]
+  73728,  // Range #332: [73728, 74649, Cuneiform]
+  74752,  // Range #333: [74752, 74868, Cuneiform]
+  74880,  // Range #334: [74880, 75075, Cuneiform]
+  77712,  // Range #335: [77712, 77810, Cypro_Minoan]
+  77824,  // Range #336: [77824, 78904, Egyptian_Hieroglyphs]
+  82944,  // Range #337: [82944, 83526, Anatolian_Hieroglyphs]
+  92160,  // Range #338: [92160, 92728, Bamum]
+  92736,  // Range #339: [92736, 92783, Mro]
+  92784,  // Range #340: [92784, 92873, Tangsa]
+  92880,  // Range #341: [92880, 92917, Bassa_Vah]
+  92928,  // Range #342: [92928, 92997, Pahawh_Hmong]
+  93008,  // Range #343: [93008, 93047, Pahawh_Hmong]
+  93053,  // Range #344: [93053, 93071, Pahawh_Hmong]
+  93760,  // Range #345: [93760, 93850, Medefaidrin]
+  93952,  // Range #346: [93952, 94087, Miao]
+  94095,  // Range #347: [94095, 94111, Miao]
+  94176,  // Range #348: [94176, 94176, Tangut]
+  94177,  // Range #349: [94177, 94177, Nushu]
+  94178,  // Range #350: [94178, 94179, Han]
+  94180,  // Range #351: [94180, 94180, Khitan_Small_Script]
+  94192,  // Range #352: [94192, 94193, Han]
+  94208,  // Range #353: [94208, 100343, Tangut]
+  100352,  // Range #354: [100352, 101119, Tangut]
+  101120,  // Range #355: [101120, 101589, Khitan_Small_Script]
+  101632,  // Range #356: [101632, 101640, Tangut]
+  110576,  // Range #357: [110576, 110592, Katakana]
+  110593,  // Range #358: [110593, 110879, Hiragana]
+  110880,  // Range #359: [110880, 110882, Katakana]
+  110928,  // Range #360: [110928, 110930, Hiragana]
+  110948,  // Range #361: [110948, 110951, Katakana]
+  110960,  // Range #362: [110960, 111355, Nushu]
+  113664,  // Range #363: [113664, 113770, Duployan]
+  113776,  // Range #364: [113776, 113800, Duployan]
+  113808,  // Range #365: [113808, 113823, Duployan]
+  119296,  // Range #366: [119296, 119365, Greek]
+  120832,  // Range #367: [120832, 121483, SignWriting]
+  121499,  // Range #368: [121499, 121519, SignWriting]
+  122624,  // Range #369: [122624, 122654, Latin]
+  122880,  // Range #370: [122880, 122922, Glagolitic]
+  123136,  // Range #371: [123136, 123215, Nyiakeng_Puachue_Hmong]
+  123536,  // Range #372: [123536, 123566, Toto]
+  123584,  // Range #373: [123584, 123641, Wancho]
+  123647,  // Range #374: [123647, 123647, Wancho]
+  124896,  // Range #375: [124896, 124926, Ethiopic]
+  124928,  // Range #376: [124928, 125142, Mende_Kikakui]
+  125184,  // Range #377: [125184, 125279, Adlam]
+  126464,  // Range #378: [126464, 126523, Arabic]
+  126530,  // Range #379: [126530, 126619, Arabic]
+  126625,  // Range #380: [126625, 126651, Arabic]
+  126704,  // Range #381: [126704, 126705, Arabic]
+  127488,  // Range #382: [127488, 127488, Hiragana]
+  131072,  // Range #383: [131072, 173791, Han]
+  173824,  // Range #384: [173824, 177976, Han]
+  177984,  // Range #385: [177984, 183969, Han]
+  183984,  // Range #386: [183984, 191456, Han]
+  194560,  // Range #387: [194560, 195101, Han]
+  196608,  // Range #388: [196608, 201546, Han]
 };
 
 const uint16 kRangeSizeMinusOne[] = {
@@ -446,8 +459,8 @@
   62,  // Range #34: [2048, 2110, Samaritan]
   30,  // Range #35: [2112, 2142, Mandaic]
   10,  // Range #36: [2144, 2154, Syriac]
-  39,  // Range #37: [2208, 2247, Arabic]
-  14,  // Range #38: [2259, 2273, Arabic]
+  33,  // Range #37: [2160, 2193, Arabic]
+  73,  // Range #38: [2200, 2273, Arabic]
   28,  // Range #39: [2275, 2303, Arabic]
   80,  // Range #40: [2304, 2384, Devanagari]
   14,  // Range #41: [2389, 2403, Devanagari]
@@ -466,32 +479,32 @@
   0,  // Range #54: [3031, 3031, Tamil]
   20,  // Range #55: [3046, 3066, Tamil]
   77,  // Range #56: [3072, 3149, Telugu]
-  5,  // Range #57: [3157, 3162, Telugu]
-  15,  // Range #58: [3168, 3183, Telugu]
-  8,  // Range #59: [3191, 3199, Telugu]
-  77,  // Range #60: [3200, 3277, Kannada]
-  1,  // Range #61: [3285, 3286, Kannada]
-  20,  // Range #62: [3294, 3314, Kannada]
-  127,  // Range #63: [3328, 3455, Malayalam]
-  94,  // Range #64: [3457, 3551, Sinhala]
-  14,  // Range #65: [3558, 3572, Sinhala]
-  57,  // Range #66: [3585, 3642, Thai]
-  27,  // Range #67: [3648, 3675, Thai]
-  94,  // Range #68: [3713, 3807, Lao]
-  212,  // Range #69: [3840, 4052, Tibetan]
-  1,  // Range #70: [4057, 4058, Tibetan]
-  159,  // Range #71: [4096, 4255, Myanmar]
-  39,  // Range #72: [4256, 4295, Georgian]
-  45,  // Range #73: [4301, 4346, Georgian]
-  3,  // Range #74: [4348, 4351, Georgian]
-  255,  // Range #75: [4352, 4607, Hangul]
-  409,  // Range #76: [4608, 5017, Ethiopic]
-  93,  // Range #77: [5024, 5117, Cherokee]
-  639,  // Range #78: [5120, 5759, Canadian_Aboriginal]
-  28,  // Range #79: [5760, 5788, Ogham]
-  74,  // Range #80: [5792, 5866, Runic]
-  10,  // Range #81: [5870, 5880, Runic]
-  20,  // Range #82: [5888, 5908, Tagalog]
+  26,  // Range #57: [3157, 3183, Telugu]
+  8,  // Range #58: [3191, 3199, Telugu]
+  77,  // Range #59: [3200, 3277, Kannada]
+  1,  // Range #60: [3285, 3286, Kannada]
+  21,  // Range #61: [3293, 3314, Kannada]
+  127,  // Range #62: [3328, 3455, Malayalam]
+  94,  // Range #63: [3457, 3551, Sinhala]
+  14,  // Range #64: [3558, 3572, Sinhala]
+  57,  // Range #65: [3585, 3642, Thai]
+  27,  // Range #66: [3648, 3675, Thai]
+  94,  // Range #67: [3713, 3807, Lao]
+  212,  // Range #68: [3840, 4052, Tibetan]
+  1,  // Range #69: [4057, 4058, Tibetan]
+  159,  // Range #70: [4096, 4255, Myanmar]
+  39,  // Range #71: [4256, 4295, Georgian]
+  45,  // Range #72: [4301, 4346, Georgian]
+  3,  // Range #73: [4348, 4351, Georgian]
+  255,  // Range #74: [4352, 4607, Hangul]
+  409,  // Range #75: [4608, 5017, Ethiopic]
+  93,  // Range #76: [5024, 5117, Cherokee]
+  639,  // Range #77: [5120, 5759, Canadian_Aboriginal]
+  28,  // Range #78: [5760, 5788, Ogham]
+  74,  // Range #79: [5792, 5866, Runic]
+  10,  // Range #80: [5870, 5880, Runic]
+  21,  // Range #81: [5888, 5909, Tagalog]
+  0,  // Range #82: [5919, 5919, Tagalog]
   20,  // Range #83: [5920, 5940, Hanunoo]
   19,  // Range #84: [5952, 5971, Buhid]
   19,  // Range #85: [5984, 6003, Tagbanwa]
@@ -512,7 +525,7 @@
   105,  // Range #100: [6688, 6793, Tai_Tham]
   9,  // Range #101: [6800, 6809, Tai_Tham]
   13,  // Range #102: [6816, 6829, Tai_Tham]
-  124,  // Range #103: [6912, 7036, Balinese]
+  126,  // Range #103: [6912, 7038, Balinese]
   63,  // Range #104: [7040, 7103, Sundanese]
   51,  // Range #105: [7104, 7155, Batak]
   3,  // Range #106: [7164, 7167, Batak]
@@ -543,7 +556,7 @@
   0,  // Range #131: [8526, 8526, Latin]
   40,  // Range #132: [8544, 8584, Latin]
   255,  // Range #133: [10240, 10495, Braille]
-  94,  // Range #134: [11264, 11358, Glagolitic]
+  95,  // Range #134: [11264, 11359, Glagolitic]
   31,  // Range #135: [11360, 11391, Latin]
   115,  // Range #136: [11392, 11507, Coptic]
   6,  // Range #137: [11513, 11519, Coptic]
@@ -575,7 +588,7 @@
   46,  // Range #163: [13008, 13054, Katakana]
   87,  // Range #164: [13056, 13143, Katakana]
   6591,  // Range #165: [13312, 19903, Han]
-  20988,  // Range #166: [19968, 40956, Han]
+  20991,  // Range #166: [19968, 40959, Han]
   1222,  // Range #167: [40960, 42182, Yi]
   47,  // Range #168: [42192, 42239, Lisu]
   299,  // Range #169: [42240, 42539, Vai]
@@ -583,208 +596,221 @@
   87,  // Range #171: [42656, 42743, Bamum]
   101,  // Range #172: [42786, 42887, Latin]
   63,  // Range #173: [42891, 42954, Latin]
-  10,  // Range #174: [42997, 43007, Latin]
-  44,  // Range #175: [43008, 43052, Syloti_Nagri]
-  55,  // Range #176: [43072, 43127, Phags_Pa]
-  69,  // Range #177: [43136, 43205, Saurashtra]
-  11,  // Range #178: [43214, 43225, Saurashtra]
-  31,  // Range #179: [43232, 43263, Devanagari]
-  45,  // Range #180: [43264, 43309, Kayah_Li]
-  0,  // Range #181: [43311, 43311, Kayah_Li]
-  35,  // Range #182: [43312, 43347, Rejang]
-  0,  // Range #183: [43359, 43359, Rejang]
-  28,  // Range #184: [43360, 43388, Hangul]
-  77,  // Range #185: [43392, 43469, Javanese]
-  15,  // Range #186: [43472, 43487, Javanese]
-  30,  // Range #187: [43488, 43518, Myanmar]
-  54,  // Range #188: [43520, 43574, Cham]
-  31,  // Range #189: [43584, 43615, Cham]
-  31,  // Range #190: [43616, 43647, Myanmar]
-  66,  // Range #191: [43648, 43714, Tai_Viet]
-  4,  // Range #192: [43739, 43743, Tai_Viet]
-  22,  // Range #193: [43744, 43766, Meetei_Mayek]
-  21,  // Range #194: [43777, 43798, Ethiopic]
-  14,  // Range #195: [43808, 43822, Ethiopic]
-  42,  // Range #196: [43824, 43866, Latin]
-  8,  // Range #197: [43868, 43876, Latin]
-  0,  // Range #198: [43877, 43877, Greek]
-  3,  // Range #199: [43878, 43881, Latin]
-  79,  // Range #200: [43888, 43967, Cherokee]
-  57,  // Range #201: [43968, 44025, Meetei_Mayek]
-  11171,  // Range #202: [44032, 55203, Hangul]
-  75,  // Range #203: [55216, 55291, Hangul]
-  473,  // Range #204: [63744, 64217, Han]
-  6,  // Range #205: [64256, 64262, Latin]
-  4,  // Range #206: [64275, 64279, Armenian]
-  50,  // Range #207: [64285, 64335, Hebrew]
-  113,  // Range #208: [64336, 64449, Arabic]
-  362,  // Range #209: [64467, 64829, Arabic]
-  119,  // Range #210: [64848, 64967, Arabic]
-  13,  // Range #211: [65008, 65021, Arabic]
-  1,  // Range #212: [65070, 65071, Cyrillic]
-  140,  // Range #213: [65136, 65276, Arabic]
-  25,  // Range #214: [65313, 65338, Latin]
-  25,  // Range #215: [65345, 65370, Latin]
-  9,  // Range #216: [65382, 65391, Katakana]
-  44,  // Range #217: [65393, 65437, Katakana]
-  60,  // Range #218: [65440, 65500, Hangul]
-  93,  // Range #219: [65536, 65629, Linear_B]
-  122,  // Range #220: [65664, 65786, Linear_B]
-  78,  // Range #221: [65856, 65934, Greek]
-  0,  // Range #222: [65952, 65952, Greek]
-  28,  // Range #223: [66176, 66204, Lycian]
-  48,  // Range #224: [66208, 66256, Carian]
-  35,  // Range #225: [66304, 66339, Old_Italic]
-  2,  // Range #226: [66349, 66351, Old_Italic]
-  26,  // Range #227: [66352, 66378, Gothic]
-  42,  // Range #228: [66384, 66426, Old_Permic]
-  31,  // Range #229: [66432, 66463, Ugaritic]
-  53,  // Range #230: [66464, 66517, Old_Persian]
-  79,  // Range #231: [66560, 66639, Deseret]
-  47,  // Range #232: [66640, 66687, Shavian]
-  41,  // Range #233: [66688, 66729, Osmanya]
-  75,  // Range #234: [66736, 66811, Osage]
-  39,  // Range #235: [66816, 66855, Elbasan]
-  51,  // Range #236: [66864, 66915, Caucasian_Albanian]
-  0,  // Range #237: [66927, 66927, Caucasian_Albanian]
-  310,  // Range #238: [67072, 67382, Linear_A]
-  21,  // Range #239: [67392, 67413, Linear_A]
-  7,  // Range #240: [67424, 67431, Linear_A]
-  63,  // Range #241: [67584, 67647, Cypriot]
-  31,  // Range #242: [67648, 67679, Imperial_Aramaic]
-  31,  // Range #243: [67680, 67711, Palmyrene]
-  30,  // Range #244: [67712, 67742, Nabataean]
-  8,  // Range #245: [67751, 67759, Nabataean]
-  21,  // Range #246: [67808, 67829, Hatran]
-  4,  // Range #247: [67835, 67839, Hatran]
-  31,  // Range #248: [67840, 67871, Phoenician]
-  25,  // Range #249: [67872, 67897, Lydian]
-  0,  // Range #250: [67903, 67903, Lydian]
-  31,  // Range #251: [67968, 67999, Meroitic_Hieroglyphs]
-  95,  // Range #252: [68000, 68095, Meroitic_Cursive]
-  6,  // Range #253: [68096, 68102, Kharoshthi]
-  60,  // Range #254: [68108, 68168, Kharoshthi]
-  8,  // Range #255: [68176, 68184, Kharoshthi]
-  31,  // Range #256: [68192, 68223, Old_South_Arabian]
-  31,  // Range #257: [68224, 68255, Old_North_Arabian]
-  54,  // Range #258: [68288, 68342, Manichaean]
-  63,  // Range #259: [68352, 68415, Avestan]
-  31,  // Range #260: [68416, 68447, Inscriptional_Parthian]
-  18,  // Range #261: [68448, 68466, Inscriptional_Pahlavi]
-  7,  // Range #262: [68472, 68479, Inscriptional_Pahlavi]
-  17,  // Range #263: [68480, 68497, Psalter_Pahlavi]
-  3,  // Range #264: [68505, 68508, Psalter_Pahlavi]
-  6,  // Range #265: [68521, 68527, Psalter_Pahlavi]
-  72,  // Range #266: [68608, 68680, Old_Turkic]
-  50,  // Range #267: [68736, 68786, Old_Hungarian]
-  50,  // Range #268: [68800, 68850, Old_Hungarian]
-  5,  // Range #269: [68858, 68863, Old_Hungarian]
-  39,  // Range #270: [68864, 68903, Hanifi_Rohingya]
-  9,  // Range #271: [68912, 68921, Hanifi_Rohingya]
-  30,  // Range #272: [69216, 69246, Arabic]
-  49,  // Range #273: [69248, 69297, Yezidi]
-  39,  // Range #274: [69376, 69415, Old_Sogdian]
-  41,  // Range #275: [69424, 69465, Sogdian]
-  27,  // Range #276: [69552, 69579, Chorasmian]
-  22,  // Range #277: [69600, 69622, Elymaic]
-  111,  // Range #278: [69632, 69743, Brahmi]
-  0,  // Range #279: [69759, 69759, Brahmi]
-  65,  // Range #280: [69760, 69825, Kaithi]
-  0,  // Range #281: [69837, 69837, Kaithi]
-  24,  // Range #282: [69840, 69864, Sora_Sompeng]
-  9,  // Range #283: [69872, 69881, Sora_Sompeng]
-  71,  // Range #284: [69888, 69959, Chakma]
-  38,  // Range #285: [69968, 70006, Mahajani]
-  95,  // Range #286: [70016, 70111, Sharada]
-  19,  // Range #287: [70113, 70132, Sinhala]
-  62,  // Range #288: [70144, 70206, Khojki]
-  41,  // Range #289: [70272, 70313, Multani]
-  58,  // Range #290: [70320, 70378, Khudawadi]
-  9,  // Range #291: [70384, 70393, Khudawadi]
-  57,  // Range #292: [70400, 70457, Grantha]
-  20,  // Range #293: [70460, 70480, Grantha]
-  0,  // Range #294: [70487, 70487, Grantha]
-  23,  // Range #295: [70493, 70516, Grantha]
-  97,  // Range #296: [70656, 70753, Newa]
-  71,  // Range #297: [70784, 70855, Tirhuta]
-  9,  // Range #298: [70864, 70873, Tirhuta]
-  93,  // Range #299: [71040, 71133, Siddham]
-  68,  // Range #300: [71168, 71236, Modi]
-  9,  // Range #301: [71248, 71257, Modi]
-  12,  // Range #302: [71264, 71276, Mongolian]
-  56,  // Range #303: [71296, 71352, Takri]
-  9,  // Range #304: [71360, 71369, Takri]
-  63,  // Range #305: [71424, 71487, Ahom]
-  59,  // Range #306: [71680, 71739, Dogra]
-  82,  // Range #307: [71840, 71922, Warang_Citi]
-  0,  // Range #308: [71935, 71935, Warang_Citi]
-  70,  // Range #309: [71936, 72006, Dives_Akuru]
-  9,  // Range #310: [72016, 72025, Dives_Akuru]
-  68,  // Range #311: [72096, 72164, Nandinagari]
-  71,  // Range #312: [72192, 72263, Zanabazar_Square]
-  82,  // Range #313: [72272, 72354, Soyombo]
-  56,  // Range #314: [72384, 72440, Pau_Cin_Hau]
-  69,  // Range #315: [72704, 72773, Bhaiksuki]
-  28,  // Range #316: [72784, 72812, Bhaiksuki]
-  70,  // Range #317: [72816, 72886, Marchen]
-  71,  // Range #318: [72960, 73031, Masaram_Gondi]
-  9,  // Range #319: [73040, 73049, Masaram_Gondi]
-  56,  // Range #320: [73056, 73112, Gunjala_Gondi]
-  9,  // Range #321: [73120, 73129, Gunjala_Gondi]
-  24,  // Range #322: [73440, 73464, Makasar]
-  0,  // Range #323: [73648, 73648, Lisu]
-  49,  // Range #324: [73664, 73713, Tamil]
-  0,  // Range #325: [73727, 73727, Tamil]
-  921,  // Range #326: [73728, 74649, Cuneiform]
-  116,  // Range #327: [74752, 74868, Cuneiform]
-  195,  // Range #328: [74880, 75075, Cuneiform]
-  1080,  // Range #329: [77824, 78904, Egyptian_Hieroglyphs]
-  582,  // Range #330: [82944, 83526, Anatolian_Hieroglyphs]
-  568,  // Range #331: [92160, 92728, Bamum]
-  47,  // Range #332: [92736, 92783, Mro]
-  37,  // Range #333: [92880, 92917, Bassa_Vah]
-  69,  // Range #334: [92928, 92997, Pahawh_Hmong]
-  39,  // Range #335: [93008, 93047, Pahawh_Hmong]
-  18,  // Range #336: [93053, 93071, Pahawh_Hmong]
-  90,  // Range #337: [93760, 93850, Medefaidrin]
-  135,  // Range #338: [93952, 94087, Miao]
-  16,  // Range #339: [94095, 94111, Miao]
-  0,  // Range #340: [94176, 94176, Tangut]
-  0,  // Range #341: [94177, 94177, Nushu]
-  0,  // Range #342: [94180, 94180, Khitan_Small_Script]
-  1,  // Range #343: [94192, 94193, Han]
-  6135,  // Range #344: [94208, 100343, Tangut]
-  767,  // Range #345: [100352, 101119, Tangut]
-  469,  // Range #346: [101120, 101589, Khitan_Small_Script]
-  8,  // Range #347: [101632, 101640, Tangut]
-  0,  // Range #348: [110592, 110592, Katakana]
-  285,  // Range #349: [110593, 110878, Hiragana]
-  2,  // Range #350: [110928, 110930, Hiragana]
-  3,  // Range #351: [110948, 110951, Katakana]
-  395,  // Range #352: [110960, 111355, Nushu]
-  106,  // Range #353: [113664, 113770, Duployan]
-  24,  // Range #354: [113776, 113800, Duployan]
-  15,  // Range #355: [113808, 113823, Duployan]
-  69,  // Range #356: [119296, 119365, Greek]
-  651,  // Range #357: [120832, 121483, SignWriting]
-  20,  // Range #358: [121499, 121519, SignWriting]
-  42,  // Range #359: [122880, 122922, Glagolitic]
-  79,  // Range #360: [123136, 123215, Nyiakeng_Puachue_Hmong]
-  57,  // Range #361: [123584, 123641, Wancho]
-  0,  // Range #362: [123647, 123647, Wancho]
-  214,  // Range #363: [124928, 125142, Mende_Kikakui]
-  95,  // Range #364: [125184, 125279, Adlam]
-  59,  // Range #365: [126464, 126523, Arabic]
-  89,  // Range #366: [126530, 126619, Arabic]
-  26,  // Range #367: [126625, 126651, Arabic]
-  1,  // Range #368: [126704, 126705, Arabic]
-  0,  // Range #369: [127488, 127488, Hiragana]
-  42717,  // Range #370: [131072, 173789, Han]
-  4148,  // Range #371: [173824, 177972, Han]
-  5985,  // Range #372: [177984, 183969, Han]
-  7472,  // Range #373: [183984, 191456, Han]
-  541,  // Range #374: [194560, 195101, Han]
-  4938,  // Range #375: [196608, 201546, Han]
+  9,  // Range #174: [42960, 42969, Latin]
+  13,  // Range #175: [42994, 43007, Latin]
+  44,  // Range #176: [43008, 43052, Syloti_Nagri]
+  55,  // Range #177: [43072, 43127, Phags_Pa]
+  69,  // Range #178: [43136, 43205, Saurashtra]
+  11,  // Range #179: [43214, 43225, Saurashtra]
+  31,  // Range #180: [43232, 43263, Devanagari]
+  45,  // Range #181: [43264, 43309, Kayah_Li]
+  0,  // Range #182: [43311, 43311, Kayah_Li]
+  35,  // Range #183: [43312, 43347, Rejang]
+  0,  // Range #184: [43359, 43359, Rejang]
+  28,  // Range #185: [43360, 43388, Hangul]
+  77,  // Range #186: [43392, 43469, Javanese]
+  15,  // Range #187: [43472, 43487, Javanese]
+  30,  // Range #188: [43488, 43518, Myanmar]
+  54,  // Range #189: [43520, 43574, Cham]
+  31,  // Range #190: [43584, 43615, Cham]
+  31,  // Range #191: [43616, 43647, Myanmar]
+  66,  // Range #192: [43648, 43714, Tai_Viet]
+  4,  // Range #193: [43739, 43743, Tai_Viet]
+  22,  // Range #194: [43744, 43766, Meetei_Mayek]
+  21,  // Range #195: [43777, 43798, Ethiopic]
+  14,  // Range #196: [43808, 43822, Ethiopic]
+  42,  // Range #197: [43824, 43866, Latin]
+  8,  // Range #198: [43868, 43876, Latin]
+  0,  // Range #199: [43877, 43877, Greek]
+  3,  // Range #200: [43878, 43881, Latin]
+  79,  // Range #201: [43888, 43967, Cherokee]
+  57,  // Range #202: [43968, 44025, Meetei_Mayek]
+  11171,  // Range #203: [44032, 55203, Hangul]
+  75,  // Range #204: [55216, 55291, Hangul]
+  473,  // Range #205: [63744, 64217, Han]
+  6,  // Range #206: [64256, 64262, Latin]
+  4,  // Range #207: [64275, 64279, Armenian]
+  50,  // Range #208: [64285, 64335, Hebrew]
+  114,  // Range #209: [64336, 64450, Arabic]
+  362,  // Range #210: [64467, 64829, Arabic]
+  135,  // Range #211: [64832, 64967, Arabic]
+  0,  // Range #212: [64975, 64975, Arabic]
+  15,  // Range #213: [65008, 65023, Arabic]
+  1,  // Range #214: [65070, 65071, Cyrillic]
+  140,  // Range #215: [65136, 65276, Arabic]
+  25,  // Range #216: [65313, 65338, Latin]
+  25,  // Range #217: [65345, 65370, Latin]
+  9,  // Range #218: [65382, 65391, Katakana]
+  44,  // Range #219: [65393, 65437, Katakana]
+  60,  // Range #220: [65440, 65500, Hangul]
+  93,  // Range #221: [65536, 65629, Linear_B]
+  122,  // Range #222: [65664, 65786, Linear_B]
+  78,  // Range #223: [65856, 65934, Greek]
+  0,  // Range #224: [65952, 65952, Greek]
+  28,  // Range #225: [66176, 66204, Lycian]
+  48,  // Range #226: [66208, 66256, Carian]
+  35,  // Range #227: [66304, 66339, Old_Italic]
+  2,  // Range #228: [66349, 66351, Old_Italic]
+  26,  // Range #229: [66352, 66378, Gothic]
+  42,  // Range #230: [66384, 66426, Old_Permic]
+  31,  // Range #231: [66432, 66463, Ugaritic]
+  53,  // Range #232: [66464, 66517, Old_Persian]
+  79,  // Range #233: [66560, 66639, Deseret]
+  47,  // Range #234: [66640, 66687, Shavian]
+  41,  // Range #235: [66688, 66729, Osmanya]
+  75,  // Range #236: [66736, 66811, Osage]
+  39,  // Range #237: [66816, 66855, Elbasan]
+  51,  // Range #238: [66864, 66915, Caucasian_Albanian]
+  0,  // Range #239: [66927, 66927, Caucasian_Albanian]
+  76,  // Range #240: [66928, 67004, Vithkuqi]
+  310,  // Range #241: [67072, 67382, Linear_A]
+  21,  // Range #242: [67392, 67413, Linear_A]
+  7,  // Range #243: [67424, 67431, Linear_A]
+  58,  // Range #244: [67456, 67514, Latin]
+  63,  // Range #245: [67584, 67647, Cypriot]
+  31,  // Range #246: [67648, 67679, Imperial_Aramaic]
+  31,  // Range #247: [67680, 67711, Palmyrene]
+  30,  // Range #248: [67712, 67742, Nabataean]
+  8,  // Range #249: [67751, 67759, Nabataean]
+  21,  // Range #250: [67808, 67829, Hatran]
+  4,  // Range #251: [67835, 67839, Hatran]
+  31,  // Range #252: [67840, 67871, Phoenician]
+  25,  // Range #253: [67872, 67897, Lydian]
+  0,  // Range #254: [67903, 67903, Lydian]
+  31,  // Range #255: [67968, 67999, Meroitic_Hieroglyphs]
+  95,  // Range #256: [68000, 68095, Meroitic_Cursive]
+  6,  // Range #257: [68096, 68102, Kharoshthi]
+  60,  // Range #258: [68108, 68168, Kharoshthi]
+  8,  // Range #259: [68176, 68184, Kharoshthi]
+  31,  // Range #260: [68192, 68223, Old_South_Arabian]
+  31,  // Range #261: [68224, 68255, Old_North_Arabian]
+  54,  // Range #262: [68288, 68342, Manichaean]
+  63,  // Range #263: [68352, 68415, Avestan]
+  31,  // Range #264: [68416, 68447, Inscriptional_Parthian]
+  18,  // Range #265: [68448, 68466, Inscriptional_Pahlavi]
+  7,  // Range #266: [68472, 68479, Inscriptional_Pahlavi]
+  17,  // Range #267: [68480, 68497, Psalter_Pahlavi]
+  3,  // Range #268: [68505, 68508, Psalter_Pahlavi]
+  6,  // Range #269: [68521, 68527, Psalter_Pahlavi]
+  72,  // Range #270: [68608, 68680, Old_Turkic]
+  50,  // Range #271: [68736, 68786, Old_Hungarian]
+  50,  // Range #272: [68800, 68850, Old_Hungarian]
+  5,  // Range #273: [68858, 68863, Old_Hungarian]
+  39,  // Range #274: [68864, 68903, Hanifi_Rohingya]
+  9,  // Range #275: [68912, 68921, Hanifi_Rohingya]
+  30,  // Range #276: [69216, 69246, Arabic]
+  49,  // Range #277: [69248, 69297, Yezidi]
+  39,  // Range #278: [69376, 69415, Old_Sogdian]
+  41,  // Range #279: [69424, 69465, Sogdian]
+  25,  // Range #280: [69488, 69513, Old_Uyghur]
+  27,  // Range #281: [69552, 69579, Chorasmian]
+  22,  // Range #282: [69600, 69622, Elymaic]
+  117,  // Range #283: [69632, 69749, Brahmi]
+  0,  // Range #284: [69759, 69759, Brahmi]
+  66,  // Range #285: [69760, 69826, Kaithi]
+  0,  // Range #286: [69837, 69837, Kaithi]
+  24,  // Range #287: [69840, 69864, Sora_Sompeng]
+  9,  // Range #288: [69872, 69881, Sora_Sompeng]
+  71,  // Range #289: [69888, 69959, Chakma]
+  38,  // Range #290: [69968, 70006, Mahajani]
+  95,  // Range #291: [70016, 70111, Sharada]
+  19,  // Range #292: [70113, 70132, Sinhala]
+  62,  // Range #293: [70144, 70206, Khojki]
+  41,  // Range #294: [70272, 70313, Multani]
+  58,  // Range #295: [70320, 70378, Khudawadi]
+  9,  // Range #296: [70384, 70393, Khudawadi]
+  57,  // Range #297: [70400, 70457, Grantha]
+  20,  // Range #298: [70460, 70480, Grantha]
+  0,  // Range #299: [70487, 70487, Grantha]
+  23,  // Range #300: [70493, 70516, Grantha]
+  97,  // Range #301: [70656, 70753, Newa]
+  71,  // Range #302: [70784, 70855, Tirhuta]
+  9,  // Range #303: [70864, 70873, Tirhuta]
+  93,  // Range #304: [71040, 71133, Siddham]
+  68,  // Range #305: [71168, 71236, Modi]
+  9,  // Range #306: [71248, 71257, Modi]
+  12,  // Range #307: [71264, 71276, Mongolian]
+  57,  // Range #308: [71296, 71353, Takri]
+  9,  // Range #309: [71360, 71369, Takri]
+  70,  // Range #310: [71424, 71494, Ahom]
+  59,  // Range #311: [71680, 71739, Dogra]
+  82,  // Range #312: [71840, 71922, Warang_Citi]
+  0,  // Range #313: [71935, 71935, Warang_Citi]
+  70,  // Range #314: [71936, 72006, Dives_Akuru]
+  9,  // Range #315: [72016, 72025, Dives_Akuru]
+  68,  // Range #316: [72096, 72164, Nandinagari]
+  71,  // Range #317: [72192, 72263, Zanabazar_Square]
+  82,  // Range #318: [72272, 72354, Soyombo]
+  15,  // Range #319: [72368, 72383, Canadian_Aboriginal]
+  56,  // Range #320: [72384, 72440, Pau_Cin_Hau]
+  69,  // Range #321: [72704, 72773, Bhaiksuki]
+  28,  // Range #322: [72784, 72812, Bhaiksuki]
+  70,  // Range #323: [72816, 72886, Marchen]
+  71,  // Range #324: [72960, 73031, Masaram_Gondi]
+  9,  // Range #325: [73040, 73049, Masaram_Gondi]
+  56,  // Range #326: [73056, 73112, Gunjala_Gondi]
+  9,  // Range #327: [73120, 73129, Gunjala_Gondi]
+  24,  // Range #328: [73440, 73464, Makasar]
+  0,  // Range #329: [73648, 73648, Lisu]
+  49,  // Range #330: [73664, 73713, Tamil]
+  0,  // Range #331: [73727, 73727, Tamil]
+  921,  // Range #332: [73728, 74649, Cuneiform]
+  116,  // Range #333: [74752, 74868, Cuneiform]
+  195,  // Range #334: [74880, 75075, Cuneiform]
+  98,  // Range #335: [77712, 77810, Cypro_Minoan]
+  1080,  // Range #336: [77824, 78904, Egyptian_Hieroglyphs]
+  582,  // Range #337: [82944, 83526, Anatolian_Hieroglyphs]
+  568,  // Range #338: [92160, 92728, Bamum]
+  47,  // Range #339: [92736, 92783, Mro]
+  89,  // Range #340: [92784, 92873, Tangsa]
+  37,  // Range #341: [92880, 92917, Bassa_Vah]
+  69,  // Range #342: [92928, 92997, Pahawh_Hmong]
+  39,  // Range #343: [93008, 93047, Pahawh_Hmong]
+  18,  // Range #344: [93053, 93071, Pahawh_Hmong]
+  90,  // Range #345: [93760, 93850, Medefaidrin]
+  135,  // Range #346: [93952, 94087, Miao]
+  16,  // Range #347: [94095, 94111, Miao]
+  0,  // Range #348: [94176, 94176, Tangut]
+  0,  // Range #349: [94177, 94177, Nushu]
+  1,  // Range #350: [94178, 94179, Han]
+  0,  // Range #351: [94180, 94180, Khitan_Small_Script]
+  1,  // Range #352: [94192, 94193, Han]
+  6135,  // Range #353: [94208, 100343, Tangut]
+  767,  // Range #354: [100352, 101119, Tangut]
+  469,  // Range #355: [101120, 101589, Khitan_Small_Script]
+  8,  // Range #356: [101632, 101640, Tangut]
+  16,  // Range #357: [110576, 110592, Katakana]
+  286,  // Range #358: [110593, 110879, Hiragana]
+  2,  // Range #359: [110880, 110882, Katakana]
+  2,  // Range #360: [110928, 110930, Hiragana]
+  3,  // Range #361: [110948, 110951, Katakana]
+  395,  // Range #362: [110960, 111355, Nushu]
+  106,  // Range #363: [113664, 113770, Duployan]
+  24,  // Range #364: [113776, 113800, Duployan]
+  15,  // Range #365: [113808, 113823, Duployan]
+  69,  // Range #366: [119296, 119365, Greek]
+  651,  // Range #367: [120832, 121483, SignWriting]
+  20,  // Range #368: [121499, 121519, SignWriting]
+  30,  // Range #369: [122624, 122654, Latin]
+  42,  // Range #370: [122880, 122922, Glagolitic]
+  79,  // Range #371: [123136, 123215, Nyiakeng_Puachue_Hmong]
+  30,  // Range #372: [123536, 123566, Toto]
+  57,  // Range #373: [123584, 123641, Wancho]
+  0,  // Range #374: [123647, 123647, Wancho]
+  30,  // Range #375: [124896, 124926, Ethiopic]
+  214,  // Range #376: [124928, 125142, Mende_Kikakui]
+  95,  // Range #377: [125184, 125279, Adlam]
+  59,  // Range #378: [126464, 126523, Arabic]
+  89,  // Range #379: [126530, 126619, Arabic]
+  26,  // Range #380: [126625, 126651, Arabic]
+  1,  // Range #381: [126704, 126705, Arabic]
+  0,  // Range #382: [127488, 127488, Hiragana]
+  42719,  // Range #383: [131072, 173791, Han]
+  4152,  // Range #384: [173824, 177976, Han]
+  5985,  // Range #385: [177984, 183969, Han]
+  7472,  // Range #386: [183984, 191456, Han]
+  541,  // Range #387: [194560, 195101, Han]
+  4938,  // Range #388: [196608, 201546, Han]
 };
 
 const uint8 kRangeScript[] = {
@@ -825,8 +851,8 @@
   126,  // Range #34: [2048, 2110, Samaritan]
   84,  // Range #35: [2112, 2142, Mandaic]
   34,  // Range #36: [2144, 2154, Syriac]
-  2,  // Range #37: [2208, 2247, Arabic]
-  2,  // Range #38: [2259, 2273, Arabic]
+  2,  // Range #37: [2160, 2193, Arabic]
+  2,  // Range #38: [2200, 2273, Arabic]
   2,  // Range #39: [2275, 2303, Arabic]
   10,  // Range #40: [2304, 2384, Devanagari]
   10,  // Range #41: [2389, 2403, Devanagari]
@@ -845,32 +871,32 @@
   35,  // Range #54: [3031, 3031, Tamil]
   35,  // Range #55: [3046, 3066, Tamil]
   36,  // Range #56: [3072, 3149, Telugu]
-  36,  // Range #57: [3157, 3162, Telugu]
-  36,  // Range #58: [3168, 3183, Telugu]
-  36,  // Range #59: [3191, 3199, Telugu]
-  21,  // Range #60: [3200, 3277, Kannada]
-  21,  // Range #61: [3285, 3286, Kannada]
-  21,  // Range #62: [3294, 3314, Kannada]
-  26,  // Range #63: [3328, 3455, Malayalam]
-  33,  // Range #64: [3457, 3551, Sinhala]
-  33,  // Range #65: [3558, 3572, Sinhala]
-  38,  // Range #66: [3585, 3642, Thai]
-  38,  // Range #67: [3648, 3675, Thai]
-  24,  // Range #68: [3713, 3807, Lao]
-  39,  // Range #69: [3840, 4052, Tibetan]
-  39,  // Range #70: [4057, 4058, Tibetan]
-  28,  // Range #71: [4096, 4255, Myanmar]
-  12,  // Range #72: [4256, 4295, Georgian]
-  12,  // Range #73: [4301, 4346, Georgian]
-  12,  // Range #74: [4348, 4351, Georgian]
-  18,  // Range #75: [4352, 4607, Hangul]
-  11,  // Range #76: [4608, 5017, Ethiopic]
-  6,  // Range #77: [5024, 5117, Cherokee]
-  40,  // Range #78: [5120, 5759, Canadian_Aboriginal]
-  29,  // Range #79: [5760, 5788, Ogham]
-  32,  // Range #80: [5792, 5866, Runic]
-  32,  // Range #81: [5870, 5880, Runic]
-  42,  // Range #82: [5888, 5908, Tagalog]
+  36,  // Range #57: [3157, 3183, Telugu]
+  36,  // Range #58: [3191, 3199, Telugu]
+  21,  // Range #59: [3200, 3277, Kannada]
+  21,  // Range #60: [3285, 3286, Kannada]
+  21,  // Range #61: [3293, 3314, Kannada]
+  26,  // Range #62: [3328, 3455, Malayalam]
+  33,  // Range #63: [3457, 3551, Sinhala]
+  33,  // Range #64: [3558, 3572, Sinhala]
+  38,  // Range #65: [3585, 3642, Thai]
+  38,  // Range #66: [3648, 3675, Thai]
+  24,  // Range #67: [3713, 3807, Lao]
+  39,  // Range #68: [3840, 4052, Tibetan]
+  39,  // Range #69: [4057, 4058, Tibetan]
+  28,  // Range #70: [4096, 4255, Myanmar]
+  12,  // Range #71: [4256, 4295, Georgian]
+  12,  // Range #72: [4301, 4346, Georgian]
+  12,  // Range #73: [4348, 4351, Georgian]
+  18,  // Range #74: [4352, 4607, Hangul]
+  11,  // Range #75: [4608, 5017, Ethiopic]
+  6,  // Range #76: [5024, 5117, Cherokee]
+  40,  // Range #77: [5120, 5759, Canadian_Aboriginal]
+  29,  // Range #78: [5760, 5788, Ogham]
+  32,  // Range #79: [5792, 5866, Runic]
+  32,  // Range #80: [5870, 5880, Runic]
+  42,  // Range #81: [5888, 5909, Tagalog]
+  42,  // Range #82: [5919, 5919, Tagalog]
   43,  // Range #83: [5920, 5940, Hanunoo]
   44,  // Range #84: [5952, 5971, Buhid]
   45,  // Range #85: [5984, 6003, Tagbanwa]
@@ -891,7 +917,7 @@
   106,  // Range #100: [6688, 6793, Tai_Tham]
   106,  // Range #101: [6800, 6809, Tai_Tham]
   106,  // Range #102: [6816, 6829, Tai_Tham]
-  62,  // Range #103: [6912, 7036, Balinese]
+  62,  // Range #103: [6912, 7038, Balinese]
   113,  // Range #104: [7040, 7103, Sundanese]
   63,  // Range #105: [7104, 7155, Batak]
   63,  // Range #106: [7164, 7167, Batak]
@@ -922,7 +948,7 @@
   25,  // Range #131: [8526, 8526, Latin]
   25,  // Range #132: [8544, 8584, Latin]
   46,  // Range #133: [10240, 10495, Braille]
-  56,  // Range #134: [11264, 11358, Glagolitic]
+  56,  // Range #134: [11264, 11359, Glagolitic]
   25,  // Range #135: [11360, 11391, Latin]
   7,  // Range #136: [11392, 11507, Coptic]
   7,  // Range #137: [11513, 11519, Coptic]
@@ -954,7 +980,7 @@
   22,  // Range #163: [13008, 13054, Katakana]
   22,  // Range #164: [13056, 13143, Katakana]
   17,  // Range #165: [13312, 19903, Han]
-  17,  // Range #166: [19968, 40956, Han]
+  17,  // Range #166: [19968, 40959, Han]
   41,  // Range #167: [40960, 42182, Yi]
   131,  // Range #168: [42192, 42239, Lisu]
   99,  // Range #169: [42240, 42539, Vai]
@@ -962,211 +988,224 @@
   130,  // Range #171: [42656, 42743, Bamum]
   25,  // Range #172: [42786, 42887, Latin]
   25,  // Range #173: [42891, 42954, Latin]
-  25,  // Range #174: [42997, 43007, Latin]
-  58,  // Range #175: [43008, 43052, Syloti_Nagri]
-  90,  // Range #176: [43072, 43127, Phags_Pa]
-  111,  // Range #177: [43136, 43205, Saurashtra]
-  111,  // Range #178: [43214, 43225, Saurashtra]
-  10,  // Range #179: [43232, 43263, Devanagari]
-  79,  // Range #180: [43264, 43309, Kayah_Li]
-  79,  // Range #181: [43311, 43311, Kayah_Li]
-  110,  // Range #182: [43312, 43347, Rejang]
-  110,  // Range #183: [43359, 43359, Rejang]
-  18,  // Range #184: [43360, 43388, Hangul]
-  78,  // Range #185: [43392, 43469, Javanese]
-  78,  // Range #186: [43472, 43487, Javanese]
-  28,  // Range #187: [43488, 43518, Myanmar]
-  66,  // Range #188: [43520, 43574, Cham]
-  66,  // Range #189: [43584, 43615, Cham]
-  28,  // Range #190: [43616, 43647, Myanmar]
-  127,  // Range #191: [43648, 43714, Tai_Viet]
-  127,  // Range #192: [43739, 43743, Tai_Viet]
-  115,  // Range #193: [43744, 43766, Meetei_Mayek]
-  11,  // Range #194: [43777, 43798, Ethiopic]
-  11,  // Range #195: [43808, 43822, Ethiopic]
-  25,  // Range #196: [43824, 43866, Latin]
-  25,  // Range #197: [43868, 43876, Latin]
-  14,  // Range #198: [43877, 43877, Greek]
-  25,  // Range #199: [43878, 43881, Latin]
-  6,  // Range #200: [43888, 43967, Cherokee]
-  115,  // Range #201: [43968, 44025, Meetei_Mayek]
-  18,  // Range #202: [44032, 55203, Hangul]
-  18,  // Range #203: [55216, 55291, Hangul]
-  17,  // Range #204: [63744, 64217, Han]
-  25,  // Range #205: [64256, 64262, Latin]
-  3,  // Range #206: [64275, 64279, Armenian]
-  19,  // Range #207: [64285, 64335, Hebrew]
-  2,  // Range #208: [64336, 64449, Arabic]
-  2,  // Range #209: [64467, 64829, Arabic]
-  2,  // Range #210: [64848, 64967, Arabic]
-  2,  // Range #211: [65008, 65021, Arabic]
-  8,  // Range #212: [65070, 65071, Cyrillic]
-  2,  // Range #213: [65136, 65276, Arabic]
-  25,  // Range #214: [65313, 65338, Latin]
-  25,  // Range #215: [65345, 65370, Latin]
-  22,  // Range #216: [65382, 65391, Katakana]
-  22,  // Range #217: [65393, 65437, Katakana]
-  18,  // Range #218: [65440, 65500, Hangul]
-  49,  // Range #219: [65536, 65629, Linear_B]
-  49,  // Range #220: [65664, 65786, Linear_B]
-  14,  // Range #221: [65856, 65934, Greek]
-  14,  // Range #222: [65952, 65952, Greek]
-  107,  // Range #223: [66176, 66204, Lycian]
-  104,  // Range #224: [66208, 66256, Carian]
-  30,  // Range #225: [66304, 66339, Old_Italic]
-  30,  // Range #226: [66349, 66351, Old_Italic]
-  13,  // Range #227: [66352, 66378, Gothic]
-  89,  // Range #228: [66384, 66426, Old_Permic]
-  53,  // Range #229: [66432, 66463, Ugaritic]
-  61,  // Range #230: [66464, 66517, Old_Persian]
-  9,  // Range #231: [66560, 66639, Deseret]
-  51,  // Range #232: [66640, 66687, Shavian]
-  50,  // Range #233: [66688, 66729, Osmanya]
-  171,  // Range #234: [66736, 66811, Osage]
-  136,  // Range #235: [66816, 66855, Elbasan]
-  159,  // Range #236: [66864, 66915, Caucasian_Albanian]
-  159,  // Range #237: [66927, 66927, Caucasian_Albanian]
-  83,  // Range #238: [67072, 67382, Linear_A]
-  83,  // Range #239: [67392, 67413, Linear_A]
-  83,  // Range #240: [67424, 67431, Linear_A]
-  47,  // Range #241: [67584, 67647, Cypriot]
-  116,  // Range #242: [67648, 67679, Imperial_Aramaic]
-  144,  // Range #243: [67680, 67711, Palmyrene]
-  143,  // Range #244: [67712, 67742, Nabataean]
-  143,  // Range #245: [67751, 67759, Nabataean]
-  162,  // Range #246: [67808, 67829, Hatran]
-  162,  // Range #247: [67835, 67839, Hatran]
-  91,  // Range #248: [67840, 67871, Phoenician]
-  108,  // Range #249: [67872, 67897, Lydian]
-  108,  // Range #250: [67903, 67903, Lydian]
-  86,  // Range #251: [67968, 67999, Meroitic_Hieroglyphs]
-  141,  // Range #252: [68000, 68095, Meroitic_Cursive]
-  57,  // Range #253: [68096, 68102, Kharoshthi]
-  57,  // Range #254: [68108, 68168, Kharoshthi]
-  57,  // Range #255: [68176, 68184, Kharoshthi]
-  133,  // Range #256: [68192, 68223, Old_South_Arabian]
-  142,  // Range #257: [68224, 68255, Old_North_Arabian]
-  121,  // Range #258: [68288, 68342, Manichaean]
-  117,  // Range #259: [68352, 68415, Avestan]
-  125,  // Range #260: [68416, 68447, Inscriptional_Parthian]
-  122,  // Range #261: [68448, 68466, Inscriptional_Pahlavi]
-  122,  // Range #262: [68472, 68479, Inscriptional_Pahlavi]
-  123,  // Range #263: [68480, 68497, Psalter_Pahlavi]
-  123,  // Range #264: [68505, 68508, Psalter_Pahlavi]
-  123,  // Range #265: [68521, 68527, Psalter_Pahlavi]
-  88,  // Range #266: [68608, 68680, Old_Turkic]
-  76,  // Range #267: [68736, 68786, Old_Hungarian]
-  76,  // Range #268: [68800, 68850, Old_Hungarian]
-  76,  // Range #269: [68858, 68863, Old_Hungarian]
-  182,  // Range #270: [68864, 68903, Hanifi_Rohingya]
-  182,  // Range #271: [68912, 68921, Hanifi_Rohingya]
-  2,  // Range #272: [69216, 69246, Arabic]
-  192,  // Range #273: [69248, 69297, Yezidi]
-  184,  // Range #274: [69376, 69415, Old_Sogdian]
-  183,  // Range #275: [69424, 69465, Sogdian]
-  189,  // Range #276: [69552, 69579, Chorasmian]
-  185,  // Range #277: [69600, 69622, Elymaic]
-  65,  // Range #278: [69632, 69743, Brahmi]
-  65,  // Range #279: [69759, 69759, Brahmi]
-  120,  // Range #280: [69760, 69825, Kaithi]
-  120,  // Range #281: [69837, 69837, Kaithi]
-  152,  // Range #282: [69840, 69864, Sora_Sompeng]
-  152,  // Range #283: [69872, 69881, Sora_Sompeng]
-  118,  // Range #284: [69888, 69959, Chakma]
-  160,  // Range #285: [69968, 70006, Mahajani]
-  151,  // Range #286: [70016, 70111, Sharada]
-  33,  // Range #287: [70113, 70132, Sinhala]
-  157,  // Range #288: [70144, 70206, Khojki]
-  164,  // Range #289: [70272, 70313, Multani]
-  145,  // Range #290: [70320, 70378, Khudawadi]
-  145,  // Range #291: [70384, 70393, Khudawadi]
-  137,  // Range #292: [70400, 70457, Grantha]
-  137,  // Range #293: [70460, 70480, Grantha]
-  137,  // Range #294: [70487, 70487, Grantha]
-  137,  // Range #295: [70493, 70516, Grantha]
-  170,  // Range #296: [70656, 70753, Newa]
-  158,  // Range #297: [70784, 70855, Tirhuta]
-  158,  // Range #298: [70864, 70873, Tirhuta]
-  166,  // Range #299: [71040, 71133, Siddham]
-  163,  // Range #300: [71168, 71236, Modi]
-  163,  // Range #301: [71248, 71257, Modi]
-  27,  // Range #302: [71264, 71276, Mongolian]
-  153,  // Range #303: [71296, 71352, Takri]
-  153,  // Range #304: [71360, 71369, Takri]
-  161,  // Range #305: [71424, 71487, Ahom]
-  178,  // Range #306: [71680, 71739, Dogra]
-  146,  // Range #307: [71840, 71922, Warang_Citi]
-  146,  // Range #308: [71935, 71935, Warang_Citi]
-  190,  // Range #309: [71936, 72006, Dives_Akuru]
-  190,  // Range #310: [72016, 72025, Dives_Akuru]
-  187,  // Range #311: [72096, 72164, Nandinagari]
-  177,  // Range #312: [72192, 72263, Zanabazar_Square]
-  176,  // Range #313: [72272, 72354, Soyombo]
-  165,  // Range #314: [72384, 72440, Pau_Cin_Hau]
-  168,  // Range #315: [72704, 72773, Bhaiksuki]
-  168,  // Range #316: [72784, 72812, Bhaiksuki]
-  169,  // Range #317: [72816, 72886, Marchen]
-  175,  // Range #318: [72960, 73031, Masaram_Gondi]
-  175,  // Range #319: [73040, 73049, Masaram_Gondi]
-  179,  // Range #320: [73056, 73112, Gunjala_Gondi]
-  179,  // Range #321: [73120, 73129, Gunjala_Gondi]
-  180,  // Range #322: [73440, 73464, Makasar]
-  131,  // Range #323: [73648, 73648, Lisu]
-  35,  // Range #324: [73664, 73713, Tamil]
-  35,  // Range #325: [73727, 73727, Tamil]
-  101,  // Range #326: [73728, 74649, Cuneiform]
-  101,  // Range #327: [74752, 74868, Cuneiform]
-  101,  // Range #328: [74880, 75075, Cuneiform]
-  71,  // Range #329: [77824, 78904, Egyptian_Hieroglyphs]
-  156,  // Range #330: [82944, 83526, Anatolian_Hieroglyphs]
-  130,  // Range #331: [92160, 92728, Bamum]
-  149,  // Range #332: [92736, 92783, Mro]
-  134,  // Range #333: [92880, 92917, Bassa_Vah]
-  75,  // Range #334: [92928, 92997, Pahawh_Hmong]
-  75,  // Range #335: [93008, 93047, Pahawh_Hmong]
-  75,  // Range #336: [93053, 93071, Pahawh_Hmong]
-  181,  // Range #337: [93760, 93850, Medefaidrin]
-  92,  // Range #338: [93952, 94087, Miao]
-  92,  // Range #339: [94095, 94111, Miao]
-  154,  // Range #340: [94176, 94176, Tangut]
-  150,  // Range #341: [94177, 94177, Nushu]
-  191,  // Range #342: [94180, 94180, Khitan_Small_Script]
-  17,  // Range #343: [94192, 94193, Han]
-  154,  // Range #344: [94208, 100343, Tangut]
-  154,  // Range #345: [100352, 101119, Tangut]
-  191,  // Range #346: [101120, 101589, Khitan_Small_Script]
-  154,  // Range #347: [101632, 101640, Tangut]
-  22,  // Range #348: [110592, 110592, Katakana]
-  20,  // Range #349: [110593, 110878, Hiragana]
-  20,  // Range #350: [110928, 110930, Hiragana]
-  22,  // Range #351: [110948, 110951, Katakana]
-  150,  // Range #352: [110960, 111355, Nushu]
-  135,  // Range #353: [113664, 113770, Duployan]
-  135,  // Range #354: [113776, 113800, Duployan]
-  135,  // Range #355: [113808, 113823, Duployan]
-  14,  // Range #356: [119296, 119365, Greek]
-  112,  // Range #357: [120832, 121483, SignWriting]
-  112,  // Range #358: [121499, 121519, SignWriting]
-  56,  // Range #359: [122880, 122922, Glagolitic]
-  186,  // Range #360: [123136, 123215, Nyiakeng_Puachue_Hmong]
-  188,  // Range #361: [123584, 123641, Wancho]
-  188,  // Range #362: [123647, 123647, Wancho]
-  140,  // Range #363: [124928, 125142, Mende_Kikakui]
-  167,  // Range #364: [125184, 125279, Adlam]
-  2,  // Range #365: [126464, 126523, Arabic]
-  2,  // Range #366: [126530, 126619, Arabic]
-  2,  // Range #367: [126625, 126651, Arabic]
-  2,  // Range #368: [126704, 126705, Arabic]
-  20,  // Range #369: [127488, 127488, Hiragana]
-  17,  // Range #370: [131072, 173789, Han]
-  17,  // Range #371: [173824, 177972, Han]
-  17,  // Range #372: [177984, 183969, Han]
-  17,  // Range #373: [183984, 191456, Han]
-  17,  // Range #374: [194560, 195101, Han]
-  17,  // Range #375: [196608, 201546, Han]
+  25,  // Range #174: [42960, 42969, Latin]
+  25,  // Range #175: [42994, 43007, Latin]
+  58,  // Range #176: [43008, 43052, Syloti_Nagri]
+  90,  // Range #177: [43072, 43127, Phags_Pa]
+  111,  // Range #178: [43136, 43205, Saurashtra]
+  111,  // Range #179: [43214, 43225, Saurashtra]
+  10,  // Range #180: [43232, 43263, Devanagari]
+  79,  // Range #181: [43264, 43309, Kayah_Li]
+  79,  // Range #182: [43311, 43311, Kayah_Li]
+  110,  // Range #183: [43312, 43347, Rejang]
+  110,  // Range #184: [43359, 43359, Rejang]
+  18,  // Range #185: [43360, 43388, Hangul]
+  78,  // Range #186: [43392, 43469, Javanese]
+  78,  // Range #187: [43472, 43487, Javanese]
+  28,  // Range #188: [43488, 43518, Myanmar]
+  66,  // Range #189: [43520, 43574, Cham]
+  66,  // Range #190: [43584, 43615, Cham]
+  28,  // Range #191: [43616, 43647, Myanmar]
+  127,  // Range #192: [43648, 43714, Tai_Viet]
+  127,  // Range #193: [43739, 43743, Tai_Viet]
+  115,  // Range #194: [43744, 43766, Meetei_Mayek]
+  11,  // Range #195: [43777, 43798, Ethiopic]
+  11,  // Range #196: [43808, 43822, Ethiopic]
+  25,  // Range #197: [43824, 43866, Latin]
+  25,  // Range #198: [43868, 43876, Latin]
+  14,  // Range #199: [43877, 43877, Greek]
+  25,  // Range #200: [43878, 43881, Latin]
+  6,  // Range #201: [43888, 43967, Cherokee]
+  115,  // Range #202: [43968, 44025, Meetei_Mayek]
+  18,  // Range #203: [44032, 55203, Hangul]
+  18,  // Range #204: [55216, 55291, Hangul]
+  17,  // Range #205: [63744, 64217, Han]
+  25,  // Range #206: [64256, 64262, Latin]
+  3,  // Range #207: [64275, 64279, Armenian]
+  19,  // Range #208: [64285, 64335, Hebrew]
+  2,  // Range #209: [64336, 64450, Arabic]
+  2,  // Range #210: [64467, 64829, Arabic]
+  2,  // Range #211: [64832, 64967, Arabic]
+  2,  // Range #212: [64975, 64975, Arabic]
+  2,  // Range #213: [65008, 65023, Arabic]
+  8,  // Range #214: [65070, 65071, Cyrillic]
+  2,  // Range #215: [65136, 65276, Arabic]
+  25,  // Range #216: [65313, 65338, Latin]
+  25,  // Range #217: [65345, 65370, Latin]
+  22,  // Range #218: [65382, 65391, Katakana]
+  22,  // Range #219: [65393, 65437, Katakana]
+  18,  // Range #220: [65440, 65500, Hangul]
+  49,  // Range #221: [65536, 65629, Linear_B]
+  49,  // Range #222: [65664, 65786, Linear_B]
+  14,  // Range #223: [65856, 65934, Greek]
+  14,  // Range #224: [65952, 65952, Greek]
+  107,  // Range #225: [66176, 66204, Lycian]
+  104,  // Range #226: [66208, 66256, Carian]
+  30,  // Range #227: [66304, 66339, Old_Italic]
+  30,  // Range #228: [66349, 66351, Old_Italic]
+  13,  // Range #229: [66352, 66378, Gothic]
+  89,  // Range #230: [66384, 66426, Old_Permic]
+  53,  // Range #231: [66432, 66463, Ugaritic]
+  61,  // Range #232: [66464, 66517, Old_Persian]
+  9,  // Range #233: [66560, 66639, Deseret]
+  51,  // Range #234: [66640, 66687, Shavian]
+  50,  // Range #235: [66688, 66729, Osmanya]
+  171,  // Range #236: [66736, 66811, Osage]
+  136,  // Range #237: [66816, 66855, Elbasan]
+  159,  // Range #238: [66864, 66915, Caucasian_Albanian]
+  159,  // Range #239: [66927, 66927, Caucasian_Albanian]
+  197,  // Range #240: [66928, 67004, Vithkuqi]
+  83,  // Range #241: [67072, 67382, Linear_A]
+  83,  // Range #242: [67392, 67413, Linear_A]
+  83,  // Range #243: [67424, 67431, Linear_A]
+  25,  // Range #244: [67456, 67514, Latin]
+  47,  // Range #245: [67584, 67647, Cypriot]
+  116,  // Range #246: [67648, 67679, Imperial_Aramaic]
+  144,  // Range #247: [67680, 67711, Palmyrene]
+  143,  // Range #248: [67712, 67742, Nabataean]
+  143,  // Range #249: [67751, 67759, Nabataean]
+  162,  // Range #250: [67808, 67829, Hatran]
+  162,  // Range #251: [67835, 67839, Hatran]
+  91,  // Range #252: [67840, 67871, Phoenician]
+  108,  // Range #253: [67872, 67897, Lydian]
+  108,  // Range #254: [67903, 67903, Lydian]
+  86,  // Range #255: [67968, 67999, Meroitic_Hieroglyphs]
+  141,  // Range #256: [68000, 68095, Meroitic_Cursive]
+  57,  // Range #257: [68096, 68102, Kharoshthi]
+  57,  // Range #258: [68108, 68168, Kharoshthi]
+  57,  // Range #259: [68176, 68184, Kharoshthi]
+  133,  // Range #260: [68192, 68223, Old_South_Arabian]
+  142,  // Range #261: [68224, 68255, Old_North_Arabian]
+  121,  // Range #262: [68288, 68342, Manichaean]
+  117,  // Range #263: [68352, 68415, Avestan]
+  125,  // Range #264: [68416, 68447, Inscriptional_Parthian]
+  122,  // Range #265: [68448, 68466, Inscriptional_Pahlavi]
+  122,  // Range #266: [68472, 68479, Inscriptional_Pahlavi]
+  123,  // Range #267: [68480, 68497, Psalter_Pahlavi]
+  123,  // Range #268: [68505, 68508, Psalter_Pahlavi]
+  123,  // Range #269: [68521, 68527, Psalter_Pahlavi]
+  88,  // Range #270: [68608, 68680, Old_Turkic]
+  76,  // Range #271: [68736, 68786, Old_Hungarian]
+  76,  // Range #272: [68800, 68850, Old_Hungarian]
+  76,  // Range #273: [68858, 68863, Old_Hungarian]
+  182,  // Range #274: [68864, 68903, Hanifi_Rohingya]
+  182,  // Range #275: [68912, 68921, Hanifi_Rohingya]
+  2,  // Range #276: [69216, 69246, Arabic]
+  192,  // Range #277: [69248, 69297, Yezidi]
+  184,  // Range #278: [69376, 69415, Old_Sogdian]
+  183,  // Range #279: [69424, 69465, Sogdian]
+  194,  // Range #280: [69488, 69513, Old_Uyghur]
+  189,  // Range #281: [69552, 69579, Chorasmian]
+  185,  // Range #282: [69600, 69622, Elymaic]
+  65,  // Range #283: [69632, 69749, Brahmi]
+  65,  // Range #284: [69759, 69759, Brahmi]
+  120,  // Range #285: [69760, 69826, Kaithi]
+  120,  // Range #286: [69837, 69837, Kaithi]
+  152,  // Range #287: [69840, 69864, Sora_Sompeng]
+  152,  // Range #288: [69872, 69881, Sora_Sompeng]
+  118,  // Range #289: [69888, 69959, Chakma]
+  160,  // Range #290: [69968, 70006, Mahajani]
+  151,  // Range #291: [70016, 70111, Sharada]
+  33,  // Range #292: [70113, 70132, Sinhala]
+  157,  // Range #293: [70144, 70206, Khojki]
+  164,  // Range #294: [70272, 70313, Multani]
+  145,  // Range #295: [70320, 70378, Khudawadi]
+  145,  // Range #296: [70384, 70393, Khudawadi]
+  137,  // Range #297: [70400, 70457, Grantha]
+  137,  // Range #298: [70460, 70480, Grantha]
+  137,  // Range #299: [70487, 70487, Grantha]
+  137,  // Range #300: [70493, 70516, Grantha]
+  170,  // Range #301: [70656, 70753, Newa]
+  158,  // Range #302: [70784, 70855, Tirhuta]
+  158,  // Range #303: [70864, 70873, Tirhuta]
+  166,  // Range #304: [71040, 71133, Siddham]
+  163,  // Range #305: [71168, 71236, Modi]
+  163,  // Range #306: [71248, 71257, Modi]
+  27,  // Range #307: [71264, 71276, Mongolian]
+  153,  // Range #308: [71296, 71353, Takri]
+  153,  // Range #309: [71360, 71369, Takri]
+  161,  // Range #310: [71424, 71494, Ahom]
+  178,  // Range #311: [71680, 71739, Dogra]
+  146,  // Range #312: [71840, 71922, Warang_Citi]
+  146,  // Range #313: [71935, 71935, Warang_Citi]
+  190,  // Range #314: [71936, 72006, Dives_Akuru]
+  190,  // Range #315: [72016, 72025, Dives_Akuru]
+  187,  // Range #316: [72096, 72164, Nandinagari]
+  177,  // Range #317: [72192, 72263, Zanabazar_Square]
+  176,  // Range #318: [72272, 72354, Soyombo]
+  40,  // Range #319: [72368, 72383, Canadian_Aboriginal]
+  165,  // Range #320: [72384, 72440, Pau_Cin_Hau]
+  168,  // Range #321: [72704, 72773, Bhaiksuki]
+  168,  // Range #322: [72784, 72812, Bhaiksuki]
+  169,  // Range #323: [72816, 72886, Marchen]
+  175,  // Range #324: [72960, 73031, Masaram_Gondi]
+  175,  // Range #325: [73040, 73049, Masaram_Gondi]
+  179,  // Range #326: [73056, 73112, Gunjala_Gondi]
+  179,  // Range #327: [73120, 73129, Gunjala_Gondi]
+  180,  // Range #328: [73440, 73464, Makasar]
+  131,  // Range #329: [73648, 73648, Lisu]
+  35,  // Range #330: [73664, 73713, Tamil]
+  35,  // Range #331: [73727, 73727, Tamil]
+  101,  // Range #332: [73728, 74649, Cuneiform]
+  101,  // Range #333: [74752, 74868, Cuneiform]
+  101,  // Range #334: [74880, 75075, Cuneiform]
+  193,  // Range #335: [77712, 77810, Cypro_Minoan]
+  71,  // Range #336: [77824, 78904, Egyptian_Hieroglyphs]
+  156,  // Range #337: [82944, 83526, Anatolian_Hieroglyphs]
+  130,  // Range #338: [92160, 92728, Bamum]
+  149,  // Range #339: [92736, 92783, Mro]
+  195,  // Range #340: [92784, 92873, Tangsa]
+  134,  // Range #341: [92880, 92917, Bassa_Vah]
+  75,  // Range #342: [92928, 92997, Pahawh_Hmong]
+  75,  // Range #343: [93008, 93047, Pahawh_Hmong]
+  75,  // Range #344: [93053, 93071, Pahawh_Hmong]
+  181,  // Range #345: [93760, 93850, Medefaidrin]
+  92,  // Range #346: [93952, 94087, Miao]
+  92,  // Range #347: [94095, 94111, Miao]
+  154,  // Range #348: [94176, 94176, Tangut]
+  150,  // Range #349: [94177, 94177, Nushu]
+  17,  // Range #350: [94178, 94179, Han]
+  191,  // Range #351: [94180, 94180, Khitan_Small_Script]
+  17,  // Range #352: [94192, 94193, Han]
+  154,  // Range #353: [94208, 100343, Tangut]
+  154,  // Range #354: [100352, 101119, Tangut]
+  191,  // Range #355: [101120, 101589, Khitan_Small_Script]
+  154,  // Range #356: [101632, 101640, Tangut]
+  22,  // Range #357: [110576, 110592, Katakana]
+  20,  // Range #358: [110593, 110879, Hiragana]
+  22,  // Range #359: [110880, 110882, Katakana]
+  20,  // Range #360: [110928, 110930, Hiragana]
+  22,  // Range #361: [110948, 110951, Katakana]
+  150,  // Range #362: [110960, 111355, Nushu]
+  135,  // Range #363: [113664, 113770, Duployan]
+  135,  // Range #364: [113776, 113800, Duployan]
+  135,  // Range #365: [113808, 113823, Duployan]
+  14,  // Range #366: [119296, 119365, Greek]
+  112,  // Range #367: [120832, 121483, SignWriting]
+  112,  // Range #368: [121499, 121519, SignWriting]
+  25,  // Range #369: [122624, 122654, Latin]
+  56,  // Range #370: [122880, 122922, Glagolitic]
+  186,  // Range #371: [123136, 123215, Nyiakeng_Puachue_Hmong]
+  196,  // Range #372: [123536, 123566, Toto]
+  188,  // Range #373: [123584, 123641, Wancho]
+  188,  // Range #374: [123647, 123647, Wancho]
+  11,  // Range #375: [124896, 124926, Ethiopic]
+  140,  // Range #376: [124928, 125142, Mende_Kikakui]
+  167,  // Range #377: [125184, 125279, Adlam]
+  2,  // Range #378: [126464, 126523, Arabic]
+  2,  // Range #379: [126530, 126619, Arabic]
+  2,  // Range #380: [126625, 126651, Arabic]
+  2,  // Range #381: [126704, 126705, Arabic]
+  20,  // Range #382: [127488, 127488, Hiragana]
+  17,  // Range #383: [131072, 173791, Han]
+  17,  // Range #384: [173824, 177976, Han]
+  17,  // Range #385: [177984, 183969, Han]
+  17,  // Range #386: [183984, 191456, Han]
+  17,  // Range #387: [194560, 195101, Han]
+  17,  // Range #388: [196608, 201546, Han]
 };
 
-const uint8 kMaxScript = 192;
+const uint8 kMaxScript = 197;
 
 }  // namespace approx_script_internal
 }  // namespace mobile