Use custom logic to check storage space.

`JobInfo.Builder.setRequiresStorageNotLow` doesn't meet out needs
because:
- The background dexopt job will also remove unused dexopt artifacts and
  downgrade unused apps, to free up space, so it should run even if the
  space is low.
- We need more fine-grained logic because some apps are on internal
  storage and some apps are on external storage.

Bug: 255565888
Test: atest ArtServiceTests
Test: -
  1. Fill the disk by `fallocate`.
  2. adb shell pm art optimize-packages bg-dexopt
  3. See compilation skipped for all apps.
Ignore-AOSP-First: ART Services.
Change-Id: I56374fbdd3f96401aa848178891ce9777feb663a
diff --git a/libartservice/service/java/com/android/server/art/DexOptimizer.java b/libartservice/service/java/com/android/server/art/DexOptimizer.java
index 21333a5..c9b5f8d 100644
--- a/libartservice/service/java/com/android/server/art/DexOptimizer.java
+++ b/libartservice/service/java/com/android/server/art/DexOptimizer.java
@@ -32,6 +32,7 @@
 import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
 import android.os.UserManager;
+import android.os.storage.StorageManager;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
@@ -49,6 +50,7 @@
 
 import com.google.auto.value.AutoValue;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -151,6 +153,7 @@
                     long cpuTimeMs = 0;
                     long sizeBytes = 0;
                     long sizeBeforeBytes = 0;
+                    boolean isSkippedDueToStorageLow = false;
                     try {
                         var target = DexoptTarget.<DexInfoType>builder()
                                                       .setDexInfo(dexInfo)
@@ -172,6 +175,23 @@
                             continue;
                         }
 
+                        try {
+                            // `StorageManager.getAllocatableBytes` returns (free space + space used
+                            // by clearable cache - low storage threshold). Since we only compare
+                            // the result with 0, the clearable cache doesn't make a difference.
+                            // When the free space is below the threshold, there should be no
+                            // clearable cache left because system cleans up cache every minute.
+                            if ((mParams.getFlags() & ArtFlags.FLAG_SKIP_IF_STORAGE_LOW) != 0
+                                    && mInjector.getStorageManager().getAllocatableBytes(
+                                               mPkg.getStorageUuid())
+                                            <= 0) {
+                                isSkippedDueToStorageLow = true;
+                                continue;
+                            }
+                        } catch (IOException e) {
+                            Log.e(TAG, "Failed to check storage. Assuming storage not low", e);
+                        }
+
                         IArtdCancellationSignal artdCancellationSignal =
                                 mInjector.getArtd().createCancellationSignal();
                         mCancellationSignal.setOnCancelListener(() -> {
@@ -208,7 +228,7 @@
                     } finally {
                         results.add(new DexContainerFileOptimizeResult(dexInfo.dexPath(),
                                 abi.isPrimaryAbi(), abi.name(), compilerFilter, status, wallTimeMs,
-                                cpuTimeMs, sizeBytes, sizeBeforeBytes));
+                                cpuTimeMs, sizeBytes, sizeBeforeBytes, isSkippedDueToStorageLow));
                         if (status != OptimizeResult.OPTIMIZE_SKIPPED
                                 && status != OptimizeResult.OPTIMIZE_PERFORMED) {
                             succeeded = false;
@@ -653,5 +673,10 @@
         public IArtd getArtd() {
             return Utils.getArtd();
         }
+
+        @NonNull
+        public StorageManager getStorageManager() {
+            return Objects.requireNonNull(mContext.getSystemService(StorageManager.class));
+        }
     }
 }