Add support for support fragments in UiListener.

Bug: 36841782
Test: SupportUiListenerTest
PiperOrigin-RevId: 192502743
Change-Id: Id06ed732528db1ae486def86ecc2f44828635d81
diff --git a/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java b/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java
index 7348914..f4552b2 100644
--- a/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java
+++ b/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java
@@ -50,6 +50,15 @@
     return UiListener.create(fragmentManager, taskId);
   }
 
+  /**
+   * Version of {@link #createUiListener(FragmentManager, String)} that accepts support fragment
+   * manager.
+   */
+  public <OutputT> SupportUiListener<OutputT> createUiListener(
+      android.support.v4.app.FragmentManager fragmentManager, String taskId) {
+    return SupportUiListener.create(fragmentManager, taskId);
+  }
+
   public static DialerExecutorComponent get(Context context) {
     return ((DialerExecutorComponent.HasComponent)
             ((HasRootComponent) context.getApplicationContext()).component())
diff --git a/java/com/android/dialer/common/concurrent/SupportUiListener.java b/java/com/android/dialer/common/concurrent/SupportUiListener.java
new file mode 100644
index 0000000..5e39586
--- /dev/null
+++ b/java/com/android/dialer/common/concurrent/SupportUiListener.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.common.concurrent;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutor.FailureListener;
+import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+
+/**
+ * A headless fragment for use in UI components that interact with ListenableFutures.
+ *
+ * <p>Callbacks are only executed if the UI component is still alive.
+ *
+ * <p>Example usage: <code><pre>
+ * public class MyActivity extends AppCompatActivity {
+ *
+ *   private SupportUiListener&lt;MyOutputType&gt uiListener;
+ *
+ *   public void onCreate(Bundle bundle) {
+ *     super.onCreate(bundle);
+ *
+ *     // Must be called in onCreate!
+ *     uiListener = DialerExecutorComponent.get(context).createUiListener(fragmentManager, taskId);
+ *   }
+ *
+ *   private void onSuccess(MyOutputType output) { ... }
+ *   private void onFailure(Throwable throwable) { ... }
+ *
+ *   private void userDidSomething() {
+ *     ListenableFuture&lt;MyOutputType&gt; future = callSomeMethodReturningListenableFuture(input);
+ *     uiListener.listen(this, future, this::onSuccess, this::onFailure);
+ *   }
+ * }
+ * </pre></code>
+ */
+public class SupportUiListener<OutputT> extends Fragment {
+
+  private CallbackWrapper<OutputT> callbackWrapper;
+
+  @MainThread
+  static <OutputT> SupportUiListener<OutputT> create(
+      FragmentManager fragmentManager, String taskId) {
+    @SuppressWarnings("unchecked")
+    SupportUiListener<OutputT> uiListener =
+        (SupportUiListener<OutputT>) fragmentManager.findFragmentByTag(taskId);
+
+    if (uiListener == null) {
+      LogUtil.i("SupportUiListener.create", "creating new SupportUiListener for " + taskId);
+      uiListener = new SupportUiListener<>();
+      // When launching an activity with the screen off, its onSaveInstanceState() is called before
+      // its fragments are created, which means we can't use commit() and need to use
+      // commitAllowingStateLoss(). This is not a problem for SupportUiListener which saves no
+      // state.
+      fragmentManager.beginTransaction().add(uiListener, taskId).commitAllowingStateLoss();
+    }
+    return uiListener;
+  }
+
+  /**
+   * Adds the specified listeners to the provided future.
+   *
+   * <p>The listeners are not called if the UI component this {@link SupportUiListener} is declared
+   * in is dead.
+   */
+  @MainThread
+  public void listen(
+      Context context,
+      @NonNull ListenableFuture<OutputT> future,
+      @NonNull SuccessListener<OutputT> successListener,
+      @NonNull FailureListener failureListener) {
+    callbackWrapper =
+        new CallbackWrapper<>(Assert.isNotNull(successListener), Assert.isNotNull(failureListener));
+    Futures.addCallback(
+        Assert.isNotNull(future),
+        callbackWrapper,
+        DialerExecutorComponent.get(context).uiExecutor());
+  }
+
+  private static class CallbackWrapper<OutputT> implements FutureCallback<OutputT> {
+    private SuccessListener<OutputT> successListener;
+    private FailureListener failureListener;
+
+    private CallbackWrapper(
+        SuccessListener<OutputT> successListener, FailureListener failureListener) {
+      this.successListener = successListener;
+      this.failureListener = failureListener;
+    }
+
+    @Override
+    public void onSuccess(@Nullable OutputT output) {
+      if (successListener == null) {
+        LogUtil.i("SupportUiListener.runTask", "task succeeded but UI is dead");
+      } else {
+        successListener.onSuccess(output);
+      }
+    }
+
+    @Override
+    public void onFailure(Throwable throwable) {
+      LogUtil.e("SupportUiListener.runTask", "task failed", throwable);
+      if (failureListener == null) {
+        LogUtil.i("SupportUiListener.runTask", "task failed but UI is dead");
+      } else {
+        failureListener.onFailure(throwable);
+      }
+    }
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setRetainInstance(true);
+    // Note: We use commitAllowingStateLoss when attaching the fragment so it may not be safe to
+    // read savedInstanceState in all situations. (But it's not anticipated that this fragment
+    // should need to rely on saved state.)
+  }
+
+  @Override
+  public void onDetach() {
+    super.onDetach();
+    LogUtil.enterBlock("SupportUiListener.onDetach");
+    if (callbackWrapper != null) {
+      callbackWrapper.successListener = null;
+      callbackWrapper.failureListener = null;
+    }
+  }
+}
+
diff --git a/java/com/android/dialer/common/concurrent/UiListener.java b/java/com/android/dialer/common/concurrent/UiListener.java
index b5922f9..a2d976f 100644
--- a/java/com/android/dialer/common/concurrent/UiListener.java
+++ b/java/com/android/dialer/common/concurrent/UiListener.java
@@ -31,6 +31,7 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 
+
 /**
  * A headless fragment for use in UI components that interact with ListenableFutures.
  *
@@ -148,3 +149,4 @@
     }
   }
 }
+
diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java
index 3cf6cb9..aca4886 100644
--- a/java/com/android/dialer/speeddial/SpeedDialFragment.java
+++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java
@@ -18,8 +18,6 @@
 
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import android.support.v7.widget.RecyclerView;
@@ -28,6 +26,8 @@
 import android.view.ViewGroup;
 import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
+import com.android.dialer.common.concurrent.SupportUiListener;
 import com.android.dialer.precall.PreCall;
 import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener;
 import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener;
@@ -35,9 +35,7 @@
 import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
 import com.android.dialer.speeddial.loader.SpeedDialUiItem;
 import com.android.dialer.speeddial.loader.UiItemLoaderComponent;
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 
 /**
@@ -57,6 +55,7 @@
   private final SuggestedContactsListener suggestedListener = new SpeedDialSuggestedListener();
 
   private SpeedDialAdapter adapter;
+  private SupportUiListener<ImmutableList<SpeedDialUiItem>> speedDialLoaderListener;
 
   public static SpeedDialFragment newInstance() {
     return new SpeedDialFragment();
@@ -73,6 +72,10 @@
         new SpeedDialAdapter(getContext(), favoritesListener, suggestedListener, headerListener);
     recyclerView.setLayoutManager(adapter.getLayoutManager(getContext()));
     recyclerView.setAdapter(adapter);
+
+    speedDialLoaderListener =
+        DialerExecutorComponent.get(getContext())
+            .createUiListener(getChildFragmentManager(), "speed_dial_loader_listener");
     return view;
   }
 
@@ -84,28 +87,16 @@
   @Override
   public void onResume() {
     super.onResume();
-    Futures.addCallback(
-        UiItemLoaderComponent.get(getContext().getApplicationContext())
-            .speedDialUiItemLoader()
-            .loadSpeedDialUiItems(),
-        new FutureCallback<List<SpeedDialUiItem>>() {
-          @Override
-          public void onSuccess(List<SpeedDialUiItem> speedDialUiItems) {
-            // TODO(calderwoodra): this is bad
-            new Handler(Looper.getMainLooper())
-                .post(
-                    () -> {
-                      adapter.setSpeedDialUiItems(speedDialUiItems);
-                      adapter.notifyDataSetChanged();
-                    });
-          }
-
-          @Override
-          public void onFailure(Throwable throwable) {
-            throw new RuntimeException(throwable);
-          }
+    speedDialLoaderListener.listen(
+        getContext(),
+        UiItemLoaderComponent.get(getContext()).speedDialUiItemLoader().loadSpeedDialUiItems(),
+        speedDialUiItems -> {
+          adapter.setSpeedDialUiItems(speedDialUiItems);
+          adapter.notifyDataSetChanged();
         },
-        MoreExecutors.directExecutor());
+        throwable -> {
+          throw new RuntimeException(throwable);
+        });
   }
 
   private class SpeedDialFragmentHeaderListener implements SpeedDialHeaderListener {
diff --git a/packages.mk b/packages.mk
index f10c332..5ccef53 100644
--- a/packages.mk
+++ b/packages.mk
@@ -27,6 +27,7 @@
 	com.android.dialer.clipboard \
 	com.android.dialer.commandline \
 	com.android.dialer.common \
+	com.android.dialer.common.concurrent.testing \
 	com.android.dialer.common.preference \
 	com.android.dialer.configprovider \
 	com.android.dialer.contactphoto \