diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/GetClipboard.java b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/GetClipboard.java new file mode 100644 index 000000000..421e6fa29 --- /dev/null +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/GetClipboard.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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.appium.espressoserver.lib.handlers; + +import android.app.Instrumentation; +import android.util.Base64; + +import java.nio.charset.StandardCharsets; + +import io.appium.espressoserver.lib.handlers.exceptions.AppiumException; +import io.appium.espressoserver.lib.handlers.exceptions.InvalidArgumentException; +import io.appium.espressoserver.lib.helpers.ClipboardHelper; +import io.appium.espressoserver.lib.model.ClipboardDataType; +import io.appium.espressoserver.lib.model.GetClipboardParams; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +public class GetClipboard implements RequestHandler { + private final Instrumentation mInstrumentation = getInstrumentation(); + + @Override + public String handle(GetClipboardParams params) throws AppiumException { + try { + return getClipboardResponse(params.getContentType()); + } catch (IllegalArgumentException e) { + throw new InvalidArgumentException(e); + } + } + + // Clip feature should run with main thread + private String getClipboardResponse(ClipboardDataType contentType) { + GetClipboardRunnable runnable = new GetClipboardRunnable(contentType); + mInstrumentation.runOnMainSync(runnable); + return runnable.getContent(); + } + + private static String toBase64String(String s) { + return Base64.encodeToString(s.getBytes(StandardCharsets.UTF_8), Base64.DEFAULT); + } + + private class GetClipboardRunnable implements Runnable { + private final ClipboardDataType contentType; + private volatile String content; + + GetClipboardRunnable(ClipboardDataType contentType) { + this.contentType = contentType; + } + + @Override + public void run() { + switch (contentType) { + case PLAINTEXT: + content = toBase64String(new ClipboardHelper(mInstrumentation.getTargetContext()).getTextData()); + break; + default: + throw new IllegalArgumentException( + String.format("Only '%s' content types are supported. '%s' is given instead", + ClipboardDataType.supportedDataTypes(), contentType)); + } + } + + public String getContent() { + return content; + } + } +} diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/SetClipboard.java b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/SetClipboard.java new file mode 100644 index 000000000..4985768bf --- /dev/null +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/SetClipboard.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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.appium.espressoserver.lib.handlers; + +import android.app.Instrumentation; +import android.util.Base64; + +import java.nio.charset.StandardCharsets; + +import io.appium.espressoserver.lib.handlers.exceptions.AppiumException; +import io.appium.espressoserver.lib.handlers.exceptions.InvalidArgumentException; +import io.appium.espressoserver.lib.helpers.ClipboardHelper; +import io.appium.espressoserver.lib.model.ClipboardDataType; +import io.appium.espressoserver.lib.model.SetClipboardParams; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +public class SetClipboard implements RequestHandler { + private final Instrumentation mInstrumentation = getInstrumentation(); + + @Override + public Void handle(SetClipboardParams params) throws AppiumException { + if (params.getContent() == null) { + throw new InvalidArgumentException("The 'content' argument is mandatory"); + } + try { + mInstrumentation.runOnMainSync(new SetClipboardRunnable( + params.getContentType(), params.getLabel(), fromBase64String(params.getContent()))); + } catch (IllegalArgumentException e) { + throw new InvalidArgumentException(e); + } + return null; + } + + private static String fromBase64String(String s) { + return new String(Base64.decode(s, Base64.DEFAULT), StandardCharsets.UTF_8); + } + + // Clip feature should run with main thread + private class SetClipboardRunnable implements Runnable { + private final ClipboardDataType contentType; + private final String label; + private final String content; + + SetClipboardRunnable(ClipboardDataType contentType, String label, String content) { + this.contentType = contentType; + this.label = label; + this.content = content; + } + + @Override + public void run() { + switch (contentType) { + case PLAINTEXT: + new ClipboardHelper(mInstrumentation.getTargetContext()).setTextData(label, content); + break; + default: + throw new IllegalArgumentException( + String.format("Only '%s' content types are supported. '%s' is given instead", + ClipboardDataType.supportedDataTypes(), contentType)); + } + } + } +} diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/exceptions/InvalidArgumentException.java b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/exceptions/InvalidArgumentException.java index 8f723e653..25ac7bf5f 100644 --- a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/exceptions/InvalidArgumentException.java +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/exceptions/InvalidArgumentException.java @@ -22,4 +22,8 @@ public InvalidArgumentException(String reason) { super(reason); } + public InvalidArgumentException(Throwable cause) { + super(cause); + } + } diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/ClipboardHelper.java b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/ClipboardHelper.java new file mode 100644 index 000000000..3521339ad --- /dev/null +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/ClipboardHelper.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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.appium.espressoserver.lib.helpers; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ClipboardHelper { + private static final int DEFAULT_LABEL_LEN = 10; + + private final Context context; + + public ClipboardHelper(Context context) { + this.context = context; + } + + private ClipboardManager getManager() { + final ClipboardManager cm = (ClipboardManager) context + .getSystemService(Context.CLIPBOARD_SERVICE); + if (cm == null) { + throw new ClipboardError("Cannot receive ClipboardManager instance from the system"); + } + return cm; + } + + @NonNull + public String getTextData() { + final ClipboardManager cm = getManager(); + if (!cm.hasPrimaryClip()) { + return ""; + } + final ClipData cd = cm.getPrimaryClip(); + if (cd == null || cd.getItemCount() == 0) { + return ""; + } + final CharSequence text = cd.getItemAt(0).coerceToText(context); + return text == null ? "" : text.toString(); + } + + public void setTextData(@Nullable String label, String data) { + final ClipboardManager cm = getManager(); + String labeltoSet = label; + if (labeltoSet == null) { + labeltoSet = data.length() >= DEFAULT_LABEL_LEN + ? data.substring(0, DEFAULT_LABEL_LEN) + : data; + } + cm.setPrimaryClip(ClipData.newPlainText(labeltoSet, data)); + } + + public static class ClipboardError extends RuntimeException { + ClipboardError(String message) { + super(message); + } + } +} diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/http/Router.java b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/http/Router.java index d00563d37..e79caa1a5 100644 --- a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/http/Router.java +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/http/Router.java @@ -39,6 +39,7 @@ import io.appium.espressoserver.lib.handlers.FindElements; import io.appium.espressoserver.lib.handlers.GetAlertText; import io.appium.espressoserver.lib.handlers.GetAttribute; +import io.appium.espressoserver.lib.handlers.GetClipboard; import io.appium.espressoserver.lib.handlers.GetDeviceInfo; import io.appium.espressoserver.lib.handlers.GetDisplayed; import io.appium.espressoserver.lib.handlers.GetEnabled; @@ -72,6 +73,7 @@ import io.appium.espressoserver.lib.handlers.ScreenshotHandler; import io.appium.espressoserver.lib.handlers.ScrollToPage; import io.appium.espressoserver.lib.handlers.SendKeys; +import io.appium.espressoserver.lib.handlers.SetClipboard; import io.appium.espressoserver.lib.handlers.SetDate; import io.appium.espressoserver.lib.handlers.SetOrientation; import io.appium.espressoserver.lib.handlers.SetTime; @@ -101,6 +103,7 @@ import io.appium.espressoserver.lib.model.DrawerActionParams; import io.appium.espressoserver.lib.model.EditorActionParams; import io.appium.espressoserver.lib.model.ElementValueParams; +import io.appium.espressoserver.lib.model.GetClipboardParams; import io.appium.espressoserver.lib.model.KeyEventParams; import io.appium.espressoserver.lib.model.Locator; import io.appium.espressoserver.lib.model.MobileBackdoorParams; @@ -111,6 +114,7 @@ import io.appium.espressoserver.lib.model.ScrollToPageParams; import io.appium.espressoserver.lib.model.Session; import io.appium.espressoserver.lib.model.SessionParams; +import io.appium.espressoserver.lib.model.SetClipboardParams; import io.appium.espressoserver.lib.model.SetDateParams; import io.appium.espressoserver.lib.model.SetTimeParams; import io.appium.espressoserver.lib.model.StartActivityParams; @@ -187,6 +191,8 @@ class Router { routeMap.addRoute(new RouteDefinition(Method.GET, "/session/:sessionId/element/:elementId/equals/:otherId", new ElementEquals(), AppiumParams.class)); routeMap.addRoute(new RouteDefinition(Method.POST, "/session/:sessionId/appium/element/:elementId/value", new ElementValue(false), ElementValueParams.class)); routeMap.addRoute(new RouteDefinition(Method.POST, "/session/:sessionId/appium/element/:elementId/replace_value", new ElementValue(true), ElementValueParams.class)); + routeMap.addRoute(new RouteDefinition(Method.POST, "/session/:sessionId/appium/device/get_clipboard", new GetClipboard(), GetClipboardParams.class)); + routeMap.addRoute(new RouteDefinition(Method.POST, "/session/:sessionId/appium/device/set_clipboard", new SetClipboard(), SetClipboardParams.class)); // touch events routeMap.addRoute(new RouteDefinition(Method.POST, "/session/:sessionId/touch/click", new PointerEventHandler(CLICK), MotionEventParams.class)); diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/ClipboardDataType.java b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/ClipboardDataType.java new file mode 100644 index 000000000..d84909e7d --- /dev/null +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/ClipboardDataType.java @@ -0,0 +1,14 @@ +package io.appium.espressoserver.lib.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.Arrays; + +public enum ClipboardDataType { + @SerializedName("PLAINTEXT") + PLAINTEXT; + + public static String supportedDataTypes() { + return Arrays.toString(values()); + } +} diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/GetClipboardParams.java b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/GetClipboardParams.java new file mode 100644 index 000000000..69f03fee0 --- /dev/null +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/GetClipboardParams.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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.appium.espressoserver.lib.model; + +import androidx.annotation.NonNull; + +@SuppressWarnings("unused") +public class GetClipboardParams extends AppiumParams { + private ClipboardDataType contentType; + + @NonNull + public ClipboardDataType getContentType() { + return this.contentType == null ? ClipboardDataType.PLAINTEXT : this.contentType; + } +} diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/SetClipboardParams.java b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/SetClipboardParams.java new file mode 100644 index 000000000..7f5cc03ae --- /dev/null +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/SetClipboardParams.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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.appium.espressoserver.lib.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +@SuppressWarnings("unused") +public class SetClipboardParams extends AppiumParams { + private ClipboardDataType contentType; + private String content; + private String label; + + @NonNull + public ClipboardDataType getContentType() { + return this.contentType == null ? ClipboardDataType.PLAINTEXT : this.contentType; + } + + @Nullable + public String getContent() { + return this.content; + } + + @Nullable + public String getLabel() { + return this.label; + } +}