Commit 58df1a56 authored by Simeon Anfinrud's avatar Simeon Anfinrud Committed by Commit Bot

[Chromecast] Reactive intent handling in CWCA.

Added some features to reactive framework to make this smooth.

This is one step to refactoring CastWebContentsActivity and
adjacent classes to be testable. The ultimate goal is to express
the logic of CastWebContentsActivity and other classes in terms
of observable events, with the Activity, Service, and Fragment
classes being thin adapters from Android API events to reactive
Observables. That will let us cleanly separate concerns in the
behavior classes and mount them in lightweight, potentially non-
Robolectric test fixtures.

Bug: Internal b/36777136
Test: cast_base_junit_tests, cast_shell_junit_tests
Change-Id: Iacdeb0084e34d99193fc2733c2dbd42d9560b16a
Reviewed-on: https://chromium-review.googlesource.com/929892
Commit-Queue: Simeon Anfinrud <sanfin@chromium.org>
Reviewed-by: default avatarLuke Halliwell <halliwell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#539978}
parent d7a4d7bd
...@@ -26,6 +26,16 @@ public class Both<A, B> { ...@@ -26,6 +26,16 @@ public class Both<A, B> {
this.second = b; this.second = b;
} }
// Can be used as a method reference.
public A getFirst() {
return this.first;
}
// Can be used as a method reference.
public B getSecond() {
return this.second;
}
@Override @Override
public String toString() { public String toString() {
return new StringBuilder() return new StringBuilder()
......
...@@ -74,6 +74,19 @@ public abstract class Observable<T> { ...@@ -74,6 +74,19 @@ public abstract class Observable<T> {
return controller; return controller;
} }
/**
* Returns an Observable that is activated only when the given Observable is not activated.
*/
public static Observable<Unit> not(Observable<?> observable) {
Controller<Unit> opposite = new Controller<>();
opposite.set(Unit.unit());
observable.watch(() -> {
opposite.reset();
return () -> opposite.set(Unit.unit());
});
return opposite;
}
// Adapter of ScopeFactory to two callbacks that Observables use to notify of state changes. // Adapter of ScopeFactory to two callbacks that Observables use to notify of state changes.
protected static class StateObserver<T> { protected static class StateObserver<T> {
private final ScopeFactory<? super T> mFactory; private final ScopeFactory<? super T> mFactory;
......
...@@ -38,4 +38,18 @@ public class BothTest { ...@@ -38,4 +38,18 @@ public class BothTest {
Both<String, String> x = Both.both("a", "b"); Both<String, String> x = Both.both("a", "b");
assertEquals(x.toString(), "a, b"); assertEquals(x.toString(), "a, b");
} }
@Test
public void testUseGetFirstAsMethodReference() {
Both<Integer, String> x = Both.both(1, "one");
Function<Both<Integer, String>, Integer> getFirst = Both::getFirst;
assertEquals((int) getFirst.apply(x), 1);
}
@Test
public void testUseGetSecondAsMethodReference() {
Both<Integer, String> x = Both.both(2, "two");
Function<Both<Integer, String>, String> getSecond = Both::getSecond;
assertEquals(getSecond.apply(x), "two");
}
} }
...@@ -399,4 +399,52 @@ public class ObservableAndControllerTest { ...@@ -399,4 +399,52 @@ public class ObservableAndControllerTest {
controller.reset(); controller.reset();
assertThat(TransitionLogger.sResult, contains("enter: a", "exit: a")); assertThat(TransitionLogger.sResult, contains("enter: a", "exit: a"));
} }
@Test
public void testNotIsActivatedAtTheStart() {
Controller<String> invertThis = new Controller<>();
List<String> result = new ArrayList<>();
Observable.not(invertThis).watch(() -> {
result.add("enter inverted");
return () -> result.add("exit inverted");
});
assertThat(result, contains("enter inverted"));
}
@Test
public void testNotIsDeactivatedAtTheStartIfSourceIsAlreadyActivated() {
Controller<String> invertThis = new Controller<>();
List<String> result = new ArrayList<>();
invertThis.set("way ahead of you");
Observable.not(invertThis).watch(() -> {
result.add("enter inverted");
return () -> result.add("exit inverted");
});
assertThat(result, emptyIterable());
}
@Test
public void testNotExitsWhenSourceIsActivated() {
Controller<String> invertThis = new Controller<>();
List<String> result = new ArrayList<>();
Observable.not(invertThis).watch(() -> {
result.add("enter inverted");
return () -> result.add("exit inverted");
});
invertThis.set("first");
assertThat(result, contains("enter inverted", "exit inverted"));
}
@Test
public void testNotReentersWhenSourceIsReset() {
Controller<String> invertThis = new Controller<>();
List<String> result = new ArrayList<>();
Observable.not(invertThis).watch(() -> {
result.add("enter inverted");
return () -> result.add("exit inverted");
});
invertThis.set("first");
invertThis.reset();
assertThat(result, contains("enter inverted", "exit inverted", "enter inverted"));
}
} }
...@@ -5,13 +5,10 @@ ...@@ -5,13 +5,10 @@
package org.chromium.chromecast.shell; package org.chromium.chromecast.shell;
import android.app.Activity; import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
...@@ -19,8 +16,11 @@ import android.view.WindowManager; ...@@ -19,8 +16,11 @@ import android.view.WindowManager;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.Toast; import android.widget.Toast;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.chromecast.base.Both;
import org.chromium.chromecast.base.Controller;
import org.chromium.chromecast.base.Observable;
import org.chromium.chromecast.base.ScopeFactories;
import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContents;
/** /**
...@@ -36,18 +36,56 @@ public class CastWebContentsActivity extends Activity { ...@@ -36,18 +36,56 @@ public class CastWebContentsActivity extends Activity {
private static final String TAG = "cr_CastWebActivity"; private static final String TAG = "cr_CastWebActivity";
private static final boolean DEBUG = true; private static final boolean DEBUG = true;
// Tracks the most recent Intent for the Activity.
private final Controller<Intent> mGotIntentState = new Controller<>();
// Set this to cause the Activity to finish.
private final Controller<String> mIsFinishingState = new Controller<>();
private CastWebContentsSurfaceHelper mSurfaceHelper; private CastWebContentsSurfaceHelper mSurfaceHelper;
{
// Create an Observable that only supplies the Intent when not finishing.
Observable<Intent> hasIntentState =
mGotIntentState.and(Observable.not(mIsFinishingState)).transform(Both::getFirst);
// Register handler for web content stopped event while we have an Intent.
hasIntentState.watch(() -> {
IntentFilter filter = new IntentFilter();
filter.addAction(CastIntents.ACTION_ON_WEB_CONTENT_STOPPED);
return new LocalBroadcastReceiverScope(filter, (Intent intent) -> {
mIsFinishingState.set("Stopped by intent: " + intent.getAction());
});
});
// Handle each new Intent.
hasIntentState.watch(ScopeFactories.onEnter(this ::handleIntent));
mIsFinishingState.watch(ScopeFactories.onEnter((String reason) -> {
if (DEBUG) Log.d(TAG, "Finishing activity: " + reason);
finish();
}));
// If a new Intent arrives after finishing, start a new Activity instead of recycling this.
mGotIntentState.and(mIsFinishingState)
.transform(Both::getFirst)
.watch(ScopeFactories.onEnter((Intent intent) -> {
Log.d(TAG,
"Got intent while finishing current activity, so start new activity.");
int flags = intent.getFlags();
flags = flags & ~Intent.FLAG_ACTIVITY_SINGLE_TOP;
intent.setFlags(flags);
startActivity(intent);
}));
}
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate"); if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (!CastBrowserHelper.initializeBrowser(getApplicationContext())) { if (!CastBrowserHelper.initializeBrowser(getApplicationContext())) {
Toast.makeText(this, R.string.browser_process_initialization_failed, Toast.LENGTH_SHORT) Toast.makeText(this, R.string.browser_process_initialization_failed, Toast.LENGTH_SHORT)
.show(); .show();
finish(); mIsFinishingState.set("Failed to initialize browser");
} }
// Set flags to both exit sleep mode when this activity starts and // Set flags to both exit sleep mode when this activity starts and
...@@ -62,24 +100,7 @@ public class CastWebContentsActivity extends Activity { ...@@ -62,24 +100,7 @@ public class CastWebContentsActivity extends Activity {
(FrameLayout) findViewById(R.id.web_contents_container), (FrameLayout) findViewById(R.id.web_contents_container),
false /* showInFragment */); false /* showInFragment */);
// Receiver to handle on web content stopped event mGotIntentState.set(getIntent());
BroadcastReceiver stopEventReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.d(TAG, "Intent action=" + intent.getAction());
getLocalBroadcastManager().unregisterReceiver(this);
finish();
}
};
IntentFilter stopReceiverFilter = new IntentFilter();
stopReceiverFilter.addAction(CastIntents.ACTION_ON_WEB_CONTENT_STOPPED);
getLocalBroadcastManager().registerReceiver(stopEventReceiver, stopReceiverFilter);
handleIntent(getIntent());
}
private LocalBroadcastManager getLocalBroadcastManager() {
return LocalBroadcastManager.getInstance(ContextUtils.getApplicationContext());
} }
protected void handleIntent(Intent intent) { protected void handleIntent(Intent intent) {
...@@ -115,22 +136,9 @@ public class CastWebContentsActivity extends Activity { ...@@ -115,22 +136,9 @@ public class CastWebContentsActivity extends Activity {
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
if (DEBUG) Log.d(TAG, "onNewIntent"); if (DEBUG) Log.d(TAG, "onNewIntent");
mGotIntentState.set(intent);
// If we're currently finishing this activity, we should start a new activity to
// display the new app.
if (isFinishing()) {
Log.d(TAG, "Activity is finishing, starting new activity.");
int flags = intent.getFlags();
flags = flags & ~Intent.FLAG_ACTIVITY_SINGLE_TOP;
intent.setFlags(flags);
startActivity(intent);
return;
}
handleIntent(intent);
} }
@Override @Override
protected void onStart() { protected void onStart() {
if (DEBUG) Log.d(TAG, "onStart"); if (DEBUG) Log.d(TAG, "onStart");
...@@ -168,6 +176,7 @@ public class CastWebContentsActivity extends Activity { ...@@ -168,6 +176,7 @@ public class CastWebContentsActivity extends Activity {
if (mSurfaceHelper != null) { if (mSurfaceHelper != null) {
mSurfaceHelper.onDestroy(); mSurfaceHelper.onDestroy();
} }
mGotIntentState.reset();
super.onDestroy(); super.onDestroy();
} }
...@@ -203,7 +212,7 @@ public class CastWebContentsActivity extends Activity { ...@@ -203,7 +212,7 @@ public class CastWebContentsActivity extends Activity {
// Stop key should end the entire session. // Stop key should end the entire session.
if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP) { if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP) {
finish(); mIsFinishingState.set("User pressed STOP key");
} }
return true; return true;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment