Merge "Disable watchdog on OSX using compile-time checks"
diff --git a/include/perfetto/ext/tracing/core/BUILD.gn b/include/perfetto/ext/tracing/core/BUILD.gn
index 5dc8c88..b263ec0 100644
--- a/include/perfetto/ext/tracing/core/BUILD.gn
+++ b/include/perfetto/ext/tracing/core/BUILD.gn
@@ -28,6 +28,7 @@
     "shared_memory_abi.h",
     "shared_memory_arbiter.h",
     "slice.h",
+    "sliced_protobuf_input_stream.h",
     "startup_trace_writer.h",
     "startup_trace_writer_registry.h",
     "trace_packet.h",
diff --git a/src/tracing/core/sliced_protobuf_input_stream.h b/include/perfetto/ext/tracing/core/sliced_protobuf_input_stream.h
similarity index 89%
rename from src/tracing/core/sliced_protobuf_input_stream.h
rename to include/perfetto/ext/tracing/core/sliced_protobuf_input_stream.h
index fe743d6..d305cf4 100644
--- a/src/tracing/core/sliced_protobuf_input_stream.h
+++ b/include/perfetto/ext/tracing/core/sliced_protobuf_input_stream.h
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-#ifndef SRC_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
-#define SRC_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
+#ifndef INCLUDE_PERFETTO_EXT_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
+#define INCLUDE_PERFETTO_EXT_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
 
 #include "perfetto/ext/tracing/core/slice.h"
 
@@ -60,4 +60,4 @@
 
 }  // namespace perfetto
 
-#endif  // SRC_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
+#endif  // INCLUDE_PERFETTO_EXT_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
diff --git a/include/perfetto/ext/tracing/core/trace_packet.h b/include/perfetto/ext/tracing/core/trace_packet.h
index 80f1018..d695165 100644
--- a/include/perfetto/ext/tracing/core/trace_packet.h
+++ b/include/perfetto/ext/tracing/core/trace_packet.h
@@ -21,7 +21,6 @@
 #include <memory>
 #include <tuple>
 
-#include <google/protobuf/io/zero_copy_stream.h>
 #include "perfetto/base/export.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/tracing/core/slice.h"
@@ -43,7 +42,6 @@
 class PERFETTO_EXPORT TracePacket {
  public:
   using const_iterator = Slices::const_iterator;
-  using ZeroCopyInputStream = ::google::protobuf::io::ZeroCopyInputStream;
 
   // The field id of protos::Trace::packet, static_assert()-ed in the unittest.
   static constexpr uint32_t kPacketFieldNumber = 1;
@@ -56,20 +54,6 @@
   // Accesses all the raw slices in the packet, for saving them to file/network.
   const Slices& slices() const { return slices_; }
 
-  // Decodes the packet. This function requires that the caller:
-  // 1) Does #include "protos/perfetto/trace/trace_packet.pb.h"
-  // 2) Links against the //protos/trace:lite target.
-  // The core service code deliberately doesn't link against that in order to
-  // avoid binary bloat. This is the reason why this is a templated function.
-  // It doesn't need to be (i.e. the caller should not specify the template
-  // argument) but doing so prevents the compiler trying to resolve the
-  // TracePacket type until it's needed, in which case the caller needs (1).
-  template <typename TracePacketType = protos::TracePacket>
-  bool Decode(TracePacketType* packet) const {
-    std::unique_ptr<ZeroCopyInputStream> istr = CreateSlicedInputStream();
-    return packet->ParseFromZeroCopyStream(istr.get());
-  }
-
   // Mutator, used only by the service and tests.
   void AddSlice(Slice);
 
@@ -86,12 +70,14 @@
   // and its size.
   std::tuple<char*, size_t> GetProtoPreamble();
 
+  // Returns the raw protobuf bytes of the slices, all stitched together into
+  // a string. Only for testing.
+  std::string GetRawBytesForTesting();
+
  private:
   TracePacket(const TracePacket&) = delete;
   TracePacket& operator=(const TracePacket&) = delete;
 
-  std::unique_ptr<ZeroCopyInputStream> CreateSlicedInputStream() const;
-
   Slices slices_;     // Not owned.
   size_t size_ = 0;   // SUM(slice.size for slice in slices_).
   char preamble_[8];  // Deliberately not initialized.
diff --git a/src/ipc/buffered_frame_deserializer.cc b/src/ipc/buffered_frame_deserializer.cc
index 9974654..dc107f7 100644
--- a/src/ipc/buffered_frame_deserializer.cc
+++ b/src/ipc/buffered_frame_deserializer.cc
@@ -22,7 +22,6 @@
 #include <type_traits>
 #include <utility>
 
-#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/utils.h"
 
@@ -167,9 +166,7 @@
   if (size == 0)
     return;
   std::unique_ptr<Frame> frame(new Frame);
-  const int sz = static_cast<int>(size);
-  ::google::protobuf::io::ArrayInputStream stream(data, sz);
-  if (frame->ParseFromBoundedZeroCopyStream(&stream, sz))
+  if (frame->ParseFromArray(data, static_cast<int>(size)))
     decoded_frames_.push_back(std::move(frame));
 }
 
diff --git a/src/tracing/BUILD.gn b/src/tracing/BUILD.gn
index 5067570..4a13c32 100644
--- a/src/tracing/BUILD.gn
+++ b/src/tracing/BUILD.gn
@@ -55,7 +55,6 @@
     "core/shared_memory_arbiter_impl.cc",
     "core/shared_memory_arbiter_impl.h",
     "core/sliced_protobuf_input_stream.cc",
-    "core/sliced_protobuf_input_stream.h",
     "core/startup_trace_writer.cc",
     "core/startup_trace_writer_registry.cc",
     "core/test_config.cc",
diff --git a/src/tracing/core/packet_stream_validator.h b/src/tracing/core/packet_stream_validator.h
index 8494f4e..af3d477 100644
--- a/src/tracing/core/packet_stream_validator.h
+++ b/src/tracing/core/packet_stream_validator.h
@@ -17,7 +17,8 @@
 #ifndef SRC_TRACING_CORE_PACKET_STREAM_VALIDATOR_H_
 #define SRC_TRACING_CORE_PACKET_STREAM_VALIDATOR_H_
 
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
+#include "perfetto/base/export.h"
+#include "perfetto/ext/tracing/core/sliced_protobuf_input_stream.h"
 
 namespace perfetto {
 
@@ -30,7 +31,7 @@
 //
 // Note that we only validate top-level fields in the trace proto; sub-messages
 // are simply skipped.
-class PacketStreamValidator {
+class PERFETTO_EXPORT PacketStreamValidator {
  public:
   PacketStreamValidator() = delete;
 
diff --git a/src/tracing/core/sliced_protobuf_input_stream.cc b/src/tracing/core/sliced_protobuf_input_stream.cc
index bf81d89..3301e63 100644
--- a/src/tracing/core/sliced_protobuf_input_stream.cc
+++ b/src/tracing/core/sliced_protobuf_input_stream.cc
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
+#include "perfetto/ext/tracing/core/sliced_protobuf_input_stream.h"
 
 #include <algorithm>
 
diff --git a/src/tracing/core/sliced_protobuf_input_stream_unittest.cc b/src/tracing/core/sliced_protobuf_input_stream_unittest.cc
index 59f52f2..fc0c075 100644
--- a/src/tracing/core/sliced_protobuf_input_stream_unittest.cc
+++ b/src/tracing/core/sliced_protobuf_input_stream_unittest.cc
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
+#include "perfetto/ext/tracing/core/sliced_protobuf_input_stream.h"
 
 #include "perfetto/ext/base/utils.h"
 #include "test/gtest_and_gmock.h"
diff --git a/src/tracing/core/startup_trace_writer_unittest.cc b/src/tracing/core/startup_trace_writer_unittest.cc
index 139456e..a7cd419 100644
--- a/src/tracing/core/startup_trace_writer_unittest.cc
+++ b/src/tracing/core/startup_trace_writer_unittest.cc
@@ -23,7 +23,6 @@
 #include "src/base/test/test_task_runner.h"
 #include "src/tracing/core/patch_list.h"
 #include "src/tracing/core/shared_memory_arbiter_impl.h"
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
 #include "src/tracing/core/trace_buffer.h"
 #include "src/tracing/test/aligned_buffer_test.h"
 #include "src/tracing/test/fake_producer_endpoint.h"
@@ -147,15 +146,10 @@
       EXPECT_EQ(static_cast<uid_t>(1),
                 sequence_properties.producer_uid_trusted);
 
-      SlicedProtobufInputStream stream(&packet.slices());
-      size_t size = 0;
-      for (const Slice& slice : packet.slices())
-        size += slice.size;
       protos::TracePacket parsed_packet;
-      bool success = parsed_packet.ParseFromBoundedZeroCopyStream(
-          &stream, static_cast<int>(size));
-      EXPECT_TRUE(success);
-      if (!success)
+      bool res = parsed_packet.ParseFromString(packet.GetRawBytesForTesting());
+      EXPECT_TRUE(res);
+      if (!res)
         break;
 
       // If the buffer size was exceeded, the data loss packet should be the
diff --git a/src/tracing/core/trace_packet.cc b/src/tracing/core/trace_packet.cc
index c3891c5..0dea150 100644
--- a/src/tracing/core/trace_packet.cc
+++ b/src/tracing/core/trace_packet.cc
@@ -18,7 +18,6 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/protozero/proto_utils.h"
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
 
 namespace perfetto {
 
@@ -63,10 +62,16 @@
   return std::make_tuple(&preamble_[0], preamble_size);
 }
 
-std::unique_ptr<TracePacket::ZeroCopyInputStream>
-TracePacket::CreateSlicedInputStream() const {
-  return std::unique_ptr<ZeroCopyInputStream>(
-      new SlicedProtobufInputStream(&slices_));
+std::string TracePacket::GetRawBytesForTesting() {
+  std::string data;
+  data.resize(size());
+  size_t pos = 0;
+  for (const Slice& slice : slices()) {
+    PERFETTO_CHECK(pos + slice.size <= data.size());
+    memcpy(&data[pos], slice.start, slice.size);
+    pos += slice.size;
+  }
+  return data;
 }
 
 }  // namespace perfetto
diff --git a/src/tracing/core/trace_packet_unittest.cc b/src/tracing/core/trace_packet_unittest.cc
index f2aaa45..0469ae8 100644
--- a/src/tracing/core/trace_packet_unittest.cc
+++ b/src/tracing/core/trace_packet_unittest.cc
@@ -63,7 +63,7 @@
   ASSERT_EQ(tp.slices().end(), ++slice);
 
   protos::TracePacket decoded_packet;
-  ASSERT_TRUE(tp.Decode(&decoded_packet));
+  ASSERT_TRUE(decoded_packet.ParseFromString(tp.GetRawBytesForTesting()));
   ASSERT_EQ(proto.for_testing().str(), decoded_packet.for_testing().str());
 }
 
@@ -94,7 +94,7 @@
   ASSERT_EQ(tp.slices().end(), ++slice);
 
   protos::TracePacket decoded_packet;
-  ASSERT_TRUE(tp.Decode(&decoded_packet));
+  ASSERT_TRUE(decoded_packet.ParseFromString(tp.GetRawBytesForTesting()));
   ASSERT_EQ(proto.for_testing().str(), decoded_packet.for_testing().str());
 }
 
@@ -105,7 +105,7 @@
   TracePacket tp;
   tp.AddSlice({ser_buf.data(), ser_buf.size() - 2});  // corrupted.
   protos::TracePacket decoded_packet;
-  ASSERT_FALSE(tp.Decode(&decoded_packet));
+  ASSERT_FALSE(decoded_packet.ParseFromString(tp.GetRawBytesForTesting()));
 }
 
 // Tests that the GetProtoPreamble() logic returns a valid preamble that allows
diff --git a/src/tracing/test/mock_consumer.cc b/src/tracing/test/mock_consumer.cc
index 867d98c..17a983d 100644
--- a/src/tracing/test/mock_consumer.cc
+++ b/src/tracing/test/mock_consumer.cc
@@ -109,7 +109,7 @@
             for (TracePacket& packet : *packets) {
               decoded_packets.emplace_back();
               protos::TracePacket* decoded_packet = &decoded_packets.back();
-              packet.Decode(decoded_packet);
+              decoded_packet->ParseFromString(packet.GetRawBytesForTesting());
             }
             if (!has_more)
               on_read_buffers();
diff --git a/src/tracing/test/tracing_integration_test.cc b/src/tracing/test/tracing_integration_test.cc
index 56cc2c9..799b614 100644
--- a/src/tracing/test/tracing_integration_test.cc
+++ b/src/tracing/test/tracing_integration_test.cc
@@ -308,7 +308,8 @@
 
             for (auto& encoded_packet : *packets) {
               protos::TracePacket packet;
-              ASSERT_TRUE(encoded_packet.Decode(&packet));
+              ASSERT_TRUE(packet.ParseFromString(
+                  encoded_packet.GetRawBytesForTesting()));
               if (packet.has_for_testing()) {
                 char buf[8];
                 sprintf(buf, "evt_%zu", num_pack_rx++);
@@ -516,7 +517,8 @@
                      std::vector<TracePacket>* packets, bool has_more) {
             for (auto& encoded_packet : *packets) {
               protos::TracePacket packet;
-              ASSERT_TRUE(encoded_packet.Decode(&packet));
+              ASSERT_TRUE(packet.ParseFromString(
+                  encoded_packet.GetRawBytesForTesting()));
               if (packet.has_for_testing()) {
                 num_test_pack_rx++;
               }
diff --git a/test/test_helper.cc b/test/test_helper.cc
index e4af816..57e8b10 100644
--- a/test/test_helper.cc
+++ b/test/test_helper.cc
@@ -61,7 +61,8 @@
 void TestHelper::OnTraceData(std::vector<TracePacket> packets, bool has_more) {
   for (auto& encoded_packet : packets) {
     protos::TracePacket packet;
-    PERFETTO_CHECK(encoded_packet.Decode(&packet));
+    PERFETTO_CHECK(
+        packet.ParseFromString(encoded_packet.GetRawBytesForTesting()));
     if (packet.has_clock_snapshot() || packet.has_trace_config() ||
         packet.has_trace_stats() || !packet.synchronization_marker().empty() ||
         packet.has_system_info()) {
diff --git a/ui/src/base/string_utils.ts b/ui/src/base/string_utils.ts
new file mode 100644
index 0000000..0ad7dd3
--- /dev/null
+++ b/ui/src/base/string_utils.ts
@@ -0,0 +1,30 @@
+// Copyright (C) 2019 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.
+
+export function uint8ArrayToBase64(buffer: Uint8Array): string {
+  return btoa(uint8ArrayToString(buffer));
+}
+
+export function uint8ArrayToString(buffer: Uint8Array): string {
+  return String.fromCharCode.apply(null, Array.from(buffer));
+}
+
+export function stringToUint8Array(str: string): Uint8Array {
+  const bufView = new Uint8Array(new ArrayBuffer(str.length));
+  const strLen = str.length;
+  for (let i = 0; i < strLen; i++) {
+    bufView[i] = str.charCodeAt(i);
+  }
+  return bufView;
+}
\ No newline at end of file
diff --git a/ui/src/base/string_utils_jsdomtest.ts b/ui/src/base/string_utils_jsdomtest.ts
new file mode 100644
index 0000000..e8bae93
--- /dev/null
+++ b/ui/src/base/string_utils_jsdomtest.ts
@@ -0,0 +1,40 @@
+// Copyright (C) 2019 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.
+
+import {
+  stringToUint8Array,
+  uint8ArrayToBase64,
+  uint8ArrayToString
+} from './string_utils';
+
+test('uint8ArrayToBase64', () => {
+  const bytes = [...'Hello, world'].map(c => c.charCodeAt(0));
+  const buffer = new Uint8Array(bytes);
+  expect(uint8ArrayToBase64(buffer)).toEqual('SGVsbG8sIHdvcmxk');
+});
+
+test('stringToBufferToString', () => {
+  const testString = 'Hello world!';
+  const buffer = stringToUint8Array(testString);
+  const convertedBack = uint8ArrayToString(buffer);
+  expect(testString).toEqual(convertedBack);
+});
+
+test('bufferToStringToBuffer', () => {
+  const bytes = [...'Hello, world'].map(c => c.charCodeAt(0));
+  const buffer = new Uint8Array(bytes);
+  const toString = uint8ArrayToString(buffer);
+  const convertedBack = stringToUint8Array(toString);
+  expect(convertedBack).toEqual(buffer);
+});
diff --git a/ui/src/chrome_extension/chrome_tracing_controller.ts b/ui/src/chrome_extension/chrome_tracing_controller.ts
index 512e633..d46f6b7 100644
--- a/ui/src/chrome_extension/chrome_tracing_controller.ts
+++ b/ui/src/chrome_extension/chrome_tracing_controller.ts
@@ -22,13 +22,15 @@
   GetTraceStatsResponse,
   ReadBuffersResponse
 } from '../controller/consumer_port_types';
+import {extractTraceConfig} from '../controller/record_controller';
+import {RpcConsumerPort} from '../controller/record_controller_interfaces';
 import {perfetto} from '../gen/protos';
 
 import {DevToolsSocket} from './devtools_socket';
 
 const CHUNK_SIZE: number = 1024 * 1024 * 64;
 
-export class ChromeTracingController {
+export class ChromeTracingController extends RpcConsumerPort {
   private streamHandle: string|undefined = undefined;
   private uiPort: chrome.runtime.Port;
   private api: ProtocolProxyApi.ProtocolApi;
@@ -36,6 +38,16 @@
   private lastBufferUsageEvent: Protocol.Tracing.BufferUsageEvent|undefined;
 
   constructor(port: chrome.runtime.Port) {
+    super({
+      onConsumerPortResponse: (message: ConsumerPortResponse) =>
+          this.uiPort.postMessage(message),
+
+      onError: (error: string) =>
+          this.uiPort.postMessage({type: 'ChromeExtensionError', error}),
+
+      onStatus: (status) =>
+          this.uiPort.postMessage({type: 'ChromeExtensionStatus', status})
+    });
     this.uiPort = port;
     this.devtoolsSocket = new DevToolsSocket();
     this.devtoolsSocket.on('close', () => this.resetState());
@@ -45,18 +57,10 @@
     this.api.Tracing.on('bufferUsage', this.onBufferUsage.bind(this));
   }
 
-  sendMessage(message: ConsumerPortResponse) {
-    this.uiPort.postMessage(message);
-  }
-
-  sendErrorMessage(error: string) {
-    this.uiPort.postMessage({type: 'ErrorResponse', result: {error}});
-  }
-
-  onMessage(request: {method: string, traceConfig: Uint8Array}) {
-    switch (request.method) {
+  handleCommand(methodName: string, requestData: Uint8Array) {
+    switch (methodName) {
       case 'EnableTracing':
-        this.enableTracing(request);
+        this.enableTracing(requestData);
         break;
       case 'FreeBuffers':
         this.freeBuffers();
@@ -74,19 +78,25 @@
         this.getCategories();
         break;
       default:
-        this.sendErrorMessage('Action not recognised');
-        console.log('Received not recognized message: ', request.method);
+        this.sendErrorMessage('Action not recognized');
+        console.log('Received not recognized message: ', methodName);
         break;
     }
   }
 
-  enableTracing(request: {method: string, traceConfig: Uint8Array}) {
+  enableTracing(enableTracingRequest: Uint8Array) {
     this.resetState();
-    const traceConfig = TraceConfig.decode(new Uint8Array(request.traceConfig));
+    const traceConfigProto = extractTraceConfig(enableTracingRequest);
+    if (!traceConfigProto) {
+      this.sendErrorMessage('Invalid trace config');
+      return;
+    }
+    const traceConfig = TraceConfig.decode(traceConfigProto);
     const chromeConfig = this.extractChromeConfig(traceConfig);
     this.handleStartTracing(chromeConfig);
   }
 
+  // TODO(nicomazz): write unit test for this
   extractChromeConfig(perfettoConfig: TraceConfig):
       Protocol.Tracing.TraceConfig {
     for (const ds of perfettoConfig.dataSources) {
@@ -106,7 +116,6 @@
   }
 
   async readBuffers(offset = 0) {
-    // TODO(nicomazz): Add error handling also in the frontend.
     if (!this.devtoolsSocket.isAttached() || this.streamHandle === undefined) {
       this.sendErrorMessage('No tracing session to read from');
       return;
diff --git a/ui/src/chrome_extension/index.ts b/ui/src/chrome_extension/index.ts
index 1bc5e36..5bce4ec 100644
--- a/ui/src/chrome_extension/index.ts
+++ b/ui/src/chrome_extension/index.ts
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {stringToUint8Array} from '../base/string_utils';
 import {ChromeTracingController} from './chrome_tracing_controller';
 
 let chromeTraceController: ChromeTracingController|undefined = undefined;
@@ -24,14 +25,19 @@
 });
 
 function onUIMessage(
-    message: {method: string, traceConfig: Uint8Array},
-    port: chrome.runtime.Port) {
+    message: {method: string, requestData: string}, port: chrome.runtime.Port) {
   if (message.method === 'ExtensionVersion') {
     port.postMessage({version: chrome.runtime.getManifest().version});
     return;
   }
-  // In the future, more targets will be supported.
-  if (chromeTraceController) chromeTraceController.onMessage(message);
+  console.assert(chromeTraceController !== undefined);
+  if (!chromeTraceController) return;
+  // ChromeExtensionConsumerPort sends the request data as string because
+  // chrome.runtime.port doesn't support ArrayBuffers.
+  const requestDataArray: Uint8Array = message.requestData ?
+      stringToUint8Array(message.requestData) :
+      new Uint8Array();
+  chromeTraceController.handleCommand(message.method, requestDataArray);
 }
 
 function enableOnlyOnPerfettoHost() {
diff --git a/ui/src/controller/adb.ts b/ui/src/controller/adb.ts
index 15c9139..b44c654 100644
--- a/ui/src/controller/adb.ts
+++ b/ui/src/controller/adb.ts
@@ -24,6 +24,8 @@
   RSAPUBLICKEY = 3,
 }
 
+const DEVICE_NOT_SET_ERROR = 'Device not set.';
+
 // This class is a basic TypeScript implementation of adb that only supports
 // shell commands. It is used to send the start tracing command to the connected
 // android device, and to automatically pull the trace after the end of the
@@ -100,6 +102,15 @@
     return new Promise<void>((resolve, _) => this.onConnected = resolve);
   }
 
+  async disconnect(): Promise<void> {
+    if (!this.dev) {
+      console.error('adb disconnect() called with no device connected');
+      return;
+    }
+    this.dev.close();
+    this.dev = undefined;
+  }
+
   async startAuthentication() {
     // USB connected, now let's authenticate.
     const VERSION =
@@ -109,7 +120,7 @@
   }
 
   findInterfaceAndEndpoint() {
-    if (!this.dev) throw Error('Device not set');
+    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
     for (const config of this.dev.configurations) {
       for (const interface_ of config.interfaces) {
         for (const alt of interface_.alternates) {
@@ -140,18 +151,18 @@
   }
 
   receiveDeviceMessages() {
-    // TODO(nicomazz): find the best (and correct) way to stop this.
-    try {
-      this.recv().then(msg => {
-        this.onMessage(msg);
-        this.receiveDeviceMessages();
-      });
-    } catch (e) {
-      // Then the usb connection is not available anymore, the recv will throw
-      // an exception here.
-      // TODO(nicomazz): Propagate this unconnected state to the UI.
-      console.log('exception on recv: ', e);
-    }
+    this.recv()
+        .then(msg => {
+          this.onMessage(msg);
+          this.receiveDeviceMessages();
+        })
+        .catch(e => {
+          // Ignore error with "DEVICE_NOT_SET_ERROR" message since it is always
+          // thrown after the device disconnects.
+          if (e.message !== DEVICE_NOT_SET_ERROR) {
+            console.error(`Exception in recv: ${e.name}. error: ${e.message}`);
+          }
+        });
   }
 
   async onMessage(msg: AdbMsg) {
@@ -244,6 +255,16 @@
     });
   }
 
+  async shellOutputAsString(cmd: string): Promise<string> {
+    const shell = await this.shell(cmd);
+
+    return new Promise<string>((resolve, _) => {
+      const output: string[] = [];
+      shell.onData = (str, _) => output.push(str);
+      shell.onClose = () => resolve(output.join());
+    });
+  }
+
   async send(
       cmd: CmdType, arg0: number, arg1: number, data?: Uint8Array|string) {
     await this.sendMsg(AdbMsgImpl.create(
@@ -304,12 +325,12 @@
 
   sendRaw(buf: Uint8Array): Promise<USBOutTransferResult> {
     console.assert(buf.length <= this.maxPayload);
-    if (!this.dev) throw Error('Device not set');
+    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
     return this.dev.transferOut(this.usbWriteEpEndpoint, buf.buffer);
   }
 
   recvRaw(dataLen: number): Promise<USBInTransferResult> {
-    if (!this.dev) throw Error('Device not set');
+    if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
     return this.dev.transferIn(this.usbReadEndpoint, dataLen);
   }
 }
@@ -363,7 +384,6 @@
     }
 
     this.adb.send('CLSE', this.localStreamId, this.remoteStreamId);
-    this.doClose();
   }
 
   async write(msg: string|Uint8Array) {
diff --git a/ui/src/controller/adb_interfaces.ts b/ui/src/controller/adb_interfaces.ts
index 91dd653..2f22a2f 100644
--- a/ui/src/controller/adb_interfaces.ts
+++ b/ui/src/controller/adb_interfaces.ts
@@ -15,12 +15,16 @@
 
 export interface Adb {
   connect(device: USBDevice): Promise<void>;
+  disconnect(): Promise<void>;
   shell(cmd: string): Promise<AdbStream>;
+  shellOutputAsString(cmd: string): Promise<string>;
 }
 
 export interface AdbStream {
   onMessage(message: AdbMsg): void;
   onData: (str: string, raw: Uint8Array) => void;
+  close(): void;
+
   onConnect: VoidCallback;
   onClose: VoidCallback;
 }
@@ -29,9 +33,18 @@
   connect(_: USBDevice): Promise<void> {
     return Promise.resolve();
   }
+
+  disconnect(): Promise<void> {
+    return Promise.resolve();
+  }
+
   shell(_: string): Promise<AdbStream> {
     return Promise.resolve(new MockAdbStream());
   }
+
+  shellOutputAsString(_: string): Promise<string> {
+    return Promise.resolve('');
+  }
 }
 
 export class MockAdbStream implements AdbStream {
@@ -39,6 +52,7 @@
   onConnect = () => {};
   onClose = () => {};
   onMessage = (_: AdbMsg) => {};
+  close() {}
 }
 
 export declare type CmdType =
diff --git a/ui/src/controller/adb_record_controller.ts b/ui/src/controller/adb_record_controller.ts
index f3d17cb..990053d 100644
--- a/ui/src/controller/adb_record_controller.ts
+++ b/ui/src/controller/adb_record_controller.ts
@@ -12,10 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {uint8ArrayToBase64} from '../base/string_utils';
+
 import {Adb, AdbStream} from './adb_interfaces';
-import {ConsumerPortResponse, ReadBuffersResponse} from './consumer_port_types';
+import {ReadBuffersResponse} from './consumer_port_types';
 import {globals} from './globals';
-import {RecordControllerMessage, uint8ArrayToBase64} from './record_controller';
+import {
+  extractDurationFromTraceConfig,
+  extractTraceConfig
+} from './record_controller';
+import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
 
 enum AdbState {
   READY,
@@ -24,39 +30,20 @@
 }
 const DEFAULT_DESTINATION_FILE = '/data/misc/perfetto-traces/trace';
 
-export class AdbRecordController {
+export class AdbConsumerPort extends RpcConsumerPort {
   // public for testing
   traceDestFile = DEFAULT_DESTINATION_FILE;
   private state = AdbState.READY;
   private adb: Adb;
   private device: USBDevice|undefined = undefined;
-  private mainControllerCallback:
-      (_: {data: ConsumerPortResponse|RecordControllerMessage}) => void;
+  private recordShell?: AdbStream;
 
-  constructor(adb: Adb, mainControllerCallback: (_: {
-                          data: ConsumerPortResponse
-                        }) => void) {
-    this.mainControllerCallback = mainControllerCallback;
+  constructor(adb: Adb, consumerPortListener: Consumer) {
+    super(consumerPortListener);
     this.adb = adb;
   }
 
-  sendMessage(message: ConsumerPortResponse|RecordControllerMessage) {
-    this.mainControllerCallback({data: message});
-  }
-
-  sendErrorMessage(message: string) {
-    console.error('Error in adb record controller: ', message);
-    this.sendMessage({type: 'RecordControllerError', message});
-  }
-
-  sendStatus(status: string) {
-    this.sendMessage({type: 'RecordControllerStatus', status});
-  }
-
   handleCommand(method: string, params: Uint8Array) {
-    // TODO(nicomazz): after having implemented the connection to the consumer
-    // port socket through adb (on a real device), this class will be a simple
-    // proxy.
     switch (method) {
       case 'EnableTracing':
         this.enableTracing(params);
@@ -64,9 +51,11 @@
       case 'ReadBuffers':
         this.readBuffers();
         break;
+      case 'DisableTracing':
+        this.disableTracing();
+        break;
       case 'FreeBuffers':  // no-op
       case 'GetTraceStats':
-      case 'DisableTracing':
         break;
       default:
         this.sendErrorMessage(`Method not recognized: ${method}`);
@@ -74,12 +63,9 @@
     }
   }
 
-  async enableTracing(configProto: Uint8Array) {
+  async enableTracing(enableTracingProto: Uint8Array) {
     try {
-      if (this.state !== AdbState.READY) {
-        console.error('Current state of AdbRecordController is not READY');
-        return;
-      }
+      console.assert(this.state === AdbState.READY);
       this.device = await this.findDevice();
 
       if (this.device === undefined) {
@@ -89,9 +75,17 @@
       this.sendStatus(
           'Check the screen of your device and allow USB debugging.');
       await this.adb.connect(this.device);
-      await this.startRecording(configProto);
-      this.sendStatus('Recording in progress...');
+      const traceConfigProto = extractTraceConfig(enableTracingProto);
 
+      if (!traceConfigProto) {
+        this.sendErrorMessage('Invalid config.');
+        return;
+      }
+
+      await this.startRecording(traceConfigProto);
+      const duration = extractDurationFromTraceConfig(traceConfigProto);
+      this.sendStatus(`Recording in progress${
+          duration ? ' for ' + duration.toString() + ' ms' : ''}...`);
     } catch (e) {
       this.sendErrorMessage(e.message);
     }
@@ -100,15 +94,17 @@
   async startRecording(configProto: Uint8Array) {
     this.state = AdbState.RECORDING;
     const recordCommand = this.generateStartTracingCommand(configProto);
-    const recordShell: AdbStream = await this.adb.shell(recordCommand);
-    let response = '';
-    recordShell.onData = (str, _) => response += str;
-    recordShell.onClose = () => {
+    this.recordShell = await this.adb.shell(recordCommand);
+    const output: string[] = [];
+    this.recordShell.onData = (str, _) => output.push(str);
+    this.recordShell.onClose = () => {
+      const response = output.join();
       if (!this.tracingEndedSuccessfully(response)) {
         this.sendErrorMessage(response);
         this.state = AdbState.READY;
         return;
       }
+      this.sendStatus('Recording ended successfully. Fetching the trace..');
       this.sendMessage({type: 'EnableTracingResponse'});
     };
   }
@@ -138,6 +134,9 @@
       // things are not working, the chunks should be sent as they are received,
       // like in the following line.
       // this.sendMessage(this.generateChunkReadResponse(str));
+      // EDIT: we should send back a response as if it was a real
+      // ReadBufferResponse, with trace packets. Here we are only sending the
+      // trace split in several pieces.
       trace += str;
     };
     readTraceShell.onClose = () => {
@@ -145,10 +144,34 @@
 
       this.sendMessage(
           this.generateChunkReadResponse(decoded, /* last */ true));
+      this.adb.disconnect();
       this.state = AdbState.READY;
     };
   }
 
+  // TODO(nicomazz): Implement cancel/reset recording.
+  async disableTracing() {
+    console.assert(this.recordShell !== undefined);
+    if (!this.recordShell) return;
+
+    // We are not using 'pidof perfetto' so that we can use more filters. 'ps -u
+    // shell' is meant to catch processes started from shell, so if there are
+    // other ongoing tracing sessions started by others, we are not killing
+    // them.
+    const pid = await this.adb.shellOutputAsString(
+        `ps -u shell | grep perfetto | awk '{print $2}'`);
+    if (pid.length === 0 || isNaN(Number(pid))) {
+      this.sendErrorMessage(
+          'Unexpected error, impossible to stop the recording');
+      console.error('Perfetto pid not found. Command output: ', pid);
+      return;
+    }
+    // Perfetto stops and finalizes the tracing session on SIGINT.
+    const killOutput =
+        await this.adb.shellOutputAsString(`kill -SIGINT ${pid}`);
+    console.assert(killOutput.length === 0);
+  }
+
   generateChunkReadResponse(data: string, last = false): ReadBuffersResponse {
     return {
       type: 'ReadBuffersResponse',
diff --git a/ui/src/controller/adb_record_controller_jsdomtest.ts b/ui/src/controller/adb_record_controller_jsdomtest.ts
index dffb563..503f92d 100644
--- a/ui/src/controller/adb_record_controller_jsdomtest.ts
+++ b/ui/src/controller/adb_record_controller_jsdomtest.ts
@@ -14,14 +14,29 @@
 
 import {dingus} from 'dingusjs';
 
+import {perfetto} from '../gen/protos';
 import {AdbStream, MockAdb, MockAdbStream} from './adb_interfaces';
-import {AdbRecordController} from './adb_record_controller';
+import {AdbConsumerPort} from './adb_record_controller';
+import {Consumer} from './record_controller_interfaces';
 
-const mainCallback = jest.fn();
+function generateMockConsumer(): Consumer {
+  return {
+    onConsumerPortResponse: jest.fn(),
+    onError: jest.fn(),
+    onStatus: jest.fn()
+  };
+}
+const mainCallback = generateMockConsumer();
 const adbMock = new MockAdb();
-const adbController = new AdbRecordController(adbMock, mainCallback);
+const adbController = new AdbConsumerPort(adbMock, mainCallback);
 const mockIntArray = new Uint8Array();
 
+const enableTracingRequest = new perfetto.protos.EnableTracingRequest();
+enableTracingRequest.traceConfig = new perfetto.protos.TraceConfig();
+const enableTracingRequestProto =
+    perfetto.protos.EnableTracingRequest.encode(enableTracingRequest).finish();
+
+
 test('handleCommand', () => {
   adbController.findDevice = () => {
     return Promise.resolve(dingus<USBDevice>());
@@ -44,9 +59,9 @@
 });
 
 test('enableTracing', async () => {
-  const mainCallback = jest.fn();
+  const mainCallback = generateMockConsumer();
   const adbMock = new MockAdb();
-  const adbController = new AdbRecordController(adbMock, mainCallback);
+  const adbController = new AdbConsumerPort(adbMock, mainCallback);
 
   adbController.sendErrorMessage =
       jest.fn().mockImplementation(s => console.error(s));
@@ -71,13 +86,11 @@
 
   adbController.generateStartTracingCommand = (_) => 'CMD';
 
-  await adbController.enableTracing(mockIntArray);
+  await adbController.enableTracing(enableTracingRequestProto);
   expect(adbShell).toBeCalledWith('CMD');
   expect(findDevice).toHaveBeenCalledTimes(1);
   expect(connectToDevice).toHaveBeenCalledTimes(1);
-  // Two messages: RecordControllerStatus asking for allow the debug, and
-  // another status to clear that message.
-  expect(sendMessage).toHaveBeenCalledTimes(2);
+  expect(sendMessage).toHaveBeenCalledTimes(0);
 
 
   stream.onData('starting tracing Wrote 123 bytes', mockIntArray);
@@ -110,4 +123,4 @@
              'Connected to the Perfetto traced service, starting tracing for \
 0 ms'))
       .toBe(false);
-});
\ No newline at end of file
+});
diff --git a/ui/src/controller/chrome_proxy_record_controller.ts b/ui/src/controller/chrome_proxy_record_controller.ts
new file mode 100644
index 0000000..c2ce289
--- /dev/null
+++ b/ui/src/controller/chrome_proxy_record_controller.ts
@@ -0,0 +1,69 @@
+// Copyright (C) 2019 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.
+
+import {uint8ArrayToString} from '../base/string_utils';
+
+import {ConsumerPortResponse, Typed} from './consumer_port_types';
+import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
+
+export interface ChromeExtensionError extends Typed {
+  error: string;
+}
+
+export interface ChromeExtensionStatus extends Typed {
+  status: string;
+}
+
+export type ChromeExtensionMessage =
+    ChromeExtensionError|ChromeExtensionStatus|ConsumerPortResponse;
+
+function isError(obj: Typed): obj is ChromeExtensionError {
+  return obj.type === 'ChromeExtensionError';
+}
+
+function isStatus(obj: Typed): obj is ChromeExtensionStatus {
+  return obj.type === 'ChromeExtensionStatus';
+}
+
+// This class acts as a proxy from the record controller (running in a worker),
+// to the frontend. This is needed because we can't directly talk with the
+// extension from a web-worker, so we use a MessagePort to communicate with the
+// frontend, that will consecutively forward it to the extension.
+export class ChromeExtensionConsumerPort extends RpcConsumerPort {
+  private extensionPort: MessagePort;
+
+  constructor(extensionPort: MessagePort, consumerPortListener: Consumer) {
+    super(consumerPortListener);
+    this.extensionPort = extensionPort;
+    this.extensionPort.onmessage = this.onExtensionMessage.bind(this);
+  }
+
+  onExtensionMessage(message: {data: ChromeExtensionMessage}) {
+    if (isError(message.data)) {
+      this.sendErrorMessage(message.data.error);
+    } else if (isStatus(message.data)) {
+      this.sendStatus(message.data.status);
+    } else {
+      this.sendMessage(message.data);
+    }
+  }
+
+  handleCommand(method: string, requestData: Uint8Array): void {
+    const buffer = uint8ArrayToString(requestData);
+    // We need to encode the buffer as a string because the message port doesn't
+    // fully support sending ArrayBuffers (they are converted to objects with
+    // indexes as keys).
+    this.extensionPort.postMessage({method, requestData: buffer});
+  }
+}
diff --git a/ui/src/controller/record_controller.ts b/ui/src/controller/record_controller.ts
index 34feb53..71b04d5 100644
--- a/ui/src/controller/record_controller.ts
+++ b/ui/src/controller/record_controller.ts
@@ -15,6 +15,7 @@
 import {ungzip} from 'pako';
 import {Message, Method, rpc, RPCImplCallback} from 'protobufjs';
 
+import {stringToUint8Array, uint8ArrayToBase64} from '../base/string_utils';
 import {Actions} from '../common/actions';
 import {
   AndroidLogConfig,
@@ -34,47 +35,27 @@
   isAndroidTarget,
   isChromeTarget,
   MAX_TIME,
-  RecordConfig
+  RecordConfig,
+  TargetOs
 } from '../common/state';
+import {perfetto} from '../gen/protos';
 
 import {AdbOverWebUsb} from './adb';
-import {AdbRecordController} from './adb_record_controller';
+import {AdbConsumerPort} from './adb_record_controller';
+import {ChromeExtensionConsumerPort} from './chrome_proxy_record_controller';
 import {
   ConsumerPortResponse,
   GetTraceStatsResponse,
   isEnableTracingResponse,
   isGetTraceStatsResponse,
   isReadBuffersResponse,
-  Typed,
 } from './consumer_port_types';
 import {Controller} from './controller';
 import {App, globals} from './globals';
+import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
 
 type RPCImplMethod = (Method|rpc.ServiceMethod<Message<{}>, Message<{}>>);
 
-export interface RecordControllerError extends Typed {
-  message: string;
-}
-
-export interface RecordControllerStatus extends Typed {
-  status: string;
-}
-
-export type RecordControllerMessage =
-    RecordControllerError|RecordControllerStatus;
-
-function isError(obj: Typed): obj is RecordControllerError {
-  return obj.type === 'RecordControllerError';
-}
-
-function isStatus(obj: Typed): obj is RecordControllerStatus {
-  return obj.type === 'RecordControllerStatus';
-}
-
-export function uint8ArrayToBase64(buffer: Uint8Array): string {
-  return btoa(String.fromCharCode.apply(null, Array.from(buffer)));
-}
-
 export function genConfigProto(uiCfg: RecordConfig): Uint8Array {
   return TraceConfig.encode(genConfig(uiCfg)).finish();
 }
@@ -436,7 +417,28 @@
   return [...message(json, 0)].join('');
 }
 
-export class RecordController extends Controller<'main'> {
+export function extractTraceConfig(enableTracingRequest: Uint8Array):
+    Uint8Array|undefined {
+  try {
+    const enableTracingObject =
+        perfetto.protos.EnableTracingRequest.decode(enableTracingRequest);
+    if (!enableTracingObject.traceConfig) return undefined;
+    return perfetto.protos.TraceConfig.encode(enableTracingObject.traceConfig)
+        .finish();
+  } catch (e) {  // This catch is for possible proto encoding/decoding issues.
+    console.error('Error extracting the config: ', e.message);
+    return undefined;
+  }
+}
+
+export function extractDurationFromTraceConfig(traceConfigProto: Uint8Array) {
+  try {
+    return perfetto.protos.TraceConfig.decode(traceConfigProto).durationMs;
+  } catch (e) {  // This catch is for possible proto encoding/decoding issues.
+    return undefined;
+  }
+}
+export class RecordController extends Controller<'main'> implements Consumer {
   private app: App;
   private config: RecordConfig|null = null;
   private extensionPort: MessagePort;
@@ -445,14 +447,14 @@
   private traceBuffer = '';
   private bufferUpdateInterval: ReturnType<typeof setTimeout>|undefined;
 
-  private adbRecordController = new AdbRecordController(
-      new AdbOverWebUsb(), this.onConsumerPortMessage.bind(this));
+  // We have a different controller for each targetOS. The correct one will be
+  // created when needed, and stored here.
+  private controllers = new Map<TargetOs, RpcConsumerPort>();
   constructor(args: {app: App, extensionPort: MessagePort}) {
     super('main');
     this.app = args.app;
     this.consumerPort = ConsumerPort.create(this.rpcImpl.bind(this));
     this.extensionPort = args.extensionPort;
-    this.extensionPort.onmessage = this.onConsumerPortMessage.bind(this);
   }
 
   run() {
@@ -510,13 +512,11 @@
     this.consumerPort.readBuffers({});
   }
 
-  onConsumerPortMessage({data}: {
-    data: ConsumerPortResponse|RecordControllerMessage
-  }) {
+  onConsumerPortResponse(data: ConsumerPortResponse) {
     if (data === undefined) return;
-
     if (isReadBuffersResponse(data)) {
       if (!data.slices) return;
+      // TODO(nicomazz): handle this as intended by consumer_port.proto.
       this.traceBuffer += data.slices[0].data;
       // TODO(nicomazz): Stream the chunks directly in the trace processor.
       if (data.slices[0].lastSliceForPacket) this.openTraceInUI();
@@ -527,77 +527,76 @@
       if (percentage) {
         globals.publish('BufferUsage', {percentage});
       }
-    } else if (isError(data)) {
-      this.handleError(data.message);
-    } else if (isStatus(data)) {
-      this.handleStatus(data.status);
+    } else {
+      console.error('Unrecognized consumer port response:', data);
     }
   }
 
   openTraceInUI() {
     this.consumerPort.freeBuffers({});
     globals.dispatch(Actions.setRecordingStatus({status: undefined}));
-    const trace = ungzip(this.stringToArrayBuffer(this.traceBuffer));
+    const trace = ungzip(stringToUint8Array(this.traceBuffer));
     globals.dispatch(Actions.openTraceFromBuffer({buffer: trace.buffer}));
     this.traceBuffer = '';
   }
 
-  stringToArrayBuffer(str: string): Uint8Array {
-    const buf = new ArrayBuffer(str.length);
-    const bufView = new Uint8Array(buf);
-    for (let i = 0, strLen = str.length; i < strLen; i++) {
-      bufView[i] = str.charCodeAt(i);
-    }
-    return bufView;
-  }
-
 
   getBufferUsagePercentage(data: GetTraceStatsResponse): number {
     if (!data.traceStats || !data.traceStats.bufferStats) return 0.0;
-    let used = 0.0, total = 0.0;
+    let maximumUsage = 0;
     for (const buffer of data.traceStats.bufferStats) {
-      used += buffer.bytesWritten as number;
-      total += buffer.bufferSize as number;
+      const used = buffer.bytesWritten as number;
+      const total = buffer.bufferSize as number;
+      maximumUsage = Math.max(maximumUsage, used / total);
     }
-    if (total === 0.0) return 0;
-    return used / total;
+    return maximumUsage;
   }
 
-  handleError(message: string) {
+  onError(message: string) {
     globals.dispatch(
         Actions.setLastRecordingError({error: message.substr(0, 150)}));
     globals.dispatch(Actions.stopRecording({}));
   }
 
-  handleStatus(message: string) {
+  onStatus(message: string) {
     globals.dispatch(Actions.setRecordingStatus({status: message}));
   }
 
   // Depending on the recording target, different implementation of the
   // consumer_port will be used.
   // - Chrome target: This forwards the messages that have to be sent
-  // to the extension to the frontend. This is necessary because this controller
-  // is running in a separate worker, that can't directly send messages to the
-  // extension.
+  // to the extension to the frontend. This is necessary because this
+  // controller is running in a separate worker, that can't directly send
+  // messages to the extension.
   // - Android device target: WebUSB is used to communicate using the adb
-  // protocol. Actually, there is no full consumer_port implementation, but only
-  // the support to start tracing and fetch the file.
+  // protocol. Actually, there is no full consumer_port implementation, but
+  // only the support to start tracing and fetch the file.
+  getTargetController(target: TargetOs): RpcConsumerPort {
+    let controller = this.controllers.get(target);
+    if (controller) return controller;
+
+    if (isChromeTarget(target)) {
+      controller = new ChromeExtensionConsumerPort(this.extensionPort, this);
+    } else if (isAndroidTarget(target)) {
+      // TODO(nicomazz): create the correct controller also based on the
+      // selected android device.
+      controller = new AdbConsumerPort(new AdbOverWebUsb(), this);
+    }
+
+    if (!controller) throw Error(`Unknown target: ${target}`);
+
+    this.controllers.set(target, controller);
+    return controller;
+  }
+
   private rpcImpl(
       method: RPCImplMethod, requestData: Uint8Array,
       _callback: RPCImplCallback) {
-    const target = this.app.state.recordConfig.targetOS;
-    if (isChromeTarget(target) && method !== null && method.name !== null &&
-        this.config !== null) {
-      this.extensionPort.postMessage(
-          {method: method.name, traceConfig: requestData});
-    } else if (isAndroidTarget(target)) {
-      // TODO(nicomazz): In theory requestData should contain the configuration
-      // proto, but in practice there are missing fields. As a temporary
-      // workaround I'm directly passing the configuration.
-      this.adbRecordController.handleCommand(
-          method.name, genConfigProto(this.config!));
-    } else {
-      console.error(`Target ${target} not supported!`);
+    try {
+      this.getTargetController(this.app.state.recordConfig.targetOS)
+          .handleCommand(method.name, requestData);
+    } catch (e) {
+      console.error(`error invoking ${method}: ${e.message}`);
     }
   }
 }
diff --git a/ui/src/controller/record_controller_interfaces.ts b/ui/src/controller/record_controller_interfaces.ts
new file mode 100644
index 0000000..afd1d8b
--- /dev/null
+++ b/ui/src/controller/record_controller_interfaces.ts
@@ -0,0 +1,36 @@
+import {ConsumerPortResponse} from './consumer_port_types';
+
+export type ConsumerPortCallback = (_: ConsumerPortResponse) => void;
+export type ErrorCallback = (_: string) => void;
+export type StatusCallback = (_: string) => void;
+
+export abstract class RpcConsumerPort {
+  // The responses of the call invocations should be sent through this listener.
+  // This is done by the 3 "send" methods in this abstract class.
+  private consumerPortListener: Consumer;
+
+  constructor(consumerPortListener: Consumer) {
+    this.consumerPortListener = consumerPortListener;
+  }
+
+  // RequestData is the proto representing the arguments of the function call.
+  abstract handleCommand(methodName: string, requestData: Uint8Array): void;
+
+  sendMessage(data: ConsumerPortResponse) {
+    this.consumerPortListener.onConsumerPortResponse(data);
+  }
+
+  sendErrorMessage(message: string) {
+    this.consumerPortListener.onError(message);
+  }
+
+  sendStatus(status: string) {
+    this.consumerPortListener.onStatus(status);
+  }
+}
+
+export interface Consumer {
+  onConsumerPortResponse(data: ConsumerPortResponse): void;
+  onError: ErrorCallback;
+  onStatus: StatusCallback;
+}
\ No newline at end of file
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/controller/record_controller_jsdomtest.ts
index 3455b52..8fb9983 100644
--- a/ui/src/controller/record_controller_jsdomtest.ts
+++ b/ui/src/controller/record_controller_jsdomtest.ts
@@ -19,18 +19,7 @@
 import {createEmptyRecordConfig, RecordConfig} from '../common/state';
 
 import {App} from './globals';
-import {
-  genConfigProto,
-  RecordController,
-  toPbtxt,
-  uint8ArrayToBase64
-} from './record_controller';
-
-test('uint8ArrayToBase64', () => {
-  const bytes = [...'Hello, world'].map(c => c.charCodeAt(0));
-  const buffer = new Uint8Array(bytes);
-  expect(uint8ArrayToBase64(buffer)).toEqual('SGVsbG8sIHdvcmxk');
-});
+import {genConfigProto, RecordController, toPbtxt} from './record_controller';
 
 test('encodeConfig', () => {
   const config = createEmptyRecordConfig();
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index f11e74a..6c4f26f 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -768,11 +768,11 @@
   const realDeviceTarget = state.androidDeviceConnected !== undefined;
   const recInProgress = state.recordingInProgress;
 
-  const startButton =
+  const start =
       m(`button${recInProgress ? '.selected' : ''}`,
         {onclick: onStartRecordingPressed},
         'Start Recording');
-  const showCmdButton =
+  const showCmd =
       m(`button`,
         {
           onclick: () => {
@@ -781,7 +781,7 @@
           }
         },
         'Show Command');
-  const stopButton =
+  const stop =
       m(`button${recInProgress ? '' : '.disabled'}`,
         {onclick: () => globals.dispatch(Actions.stopRecording({}))},
         'Stop Recording');
@@ -790,14 +790,13 @@
 
   const targetOs = state.recordConfig.targetOS;
   if (isAndroidTarget(targetOs)) {
-    buttons.push(showCmdButton);
-    if (realDeviceTarget) buttons.push(startButton);
-    // TODO(nicomazz): Support stop recording on Android devices.
+    buttons.push(showCmd);
+    if (realDeviceTarget) buttons.push(recInProgress ? stop : start);
   } else if (isChromeTarget(targetOs) && state.extensionInstalled) {
-    buttons.push(startButton);
-    if (recInProgress) buttons.push(stopButton);
+    buttons.push(start);
+    if (recInProgress) buttons.push(stop);
   } else if (isLinuxTarget(targetOs)) {
-    buttons.push(showCmdButton);
+    buttons.push(showCmd);
   }
 
   return m('.button', buttons);