Merge "Enable overlays on background thread."
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a81916b..1a92f7b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -9571,6 +9571,8 @@
 
     <!-- [CHAR_LIMIT=NONE] Developer Settings: Label for the option that turns off all overlays in a given category. -->
     <string name="overlay_option_device_default">Device default</string>
+    <!-- [CHAR_LIMIT=NONE] Developer Settings: Toast displayed to the user when an overlay fails to apply. -->
+    <string name="overlay_toast_failed_to_apply">Failed to apply overlay</string>
 
     <!-- [CHAR_LIMIT=60] Label for special access screen -->
     <string name="special_access">Special app access</string>
diff --git a/src/com/android/settings/development/OverlayCategoryPreferenceController.java b/src/com/android/settings/development/OverlayCategoryPreferenceController.java
index 9db8a2f..0ba9d79 100644
--- a/src/com/android/settings/development/OverlayCategoryPreferenceController.java
+++ b/src/com/android/settings/development/OverlayCategoryPreferenceController.java
@@ -22,9 +22,12 @@
 import android.content.om.IOverlayManager;
 import android.content.om.OverlayInfo;
 import android.content.pm.PackageManager;
+import android.os.AsyncTask;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
 
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.ListPreference;
@@ -38,6 +41,7 @@
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * Preference controller to allow users to choose an overlay from a list for a given category.
@@ -46,6 +50,7 @@
  */
 public class OverlayCategoryPreferenceController extends DeveloperOptionsPreferenceController
         implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin {
+    private static final String TAG = "OverlayCategoryPC";
     @VisibleForTesting
     static final String PACKAGE_DEVICE_DEFAULT = "package_device_default";
     private static final String OVERLAY_TARGET_PACKAGE = "android";
@@ -100,12 +105,11 @@
     }
 
     private boolean setOverlay(String packageName) {
-        String currentPackageName = null;
-        for (OverlayInfo o : getOverlayInfos()) {
-            if (o.isEnabled()) {
-                currentPackageName = o.packageName;
-            }
-        }
+        final String currentPackageName = getOverlayInfos().stream()
+                .filter(info -> info.isEnabled())
+                .map(info -> info.packageName)
+                .findFirst()
+                .orElse(null);
 
         if (PACKAGE_DEVICE_DEFAULT.equals(packageName) && TextUtils.isEmpty(currentPackageName)
                 || TextUtils.equals(packageName, currentPackageName)) {
@@ -113,18 +117,33 @@
             return true;
         }
 
-        final boolean result;
-        try {
-            if (PACKAGE_DEVICE_DEFAULT.equals(packageName)) {
-                result = mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM);
-            } else {
-                result = mOverlayManager.setEnabledExclusiveInCategory(packageName, USER_SYSTEM);
+        new AsyncTask<Void, Void, Boolean>() {
+            @Override
+            protected Boolean doInBackground(Void... params) {
+                try {
+                    if (PACKAGE_DEVICE_DEFAULT.equals(packageName)) {
+                        return mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM);
+                    } else {
+                        return mOverlayManager.setEnabledExclusiveInCategory(packageName, USER_SYSTEM);
+                    }
+                } catch (RemoteException re) {
+                    Log.w(TAG, "Error enabling overlay.", re);
+                    return false;
+                }
             }
-        } catch (RemoteException re) {
-            throw re.rethrowFromSystemServer();
-        }
-        updateState(mPreference);
-        return result;
+
+            @Override
+            protected void onPostExecute(Boolean success) {
+                updateState(mPreference);
+                if (!success) {
+                    Toast.makeText(
+                            mContext, R.string.overlay_toast_failed_to_apply, Toast.LENGTH_LONG)
+                            .show();
+                }
+            }
+        }.execute();
+
+        return true; // Assume success; toast on failure.
     }
 
     @Override
diff --git a/tests/robotests/src/com/android/settings/development/OverlayCategoryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/OverlayCategoryPreferenceControllerTest.java
index 3938b89..8a90b20 100644
--- a/tests/robotests/src/com/android/settings/development/OverlayCategoryPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/development/OverlayCategoryPreferenceControllerTest.java
@@ -31,6 +31,8 @@
 import android.content.pm.PackageManager;
 import android.os.RemoteException;
 
+import com.android.settings.R;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,6 +40,8 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.shadows.ShadowToast;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -105,22 +109,65 @@
         mockCurrentOverlays(ONE_DISABLED, TWO_DISABLED);
 
         mController.onPreferenceChange(null, TWO_DISABLED.packageName);
+        ShadowApplication.runBackgroundTasks();
 
         verify(mOverlayManager)
             .setEnabledExclusiveInCategory(eq(TWO_DISABLED.packageName), anyInt());
     }
 
     @Test
+    public void onPreferenceChange_enable_fails() throws Exception {
+        mockCurrentOverlays(ONE_DISABLED, TWO_DISABLED);
+        when(mOverlayManager.setEnabledExclusiveInCategory(eq(TWO_DISABLED.packageName), anyInt()))
+                .thenReturn(false);
+
+        mController.onPreferenceChange(null, TWO_DISABLED.packageName);
+        ShadowApplication.runBackgroundTasks();
+
+        assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo(
+                RuntimeEnvironment.application.getString(R.string.overlay_toast_failed_to_apply));
+    }
+
+    @Test
     public void onPreferenceChange_disable() throws Exception {
         mockCurrentOverlays(ONE_DISABLED, TWO_ENABLED);
 
         mController.onPreferenceChange(
                 null, OverlayCategoryPreferenceController.PACKAGE_DEVICE_DEFAULT);
+        ShadowApplication.runBackgroundTasks();
 
         verify(mOverlayManager).setEnabled(eq(TWO_ENABLED.packageName), eq(false), anyInt());
     }
 
     @Test
+    public void onPreferenceChange_disable_fails() throws Exception {
+        mockCurrentOverlays(ONE_DISABLED, TWO_ENABLED);
+        when(mOverlayManager.setEnabled(eq(TWO_ENABLED.packageName), eq(false), anyInt()))
+                .thenReturn(false);
+
+        mController.onPreferenceChange(
+                null, OverlayCategoryPreferenceController.PACKAGE_DEVICE_DEFAULT);
+        ShadowApplication.runBackgroundTasks();
+
+        assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo(
+                RuntimeEnvironment.application.getString(R.string.overlay_toast_failed_to_apply));
+    }
+
+    @Test
+    public void onPreferenceChange_disable_throws() throws Exception {
+        mockCurrentOverlays(ONE_DISABLED, TWO_ENABLED);
+        when(mOverlayManager.setEnabled(eq(TWO_ENABLED.packageName), eq(false), anyInt()))
+                .thenThrow(new RemoteException());
+
+        mController.onPreferenceChange(
+                null, OverlayCategoryPreferenceController.PACKAGE_DEVICE_DEFAULT);
+        ShadowApplication.runBackgroundTasks();
+
+        assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo(
+                RuntimeEnvironment.application.getString(R.string.overlay_toast_failed_to_apply));
+    }
+
+    @Test
     public void updateState_enabled() {
         mockCurrentOverlays(ONE_DISABLED, TWO_ENABLED);