Loading java/com/android/dialer/app/res/values/styles.xml +4 −1 Original line number Diff line number Diff line Loading @@ -21,7 +21,7 @@ <item name="android:colorAccent">@color/dialtacts_theme_color</item> </style> <style name="DialtactsTheme" parent="DialerThemeBase"> <style name="DialtactsThemeBase" parent="DialerThemeBase"> <!-- Styles that require AppCompat compatibility, remember to update both sets --> <item name="android:windowActionBarOverlay">true</item> Loading Loading @@ -77,6 +77,9 @@ <item name="dialpad_style">@style/Dialpad.Light</item> </style> <style name="DialtactsTheme" parent="DialtactsThemeBase"> </style> <!-- Action bar overflow menu icon. White with no shadow. --> <style name="DialtactsActionBarOverflowWhite" parent="@android:style/Widget.Material.Light.ActionButton.Overflow"> Loading java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java +48 −13 Original line number Diff line number Diff line Loading @@ -29,39 +29,74 @@ import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.calllog.datasources.DataSources; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor.Worker; import com.android.dialer.common.concurrent.Annotations.UiSerial; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.storage.Unencrypted; import com.google.common.util.concurrent.ListenableScheduledFuture; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.inject.Inject; /** * Worker which brings the annotated call log up to date, if necessary. * * <p>Accepts a boolean which indicates if the dirty check should be skipped. /** Brings the annotated call log up to date, if necessary. */ public class RefreshAnnotatedCallLogWorker { /* * This is a reasonable time that it might take between related call log writes, that also * shouldn't slow down single-writes too much. For example, when populating the database using * the simulator, using this value results in ~6 refresh cycles (on a release build) to write 120 * call log entries. */ public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Void> { private static final long WAIT_MILLIS = 100L; private final Context appContext; private final DataSources dataSources; private final SharedPreferences sharedPreferences; private final ListeningScheduledExecutorService listeningScheduledExecutorService; private ListenableScheduledFuture<Void> scheduledFuture; @Inject RefreshAnnotatedCallLogWorker( @ApplicationContext Context appContext, DataSources dataSources, @Unencrypted SharedPreferences sharedPreferences) { @Unencrypted SharedPreferences sharedPreferences, @UiSerial ScheduledExecutorService serialUiExecutorService) { this.appContext = appContext; this.dataSources = dataSources; this.sharedPreferences = sharedPreferences; this.listeningScheduledExecutorService = MoreExecutors.listeningDecorator(serialUiExecutorService); } @Override public Void doInBackground(Boolean skipDirtyCheck) /** Checks if the annotated call log is dirty and refreshes it if necessary. */ public ListenableScheduledFuture<Void> refreshWithDirtyCheck() { return refresh(true); } /** Refreshes the annotated call log, bypassing dirty checks. */ public ListenableScheduledFuture<Void> refreshWithoutDirtyCheck() { return refresh(false); } private ListenableScheduledFuture<Void> refresh(boolean checkDirty) { if (scheduledFuture != null) { LogUtil.i("RefreshAnnotatedCallLogWorker.refresh", "cancelling waiting task"); scheduledFuture.cancel(false /* mayInterrupt */); } scheduledFuture = listeningScheduledExecutorService.schedule( () -> doInBackground(checkDirty), WAIT_MILLIS, TimeUnit.MILLISECONDS); return scheduledFuture; } @WorkerThread private Void doInBackground(boolean checkDirty) throws RemoteException, OperationApplicationException { LogUtil.enterBlock("RefreshAnnotatedCallLogWorker.doInBackground"); long startTime = System.currentTimeMillis(); checkDirtyAndRebuildIfNecessary(appContext, skipDirtyCheck); checkDirtyAndRebuildIfNecessary(appContext, checkDirty); LogUtil.i( "RefreshAnnotatedCallLogWorker.doInBackground", "took %dms", Loading @@ -70,7 +105,7 @@ public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Void> { } @WorkerThread private void checkDirtyAndRebuildIfNecessary(Context appContext, boolean skipDirtyCheck) private void checkDirtyAndRebuildIfNecessary(Context appContext, boolean checkDirty) throws RemoteException, OperationApplicationException { Assert.isWorkerThread(); Loading @@ -86,7 +121,7 @@ public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Void> { "annotated call log has been marked dirty or does not exist"); } boolean isDirty = skipDirtyCheck || forceRebuildPrefValue || isDirty(appContext); boolean isDirty = !checkDirty || forceRebuildPrefValue || isDirty(appContext); LogUtil.i( "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", Loading java/com/android/dialer/calllog/ui/NewCallLogFragment.java +15 −23 Original line number Diff line number Diff line Loading @@ -28,24 +28,18 @@ import android.view.ViewGroup; import com.android.dialer.calllog.CallLogComponent; import com.android.dialer.calllog.CallLogFramework; import com.android.dialer.calllog.CallLogFramework.CallLogUi; import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.common.concurrent.DialerExecutorFactory; import com.android.dialer.common.concurrent.UiListener; import com.google.common.util.concurrent.ListenableScheduledFuture; /** The "new" call log fragment implementation, which is built on top of the annotated call log. */ public final class NewCallLogFragment extends Fragment implements CallLogUi, LoaderCallbacks<Cursor> { /* * This is a reasonable time that it might take between related call log writes, that also * shouldn't slow down single-writes too much. For example, when populating the database using * the simulator, using this value results in ~6 refresh cycles (on a release build) to write 120 * call log entries. */ private static final long WAIT_MILLIS = 100L; private DialerExecutor<Boolean> refreshAnnotatedCallLogTask; private RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker; private UiListener<Void> refreshAnnotatedCallLogListener; private RecyclerView recyclerView; public NewCallLogFragment() { Loading @@ -62,17 +56,12 @@ public final class NewCallLogFragment extends Fragment CallLogFramework callLogFramework = component.callLogFramework(); callLogFramework.attachUi(this); DialerExecutorFactory dialerExecutorFactory = DialerExecutorComponent.get(getContext()).dialerExecutorFactory(); // TODO(zachh): Use support fragment manager and add support for them in executors library. refreshAnnotatedCallLogTask = dialerExecutorFactory .createUiTaskBuilder( getActivity().getFragmentManager(), "NewCallLogFragment.refreshAnnotatedCallLog", component.getRefreshAnnotatedCallLogWorker()) .build(); refreshAnnotatedCallLogListener = DialerExecutorComponent.get(getContext()) .createUiListener( getActivity().getFragmentManager(), "NewCallLogFragment.refreshAnnotatedCallLog"); refreshAnnotatedCallLogWorker = component.getRefreshAnnotatedCallLogWorker(); } @Override Loading Loading @@ -120,13 +109,16 @@ public final class NewCallLogFragment extends Fragment private void checkAnnotatedCallLogDirtyAndRefreshIfNecessary() { LogUtil.enterBlock("NewCallLogFragment.checkAnnotatedCallLogDirtyAndRefreshIfNecessary"); refreshAnnotatedCallLogTask.executeSerialWithWait(false /* skipDirtyCheck */, WAIT_MILLIS); ListenableScheduledFuture<Void> future = refreshAnnotatedCallLogWorker.refreshWithDirtyCheck(); refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); } @Override public void invalidateUi() { LogUtil.enterBlock("NewCallLogFragment.invalidateUi"); refreshAnnotatedCallLogTask.executeSerialWithWait(true /* skipDirtyCheck */, WAIT_MILLIS); ListenableScheduledFuture<Void> future = refreshAnnotatedCallLogWorker.refreshWithoutDirtyCheck(); refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); } @Override Loading java/com/android/dialer/common/concurrent/DialerExecutorComponent.java +11 −0 Original line number Diff line number Diff line Loading @@ -16,9 +16,12 @@ package com.android.dialer.common.concurrent; import android.app.FragmentManager; import android.content.Context; import com.android.dialer.common.concurrent.Annotations.NonUiParallel; import com.android.dialer.common.concurrent.Annotations.Ui; import com.android.dialer.inject.HasRootComponent; import com.google.common.util.concurrent.ListeningExecutorService; import dagger.Subcomponent; import java.util.concurrent.ExecutorService; Loading @@ -28,6 +31,14 @@ public abstract class DialerExecutorComponent { public abstract DialerExecutorFactory dialerExecutorFactory(); @Ui public abstract ListeningExecutorService uiExecutorService(); public <OutputT> UiListener<OutputT> createUiListener( FragmentManager fragmentManager, String taskId) { return UiListener.create(uiExecutorService(), fragmentManager, taskId); } @NonUiParallel public abstract ExecutorService lowPriorityThreadPool(); Loading java/com/android/dialer/common/concurrent/UiListener.java 0 → 100644 +143 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 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.app.Fragment; import android.app.FragmentManager; import android.os.Bundle; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; 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; import java.util.concurrent.Executor; /** * 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 Activity { * * private UiListener<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(future, this::onSuccess, this::onFailure); * } * } * </pre></code> */ public class UiListener<OutputT> extends Fragment { private Executor uiThreadExecutor; private CallbackWrapper<OutputT> callbackWrapper; @MainThread static <OutputT> UiListener<OutputT> create( Executor uiThreadExecutor, FragmentManager fragmentManager, String taskId) { @SuppressWarnings("unchecked") UiListener<OutputT> uiListener = (UiListener<OutputT>) fragmentManager.findFragmentByTag(taskId); if (uiListener == null) { LogUtil.i("UiListener.create", "creating new UiListener for " + taskId); uiListener = new UiListener<>(); uiListener.uiThreadExecutor = uiThreadExecutor; fragmentManager.beginTransaction().add(uiListener, taskId).commit(); } return uiListener; } /** * Adds the specified listeners to the provided future. * * <p>The listeners are not called if the UI component this {@link UiListener} is declared in is * dead. */ @MainThread public void listen( @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, uiThreadExecutor); } 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("UiListener.runTask", "task succeeded but UI is dead"); } else { successListener.onSuccess(output); } } @Override public void onFailure(Throwable throwable) { LogUtil.e("UiListener.runTask", "task failed", throwable); if (failureListener == null) { LogUtil.i("UiListener.runTask", "task failed but UI is dead"); } else { failureListener.onFailure(throwable); } } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public void onDetach() { super.onDetach(); LogUtil.enterBlock("UiListener.onDetach"); if (callbackWrapper != null) { callbackWrapper.successListener = null; callbackWrapper.failureListener = null; } } } Loading
java/com/android/dialer/app/res/values/styles.xml +4 −1 Original line number Diff line number Diff line Loading @@ -21,7 +21,7 @@ <item name="android:colorAccent">@color/dialtacts_theme_color</item> </style> <style name="DialtactsTheme" parent="DialerThemeBase"> <style name="DialtactsThemeBase" parent="DialerThemeBase"> <!-- Styles that require AppCompat compatibility, remember to update both sets --> <item name="android:windowActionBarOverlay">true</item> Loading Loading @@ -77,6 +77,9 @@ <item name="dialpad_style">@style/Dialpad.Light</item> </style> <style name="DialtactsTheme" parent="DialtactsThemeBase"> </style> <!-- Action bar overflow menu icon. White with no shadow. --> <style name="DialtactsActionBarOverflowWhite" parent="@android:style/Widget.Material.Light.ActionButton.Overflow"> Loading
java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java +48 −13 Original line number Diff line number Diff line Loading @@ -29,39 +29,74 @@ import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.calllog.datasources.DataSources; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor.Worker; import com.android.dialer.common.concurrent.Annotations.UiSerial; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.storage.Unencrypted; import com.google.common.util.concurrent.ListenableScheduledFuture; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.inject.Inject; /** * Worker which brings the annotated call log up to date, if necessary. * * <p>Accepts a boolean which indicates if the dirty check should be skipped. /** Brings the annotated call log up to date, if necessary. */ public class RefreshAnnotatedCallLogWorker { /* * This is a reasonable time that it might take between related call log writes, that also * shouldn't slow down single-writes too much. For example, when populating the database using * the simulator, using this value results in ~6 refresh cycles (on a release build) to write 120 * call log entries. */ public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Void> { private static final long WAIT_MILLIS = 100L; private final Context appContext; private final DataSources dataSources; private final SharedPreferences sharedPreferences; private final ListeningScheduledExecutorService listeningScheduledExecutorService; private ListenableScheduledFuture<Void> scheduledFuture; @Inject RefreshAnnotatedCallLogWorker( @ApplicationContext Context appContext, DataSources dataSources, @Unencrypted SharedPreferences sharedPreferences) { @Unencrypted SharedPreferences sharedPreferences, @UiSerial ScheduledExecutorService serialUiExecutorService) { this.appContext = appContext; this.dataSources = dataSources; this.sharedPreferences = sharedPreferences; this.listeningScheduledExecutorService = MoreExecutors.listeningDecorator(serialUiExecutorService); } @Override public Void doInBackground(Boolean skipDirtyCheck) /** Checks if the annotated call log is dirty and refreshes it if necessary. */ public ListenableScheduledFuture<Void> refreshWithDirtyCheck() { return refresh(true); } /** Refreshes the annotated call log, bypassing dirty checks. */ public ListenableScheduledFuture<Void> refreshWithoutDirtyCheck() { return refresh(false); } private ListenableScheduledFuture<Void> refresh(boolean checkDirty) { if (scheduledFuture != null) { LogUtil.i("RefreshAnnotatedCallLogWorker.refresh", "cancelling waiting task"); scheduledFuture.cancel(false /* mayInterrupt */); } scheduledFuture = listeningScheduledExecutorService.schedule( () -> doInBackground(checkDirty), WAIT_MILLIS, TimeUnit.MILLISECONDS); return scheduledFuture; } @WorkerThread private Void doInBackground(boolean checkDirty) throws RemoteException, OperationApplicationException { LogUtil.enterBlock("RefreshAnnotatedCallLogWorker.doInBackground"); long startTime = System.currentTimeMillis(); checkDirtyAndRebuildIfNecessary(appContext, skipDirtyCheck); checkDirtyAndRebuildIfNecessary(appContext, checkDirty); LogUtil.i( "RefreshAnnotatedCallLogWorker.doInBackground", "took %dms", Loading @@ -70,7 +105,7 @@ public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Void> { } @WorkerThread private void checkDirtyAndRebuildIfNecessary(Context appContext, boolean skipDirtyCheck) private void checkDirtyAndRebuildIfNecessary(Context appContext, boolean checkDirty) throws RemoteException, OperationApplicationException { Assert.isWorkerThread(); Loading @@ -86,7 +121,7 @@ public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Void> { "annotated call log has been marked dirty or does not exist"); } boolean isDirty = skipDirtyCheck || forceRebuildPrefValue || isDirty(appContext); boolean isDirty = !checkDirty || forceRebuildPrefValue || isDirty(appContext); LogUtil.i( "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", Loading
java/com/android/dialer/calllog/ui/NewCallLogFragment.java +15 −23 Original line number Diff line number Diff line Loading @@ -28,24 +28,18 @@ import android.view.ViewGroup; import com.android.dialer.calllog.CallLogComponent; import com.android.dialer.calllog.CallLogFramework; import com.android.dialer.calllog.CallLogFramework.CallLogUi; import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.common.concurrent.DialerExecutorFactory; import com.android.dialer.common.concurrent.UiListener; import com.google.common.util.concurrent.ListenableScheduledFuture; /** The "new" call log fragment implementation, which is built on top of the annotated call log. */ public final class NewCallLogFragment extends Fragment implements CallLogUi, LoaderCallbacks<Cursor> { /* * This is a reasonable time that it might take between related call log writes, that also * shouldn't slow down single-writes too much. For example, when populating the database using * the simulator, using this value results in ~6 refresh cycles (on a release build) to write 120 * call log entries. */ private static final long WAIT_MILLIS = 100L; private DialerExecutor<Boolean> refreshAnnotatedCallLogTask; private RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker; private UiListener<Void> refreshAnnotatedCallLogListener; private RecyclerView recyclerView; public NewCallLogFragment() { Loading @@ -62,17 +56,12 @@ public final class NewCallLogFragment extends Fragment CallLogFramework callLogFramework = component.callLogFramework(); callLogFramework.attachUi(this); DialerExecutorFactory dialerExecutorFactory = DialerExecutorComponent.get(getContext()).dialerExecutorFactory(); // TODO(zachh): Use support fragment manager and add support for them in executors library. refreshAnnotatedCallLogTask = dialerExecutorFactory .createUiTaskBuilder( getActivity().getFragmentManager(), "NewCallLogFragment.refreshAnnotatedCallLog", component.getRefreshAnnotatedCallLogWorker()) .build(); refreshAnnotatedCallLogListener = DialerExecutorComponent.get(getContext()) .createUiListener( getActivity().getFragmentManager(), "NewCallLogFragment.refreshAnnotatedCallLog"); refreshAnnotatedCallLogWorker = component.getRefreshAnnotatedCallLogWorker(); } @Override Loading Loading @@ -120,13 +109,16 @@ public final class NewCallLogFragment extends Fragment private void checkAnnotatedCallLogDirtyAndRefreshIfNecessary() { LogUtil.enterBlock("NewCallLogFragment.checkAnnotatedCallLogDirtyAndRefreshIfNecessary"); refreshAnnotatedCallLogTask.executeSerialWithWait(false /* skipDirtyCheck */, WAIT_MILLIS); ListenableScheduledFuture<Void> future = refreshAnnotatedCallLogWorker.refreshWithDirtyCheck(); refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); } @Override public void invalidateUi() { LogUtil.enterBlock("NewCallLogFragment.invalidateUi"); refreshAnnotatedCallLogTask.executeSerialWithWait(true /* skipDirtyCheck */, WAIT_MILLIS); ListenableScheduledFuture<Void> future = refreshAnnotatedCallLogWorker.refreshWithoutDirtyCheck(); refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); } @Override Loading
java/com/android/dialer/common/concurrent/DialerExecutorComponent.java +11 −0 Original line number Diff line number Diff line Loading @@ -16,9 +16,12 @@ package com.android.dialer.common.concurrent; import android.app.FragmentManager; import android.content.Context; import com.android.dialer.common.concurrent.Annotations.NonUiParallel; import com.android.dialer.common.concurrent.Annotations.Ui; import com.android.dialer.inject.HasRootComponent; import com.google.common.util.concurrent.ListeningExecutorService; import dagger.Subcomponent; import java.util.concurrent.ExecutorService; Loading @@ -28,6 +31,14 @@ public abstract class DialerExecutorComponent { public abstract DialerExecutorFactory dialerExecutorFactory(); @Ui public abstract ListeningExecutorService uiExecutorService(); public <OutputT> UiListener<OutputT> createUiListener( FragmentManager fragmentManager, String taskId) { return UiListener.create(uiExecutorService(), fragmentManager, taskId); } @NonUiParallel public abstract ExecutorService lowPriorityThreadPool(); Loading
java/com/android/dialer/common/concurrent/UiListener.java 0 → 100644 +143 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 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.app.Fragment; import android.app.FragmentManager; import android.os.Bundle; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; 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; import java.util.concurrent.Executor; /** * 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 Activity { * * private UiListener<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(future, this::onSuccess, this::onFailure); * } * } * </pre></code> */ public class UiListener<OutputT> extends Fragment { private Executor uiThreadExecutor; private CallbackWrapper<OutputT> callbackWrapper; @MainThread static <OutputT> UiListener<OutputT> create( Executor uiThreadExecutor, FragmentManager fragmentManager, String taskId) { @SuppressWarnings("unchecked") UiListener<OutputT> uiListener = (UiListener<OutputT>) fragmentManager.findFragmentByTag(taskId); if (uiListener == null) { LogUtil.i("UiListener.create", "creating new UiListener for " + taskId); uiListener = new UiListener<>(); uiListener.uiThreadExecutor = uiThreadExecutor; fragmentManager.beginTransaction().add(uiListener, taskId).commit(); } return uiListener; } /** * Adds the specified listeners to the provided future. * * <p>The listeners are not called if the UI component this {@link UiListener} is declared in is * dead. */ @MainThread public void listen( @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, uiThreadExecutor); } 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("UiListener.runTask", "task succeeded but UI is dead"); } else { successListener.onSuccess(output); } } @Override public void onFailure(Throwable throwable) { LogUtil.e("UiListener.runTask", "task failed", throwable); if (failureListener == null) { LogUtil.i("UiListener.runTask", "task failed but UI is dead"); } else { failureListener.onFailure(throwable); } } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public void onDetach() { super.onDetach(); LogUtil.enterBlock("UiListener.onDetach"); if (callbackWrapper != null) { callbackWrapper.successListener = null; callbackWrapper.failureListener = null; } } }