blob: c20d3976e3276697989054021df20901329b818f [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.baku.examples.distro;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.os.Bundle;
import android.util.Log;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.chromium.base.PathUtils;
import org.joda.time.Duration;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import io.flutter.view.FlutterMain;
import io.flutter.view.FlutterView;
import io.v.android.VAndroidContext;
import io.v.android.VAndroidContexts;
import io.v.android.security.BlessingsManager;
import io.v.v23.options.RpcOptions;
import io.v.v23.security.Blessings;
import io.v.v23.vdl.ClientStream;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
import java8.util.Maps;
import lombok.RequiredArgsConstructor;
import rx.Subscription;
/**
* Activity representing the example 'app', a.k.a. the initiator/originator/master.
*/
public class DistroActivity extends Activity {
private static final String TAG = DistroActivity.class.getSimpleName();
private static final Duration PING_TIMEOUT = Duration.standardSeconds(3);
private static final long POLL_INTERVAL = 750;
private VAndroidContext context;
private FlutterView flutterView;
private final Map<String, ConnectionMonitor> clients = new HashMap<>();
private ScheduledExecutorService poller;
private Subscription subscription;
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
poller = new ScheduledThreadPoolExecutor(1);
FlutterMain.ensureInitializationComplete(getApplicationContext(), null);
setContentView(R.layout.flutter_layout);
flutterView = (FlutterView) findViewById(R.id.flutter_view);
File appBundle = new File(PathUtils.getDataDirectory(this),
FlutterMain.APP_BUNDLE);
flutterView.runFromBundle(appBundle.getPath(), null);
flutterView.sendToFlutter("runApp", "", r -> {
context = VAndroidContexts.withDefaults(this, savedInstanceState);
Futures.addCallback(BlessingsManager
.getBlessings(context.getVContext(), this, "blessings", true),
new FutureCallback<Blessings>() {
@Override
public void onSuccess(final Blessings blessings) {
onBlessingsAvailable(blessings);
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "Unable to attain blessings", t);
}
});
});
}
private void onBlessingsAvailable(final Blessings blessings) {
final Intent castIntent = new Intent(DistroActivity.this,
DistroAndroidService.class);
try {
castIntent.putExtra(DistroAndroidService.BLESSINGS_EXTRA,
VomUtil.encodeToString(blessings, Blessings.class));
} catch (final VException e) {
Log.e(TAG, "Unable to encode blessings", e);
}
startService(castIntent);
subscription = startScanning();
flutterView.addOnMessageListener("initiateCast", this::initiateCast);
flutterView.addOnMessageListener("updateCast", this::updateCast);
flutterView.addOnMessageListener("terminateCast", this::terminateCast);
}
@Override
protected void onDestroy() {
if (subscription != null) {
subscription.unsubscribe();
}
poller.shutdown();
clients.clear();
if (flutterView != null) {
flutterView.destroy();
}
// TODO(rosswang): proactively signal cast termination; right now doing that intelligently
// would necessitate keeping some extra state in the Java layer that's already in the
// Flutter layer, so let's wait until the Flutter plug-in system is better fleshed out. For
// now, we'll just wait for the (short) timeout.
context.close();
Log.i(TAG, "Closed Vanadium context");
super.onDestroy();
}
@Override
protected void onPause() {
super.onPause();
flutterView.onPause();
}
@Override
protected void onResume() {
super.onResume();
flutterView.onResume();
}
@Override
protected void onNewIntent(Intent intent) {
// Reload the Flutter Dart code when the activity receives an intent
// from the "flutter refresh" command.
// This feature should only be enabled during development. Use the
// debuggable flag as an indicator that we are in development mode.
if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
if (Intent.ACTION_RUN.equals(intent.getAction())) {
flutterView.runFromBundle(intent.getDataString(),
intent.getStringExtra("snapshot"));
}
}
}
private boolean isActive() {
return subscription != null && !subscription.isUnsubscribed();
}
private static abstract class TrappingCallback implements FutureCallback<Object> {
@Override
public void onSuccess(final @Nullable Object result) {
}
}
private class ConnectionMonitor implements FutureCallback<String> {
public final String name;
private final DistroClient client;
private ClientStream<State, State, Void> castStream;
private ListenableFuture<String> poll;
private final FutureCallback<Object> castTerminated = new FutureCallback<Object>() {
private void notifyFlutter() {
if (isActive()) {
flutterView.sendToFlutter("castTerminated", name);
}
}
@Override
public void onSuccess(final @Nullable Object result) {
Log.i(TAG, "Cast terminated by " + name);
notifyFlutter();
}
@Override
public void onFailure(final Throwable t) {
Log.i(TAG, "Cast terminated by " + name, t);
notifyFlutter();
}
};
private final TrappingCallback
trapDeviceOffline = new TrappingCallback() {
@Override
public void onFailure(final Throwable t) {
Log.i(TAG, "Lost device " + name, t);
if (isActive()) {
flutterView.sendToFlutter("deviceOffline", name);
terminateCast();
clients.remove(name);
}
}
},
trapCastTerminated = new TrappingCallback() {
@Override
public void onFailure(final Throwable t) {
castTerminated.onFailure(t);
};
};
public ConnectionMonitor(final String name) {
this.name = name;
client = DistroClientFactory.getDistroClient(name);
poll();
}
public void poll() {
poll = client.getDescription(context.getVContext().withTimeout(PING_TIMEOUT));
Futures.addCallback(poll, this);
}
@Override
public void onSuccess(final String description) {
if (isActive()) {
final JSONObject message = new JSONObject();
try {
message.put("name", name);
message.put("description", description);
} catch (final JSONException wtf) {
throw new RuntimeException(wtf);
}
flutterView.sendToFlutter("deviceOnline", message.toString());
poller.schedule(this::poll, POLL_INTERVAL, TimeUnit.MILLISECONDS);
}
}
@Override
public void onFailure(final Throwable t) {
trapDeviceOffline.onFailure(t);
}
public ConnectionMonitor initiateCast() {
castStream = client.cast(context.getVContext(), new RpcOptions()
.channelTimeout(DistroAndroidService.CHANNEL_TIMEOUT));
Futures.addCallback(castStream.recv(), castTerminated);
return this;
}
public void updateCast(final String data) {
Futures.addCallback(castStream.send(new State(data)), trapCastTerminated);
}
public void terminateCast() {
if (castStream != null) {
Futures.addCallback(castStream.finish(), castTerminated);
castStream = null;
}
}
}
private Subscription startScanning() {
return Disco.scanContinuously(context)
.subscribe(name -> Maps.computeIfAbsent(clients, name, ConnectionMonitor::new),
t -> context.getErrorReporter().onError(R.string.err_scan, t));
}
@RequiredArgsConstructor
private static class CastMessage {
public final String name, data;
public static CastMessage fromJson(final String json) {
final JSONObject message;
try {
message = new JSONObject(json);
return new CastMessage(
message.getString("name"),
message.getString("data"));
} catch (final JSONException e) {
throw new IllegalArgumentException(e);
}
}
}
private String initiateCast(final String json) {
final CastMessage message = CastMessage.fromJson(json);
clients.get(message.name)
.initiateCast()
.updateCast(message.data);
return null;
}
private String updateCast(final String json) {
final CastMessage message = CastMessage.fromJson(json);
clients.get(message.name)
.updateCast(message.data);
return null;
}
private String terminateCast(final String name) {
final ConnectionMonitor conn = clients.get(name);
if (conn != null) {
conn.terminateCast();
}
return null;
}
}