Support logging chained WorkSources from JobScheduler.

Controller by a feature flag, which this change also introduces.

Bug: 62390666
Bug: 70892390
Test: manual, via JobSchedulerTestApp
Test: CtsJobSchedulerTestCases

Change-Id: Ic7dfb7861f7987a1b6eeb4cf3851193e9ad7da84
diff --git a/core/java/android/os/WorkSource.java b/core/java/android/os/WorkSource.java
index d0c2870..81d7506 100644
--- a/core/java/android/os/WorkSource.java
+++ b/core/java/android/os/WorkSource.java
@@ -1,7 +1,10 @@
 package android.os;
 
 import android.annotation.Nullable;
+import android.content.Context;
 import android.os.WorkSourceProto;
+import android.provider.Settings;
+import android.provider.Settings.Global;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
 
@@ -109,6 +112,17 @@
         }
     }
 
+    /**
+     * Whether system services should create {@code WorkChains} (wherever possible) in the place
+     * of flat UID lists.
+     *
+     * @hide
+     */
+    public static boolean isChainedBatteryAttributionEnabled(Context context) {
+        return Settings.Global.getInt(context.getContentResolver(),
+                Global.CHAINED_BATTERY_ATTRIBUTION_ENABLED, 0) == 1;
+    }
+
     /** @hide */
     public int size() {
         return mNum;
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 1ea4861..cadca24 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11248,6 +11248,16 @@
          */
         public static final String OVERRIDE_SETTINGS_PROVIDER_RESTORE_ANY_VERSION =
                 "override_settings_provider_restore_any_version";
+        /**
+         * Flag to toggle whether system services report attribution chains when they attribute
+         * battery use via a {@code WorkSource}.
+         *
+         * Type: int (0 to disable, 1 to enable)
+         *
+         * @hide
+         */
+        public static final String CHAINED_BATTERY_ATTRIBUTION_ENABLED =
+                "chained_battery_attribution_enabled";
 
         /**
          * Settings to backup. This is here so that it's in the same place as the settings
diff --git a/core/proto/android/providers/settings.proto b/core/proto/android/providers/settings.proto
index fd28322..27fbb24 100644
--- a/core/proto/android/providers/settings.proto
+++ b/core/proto/android/providers/settings.proto
@@ -394,8 +394,9 @@
     optional SettingProto wifi_connected_mac_randomization_enabled = 350;
     optional SettingProto show_restart_in_crash_dialog = 351;
     optional SettingProto show_mute_in_crash_dialog = 352;
+    optional SettingProto chained_battery_attribution_enabled = 353;
 
-    // Next tag = 353;
+    // Next tag = 354;
 }
 
 message SecureSettingsProto {
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 225b685..1520fb6 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -444,7 +444,8 @@
                     Settings.Global.ZEN_MODE_CONFIG_ETAG,
                     Settings.Global.ZEN_MODE_RINGER_LEVEL,
                     Settings.Global.ZRAM_ENABLED,
-                    Settings.Global.OVERRIDE_SETTINGS_PROVIDER_RESTORE_ANY_VERSION);
+                    Settings.Global.OVERRIDE_SETTINGS_PROVIDER_RESTORE_ANY_VERSION,
+                    Settings.Global.CHAINED_BATTERY_ATTRIBUTION_ENABLED);
 
     private static final Set<String> BACKUP_BLACKLISTED_SECURE_SETTINGS =
              newHashSet(
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index d556db4..6970c86 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.provider.Settings.Global;
 import android.providers.settings.GlobalSettingsProto;
 import android.providers.settings.SecureSettingsProto;
 import android.providers.settings.SettingProto;
@@ -1137,6 +1138,12 @@
         dumpSetting(s, p,
                 Settings.Global.SHOW_MUTE_IN_CRASH_DIALOG,
                 GlobalSettingsProto.SHOW_MUTE_IN_CRASH_DIALOG);
+        dumpSetting(s, p,
+                Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED,
+                GlobalSettingsProto.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED);
+        dumpSetting(s, p,
+                Global.CHAINED_BATTERY_ATTRIBUTION_ENABLED,
+                GlobalSettingsProto.CHAINED_BATTERY_ATTRIBUTION_ENABLED);
     }
 
     /** Dump a single {@link SettingsState.Setting} to a proto buf */
diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java
index 83a3c19..f23147b 100644
--- a/services/core/java/com/android/server/job/JobServiceContext.java
+++ b/services/core/java/com/android/server/job/JobServiceContext.java
@@ -400,7 +400,7 @@
                     (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
             PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                     runningJob.getTag());
-            wl.setWorkSource(new WorkSource(runningJob.getSourceUid()));
+            wl.setWorkSource(deriveWorkSource(runningJob));
             wl.setReferenceCounted(false);
             wl.acquire();
 
@@ -419,6 +419,19 @@
         }
     }
 
+    private WorkSource deriveWorkSource(JobStatus runningJob) {
+        final int jobUid = runningJob.getSourceUid();
+        if (WorkSource.isChainedBatteryAttributionEnabled(mContext)) {
+            WorkSource workSource = new WorkSource();
+            workSource.createWorkChain()
+                    .addNode(jobUid, null)
+                    .addNode(android.os.Process.SYSTEM_UID, "JobScheduler");
+            return workSource;
+        } else {
+            return new WorkSource(jobUid);
+        }
+    }
+
     /** If the client service crashes we reschedule this job and clean up. */
     @Override
     public void onServiceDisconnected(ComponentName name) {
diff --git a/services/core/java/com/android/server/job/controllers/TimeController.java b/services/core/java/com/android/server/job/controllers/TimeController.java
index bbee0eb..a91f5a4 100644
--- a/services/core/java/com/android/server/job/controllers/TimeController.java
+++ b/services/core/java/com/android/server/job/controllers/TimeController.java
@@ -18,9 +18,11 @@
 
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 
+import android.annotation.Nullable;
 import android.app.AlarmManager;
 import android.app.AlarmManager.OnAlarmListener;
 import android.content.Context;
+import android.os.Process;
 import android.os.UserHandle;
 import android.os.WorkSource;
 import android.util.Slog;
@@ -52,6 +54,8 @@
     private long mNextJobExpiredElapsedMillis;
     private long mNextDelayExpiredElapsedMillis;
 
+    private final boolean mChainedAttributionEnabled;
+
     private AlarmManager mAlarmService = null;
     /** List of tracked jobs, sorted asc. by deadline */
     private final List<JobStatus> mTrackedJobs = new LinkedList<>();
@@ -71,6 +75,7 @@
 
         mNextJobExpiredElapsedMillis = Long.MAX_VALUE;
         mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
+        mChainedAttributionEnabled = WorkSource.isChainedBatteryAttributionEnabled(context);
     }
 
     /**
@@ -113,7 +118,7 @@
             maybeUpdateAlarmsLocked(
                     job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
                     job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE,
-                    new WorkSource(job.getSourceUid(), job.getSourcePackageName()));
+                    deriveWorkSource(job.getSourceUid(), job.getSourcePackageName()));
         }
     }
 
@@ -179,9 +184,8 @@
                     break;
                 }
             }
-            setDeadlineExpiredAlarmLocked(nextExpiryTime, nextExpiryPackageName != null
-                    ? new WorkSource(nextExpiryUid, nextExpiryPackageName)
-                    : new WorkSource(nextExpiryUid));
+            setDeadlineExpiredAlarmLocked(nextExpiryTime,
+                    deriveWorkSource(nextExpiryUid, nextExpiryPackageName));
         }
     }
 
@@ -236,9 +240,20 @@
             if (ready) {
                 mStateChangedListener.onControllerStateChanged();
             }
-            setDelayExpiredAlarmLocked(nextDelayTime, nextDelayPackageName != null
-                    ? new WorkSource(nextDelayUid, nextDelayPackageName)
-                    : new WorkSource(nextDelayUid));
+            setDelayExpiredAlarmLocked(nextDelayTime,
+                    deriveWorkSource(nextDelayUid, nextDelayPackageName));
+        }
+    }
+
+    private WorkSource deriveWorkSource(int uid, @Nullable String packageName) {
+        if (mChainedAttributionEnabled) {
+            WorkSource ws = new WorkSource();
+            ws.createWorkChain()
+                    .addNode(uid, packageName)
+                    .addNode(Process.SYSTEM_UID, "JobScheduler");
+            return ws;
+        } else {
+            return packageName == null ? new WorkSource(uid) : new WorkSource(uid, packageName);
         }
     }