Implement dialer command line interface

This CL adds a generic interface to perform action and retrieve data in dialer.

Test: Unit tests
PiperOrigin-RevId: 182128015
Change-Id: Ib45ce9808bd4e0e4a9c9e05c0017e19c3213bb0d
diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml
index 3652c95..0c1a362 100644
--- a/java/com/android/dialer/app/AndroidManifest.xml
+++ b/java/com/android/dialer/app/AndroidManifest.xml
@@ -131,6 +131,11 @@
       </intent-filter>
     </receiver>
 
+    <receiver
+        android:exported="false"
+        android:name="com.android.dialer.commandline.CommandLineReceiver">
+    </receiver>
+
     <provider
       android:authorities="com.android.dialer.files"
       android:exported="false"
diff --git a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java
index 1be6759..474f666 100644
--- a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java
+++ b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java
@@ -18,6 +18,7 @@
 
 import com.android.dialer.binary.basecomponent.BaseDialerRootComponent;
 import com.android.dialer.calllog.CallLogModule;
+import com.android.dialer.commandline.CommandLineModule;
 import com.android.dialer.common.concurrent.DialerExecutorModule;
 import com.android.dialer.configprovider.SharedPrefConfigProviderModule;
 import com.android.dialer.duo.stub.StubDuoModule;
@@ -44,6 +45,7 @@
 @Component(
   modules = {
     CallLogModule.class,
+    CommandLineModule.class,
     ContextModule.class,
     DialerExecutorModule.class,
     PhoneLookupModule.class,
diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
index 37e04ac..7e61af4 100644
--- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
+++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
@@ -19,6 +19,7 @@
 import com.android.dialer.calllog.CallLogComponent;
 import com.android.dialer.calllog.database.CallLogDatabaseComponent;
 import com.android.dialer.calllog.ui.CallLogUiComponent;
+import com.android.dialer.commandline.CommandLineComponent;
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.configprovider.ConfigProviderComponent;
 import com.android.dialer.duo.DuoComponent;
@@ -48,6 +49,7 @@
         CallLogDatabaseComponent.HasComponent,
         CallLogUiComponent.HasComponent,
         ConfigProviderComponent.HasComponent,
+        CommandLineComponent.HasComponent,
         DialerExecutorComponent.HasComponent,
         DuoComponent.HasComponent,
         EnrichedCallComponent.HasComponent,
diff --git a/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java b/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java
index d9d0d26..12038b7 100644
--- a/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java
+++ b/java/com/android/dialer/binary/google/GoogleStubDialerRootComponent.java
@@ -18,6 +18,7 @@
 
 import com.android.dialer.binary.basecomponent.BaseDialerRootComponent;
 import com.android.dialer.calllog.CallLogModule;
+import com.android.dialer.commandline.CommandLineModule;
 import com.android.dialer.common.concurrent.DialerExecutorModule;
 import com.android.dialer.configprovider.SharedPrefConfigProviderModule;
 import com.android.dialer.duo.stub.StubDuoModule;
@@ -48,6 +49,7 @@
   modules = {
     CallLocationModule.class,
     CallLogModule.class,
+    CommandLineModule.class,
     ContextModule.class,
     DialerExecutorModule.class,
     MapsModule.class,
diff --git a/java/com/android/dialer/commandline/Command.java b/java/com/android/dialer/commandline/Command.java
new file mode 100644
index 0000000..5a35d40
--- /dev/null
+++ b/java/com/android/dialer/commandline/Command.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 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.commandline;
+
+import android.support.annotation.NonNull;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+
+/** Handles a Command from {@link CommandLineReceiver}. */
+public interface Command {
+
+  ListenableFuture<String> run(ImmutableList<String> args);
+
+  /** Describe the command when "help" is listing available commands. */
+  @NonNull
+  String getShortDescription();
+}
diff --git a/java/com/android/dialer/commandline/CommandLineComponent.java b/java/com/android/dialer/commandline/CommandLineComponent.java
new file mode 100644
index 0000000..c9abc53
--- /dev/null
+++ b/java/com/android/dialer/commandline/CommandLineComponent.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 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.commandline;
+
+import android.content.Context;
+import com.android.dialer.function.Supplier;
+import com.android.dialer.inject.HasRootComponent;
+import com.google.common.collect.ImmutableMap;
+import dagger.Subcomponent;
+
+/** Component to get all available commands. */
+@Subcomponent
+public abstract class CommandLineComponent {
+
+  public abstract Supplier<ImmutableMap<String, Command>> commandSupplier();
+
+  public static CommandLineComponent get(Context context) {
+    return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component())
+        .commandLineComponent();
+  }
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    CommandLineComponent commandLineComponent();
+  }
+}
diff --git a/java/com/android/dialer/commandline/CommandLineModule.java b/java/com/android/dialer/commandline/CommandLineModule.java
new file mode 100644
index 0000000..366899e
--- /dev/null
+++ b/java/com/android/dialer/commandline/CommandLineModule.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2018 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.commandline;
+
+import com.android.dialer.commandline.impl.Echo;
+import com.android.dialer.commandline.impl.Help;
+import com.android.dialer.commandline.impl.Version;
+import com.android.dialer.function.Supplier;
+import com.google.common.collect.ImmutableMap;
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Inject;
+
+/** Provides {@link Command} */
+@Module
+public abstract class CommandLineModule {
+
+  @Provides
+  static Supplier<ImmutableMap<String, Command>> provideCommandSupplier(
+      AospCommandInjector aospCommandInjector) {
+
+    return aospCommandInjector.inject(CommandSupplier.builder()).build();
+  }
+
+  /** Injects standard commands to the builder */
+  public static class AospCommandInjector {
+    private final Help help;
+    private final Version version;
+    private final Echo echo;
+
+    @Inject
+    AospCommandInjector(Help help, Version version, Echo echo) {
+      this.help = help;
+      this.version = version;
+      this.echo = echo;
+    }
+
+    public CommandSupplier.Builder inject(CommandSupplier.Builder builder) {
+      builder.addCommand("help", help);
+      builder.addCommand("version", version);
+      builder.addCommand("echo", echo);
+      return builder;
+    }
+  }
+}
diff --git a/java/com/android/dialer/commandline/CommandLineReceiver.java b/java/com/android/dialer/commandline/CommandLineReceiver.java
new file mode 100644
index 0000000..baaadf0
--- /dev/null
+++ b/java/com/android/dialer/commandline/CommandLineReceiver.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 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.commandline;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import com.android.dialer.buildtype.BuildType;
+import com.android.dialer.common.LogUtil;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
+
+/**
+ * Receives broadcasts to the component from adb shell. Must be on bugfood or have debug logging
+ * enabled.
+ */
+public class CommandLineReceiver extends BroadcastReceiver {
+
+  public static final String COMMAND = "command";
+  public static final String ARGS = "args";
+  public static final String TAG = "tag";
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    String outputTag = intent.getStringExtra(TAG);
+    if (outputTag == null) {
+      LogUtil.e("CommandLineReceiver", "missing tag");
+      return;
+    }
+    if (!LogUtil.isDebugEnabled() && BuildType.get() != BuildType.BUGFOOD) {
+      LogUtil.i(outputTag, "DISABLED");
+      return;
+    }
+    Command command =
+        CommandLineComponent.get(context)
+            .commandSupplier()
+            .get()
+            .get(intent.getStringExtra(COMMAND));
+    if (command == null) {
+      LogUtil.i(outputTag, "unknown command " + intent.getStringExtra(COMMAND));
+      return;
+    }
+
+    ImmutableList<String> args =
+        intent.hasExtra(ARGS)
+            ? ImmutableList.copyOf(intent.getStringArrayExtra(ARGS))
+            : ImmutableList.of();
+
+    try {
+      Futures.addCallback(
+          command.run(args),
+          new FutureCallback<String>() {
+            @Override
+            public void onSuccess(String response) {
+              if (TextUtils.isEmpty(response)) {
+                LogUtil.i(outputTag, "EMPTY");
+              } else {
+                LogUtil.i(outputTag, response);
+              }
+            }
+
+            @Override
+            public void onFailure(Throwable throwable) {
+              // LogUtil.e(tag, message, e) prints 2 entries where only the first one can be
+              // intercepted by the script. Compose the string instead.
+              LogUtil.e(outputTag, "error running command future", throwable);
+            }
+          },
+          MoreExecutors.directExecutor());
+    } catch (Throwable throwable) {
+      LogUtil.e(outputTag, "error running command", throwable);
+    }
+  }
+}
diff --git a/java/com/android/dialer/commandline/CommandSupplier.java b/java/com/android/dialer/commandline/CommandSupplier.java
new file mode 100644
index 0000000..7789258
--- /dev/null
+++ b/java/com/android/dialer/commandline/CommandSupplier.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 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.commandline;
+
+import com.android.dialer.function.Supplier;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+
+/** Supplies commands */
+@AutoValue
+public abstract class CommandSupplier implements Supplier<ImmutableMap<String, Command>> {
+
+  public static Builder builder() {
+    return new AutoValue_CommandSupplier.Builder();
+  }
+
+  public abstract ImmutableMap<String, Command> commands();
+
+  @Override
+  public ImmutableMap<String, Command> get() {
+    return commands();
+  }
+
+  /** builder for the supplier */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    abstract ImmutableMap.Builder<String, Command> commandsBuilder();
+
+    public Builder addCommand(String key, Command command) {
+      commandsBuilder().put(key, command);
+      return this;
+    }
+
+    public abstract CommandSupplier build();
+  }
+}
diff --git a/java/com/android/dialer/commandline/impl/Echo.java b/java/com/android/dialer/commandline/impl/Echo.java
new file mode 100644
index 0000000..b5f2f08
--- /dev/null
+++ b/java/com/android/dialer/commandline/impl/Echo.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 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.commandline.impl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import com.android.dialer.commandline.Command;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import javax.inject.Inject;
+
+/** Print arguments. */
+public class Echo implements Command {
+
+  @VisibleForTesting
+  @Inject
+  public Echo() {}
+
+  @Override
+  public ListenableFuture<String> run(ImmutableList<String> args) {
+    return Futures.immediateFuture(TextUtils.join(" ", args));
+  }
+
+  @NonNull
+  @Override
+  public String getShortDescription() {
+    return "@hide Print all arguments.";
+  }
+}
diff --git a/java/com/android/dialer/commandline/impl/Help.java b/java/com/android/dialer/commandline/impl/Help.java
new file mode 100644
index 0000000..d0e0080
--- /dev/null
+++ b/java/com/android/dialer/commandline/impl/Help.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2018 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.commandline.impl;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import com.android.dialer.commandline.Command;
+import com.android.dialer.commandline.CommandLineComponent;
+import com.android.dialer.inject.ApplicationContext;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutionException;
+import javax.inject.Inject;
+
+/** List available commands */
+public class Help implements Command {
+
+  private final Context context;
+
+  @Inject
+  Help(@ApplicationContext Context context) {
+    this.context = context;
+  }
+
+  @Override
+  public ListenableFuture<String> run(ImmutableList<String> args) {
+    boolean showHidden = args.contains("--showHidden");
+
+    StringBuilder stringBuilder = new StringBuilder();
+    ImmutableMap<String, Command> commands =
+        CommandLineComponent.get(context).commandSupplier().get();
+    stringBuilder
+        .append(runOrThrow(commands.get("version")))
+        .append("\n")
+        .append("\n")
+        .append("usage: <command> [args...]\n")
+        .append("\n")
+        .append("<command>\n");
+
+    for (Entry<String, Command> entry : commands.entrySet()) {
+      String description = entry.getValue().getShortDescription();
+      if (!showHidden && description.startsWith("@hide ")) {
+        continue;
+      }
+      stringBuilder
+          .append("\t")
+          .append(entry.getKey())
+          .append("\t")
+          .append(description)
+          .append("\n");
+    }
+
+    return Futures.immediateFuture(stringBuilder.toString());
+  }
+
+  private static String runOrThrow(Command command) {
+    try {
+      return command.run(ImmutableList.of()).get();
+    } catch (InterruptedException e) {
+      Thread.interrupted();
+      throw new RuntimeException(e);
+    } catch (ExecutionException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @NonNull
+  @Override
+  public String getShortDescription() {
+    return "Print this message";
+  }
+}
diff --git a/java/com/android/dialer/commandline/impl/Version.java b/java/com/android/dialer/commandline/impl/Version.java
new file mode 100644
index 0000000..5dfad9a
--- /dev/null
+++ b/java/com/android/dialer/commandline/impl/Version.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 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.commandline.impl;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.support.annotation.NonNull;
+import com.android.dialer.commandline.Command;
+import com.android.dialer.inject.ApplicationContext;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.Locale;
+import javax.inject.Inject;
+
+/** Print the version name and code. */
+public class Version implements Command {
+
+  private final Context appContext;
+
+  @Inject
+  Version(@ApplicationContext Context context) {
+    this.appContext = context;
+  }
+
+  @Override
+  public ListenableFuture<String> run(ImmutableList<String> args) {
+    try {
+      PackageInfo info =
+          appContext.getPackageManager().getPackageInfo(appContext.getPackageName(), 0);
+      return Futures.immediateFuture(
+          String.format(Locale.US, "%s(%d)", info.versionName, info.versionCode));
+    } catch (NameNotFoundException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @NonNull
+  @Override
+  public String getShortDescription() {
+    return "Print dialer version";
+  }
+}
diff --git a/java/com/android/dialer/common/LogUtil.java b/java/com/android/dialer/common/LogUtil.java
index 32d7b96..126ebf2 100644
--- a/java/com/android/dialer/common/LogUtil.java
+++ b/java/com/android/dialer/common/LogUtil.java
@@ -133,9 +133,12 @@
    */
   public static void e(@NonNull String tag, @Nullable String msg, @NonNull Throwable throwable) {
     if (!TextUtils.isEmpty(msg)) {
-      println(android.util.Log.ERROR, TAG, tag, msg);
+      println(
+          android.util.Log.ERROR,
+          TAG,
+          tag,
+          msg + "\n" + android.util.Log.getStackTraceString(throwable));
     }
-    println(android.util.Log.ERROR, TAG, tag, android.util.Log.getStackTraceString(throwable));
   }
 
   /**
diff --git a/packages.mk b/packages.mk
index d06b028..2a445a3 100644
--- a/packages.mk
+++ b/packages.mk
@@ -24,6 +24,7 @@
 	com.android.dialer.calllog.ui.menu \
 	com.android.dialer.calllogutils \
 	com.android.dialer.clipboard \
+	com.android.dialer.commandline \
 	com.android.dialer.common \
 	com.android.dialer.configprovider \
 	com.android.dialer.contactactions \