Merge "Undeprecate injector API to allow change summary"
diff --git a/api/current.txt b/api/current.txt
index 3f5abe9..e600870 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -22866,9 +22866,10 @@
     ctor public SettingInjectorService(java.lang.String);
     method public final android.os.IBinder onBind(android.content.Intent);
     method protected abstract boolean onGetEnabled();
-    method protected abstract deprecated java.lang.String onGetSummary();
+    method protected abstract java.lang.String onGetSummary();
     method public final void onStart(android.content.Intent, int);
     method public final int onStartCommand(android.content.Intent, int, int);
+    method public static final void refreshSettings(android.content.Context);
     field public static final java.lang.String ACTION_INJECTED_SETTING_CHANGED = "android.location.InjectedSettingChanged";
     field public static final java.lang.String ACTION_SERVICE_INTENT = "android.location.SettingInjectorService";
     field public static final java.lang.String ATTRIBUTES_NAME = "injected-location-setting";
diff --git a/location/java/android/location/SettingInjectorService.java b/location/java/android/location/SettingInjectorService.java
index fcd2cde..c201770 100644
--- a/location/java/android/location/SettingInjectorService.java
+++ b/location/java/android/location/SettingInjectorService.java
@@ -17,6 +17,7 @@
 package android.location;
 
 import android.app.Service;
+import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -26,7 +27,7 @@
 import android.util.Log;
 
 /**
- * Dynamically specifies the enabled status of a preference injected into
+ * Dynamically specifies the summary (subtitle) and enabled status of a preference injected into
  * the list of app settings displayed by the system settings app
  * <p/>
  * For use only by apps that are included in the system image, for preferences that affect multiple
@@ -71,12 +72,13 @@
  * </ul>
  *
  * To ensure a good user experience, your {@link android.app.Application#onCreate()},
- * and {@link #onGetEnabled()} methods must all be fast. If either is slow,
- * it can delay the display of settings values for other apps as well. Note further that these
- * methods are called on your app's UI thread.
+ * {@link #onGetSummary()}, and {@link #onGetEnabled()} methods must all be fast. If any are slow,
+ * it can delay the display of settings values for other apps as well. Note further that all are
+ * called on your app's UI thread.
  * <p/>
  * For compactness, only one copy of a given setting should be injected. If each account has a
- * distinct value for the setting, then only {@code settingsActivity} should display the value for
+ * distinct value for the setting, then the {@link #onGetSummary()} value should represent a summary
+ * of the state across all of the accounts and {@code settingsActivity} should display the value for
  * each account.
  */
 public abstract class SettingInjectorService extends Service {
@@ -108,6 +110,14 @@
             "android.location.InjectedSettingChanged";
 
     /**
+     * Name of the bundle key for the string specifying the summary for the setting (e.g., "ON" or
+     * "OFF").
+     *
+     * @hide
+     */
+    public static final String SUMMARY_KEY = "summary";
+
+    /**
      * Name of the bundle key for the string specifying whether the setting is currently enabled.
      *
      * @hide
@@ -150,36 +160,41 @@
     }
 
     private void onHandleIntent(Intent intent) {
-
-        boolean enabled;
+        String summary = null;
+        boolean enabled = false;
         try {
+            summary = onGetSummary();
             enabled = onGetEnabled();
-        } catch (RuntimeException e) {
-            // Exception. Send status anyway, so that settings injector can immediately start
-            // loading the status of the next setting.
-            sendStatus(intent, true);
-            throw e;
+        } finally {
+            // If exception happens, send status anyway, so that settings injector can immediately
+            // start loading the status of the next setting. But leave the exception uncaught to
+            // crash the injector service itself.
+            sendStatus(intent, summary, enabled);
         }
-
-        sendStatus(intent, enabled);
     }
 
     /**
      * Send the enabled values back to the caller via the messenger encoded in the
      * intent.
      */
-    private void sendStatus(Intent intent, boolean enabled) {
+    private void sendStatus(Intent intent, String summary, boolean enabled) {
+        Messenger messenger = intent.getParcelableExtra(MESSENGER_KEY);
+        // Bail out to avoid crashing GmsCore with incoming malicious Intent.
+        if (messenger == null) {
+            return;
+        }
+
         Message message = Message.obtain();
         Bundle bundle = new Bundle();
+        bundle.putString(SUMMARY_KEY, summary);
         bundle.putBoolean(ENABLED_KEY, enabled);
         message.setData(bundle);
 
         if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, mName + ": received " + intent
+            Log.d(TAG, mName + ": received " + intent + ", summary=" + summary
                     + ", enabled=" + enabled + ", sending message: " + message);
         }
 
-        Messenger messenger = intent.getParcelableExtra(MESSENGER_KEY);
         try {
             messenger.send(message);
         } catch (RemoteException e) {
@@ -188,14 +203,12 @@
     }
 
     /**
-     * This method is no longer called, because status values are no longer shown for any injected
-     * setting.
+     * Returns the {@link android.preference.Preference#getSummary()} value (allowed to be null or
+     * empty). Should not perform unpredictably-long operations such as network access--see the
+     * running-time comments in the class-level javadoc.
      *
-     * @return ignored
-     *
-     * @deprecated not called any more
+     * @return the {@link android.preference.Preference#getSummary()} value
      */
-    @Deprecated
     protected abstract String onGetSummary();
 
     /**
@@ -217,4 +230,12 @@
      * @return the {@link android.preference.Preference#isEnabled()} value
      */
     protected abstract boolean onGetEnabled();
+
+    /**
+     * Sends a broadcast to refresh the injected settings on location settings page.
+     */
+    public static final void refreshSettings(Context context) {
+        Intent intent = new Intent(ACTION_INJECTED_SETTING_CHANGED);
+        context.sendBroadcast(intent);
+    }
 }
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 2823149..842779d 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -831,6 +831,10 @@
     <!-- Toast message shown when setting a new local backup password fails due to the user not supplying the correct existing password. The phrasing here is deliberately quite general. [CHAR LIMIT=80] -->
     <string name="local_backup_password_toast_validation_failure">Failure setting backup password</string>
 
+    <!-- [CHAR LIMIT=30] Location mode screen, temporary summary text to show as the status of a location
+      setting injected by an external app while the app is being queried for the actual value -->
+    <string name="loading_injected_setting_summary">Loading\u2026</string>
+
     <!-- Name of each color mode for the display. [CHAR LIMIT=40] -->
     <string-array name="color_mode_names">
         <item>Vibrant (default)</item>
diff --git a/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java b/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java
index 780fcba..74057be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java
+++ b/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java
@@ -37,6 +37,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.IconDrawableFactory;
 import android.util.Log;
@@ -44,11 +45,16 @@
 
 import androidx.preference.Preference;
 
+import com.android.settingslib.R;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -102,7 +108,7 @@
     public SettingsInjector(Context context) {
         mContext = context;
         mSettings = new HashSet<Setting>();
-        mHandler = new StatusLoadingHandler();
+        mHandler = new StatusLoadingHandler(mSettings);
     }
 
     /**
@@ -165,7 +171,7 @@
             Log.e(TAG, "Can't get ApplicationInfo for " + setting.packageName, e);
         }
         preference.setTitle(setting.title);
-        preference.setSummary(null);
+        preference.setSummary(R.string.loading_injected_setting_summary);
         preference.setIcon(appIcon);
         preference.setOnPreferenceClickListener(new ServiceSettingClickedListener(setting));
     }
@@ -180,6 +186,7 @@
         final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
         final List<UserHandle> profiles = um.getUserProfiles();
         ArrayList<Preference> prefs = new ArrayList<>();
+        mSettings.clear();
         for (UserHandle userHandle : profiles) {
             if (profileId == UserHandle.USER_CURRENT || profileId == userHandle.getIdentifier()) {
                 Iterable<InjectedSetting> settings = getSettings(userHandle);
@@ -363,31 +370,28 @@
      * SettingInjectorService}, so to reduce memory pressure we don't want to load too many at
      * once.
      */
-    private final class StatusLoadingHandler extends Handler {
+    private static final class StatusLoadingHandler extends Handler {
+        /**
+         * References all the injected settings.
+         */
+        WeakReference<Set<Setting>> mAllSettings;
 
         /**
          * Settings whose status values need to be loaded. A set is used to prevent redundant loads.
          */
-        private Set<Setting> mSettingsToLoad = new HashSet<Setting>();
+        private Deque<Setting> mSettingsToLoad = new ArrayDeque<Setting>();
 
         /**
          * Settings that are being loaded now and haven't timed out. In practice this should have
          * zero or one elements.
          */
-        private Set<Setting> mSettingsBeingLoaded = new HashSet<Setting>();
+        private Set<Setting> mSettingsBeingLoaded = new ArraySet<Setting>();
 
-        /**
-         * Settings that are being loaded but have timed out. If only one setting has timed out, we
-         * will go ahead and start loading the next setting so that one slow load won't delay the
-         * load of the other settings.
-         */
-        private Set<Setting> mTimedOutSettings = new HashSet<Setting>();
-
-        private boolean mReloadRequested;
-
-        private StatusLoadingHandler() {
+        public StatusLoadingHandler(Set<Setting> allSettings) {
             super(Looper.getMainLooper());
+            mAllSettings = new WeakReference<>(allSettings);
         }
+
         @Override
         public void handleMessage(Message msg) {
             if (Log.isLoggable(TAG, Log.DEBUG)) {
@@ -396,20 +400,24 @@
 
             // Update state in response to message
             switch (msg.what) {
-                case WHAT_RELOAD:
-                    mReloadRequested = true;
+                case WHAT_RELOAD: {
+                    final Set<Setting> allSettings = mAllSettings.get();
+                    if (allSettings != null) {
+                        // Reload requested, so must reload all settings
+                        mSettingsToLoad.clear();
+                        mSettingsToLoad.addAll(allSettings);
+                    }
                     break;
+                }
                 case WHAT_RECEIVED_STATUS:
                     final Setting receivedSetting = (Setting) msg.obj;
                     receivedSetting.maybeLogElapsedTime();
                     mSettingsBeingLoaded.remove(receivedSetting);
-                    mTimedOutSettings.remove(receivedSetting);
                     removeMessages(WHAT_TIMEOUT, receivedSetting);
                     break;
                 case WHAT_TIMEOUT:
                     final Setting timedOutSetting = (Setting) msg.obj;
                     mSettingsBeingLoaded.remove(timedOutSetting);
-                    mTimedOutSettings.add(timedOutSetting);
                     if (Log.isLoggable(TAG, Log.WARN)) {
                         Log.w(TAG, "Timed out after " + timedOutSetting.getElapsedTime()
                                 + " millis trying to get status for: " + timedOutSetting);
@@ -421,37 +429,22 @@
 
             // Decide whether to load additional settings based on the new state. Start by seeing
             // if we have headroom to load another setting.
-            if (mSettingsBeingLoaded.size() > 0 || mTimedOutSettings.size() > 1) {
+            if (mSettingsBeingLoaded.size() > 0) {
                 // Don't load any more settings until one of the pending settings has completed.
-                // To reduce memory pressure, we want to be loading at most one setting (plus at
-                // most one timed-out setting) at a time. This means we'll be responsible for
-                // bringing in at most two services.
+                // To reduce memory pressure, we want to be loading at most one setting.
                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
                     Log.v(TAG, "too many services already live for " + msg + ", " + this);
                 }
                 return;
             }
 
-            if (mReloadRequested && mSettingsToLoad.isEmpty() && mSettingsBeingLoaded.isEmpty()
-                    && mTimedOutSettings.isEmpty()) {
-                if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                    Log.v(TAG, "reloading because idle and reload requesteed " + msg + ", " + this);
-                }
-                // Reload requested, so must reload all settings
-                mSettingsToLoad.addAll(mSettings);
-                mReloadRequested = false;
-            }
-
-            // Remove the next setting to load from the queue, if any
-            Iterator<Setting> iter = mSettingsToLoad.iterator();
-            if (!iter.hasNext()) {
+            if (mSettingsToLoad.isEmpty()) {
                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
                     Log.v(TAG, "nothing left to do for " + msg + ", " + this);
                 }
                 return;
             }
-            Setting setting = iter.next();
-            iter.remove();
+            Setting setting = mSettingsToLoad.removeFirst();
 
             // Request the status value
             setting.startService();
@@ -473,21 +466,48 @@
             return "StatusLoadingHandler{" +
                     "mSettingsToLoad=" + mSettingsToLoad +
                     ", mSettingsBeingLoaded=" + mSettingsBeingLoaded +
-                    ", mTimedOutSettings=" + mTimedOutSettings +
-                    ", mReloadRequested=" + mReloadRequested +
                     '}';
         }
     }
 
+    private static class MessengerHandler extends Handler {
+        private WeakReference<Setting> mSettingRef;
+        private Handler mHandler;
+
+        public MessengerHandler(Setting setting, Handler handler) {
+            mSettingRef = new WeakReference(setting);
+            mHandler = handler;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            final Setting setting = mSettingRef.get();
+            if (setting == null) {
+                return;
+            }
+            final Preference preference = setting.preference;
+            Bundle bundle = msg.getData();
+            boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true);
+            String summary = bundle.getString(SettingInjectorService.SUMMARY_KEY, null);
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle);
+            }
+            preference.setSummary(summary);
+            preference.setEnabled(enabled);
+            mHandler.sendMessage(
+                    mHandler.obtainMessage(WHAT_RECEIVED_STATUS, setting));
+        }
+    }
+
     /**
      * Represents an injected setting and the corresponding preference.
      */
     protected final class Setting {
-
         public final InjectedSetting setting;
         public final Preference preference;
         public long startMillis;
 
+
         public Setting(InjectedSetting setting, Preference preference) {
             this.setting = setting;
             this.preference = preference;
@@ -502,20 +522,6 @@
         }
 
         /**
-         * Returns true if they both have the same {@link #setting} value. Ignores mutable
-         * {@link #preference} and {@link #startMillis} so that it's safe to use in sets.
-         */
-        @Override
-        public boolean equals(Object o) {
-            return this == o || o instanceof Setting && setting.equals(((Setting) o).setting);
-        }
-
-        @Override
-        public int hashCode() {
-            return setting.hashCode();
-        }
-
-        /**
          * Starts the service to fetch for the current status for the setting, and updates the
          * preference when the service replies.
          */
@@ -529,20 +535,7 @@
                 }
                 return;
             }
-            Handler handler = new Handler() {
-                @Override
-                public void handleMessage(Message msg) {
-                    Bundle bundle = msg.getData();
-                    boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true);
-                    if (Log.isLoggable(TAG, Log.DEBUG)) {
-                        Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle);
-                    }
-                    preference.setSummary(null);
-                    preference.setEnabled(enabled);
-                    mHandler.sendMessage(
-                            mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this));
-                }
-            };
+            Handler handler = new MessengerHandler(this, mHandler);
             Messenger messenger = new Messenger(handler);
 
             Intent intent = setting.getServiceIntent();