diff options
author | Eric Gribkoff <ericgribkoff@google.com> | 2017-12-06 22:55:09 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-12-06 22:55:09 -0800 |
commit | 94bbe64e22ee75ef9a685e5c355005a6fcaef39e (patch) | |
tree | b81855f08290b8ac1dd00d5e420a4b60ce904503 /examples | |
parent | b9278e70112f472224a6944d43a6f158dfd8262c (diff) | |
download | grpc-grpc-java-94bbe64e22ee75ef9a685e5c355005a6fcaef39e.tar.gz |
examples: add Android client side caching example app
Diffstat (limited to 'examples')
19 files changed, 1520 insertions, 0 deletions
diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle new file mode 100644 index 000000000..5c0c647dc --- /dev/null +++ b/examples/android/clientcache/app/build.gradle @@ -0,0 +1,67 @@ +apply plugin: 'com.android.application' +apply plugin: 'com.google.protobuf' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + + defaultConfig { + applicationId "io.grpc.android.clientcacheexample" + minSdkVersion 19 + targetSdkVersion 25 + multiDexEnabled true + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + disable 'InvalidPackage', 'HardcodedText' + textReport true + textOutput "stdout" + } +} + +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.4.0' + } + plugins { + javalite { + artifact = "com.google.protobuf:protoc-gen-javalite:3.0.0" + } + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.9.0-SNAPSHOT' // CURRENT_GRPC_VERSION + } + } + generateProtoTasks { + all().each { task -> + task.plugins { + javalite {} + grpc { + // Options added to --grpc_out + option 'lite' + } + } + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:25.0.0' + + // You need to build grpc-java to obtain these libraries below. + compile 'io.grpc:grpc-okhttp:1.9.0-SNAPSHOT' // CURRENT_GRPC_VERSION + compile 'io.grpc:grpc-protobuf-lite:1.9.0-SNAPSHOT' // CURRENT_GRPC_VERSION + compile 'io.grpc:grpc-stub:1.9.0-SNAPSHOT' // CURRENT_GRPC_VERSION + compile 'javax.annotation:javax.annotation-api:1.2' + + testCompile 'junit:junit:4.12' + testCompile 'com.google.truth:truth:0.28' + testCompile 'io.grpc:grpc-testing:1.9.0-SNAPSHOT' +} diff --git a/examples/android/clientcache/app/proguard-rules.pro b/examples/android/clientcache/app/proguard-rules.pro new file mode 100644 index 000000000..1507a5267 --- /dev/null +++ b/examples/android/clientcache/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/clientcache/app/src/main/AndroidManifest.xml b/examples/android/clientcache/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d31cbd940 --- /dev/null +++ b/examples/android/clientcache/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.clientcacheexample" > + + <uses-permission android:name="android.permission.INTERNET" /> + + <application + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:theme="@style/Base.V7.Theme.AppCompat.Light" > + <activity + android:name="io.grpc.clientcacheexample.ClientCacheExampleActivity" + 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/clientcache/app/src/main/java/io/grpc/clientcacheexample/ClientCacheExampleActivity.java b/examples/android/clientcache/app/src/main/java/io/grpc/clientcacheexample/ClientCacheExampleActivity.java new file mode 100644 index 000000000..831c1030a --- /dev/null +++ b/examples/android/clientcache/app/src/main/java/io/grpc/clientcacheexample/ClientCacheExampleActivity.java @@ -0,0 +1,152 @@ +/* + * Copyright 2015, gRPC Authors All rights reserved. + * + * 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.clientcacheexample; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; +import android.text.method.ScrollingMovementMethod; +import android.util.Log; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.TextView; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientInterceptors; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.MethodDescriptor; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.stub.ClientCalls; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.concurrent.TimeUnit; + +public final class ClientCacheExampleActivity extends AppCompatActivity { + private static final int CACHE_SIZE_IN_BYTES = 1 * 1024 * 1024; // 1MB + private static final String TAG = "grpcCacheExample"; + private Button mSendButton; + private EditText mHostEdit; + private EditText mPortEdit; + private EditText mMessageEdit; + private TextView mResultText; + private CheckBox getCheckBox; + private CheckBox noCacheCheckBox; + private CheckBox onlyIfCachedCheckBox; + private SafeMethodCachingInterceptor.Cache cache; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_clientcacheexample); + mSendButton = (Button) findViewById(R.id.send_button); + mHostEdit = (EditText) findViewById(R.id.host_edit_text); + mPortEdit = (EditText) findViewById(R.id.port_edit_text); + mMessageEdit = (EditText) findViewById(R.id.message_edit_text); + getCheckBox = (CheckBox) findViewById(R.id.get_checkbox); + noCacheCheckBox = (CheckBox) findViewById(R.id.no_cache_checkbox); + onlyIfCachedCheckBox = (CheckBox) findViewById(R.id.only_if_cached_checkbox); + mResultText = (TextView) findViewById(R.id.grpc_response_text); + mResultText.setMovementMethod(new ScrollingMovementMethod()); + + cache = SafeMethodCachingInterceptor.newLruCache(CACHE_SIZE_IN_BYTES); + } + + /** Sends RPC. Invoked when app button is pressed. */ + public void sendMessage(View view) { + ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)) + .hideSoftInputFromWindow(mHostEdit.getWindowToken(), 0); + mSendButton.setEnabled(false); + new GrpcTask().execute(); + } + + private class GrpcTask extends AsyncTask<Void, Void, String> { + private String host; + private String message; + private int port; + private ManagedChannel channel; + + @Override + protected void onPreExecute() { + host = mHostEdit.getText().toString(); + message = mMessageEdit.getText().toString(); + String portStr = mPortEdit.getText().toString(); + port = TextUtils.isEmpty(portStr) ? 0 : Integer.valueOf(portStr); + mResultText.setText(""); + } + + @Override + protected String doInBackground(Void... nothing) { + try { + channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext(true).build(); + Channel channelToUse = + ClientInterceptors.intercept( + channel, SafeMethodCachingInterceptor.newSafeMethodCachingInterceptor(cache)); + HelloRequest message = HelloRequest.newBuilder().setName(this.message).build(); + HelloReply reply; + if (getCheckBox.isChecked()) { + MethodDescriptor<HelloRequest, HelloReply> safeCacheableUnaryCallMethod = + GreeterGrpc.getSayHelloMethod().toBuilder().setSafe(true).build(); + CallOptions callOptions = CallOptions.DEFAULT; + if (noCacheCheckBox.isChecked()) { + callOptions = + callOptions.withOption(SafeMethodCachingInterceptor.NO_CACHE_CALL_OPTION, true); + } + if (onlyIfCachedCheckBox.isChecked()) { + callOptions = + callOptions.withOption( + SafeMethodCachingInterceptor.ONLY_IF_CACHED_CALL_OPTION, true); + } + reply = + ClientCalls.blockingUnaryCall( + channelToUse, safeCacheableUnaryCallMethod, callOptions, message); + } else { + GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channelToUse); + reply = stub.sayHello(message); + } + return reply.getMessage(); + } catch (Exception e) { + Log.e(TAG, "RPC failed", 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) { + if (channel != null) { + try { + channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + mResultText.setText(result); + mSendButton.setEnabled(true); + } + } +} diff --git a/examples/android/clientcache/app/src/main/java/io/grpc/clientcacheexample/SafeMethodCachingInterceptor.java b/examples/android/clientcache/app/src/main/java/io/grpc/clientcacheexample/SafeMethodCachingInterceptor.java new file mode 100644 index 000000000..4ae0c01c5 --- /dev/null +++ b/examples/android/clientcache/app/src/main/java/io/grpc/clientcacheexample/SafeMethodCachingInterceptor.java @@ -0,0 +1,300 @@ +package io.grpc.clientcacheexample; + +import android.util.Log; +import android.util.LruCache; +import com.google.common.base.Splitter; +import com.google.protobuf.MessageLite; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.Deadline; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * An example of an on-device cache for Android implemented using the {@link ClientInterceptor} API. + * + * <p>Client-side cache-control directives are not directly supported. Instead, two call options can + * be added to the call: no-cache (always go to the network) or only-if-cached (never use network; + * if response is not in cache, the request fails). + * + * <p>This interceptor respects the cache-control directives in the server's response: max-age + * determines when the cache entry goes stale. no-cache, no-store, and no-transform entirely skip + * caching of the response. must-revalidate is ignored, as the cache does not support returning + * stale responses. + * + * <p>Note: other response headers besides cache-control (such as Expiration, Varies) are ignored by + * this implementation. + */ +final class SafeMethodCachingInterceptor implements ClientInterceptor { + static CallOptions.Key<Boolean> NO_CACHE_CALL_OPTION = CallOptions.Key.of("no-cache", false); + static CallOptions.Key<Boolean> ONLY_IF_CACHED_CALL_OPTION = + CallOptions.Key.of("only-if-cached", false); + private static final String TAG = "grpcCacheExample"; + + public static final class Key { + private final String fullMethodName; + private final MessageLite request; + + public Key(String fullMethodName, MessageLite request) { + this.fullMethodName = fullMethodName; + this.request = request; + } + + @Override + public boolean equals(Object object) { + if (object instanceof Key) { + Key other = (Key) object; + return Objects.equals(this.fullMethodName, other.fullMethodName) + && Objects.equals(this.request, other.request); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(fullMethodName, request); + } + } + + public static final class Value { + private final MessageLite response; + private final Deadline maxAgeDeadline; + + public Value(MessageLite response, Deadline maxAgeDeadline) { + this.response = response; + this.maxAgeDeadline = maxAgeDeadline; + } + + @Override + public boolean equals(Object object) { + if (object instanceof Value) { + Value other = (Value) object; + return Objects.equals(this.response, other.response) + && Objects.equals(this.maxAgeDeadline, other.maxAgeDeadline); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(response, maxAgeDeadline); + } + } + + public interface Cache { + void put(Key key, Value value); + + Value get(Key key); + + void remove(Key key); + + void clear(); + } + + /** + * Obtain a new cache with a least-recently used eviction policy and the specified size limit. The + * backing caching implementation is provided by {@link LruCache}. It is safe for a single cache + * to be shared across multiple {@link SafeMethodCachingInterceptor}s without synchronization. + */ + public static Cache newLruCache(final int cacheSizeInBytes) { + return new Cache() { + private final LruCache<Key, Value> lruCache = + new LruCache<Key, Value>(cacheSizeInBytes) { + protected int sizeOf(Key key, Value value) { + return value.response.getSerializedSize(); + } + }; + + @Override + public void put(Key key, Value value) { + lruCache.put(key, value); + } + + @Override + public Value get(Key key) { + return lruCache.get(key); + } + + @Override + public void remove(Key key) { + lruCache.remove(key); + } + + @Override + public void clear() { + lruCache.evictAll(); + } + }; + } + + public static SafeMethodCachingInterceptor newSafeMethodCachingInterceptor(Cache cache) { + return newSafeMethodCachingInterceptor(cache, DEFAULT_MAX_AGE_SECONDS); + } + + public static SafeMethodCachingInterceptor newSafeMethodCachingInterceptor( + Cache cache, int defaultMaxAge) { + return new SafeMethodCachingInterceptor(cache, defaultMaxAge); + } + + private static int DEFAULT_MAX_AGE_SECONDS = 3600; + + private static final Metadata.Key<String> CACHE_CONTROL_KEY = + Metadata.Key.of("cache-control", Metadata.ASCII_STRING_MARSHALLER); + + private static final Splitter CACHE_CONTROL_SPLITTER = + Splitter.on(',').trimResults().omitEmptyStrings(); + + private final Cache internalCache; + private final int defaultMaxAge; + + private SafeMethodCachingInterceptor(Cache cache, int defaultMaxAge) { + this.internalCache = cache; + this.defaultMaxAge = defaultMaxAge; + } + + @Override + public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall( + final MethodDescriptor<ReqT, RespT> method, final CallOptions callOptions, Channel next) { + // Currently only unary methods can be marked safe, but check anyways. + if (!method.isSafe() || method.getType() != MethodDescriptor.MethodType.UNARY) { + return next.newCall(method, callOptions); + } + + final String fullMethodName = method.getFullMethodName(); + + return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>( + next.newCall(method, callOptions)) { + private Listener<RespT> interceptedListener; + private Key requestKey; + private boolean cacheResponse = true; + private volatile String cacheOptionsErrorMsg; + + @Override + public void start(Listener<RespT> responseListener, Metadata headers) { + interceptedListener = + new ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>( + responseListener) { + private Deadline deadline; + private int maxAge = -1; + + @Override + public void onHeaders(Metadata headers) { + Iterable<String> cacheControlHeaders = headers.getAll(CACHE_CONTROL_KEY); + if (cacheResponse && cacheControlHeaders != null) { + for (String cacheControlHeader : cacheControlHeaders) { + for (String directive : CACHE_CONTROL_SPLITTER.split(cacheControlHeader)) { + if (directive.equalsIgnoreCase("no-cache")) { + cacheResponse = false; + break; + } else if (directive.equalsIgnoreCase("no-store")) { + cacheResponse = false; + break; + } else if (directive.equalsIgnoreCase("no-transform")) { + cacheResponse = false; + break; + } else if (directive.toLowerCase().startsWith("max-age")) { + String[] parts = directive.split("="); + if (parts.length == 2) { + try { + maxAge = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + Log.e(TAG, "max-age directive failed to parse", e); + continue; + } + } + } + } + } + } + if (cacheResponse) { + if (maxAge > -1) { + deadline = Deadline.after(maxAge, TimeUnit.SECONDS); + } else { + deadline = Deadline.after(defaultMaxAge, TimeUnit.SECONDS); + } + } + super.onHeaders(headers); + } + + @Override + public void onMessage(RespT message) { + if (cacheResponse && !deadline.isExpired()) { + Value value = new Value((MessageLite) message, deadline); + internalCache.put(requestKey, value); + } + super.onMessage(message); + } + + @Override + public void onClose(Status status, Metadata trailers) { + if (cacheOptionsErrorMsg != null) { + // UNAVAILABLE is the canonical gRPC mapping for HTTP response code 504 (as used + // by the built-in Android HTTP request cache). + super.onClose( + Status.UNAVAILABLE.withDescription(cacheOptionsErrorMsg), new Metadata()); + } else { + super.onClose(status, trailers); + } + } + }; + delegate().start(interceptedListener, headers); + } + + @Override + public void sendMessage(ReqT message) { + boolean noCache = callOptions.getOption(NO_CACHE_CALL_OPTION); + boolean onlyIfCached = callOptions.getOption(ONLY_IF_CACHED_CALL_OPTION); + + if (noCache) { + if (onlyIfCached) { + cacheOptionsErrorMsg = "Unsatisfiable Request (no-cache and only-if-cached conflict)"; + super.cancel(cacheOptionsErrorMsg, null); + return; + } + cacheResponse = false; + super.sendMessage(message); + return; + } + + // Check the cache + requestKey = new Key(fullMethodName, (MessageLite) message); + Value cachedResponse = internalCache.get(requestKey); + if (cachedResponse != null) { + if (cachedResponse.maxAgeDeadline.isExpired()) { + internalCache.remove(requestKey); + } else { + cacheResponse = false; // already cached + interceptedListener.onMessage((RespT) cachedResponse.response); + Metadata metadata = new Metadata(); + interceptedListener.onClose(Status.OK, metadata); + return; + } + } + + if (onlyIfCached) { + cacheOptionsErrorMsg = + "Unsatisfiable Request (only-if-cached set, but value not in cache)"; + super.cancel(cacheOptionsErrorMsg, null); + return; + } + super.sendMessage(message); + } + + @Override + public void halfClose() { + if (cacheOptionsErrorMsg != null) { + // already canceled + return; + } + super.halfClose(); + } + }; + } +} diff --git a/examples/android/clientcache/app/src/main/proto/helloworld.proto b/examples/android/clientcache/app/src/main/proto/helloworld.proto new file mode 100644 index 000000000..7469e1e47 --- /dev/null +++ b/examples/android/clientcache/app/src/main/proto/helloworld.proto @@ -0,0 +1,45 @@ +// Copyright 2015, gRPC Authors +// All rights reserved. +// +// 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) {} + + rpc SayAnotherHello (HelloRequest) returns (HelloReply) {} + +} + +service AnotherGreeter { + 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/clientcache/app/src/main/res/layout/activity_clientcacheexample.xml b/examples/android/clientcache/app/src/main/res/layout/activity_clientcacheexample.xml new file mode 100644 index 000000000..2e48e8a27 --- /dev/null +++ b/examples/android/clientcache/app/src/main/res/layout/activity_clientcacheexample.xml @@ -0,0 +1,73 @@ +<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" /> + + <CheckBox android:id="@+id/get_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Use GET" + /> + + <CheckBox android:id="@+id/no_cache_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Cache-control: no-cache (only for GET)" + /> + + <CheckBox android:id="@+id/only_if_cached_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Cache-control: only-if-cached (only for GET)" + /> + + <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/clientcache/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/android/clientcache/app/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..cde69bccc --- /dev/null +++ b/examples/android/clientcache/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/examples/android/clientcache/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/android/clientcache/app/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..c133a0cbd --- /dev/null +++ b/examples/android/clientcache/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/examples/android/clientcache/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/android/clientcache/app/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..bfa42f0e7 --- /dev/null +++ b/examples/android/clientcache/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/examples/android/clientcache/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/android/clientcache/app/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..324e72cdd --- /dev/null +++ b/examples/android/clientcache/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/examples/android/clientcache/app/src/main/res/values/strings.xml b/examples/android/clientcache/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..0f8d82d95 --- /dev/null +++ b/examples/android/clientcache/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<resources> + <string name="app_name">GrpcClientCacheExample</string> +</resources> diff --git a/examples/android/clientcache/app/src/test/java/io/grpc/clientcacheexample/SafeMethodCachingInterceptorTest.java b/examples/android/clientcache/app/src/test/java/io/grpc/clientcacheexample/SafeMethodCachingInterceptorTest.java new file mode 100644 index 000000000..4fd5bdb0c --- /dev/null +++ b/examples/android/clientcache/app/src/test/java/io/grpc/clientcacheexample/SafeMethodCachingInterceptorTest.java @@ -0,0 +1,557 @@ +package io.grpc.clientcacheexample; + +import com.google.common.truth.Truth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientInterceptors; +import io.grpc.ForwardingServerCall; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.examples.helloworld.AnotherGreeterGrpc; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.stub.ClientCalls; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcServerRule; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +public class SafeMethodCachingInterceptorTest { + private static final Metadata.Key<String> CACHE_CONTROL_METADATA_KEY = + Metadata.Key.of("cache-control", Metadata.ASCII_STRING_MARSHALLER); + + @Rule public final GrpcServerRule grpcServerRule = new GrpcServerRule().directExecutor(); + + private final GreeterGrpc.GreeterImplBase greeterServiceImpl = + new GreeterGrpc.GreeterImplBase() { + private int count = 1; + + @Override + public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) { + HelloReply reply = + HelloReply.newBuilder().setMessage("Hello " + req.getName() + " " + count++).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void sayAnotherHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) { + HelloReply reply = + HelloReply.newBuilder() + .setMessage("Hello again " + req.getName() + " " + count++) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + }; + + private final AnotherGreeterGrpc.AnotherGreeterImplBase anotherGreeterServiceImpl = + new AnotherGreeterGrpc.AnotherGreeterImplBase() { + private int count = 1; + + @Override + public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) { + HelloReply reply = + HelloReply.newBuilder().setMessage("Hey " + req.getName() + " " + count++).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + }; + + private final List<String> cacheControlDirectives = new ArrayList<String>(); + private ServerInterceptor injectCacheControlInterceptor = + new ServerInterceptor() { + @Override + public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( + ServerCall<ReqT, RespT> call, + final Metadata requestHeaders, + ServerCallHandler<ReqT, RespT> next) { + return next.startCall( + new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) { + @Override + public void sendHeaders(Metadata headers) { + for (String cacheControlDirective : cacheControlDirectives) { + headers.put(CACHE_CONTROL_METADATA_KEY, cacheControlDirective); + } + super.sendHeaders(headers); + } + }, + requestHeaders); + } + }; + + private final HelloRequest message = HelloRequest.newBuilder().setName("Test Name").build(); + private final MethodDescriptor<HelloRequest, HelloReply> safeGreeterSayHelloMethod = + GreeterGrpc.getSayHelloMethod().toBuilder().setSafe(true).build(); + private final TestCache cache = new TestCache(); + + private ManagedChannel baseChannel; + private Channel channelToUse; + + @Before + public void setUp() throws Exception { + grpcServerRule + .getServiceRegistry() + .addService( + ServerInterceptors.intercept(greeterServiceImpl, injectCacheControlInterceptor)); + grpcServerRule.getServiceRegistry().addService(anotherGreeterServiceImpl); + baseChannel = grpcServerRule.getChannel(); + + SafeMethodCachingInterceptor interceptor = + SafeMethodCachingInterceptor.newSafeMethodCachingInterceptor(cache); + + channelToUse = ClientInterceptors.intercept(baseChannel, interceptor); + } + + @After + public void tearDown() { + baseChannel.shutdown(); + } + + @Test + public void safeCallsAreCached() { + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertSame(reply1, reply2); + } + + @Test + public void safeCallsAreCachedWithCopiedMethodDescriptor() { + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, + safeGreeterSayHelloMethod.toBuilder().build(), + CallOptions.DEFAULT, + message); + + assertSame(reply1, reply2); + } + + @Test + public void requestWithNoCacheOptionSkipsCache() { + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, + safeGreeterSayHelloMethod, + CallOptions.DEFAULT.withOption(SafeMethodCachingInterceptor.NO_CACHE_CALL_OPTION, true), + message); + HelloReply reply3 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + assertSame(reply1, reply3); + } + + @Test + public void requestWithOnlyIfCachedOption_unavailableIfNotInCache() { + try { + ClientCalls.blockingUnaryCall( + channelToUse, + safeGreeterSayHelloMethod, + CallOptions.DEFAULT.withOption( + SafeMethodCachingInterceptor.ONLY_IF_CACHED_CALL_OPTION, true), + message); + fail("Expected call to fail"); + } catch (StatusRuntimeException sre) { + assertEquals(Status.UNAVAILABLE.getCode(), sre.getStatus().getCode()); + assertEquals( + "Unsatisfiable Request (only-if-cached set, but value not in cache)", + sre.getStatus().getDescription()); + } + } + + @Test + public void requestWithOnlyIfCachedOption_usesCache() { + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, + safeGreeterSayHelloMethod, + CallOptions.DEFAULT.withOption( + SafeMethodCachingInterceptor.ONLY_IF_CACHED_CALL_OPTION, true), + message); + + assertSame(reply1, reply2); + } + + @Test + public void requestWithNoCacheAndOnlyIfCached_fails() { + try { + ClientCalls.blockingUnaryCall( + channelToUse, + safeGreeterSayHelloMethod, + CallOptions.DEFAULT + .withOption(SafeMethodCachingInterceptor.NO_CACHE_CALL_OPTION, true) + .withOption(SafeMethodCachingInterceptor.ONLY_IF_CACHED_CALL_OPTION, true), + message); + fail("Expected call to fail"); + } catch (StatusRuntimeException sre) { + assertEquals(Status.UNAVAILABLE.getCode(), sre.getStatus().getCode()); + assertEquals( + "Unsatisfiable Request (no-cache and only-if-cached conflict)", + sre.getStatus().getDescription()); + } + } + + @Test + public void responseNoCacheDirective_notCached() throws Exception { + cacheControlDirectives.add("no-cache"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + assertNotEquals(reply1, reply2); + Truth.assertThat(cache.internalCache).isEmpty(); + Truth.assertThat(cache.removedKeys).isEmpty(); + } + + @Test + public void responseNoStoreDirective_notCached() throws Exception { + cacheControlDirectives.add("no-store"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + Truth.assertThat(cache.internalCache).isEmpty(); + Truth.assertThat(cache.removedKeys).isEmpty(); + } + + @Test + public void responseNoTransformDirective_notCached() throws Exception { + cacheControlDirectives.add("no-transform"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + Truth.assertThat(cache.internalCache).isEmpty(); + Truth.assertThat(cache.removedKeys).isEmpty(); + } + + @Test + public void responseMustRevalidateDirective_isIgnored() throws Exception { + cacheControlDirectives.add("must-revalidate"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertSame(reply1, reply2); + } + + @Test + public void responseMaxAge_caseInsensitive() throws Exception { + cacheControlDirectives.add("MaX-aGe=0"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + Truth.assertThat(cache.internalCache).isEmpty(); + Truth.assertThat(cache.removedKeys).isEmpty(); + } + + @Test + public void responseNoCache_caseInsensitive() throws Exception { + cacheControlDirectives.add("No-CaCHe"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + Truth.assertThat(cache.internalCache).isEmpty(); + Truth.assertThat(cache.removedKeys).isEmpty(); + } + + @Test + public void combinedResponseCacheControlDirectives_parsesWithoutError() throws Exception { + cacheControlDirectives.add("max-age=1,no-store , no-cache"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + Truth.assertThat(cache.internalCache).isEmpty(); + Truth.assertThat(cache.removedKeys).isEmpty(); + } + + @Test + public void separateResponseCacheControlDirectives_parsesWithoutError() throws Exception { + cacheControlDirectives.add("max-age=1"); + cacheControlDirectives.add("no-store , no-cache"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + Truth.assertThat(cache.internalCache).isEmpty(); + Truth.assertThat(cache.removedKeys).isEmpty(); + } + + @Test + public void afterResponseMaxAge_cacheEntryInvalidated() throws Exception { + cacheControlDirectives.add("max-age=1"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + assertSame(reply1, reply2); + + // Wait for cache entry to expire + sleepAtLeast(1001); + + assertNotEquals( + reply1, + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message)); + Truth.assertThat(cache.removedKeys).hasSize(1); + assertEquals( + new SafeMethodCachingInterceptor.Key( + GreeterGrpc.getSayHelloMethod().getFullMethodName(), message), + cache.removedKeys.get(0)); + } + + @Test + public void invalidResponseMaxAge_usesDefault() throws Exception { + SafeMethodCachingInterceptor interceptorWithCustomMaxAge = + SafeMethodCachingInterceptor.newSafeMethodCachingInterceptor(cache, 1); + channelToUse = ClientInterceptors.intercept(baseChannel, interceptorWithCustomMaxAge); + cacheControlDirectives.add("max-age=-10"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + assertEquals(reply1, reply2); + + // Wait for cache entry to expire + sleepAtLeast(1001); + + assertNotEquals( + reply1, + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message)); + Truth.assertThat(cache.removedKeys).hasSize(1); + assertEquals( + new SafeMethodCachingInterceptor.Key( + GreeterGrpc.getSayHelloMethod().getFullMethodName(), message), + cache.removedKeys.get(0)); + } + + @Test + public void responseMaxAgeZero_notAddedToCache() throws Exception { + cacheControlDirectives.add("max-age=0"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + Truth.assertThat(cache.internalCache).isEmpty(); + Truth.assertThat(cache.removedKeys).isEmpty(); + } + + @Test + public void cacheHit_doesNotResetExpiration() throws Exception { + cacheControlDirectives.add("max-age=1"); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + sleepAtLeast(1001); + + HelloReply reply3 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + + assertSame(reply1, reply2); + assertNotEquals(reply1, reply3); + Truth.assertThat(cache.internalCache).hasSize(1); + Truth.assertThat(cache.removedKeys).hasSize(1); + } + + @Test + public void afterDefaultMaxAge_cacheEntryInvalidated() throws Exception { + SafeMethodCachingInterceptor interceptorWithCustomMaxAge = + SafeMethodCachingInterceptor.newSafeMethodCachingInterceptor(cache, 1); + channelToUse = ClientInterceptors.intercept(baseChannel, interceptorWithCustomMaxAge); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + assertSame(reply1, reply2); + + // Wait for cache entry to expire + sleepAtLeast(1001); + + assertNotEquals( + reply1, + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message)); + Truth.assertThat(cache.removedKeys).hasSize(1); + assertEquals( + new SafeMethodCachingInterceptor.Key( + GreeterGrpc.getSayHelloMethod().getFullMethodName(), message), + cache.removedKeys.get(0)); + } + + @Test + public void unsafeCallsAreNotCached() { + GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channelToUse); + + HelloReply reply1 = stub.sayHello(message); + HelloReply reply2 = stub.sayHello(message); + + assertNotEquals(reply1, reply2); + } + + @Test + public void differentMethodCallsAreNotConflated() { + MethodDescriptor<HelloRequest, HelloReply> anotherSafeMethod = + GreeterGrpc.getSayAnotherHelloMethod().toBuilder().setSafe(true).build(); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, anotherSafeMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + } + + @Test + public void differentServiceCallsAreNotConflated() { + MethodDescriptor<HelloRequest, HelloReply> anotherSafeMethod = + AnotherGreeterGrpc.getSayHelloMethod().toBuilder().setSafe(true).build(); + + HelloReply reply1 = + ClientCalls.blockingUnaryCall( + channelToUse, safeGreeterSayHelloMethod, CallOptions.DEFAULT, message); + HelloReply reply2 = + ClientCalls.blockingUnaryCall( + channelToUse, anotherSafeMethod, CallOptions.DEFAULT, message); + + assertNotEquals(reply1, reply2); + } + + private static void sleepAtLeast(long millis) throws InterruptedException { + long delay = TimeUnit.MILLISECONDS.toNanos(millis); + long end = System.nanoTime() + delay; + while (delay > 0) { + TimeUnit.NANOSECONDS.sleep(delay); + delay = end - System.nanoTime(); + } + } + + private static class TestCache implements SafeMethodCachingInterceptor.Cache { + private Map<SafeMethodCachingInterceptor.Key, SafeMethodCachingInterceptor.Value> + internalCache = + new HashMap<SafeMethodCachingInterceptor.Key, SafeMethodCachingInterceptor.Value>(); + private List<SafeMethodCachingInterceptor.Key> removedKeys = + new ArrayList<SafeMethodCachingInterceptor.Key>(); + + @Override + public void put( + SafeMethodCachingInterceptor.Key key, SafeMethodCachingInterceptor.Value value) { + internalCache.put(key, value); + } + + @Override + public SafeMethodCachingInterceptor.Value get(SafeMethodCachingInterceptor.Key key) { + return internalCache.get(key); + } + + @Override + public void remove(SafeMethodCachingInterceptor.Key key) { + removedKeys.add(key); + internalCache.remove(key); + } + + @Override + public void clear() {} + } +} diff --git a/examples/android/clientcache/build.gradle b/examples/android/clientcache/build.gradle new file mode 100644 index 000000000..be0b49fe9 --- /dev/null +++ b/examples/android/clientcache/build.gradle @@ -0,0 +1,21 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.1' + classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.1" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + mavenLocal() + } +} diff --git a/examples/android/clientcache/gradle/wrapper/gradle-wrapper.jar b/examples/android/clientcache/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 000000000..36823b7ec --- /dev/null +++ b/examples/android/clientcache/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/android/clientcache/gradle/wrapper/gradle-wrapper.properties b/examples/android/clientcache/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..37c7766f3 --- /dev/null +++ b/examples/android/clientcache/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jul 11 13:59:44 PDT 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.1-bin.zip diff --git a/examples/android/clientcache/gradlew b/examples/android/clientcache/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/examples/android/clientcache/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/android/clientcache/gradlew.bat b/examples/android/clientcache/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/examples/android/clientcache/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/android/clientcache/settings.gradle b/examples/android/clientcache/settings.gradle new file mode 100644 index 000000000..e7b4def49 --- /dev/null +++ b/examples/android/clientcache/settings.gradle @@ -0,0 +1 @@ +include ':app' |