From a5a7d014d36fc42a428794b7d62070549cc8ba7b Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Wed, 11 Apr 2018 13:50:14 -0700 Subject: Add support for support fragments in UiListener. Bug: 36841782 Test: SupportUiListenerTest PiperOrigin-RevId: 192502743 Change-Id: Id06ed732528db1ae486def86ecc2f44828635d81 --- .../common/concurrent/DialerExecutorComponent.java | 9 ++ .../common/concurrent/SupportUiListener.java | 154 +++++++++++++++++++++ .../dialer/common/concurrent/UiListener.java | 2 + 3 files changed, 165 insertions(+) create mode 100644 java/com/android/dialer/common/concurrent/SupportUiListener.java (limited to 'java/com/android/dialer/common/concurrent') diff --git a/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java b/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java index 734891430..f4552b203 100644 --- a/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java +++ b/java/com/android/dialer/common/concurrent/DialerExecutorComponent.java @@ -50,6 +50,15 @@ public abstract class DialerExecutorComponent { return UiListener.create(fragmentManager, taskId); } + /** + * Version of {@link #createUiListener(FragmentManager, String)} that accepts support fragment + * manager. + */ + public SupportUiListener 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 000000000..5e3958619 --- /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. + * + *

Callbacks are only executed if the UI component is still alive. + * + *

Example usage:

+ * public class MyActivity extends AppCompatActivity {
+ *
+ *   private SupportUiListener<MyOutputType> 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<MyOutputType> future = callSomeMethodReturningListenableFuture(input);
+ *     uiListener.listen(this, future, this::onSuccess, this::onFailure);
+ *   }
+ * }
+ * 
+ */ +public class SupportUiListener extends Fragment { + + private CallbackWrapper callbackWrapper; + + @MainThread + static SupportUiListener create( + FragmentManager fragmentManager, String taskId) { + @SuppressWarnings("unchecked") + SupportUiListener uiListener = + (SupportUiListener) 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. + * + *

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 future, + @NonNull SuccessListener 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 implements FutureCallback { + private SuccessListener successListener; + private FailureListener failureListener; + + private CallbackWrapper( + SuccessListener 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 b5922f9c8..a2d976f3b 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.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. * @@ -148,3 +149,4 @@ public class UiListener extends Fragment { } } } + -- cgit v1.2.3