Update FragUtils to encourage better readability in Activities.

Bug: 72525324
Test: existing
PiperOrigin-RevId: 183776841
Change-Id: Ia78002d3da823a228cf5a29f93cd53ad21105f94
diff --git a/java/com/android/dialer/common/FragmentUtils.java b/java/com/android/dialer/common/FragmentUtils.java
index ad7ec73..947a9b2 100644
--- a/java/com/android/dialer/common/FragmentUtils.java
+++ b/java/com/android/dialer/common/FragmentUtils.java
@@ -16,13 +16,11 @@
 
 package com.android.dialer.common;
 
-import android.app.Activity;
 import android.support.annotation.CheckResult;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
 
 /** Utility methods for working with Fragments */
 public class FragmentUtils {
@@ -35,8 +33,8 @@
   }
 
   /**
-   * @return The parent of frag that implements the callbackInterface or null if no such parent can
-   *     be found
+   * Returns an instance of the {@code callbackInterface} that is defined in the parent of the
+   * {@code fragment}, or null if no such call back can be found.
    */
   @CheckResult(suggest = "#checkParent(Fragment, Class)}")
   @Nullable
@@ -52,18 +50,22 @@
       @SuppressWarnings("unchecked") // Casts are checked using runtime methods
       T parent = (T) parentFragment;
       return parent;
-    } else {
-      FragmentActivity activity = fragment.getActivity();
-      if (callbackInterface.isInstance(activity)) {
-        @SuppressWarnings("unchecked") // Casts are checked using runtime methods
-        T parent = (T) activity;
-        return parent;
-      }
+    } else if (callbackInterface.isInstance(fragment.getActivity())) {
+      @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+      T parent = (T) fragment.getActivity();
+      return parent;
+    } else if (fragment.getActivity() instanceof FragmentUtilListener) {
+      @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+      T parent = ((FragmentUtilListener) fragment.getActivity()).getImpl(callbackInterface);
+      return parent;
     }
     return null;
   }
 
-  /** Version of {@link #getParent(Fragment, Class)} which supports {@link android.app.Fragment}. */
+  /**
+   * Returns an instance of the {@code callbackInterface} that is defined in the parent of the
+   * {@code fragment}, or null if no such call back can be found.
+   */
   @CheckResult(suggest = "#checkParent(Fragment, Class)}")
   @Nullable
   public static <T> T getParent(
@@ -79,13 +81,14 @@
       @SuppressWarnings("unchecked") // Casts are checked using runtime methods
       T parent = (T) parentFragment;
       return parent;
-    } else {
-      Activity activity = fragment.getActivity();
-      if (callbackInterface.isInstance(activity)) {
-        @SuppressWarnings("unchecked") // Casts are checked using runtime methods
-        T parent = (T) activity;
-        return parent;
-      }
+    } else if (callbackInterface.isInstance(fragment.getActivity())) {
+      @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+      T parent = (T) fragment.getActivity();
+      return parent;
+    } else if (fragment.getActivity() instanceof FragmentUtilListener) {
+      @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+      T parent = ((FragmentUtilListener) fragment.getActivity()).getImpl(callbackInterface);
+      return parent;
     }
     return null;
   }
@@ -133,4 +136,12 @@
               + parent);
     }
   }
+
+  /** Useful interface for activities that don't want to implement arbitrary listeners. */
+  public interface FragmentUtilListener {
+
+    /** Returns an implementation of T if parent has one, otherwise null. */
+    @Nullable
+    <T> T getImpl(Class<T> callbackInterface);
+  }
 }
diff --git a/java/com/android/dialer/dialpadview/DialpadFragment.java b/java/com/android/dialer/dialpadview/DialpadFragment.java
index 6b8401e..6801590 100644
--- a/java/com/android/dialer/dialpadview/DialpadFragment.java
+++ b/java/com/android/dialer/dialpadview/DialpadFragment.java
@@ -415,7 +415,8 @@
               if (isDigitsEmpty()) {
                 if (getActivity() != null) {
                   LogUtil.i("DialpadFragment.onCreateView", "dialpad spacer touched");
-                  return ((HostInterface) getActivity()).onDialpadSpacerTouchWithEmptyQuery();
+                  return FragmentUtils.getParentUnsafe(this, HostInterface.class)
+                      .onDialpadSpacerTouchWithEmptyQuery();
                 }
                 return true;
               }
diff --git a/java/com/android/dialer/main/impl/MainActivity.java b/java/com/android/dialer/main/impl/MainActivity.java
index a7a9e6c..0308b89 100644
--- a/java/com/android/dialer/main/impl/MainActivity.java
+++ b/java/com/android/dialer/main/impl/MainActivity.java
@@ -22,14 +22,17 @@
 import android.os.Bundle;
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract.QuickContact;
+import android.support.annotation.Nullable;
 import android.support.design.widget.FloatingActionButton;
 import android.support.v4.app.FragmentTransaction;
 import android.support.v7.app.AppCompatActivity;
 import android.view.View;
 import android.widget.ImageView;
 import com.android.dialer.calllog.ui.NewCallLogFragment;
+import com.android.dialer.common.FragmentUtils.FragmentUtilListener;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
+import com.android.dialer.common.concurrent.UiListener;
 import com.android.dialer.compat.CompatUtils;
 import com.android.dialer.constants.ActivityRequestCodes;
 import com.android.dialer.contactsfragment.ContactsFragment;
@@ -48,23 +51,28 @@
 import com.android.dialer.speeddial.SpeedDialFragment;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.voicemail.listui.NewVoicemailFragment;
+import com.google.common.util.concurrent.ListenableFuture;
 
 /** This is the main activity for dialer. It hosts favorites, call log, search, dialpad, etc... */
-public final class MainActivity extends AppCompatActivity
-    implements OnContactSelectedListener,
-        OnDialpadQueryChangedListener,
-        DialpadListener,
-        DialpadFragment.HostInterface,
-        SearchFragmentListener {
+public final class MainActivity extends AppCompatActivity implements FragmentUtilListener {
 
   private static final String KEY_SAVED_LANGUAGE_CODE = "saved_language_code";
 
+  private final MainOnContactSelectedListener onContactSelectedListener =
+      new MainOnContactSelectedListener(this);
+  private final MainDialpadFragmentHost dialpadFragmentHostInterface =
+      new MainDialpadFragmentHost();
+
   private MainSearchController searchController;
+  private MainOnDialpadQueryChangedListener onDialpadQueryChangedListener;
+  private MainDialpadListener dialpadListener;
+  private MainSearchFragmentListener searchFragmentListener;
 
   /** Language the device was in last time {@link #onSaveInstanceState(Bundle)} was called. */
   private String savedLanguageCode;
 
   private View snackbarContainer;
+  private UiListener<String> getLastOutgoingCallListener;
 
   /**
    * @param context Context of the application package implementing MainActivity class.
@@ -81,10 +89,17 @@
     super.onCreate(savedInstanceState);
     LogUtil.enterBlock("MainActivity.onCreate");
     setContentView(R.layout.main_activity);
+    initUiListeners();
     initLayout(savedInstanceState);
     SmartDialPrefix.initializeNanpSettings(this);
   }
 
+  private void initUiListeners() {
+    getLastOutgoingCallListener =
+        DialerExecutorComponent.get(this)
+            .createUiListener(getFragmentManager(), "Query last phone number");
+  }
+
   private void initLayout(Bundle savedInstanceState) {
     snackbarContainer = findViewById(R.id.coordinator_layout);
 
@@ -100,6 +115,10 @@
     searchController = new MainSearchController(this, bottomNav, fab, toolbar);
     toolbar.setSearchBarListener(searchController);
 
+    onDialpadQueryChangedListener = new MainOnDialpadQueryChangedListener(searchController);
+    dialpadListener = new MainDialpadListener(this, searchController, getLastOutgoingCallListener);
+    searchFragmentListener = new MainSearchFragmentListener(searchController);
+
     // Restore our view state if needed, else initialize as if the app opened for the first time
     if (savedInstanceState != null) {
       savedLanguageCode = savedInstanceState.getString(KEY_SAVED_LANGUAGE_CODE);
@@ -152,39 +171,6 @@
   }
 
   @Override
-  public void onContactSelected(ImageView photo, Uri contactUri, long contactId) {
-    // TODO(calderwoodra): Add impression logging
-    QuickContact.showQuickContact(
-        this, photo, contactUri, QuickContact.MODE_LARGE, null /* excludeMimes */);
-  }
-
-  @Override // OnDialpadQueryChangedListener
-  public void onDialpadQueryChanged(String query) {
-    searchController.onDialpadQueryChanged(query);
-  }
-
-  @Override // DialpadListener
-  public void getLastOutgoingCall(LastOutgoingCallCallback callback) {
-    DialerExecutorComponent.get(this)
-        .dialerExecutorFactory()
-        .createUiTaskBuilder(
-            getFragmentManager(), "Query last phone number", Calls::getLastOutgoingCall)
-        .onSuccess(output -> callback.lastOutgoingCall(output))
-        .build()
-        .executeParallel(this);
-  }
-
-  @Override // DialpadListener
-  public void onDialpadShown() {
-    searchController.onDialpadShown();
-  }
-
-  @Override // DialpadListener
-  public void onCallPlacedFromDialpad() {
-    // TODO(calderwoodra): logging
-  }
-
-  @Override
   public void onBackPressed() {
     if (searchController.onBackPressed()) {
       return;
@@ -192,20 +178,120 @@
     super.onBackPressed();
   }
 
-  @Override // DialpadFragment.HostInterface
-  public boolean onDialpadSpacerTouchWithEmptyQuery() {
-    // No-op, just let the clicks fall through to the search list
-    return false;
+  @Nullable
+  @Override
+  @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+  public <T> T getImpl(Class<T> callbackInterface) {
+    if (callbackInterface.isInstance(onContactSelectedListener)) {
+      return (T) onContactSelectedListener;
+    } else if (callbackInterface.isInstance(onDialpadQueryChangedListener)) {
+      return (T) onDialpadQueryChangedListener;
+    } else if (callbackInterface.isInstance(dialpadListener)) {
+      return (T) dialpadListener;
+    } else if (callbackInterface.isInstance(dialpadFragmentHostInterface)) {
+      return (T) dialpadFragmentHostInterface;
+    } else if (callbackInterface.isInstance(searchFragmentListener)) {
+      return (T) searchFragmentListener;
+    } else {
+      return null;
+    }
   }
 
-  @Override // SearchFragmentListener
-  public void onSearchListTouch() {
-    searchController.onSearchListTouch();
+  /** @see OnContactSelectedListener */
+  private static final class MainOnContactSelectedListener implements OnContactSelectedListener {
+
+    private final Context context;
+
+    MainOnContactSelectedListener(Context context) {
+      this.context = context;
+    }
+
+    @Override
+    public void onContactSelected(ImageView photo, Uri contactUri, long contactId) {
+      // TODO(calderwoodra): Add impression logging
+      QuickContact.showQuickContact(
+          context, photo, contactUri, QuickContact.MODE_LARGE, null /* excludeMimes */);
+    }
   }
 
-  @Override // SearchFragmentListener
-  public void onCallPlacedFromSearch() {
-    // TODO(calderwoodra): logging
+  /** @see OnDialpadQueryChangedListener */
+  private static final class MainOnDialpadQueryChangedListener
+      implements OnDialpadQueryChangedListener {
+
+    private final MainSearchController searchController;
+
+    MainOnDialpadQueryChangedListener(MainSearchController searchController) {
+      this.searchController = searchController;
+    }
+
+    @Override
+    public void onDialpadQueryChanged(String query) {
+      searchController.onDialpadQueryChanged(query);
+    }
+  }
+
+  /** @see DialpadListener */
+  private static final class MainDialpadListener implements DialpadListener {
+
+    private final MainSearchController searchController;
+    private final Context context;
+    private final UiListener<String> listener;
+
+    MainDialpadListener(
+        Context context, MainSearchController searchController, UiListener<String> uiListener) {
+      this.context = context;
+      this.searchController = searchController;
+      this.listener = uiListener;
+    }
+
+    @Override
+    public void getLastOutgoingCall(LastOutgoingCallCallback callback) {
+      ListenableFuture<String> listenableFuture =
+          DialerExecutorComponent.get(context)
+              .backgroundExecutor()
+              .submit(() -> Calls.getLastOutgoingCall(context));
+      listener.listen(context, listenableFuture, callback::lastOutgoingCall, throwable -> {});
+    }
+
+    @Override
+    public void onDialpadShown() {
+      searchController.onDialpadShown();
+    }
+
+    @Override
+    public void onCallPlacedFromDialpad() {
+      // TODO(calderwoodra): logging
+    }
+  }
+
+  /** @see SearchFragmentListener */
+  private static final class MainSearchFragmentListener implements SearchFragmentListener {
+
+    private final MainSearchController searchController;
+
+    MainSearchFragmentListener(MainSearchController searchController) {
+      this.searchController = searchController;
+    }
+
+    @Override
+    public void onSearchListTouch() {
+      searchController.onSearchListTouch();
+    }
+
+    @Override
+    public void onCallPlacedFromSearch() {
+      // TODO(calderwoodra): logging
+    }
+  }
+
+  /** @see DialpadFragment.HostInterface */
+  private static final class MainDialpadFragmentHost implements DialpadFragment.HostInterface {
+
+    @Override
+    public boolean onDialpadSpacerTouchWithEmptyQuery() {
+      // No-op, just let the clicks fall through to the search list
+      return false;
+    }
   }
 
   /**