examples/android: add example for grpc running under StrictMode (#5527)
* added android example running under strictmode to detect grpc clear text network transport
* renamed andriod strictmode example
* use OkHttpChannelBuilder's api to feed with user's own transport executor
* bump gradle and proto-plugin version to match that in master
* add README with a simple demo image
* Update README to use relative link to helloworld
diff --git a/examples/android/strictmode/README.md b/examples/android/strictmode/README.md
new file mode 100644
index 0000000..1ed2aa6
--- /dev/null
+++ b/examples/android/strictmode/README.md
@@ -0,0 +1,8 @@
+gRPC Android StrictMode Example
+========================
+
+- This example intends to show the compatibility of gRPC with Android StrictMode.
+- Android SDK version 28 is required for [`StrictMode.VmPolicy.Builder.penaltyListener`](https://developer.android.com/reference/android/os/StrictMode.VmPolicy.Builder.html#penaltyListener(java.util.concurrent.Executor,%20android.os.StrictMode.OnVmViolationListener)) used in the example.
+- This example does the same thing as [HelloWorld example](../helloworld) except popping up a dialog for detected StrictMode policy violation (shown below).
+
+
diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle
new file mode 100644
index 0000000..cfac883
--- /dev/null
+++ b/examples/android/strictmode/app/build.gradle
@@ -0,0 +1,55 @@
+apply plugin: 'com.android.application'
+apply plugin: 'com.google.protobuf'
+
+android {
+ compileSdkVersion 28
+
+ defaultConfig {
+ applicationId "io.grpc.strictmodehelloworldexample"
+ // API level 28 is required for StrictMode penaltyListener
+ minSdkVersion 28
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ debug { minifyEnabled false }
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ lintOptions {
+ disable 'GoogleAppIndexingWarning', 'HardcodedText', 'InvalidPackage'
+ textReport true
+ textOutput "stdout"
+ }
+}
+
+protobuf {
+ protoc { artifact = 'com.google.protobuf:protoc:3.7.0' }
+ plugins {
+ javalite { artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0" }
+ grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.21.0-SNAPSHOT' // CURRENT_GRPC_VERSION
+ }
+ }
+ generateProtoTasks {
+ all().each { task ->
+ task.plugins {
+ javalite {}
+ grpc { // Options added to --grpc_out
+ option 'lite' }
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation 'com.android.support:appcompat-v7:28.0.0'
+
+ // You need to build grpc-java to obtain these libraries below.
+ implementation 'io.grpc:grpc-okhttp:1.21.0-SNAPSHOT' // CURRENT_GRPC_VERSION
+ implementation 'io.grpc:grpc-protobuf-lite:1.21.0-SNAPSHOT' // CURRENT_GRPC_VERSION
+ implementation 'io.grpc:grpc-stub:1.21.0-SNAPSHOT' // CURRENT_GRPC_VERSION
+ implementation 'javax.annotation:javax.annotation-api:1.2'
+}
diff --git a/examples/android/strictmode/app/proguard-rules.pro b/examples/android/strictmode/app/proguard-rules.pro
new file mode 100644
index 0000000..1507a52
--- /dev/null
+++ b/examples/android/strictmode/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in $ANDROID_HOME/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+-dontwarn com.google.common.**
+# Ignores: can't find referenced class javax.lang.model.element.Modifier
+-dontwarn com.google.errorprone.annotations.**
+-dontwarn javax.naming.**
+-dontwarn okio.**
+-dontwarn sun.misc.Unsafe
diff --git a/examples/android/strictmode/app/src/main/AndroidManifest.xml b/examples/android/strictmode/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..41c63f0
--- /dev/null
+++ b/examples/android/strictmode/app/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.grpc.strictmodehelloworldexample" >
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:allowBackup="false"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/Base.V7.Theme.AppCompat.Light" >
+ <activity
+ android:name=".StrictModeHelloworldActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/examples/android/strictmode/app/src/main/java/io/grpc/strictmodehelloworldexample/StrictModeHelloworldActivity.java b/examples/android/strictmode/app/src/main/java/io/grpc/strictmodehelloworldexample/StrictModeHelloworldActivity.java
new file mode 100644
index 0000000..44e37cc
--- /dev/null
+++ b/examples/android/strictmode/app/src/main/java/io/grpc/strictmodehelloworldexample/StrictModeHelloworldActivity.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2015 The gRPC Authors
+ *
+ * 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 io.grpc.strictmodehelloworldexample;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.os.StrictMode.OnVmViolationListener;
+import android.os.StrictMode.VmPolicy;
+import android.os.strictmode.Violation;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.text.method.ScrollingMovementMethod;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import io.grpc.ManagedChannel;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.okhttp.OkHttpChannelBuilder;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class StrictModeHelloworldActivity extends AppCompatActivity {
+ private Button sendButton;
+ private EditText hostEdit;
+ private EditText portEdit;
+ private EditText messageEdit;
+ private TextView resultText;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ VmPolicy policy =
+ new StrictMode.VmPolicy.Builder()
+ .detectCleartextNetwork()
+ .penaltyListener(
+ MoreExecutors.directExecutor(),
+ new OnVmViolationListener() {
+ @Override
+ public void onVmViolation(final Violation v) {
+ runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ new AlertDialog.Builder(StrictModeHelloworldActivity.this)
+ .setTitle("StrictMode VM Violation")
+ .setMessage(v.getLocalizedMessage())
+ .show();
+ }
+ });
+ }
+ })
+ .penaltyLog()
+ .build();
+ StrictMode.setVmPolicy(policy);
+ setContentView(R.layout.activity_strictmodehelloworld);
+ sendButton = (Button) findViewById(R.id.send_button);
+ hostEdit = (EditText) findViewById(R.id.host_edit_text);
+ portEdit = (EditText) findViewById(R.id.port_edit_text);
+ messageEdit = (EditText) findViewById(R.id.message_edit_text);
+ resultText = (TextView) findViewById(R.id.grpc_response_text);
+ resultText.setMovementMethod(new ScrollingMovementMethod());
+ }
+
+ public void sendMessage(View view) {
+ ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE))
+ .hideSoftInputFromWindow(hostEdit.getWindowToken(), 0);
+ sendButton.setEnabled(false);
+ resultText.setText("");
+ new GrpcTask(this)
+ .execute(
+ hostEdit.getText().toString(),
+ messageEdit.getText().toString(),
+ portEdit.getText().toString());
+ }
+
+ private static class GrpcTask extends AsyncTask<String, Void, String> {
+ private final WeakReference<Activity> activityReference;
+ private ManagedChannel channel;
+
+ private GrpcTask(Activity activity) {
+ this.activityReference = new WeakReference<Activity>(activity);
+ }
+
+ @Override
+ protected String doInBackground(String... params) {
+ String host = params[0];
+ String message = params[1];
+ String portStr = params[2];
+ int port = TextUtils.isEmpty(portStr) ? 0 : Integer.valueOf(portStr);
+ try {
+ channel =
+ OkHttpChannelBuilder.forAddress(host, port)
+ .transportExecutor(new NetworkTaggingExecutor(0xFDD))
+ .usePlaintext()
+ .build();
+ GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel);
+ HelloRequest request = HelloRequest.newBuilder().setName(message).build();
+ HelloReply reply = stub.sayHello(request);
+ return reply.getMessage();
+ } catch (Exception e) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ e.printStackTrace(pw);
+ pw.flush();
+ return String.format("Failed... : %n%s", sw);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(String result) {
+ try {
+ channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ Activity activity = activityReference.get();
+ if (activity == null) {
+ return;
+ }
+ TextView resultText = (TextView) activity.findViewById(R.id.grpc_response_text);
+ Button sendButton = (Button) activity.findViewById(R.id.send_button);
+ resultText.setText(result);
+ sendButton.setEnabled(true);
+ }
+ }
+
+ private static class NetworkTaggingExecutor extends ThreadPoolExecutor {
+
+ private int trafficStatsTag;
+
+ NetworkTaggingExecutor(int tag) {
+ super(
+ 0,
+ Integer.MAX_VALUE,
+ 60L,
+ TimeUnit.SECONDS,
+ new SynchronousQueue<Runnable>(),
+ new ThreadFactoryBuilder().setDaemon(true).setNameFormat("grpc-android-%d").build());
+ trafficStatsTag = tag;
+ }
+
+ @Override
+ protected void beforeExecute(Thread t, Runnable r) {
+ TrafficStats.setThreadStatsTag(trafficStatsTag);
+ super.beforeExecute(t, r);
+ }
+
+ @Override
+ protected void afterExecute(Runnable r, Throwable t) {
+ TrafficStats.clearThreadStatsTag();
+ super.afterExecute(r, t);
+ }
+ }
+}
diff --git a/examples/android/strictmode/app/src/main/proto/helloworld.proto b/examples/android/strictmode/app/src/main/proto/helloworld.proto
new file mode 100644
index 0000000..c60d941
--- /dev/null
+++ b/examples/android/strictmode/app/src/main/proto/helloworld.proto
@@ -0,0 +1,37 @@
+// Copyright 2015 The gRPC Authors
+//
+// 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.
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "io.grpc.examples.helloworld";
+option java_outer_classname = "HelloWorldProto";
+option objc_class_prefix = "HLW";
+
+package helloworld;
+
+// The greeting service definition.
+service Greeter {
+ // Sends a greeting
+ rpc SayHello (HelloRequest) returns (HelloReply) {}
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+ string name = 1;
+}
+
+// The response message containing the greetings
+message HelloReply {
+ string message = 1;
+}
diff --git a/examples/android/strictmode/app/src/main/res/layout/activity_strictmodehelloworld.xml b/examples/android/strictmode/app/src/main/res/layout/activity_strictmodehelloworld.xml
new file mode 100644
index 0000000..18b05c3
--- /dev/null
+++ b/examples/android/strictmode/app/src/main/res/layout/activity_strictmodehelloworld.xml
@@ -0,0 +1,55 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <EditText
+ android:id="@+id/host_edit_text"
+ android:layout_weight="2"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:hint="Enter Host" />
+ <EditText
+ android:id="@+id/port_edit_text"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:inputType="numberDecimal"
+ android:hint="Enter Port" />
+ </LinearLayout>
+
+
+ <EditText
+ android:id="@+id/message_edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="Enter message to send" />
+
+ <Button
+ android:id="@+id/send_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="sendMessage"
+ android:text="Send Grpc Request" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="12dp"
+ android:paddingBottom="12dp"
+ android:textSize="16sp"
+ android:text="Response:" />
+
+ <TextView
+ android:id="@+id/grpc_response_text"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars = "vertical"
+ android:textSize="16sp" />
+
+</LinearLayout>
diff --git a/examples/android/strictmode/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/android/strictmode/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/examples/android/strictmode/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/examples/android/strictmode/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/android/strictmode/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/examples/android/strictmode/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/examples/android/strictmode/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/android/strictmode/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/examples/android/strictmode/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/android/strictmode/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/android/strictmode/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/examples/android/strictmode/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/android/strictmode/app/src/main/res/values/strings.xml b/examples/android/strictmode/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..53389a0
--- /dev/null
+++ b/examples/android/strictmode/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">GrpcStrictModeHelloworldExample</string>
+</resources>
diff --git a/examples/android/strictmode/build.gradle b/examples/android/strictmode/build.gradle
new file mode 100644
index 0000000..4e934ef
--- /dev/null
+++ b/examples/android/strictmode/build.gradle
@@ -0,0 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.3.0'
+ classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.8"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ mavenLocal()
+ }
+}
diff --git a/examples/android/strictmode/demo.png b/examples/android/strictmode/demo.png
new file mode 100644
index 0000000..dd409cf
--- /dev/null
+++ b/examples/android/strictmode/demo.png
Binary files differ
diff --git a/examples/android/strictmode/settings.gradle b/examples/android/strictmode/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/examples/android/strictmode/settings.gradle
@@ -0,0 +1 @@
+include ':app'