| // 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; |
| } |
| } |