diff --git a/src/main/java/io/appium/java_client/MobileCommand.java b/src/main/java/io/appium/java_client/MobileCommand.java index 023c0974e..c58997068 100644 --- a/src/main/java/io/appium/java_client/MobileCommand.java +++ b/src/main/java/io/appium/java_client/MobileCommand.java @@ -18,6 +18,8 @@ import com.google.common.collect.ImmutableMap; +import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions; +import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions; import org.apache.commons.lang3.StringUtils; import org.openqa.selenium.remote.CommandInfo; import org.openqa.selenium.remote.http.HttpMethod; @@ -53,6 +55,8 @@ public class MobileCommand { protected static final String GET_PERFORMANCE_DATA; protected static final String GET_SUPPORTED_PERFORMANCE_DATA_TYPES; + public static final String START_RECORDING_SCREEN; + public static final String STOP_RECORDING_SCREEN; protected static final String HIDE_KEYBOARD; protected static final String LOCK; @@ -82,7 +86,7 @@ public class MobileCommand { protected static final String SET_SETTINGS; protected static final String GET_CURRENT_PACKAGE; - public static final Map commandRepository; + public static final Map commandRepository; static { RESET = "reset"; @@ -104,6 +108,9 @@ public class MobileCommand { GET_PERFORMANCE_DATA = "getPerformanceData"; GET_SUPPORTED_PERFORMANCE_DATA_TYPES = "getSuppportedPerformanceDataTypes"; + START_RECORDING_SCREEN = "startRecordingScreen"; + STOP_RECORDING_SCREEN = "stopRecordingScreen"; + HIDE_KEYBOARD = "hideKeyboard"; LOCK = "lock"; SHAKE = "shake"; @@ -150,12 +157,15 @@ public class MobileCommand { commandRepository.put(GET_SETTINGS, getC("/session/:sessionId/appium/settings")); commandRepository.put(SET_SETTINGS, postC("/session/:sessionId/appium/settings")); commandRepository.put(GET_DEVICE_TIME, getC("/session/:sessionId/appium/device/system_time")); - commandRepository.put(GET_SESSION,getC("/session/:sessionId/")); + commandRepository.put(GET_SESSION, getC("/session/:sessionId/")); commandRepository.put(GET_SUPPORTED_PERFORMANCE_DATA_TYPES, - postC("/session/:sessionId/appium/performanceData/types")); + postC("/session/:sessionId/appium/performanceData/types")); commandRepository.put(GET_PERFORMANCE_DATA, - postC("/session/:sessionId/appium/getPerformanceData")); - + postC("/session/:sessionId/appium/getPerformanceData")); + commandRepository.put(START_RECORDING_SCREEN, + postC("/session/:sessionId/appium/start_recording_screen")); + commandRepository.put(STOP_RECORDING_SCREEN, + postC("/session/:sessionId/appium/stop_recording_screen")); //iOS commandRepository.put(SHAKE, postC("/session/:sessionId/appium/device/shake")); commandRepository.put(TOUCH_ID, postC("/session/:sessionId/appium/simulator/touch_id")); @@ -163,31 +173,31 @@ public class MobileCommand { postC("/session/:sessionId/appium/simulator/toggle_touch_id_enrollment")); //Android commandRepository.put(CURRENT_ACTIVITY, - getC("/session/:sessionId/appium/device/current_activity")); + getC("/session/:sessionId/appium/device/current_activity")); commandRepository.put(END_TEST_COVERAGE, - postC("/session/:sessionId/appium/app/end_test_coverage")); + postC("/session/:sessionId/appium/app/end_test_coverage")); commandRepository.put(GET_DISPLAY_DENSITY, getC("/session/:sessionId/appium/device/display_density")); commandRepository.put(GET_NETWORK_CONNECTION, getC("/session/:sessionId/network_connection")); commandRepository.put(GET_SYSTEM_BARS, getC("/session/:sessionId/appium/device/system_bars")); commandRepository.put(IS_KEYBOARD_SHOWN, getC("/session/:sessionId/appium/device/is_keyboard_shown")); commandRepository.put(IS_LOCKED, postC("/session/:sessionId/appium/device/is_locked")); commandRepository.put(LONG_PRESS_KEY_CODE, - postC("/session/:sessionId/appium/device/long_press_keycode")); + postC("/session/:sessionId/appium/device/long_press_keycode")); commandRepository.put(FINGER_PRINT, postC("/session/:sessionId/appium/device/finger_print")); commandRepository.put(OPEN_NOTIFICATIONS, - postC("/session/:sessionId/appium/device/open_notifications")); + postC("/session/:sessionId/appium/device/open_notifications")); commandRepository.put(PRESS_KEY_CODE, - postC("/session/:sessionId/appium/device/press_keycode")); + postC("/session/:sessionId/appium/device/press_keycode")); commandRepository.put(PUSH_FILE, postC("/session/:sessionId/appium/device/push_file")); commandRepository.put(SET_NETWORK_CONNECTION, - postC("/session/:sessionId/network_connection")); + postC("/session/:sessionId/network_connection")); commandRepository.put(START_ACTIVITY, - postC("/session/:sessionId/appium/device/start_activity")); + postC("/session/:sessionId/appium/device/start_activity")); commandRepository.put(TOGGLE_LOCATION_SERVICES, - postC("/session/:sessionId/appium/device/toggle_location_services")); + postC("/session/:sessionId/appium/device/toggle_location_services")); commandRepository.put(UNLOCK, postC("/session/:sessionId/appium/device/unlock")); - commandRepository. put(REPLACE_VALUE, postC("/session/:sessionId/appium/element/:id/replace_value")); - commandRepository.put(GET_CURRENT_PACKAGE,getC("/session/:sessionId/appium/device/current_package")); + commandRepository.put(REPLACE_VALUE, postC("/session/:sessionId/appium/element/:id/replace_value")); + commandRepository.put(GET_CURRENT_PACKAGE, getC("/session/:sessionId/appium/device/current_package")); } /** @@ -246,8 +256,8 @@ public static AppiumCommandInfo deleteC(String url) { */ public static Map.Entry> hideKeyboardCommand(String strategy, String keyName) { - String[] parameters = new String[] {"strategy", "key"}; - Object[] values = new Object[] {strategy, keyName}; + String[] parameters = new String[]{"strategy", "key"}; + Object[] values = new Object[]{strategy, keyName}; return new AbstractMap.SimpleEntry<>( HIDE_KEYBOARD, prepareArguments(parameters, values)); } @@ -260,7 +270,7 @@ public static AppiumCommandInfo deleteC(String url) { * @return built {@link ImmutableMap}. */ public static ImmutableMap prepareArguments(String param, - Object value) { + Object value) { ImmutableMap.Builder builder = ImmutableMap.builder(); builder.put(param, value); return builder.build(); @@ -274,7 +284,7 @@ public static ImmutableMap prepareArguments(String param, * @return built {@link ImmutableMap}. */ public static ImmutableMap prepareArguments(String[] params, - Object[] values) { + Object[] values) { ImmutableMap.Builder builder = ImmutableMap.builder(); for (int i = 0; i < params.length; i++) { if (!StringUtils.isBlank(params[i]) && (values[i] != null)) { @@ -308,8 +318,8 @@ public static ImmutableMap prepareArguments(String[] params, */ public static Map.Entry> pressKeyCodeCommand(int key, Integer metastate) { - String[] parameters = new String[] {"keycode", "metastate"}; - Object[] values = new Object[] {key, metastate}; + String[] parameters = new String[]{"keycode", "metastate"}; + Object[] values = new Object[]{key, metastate}; return new AbstractMap.SimpleEntry<>( PRESS_KEY_CODE, prepareArguments(parameters, values)); } @@ -338,8 +348,8 @@ public static ImmutableMap prepareArguments(String[] params, */ public static Map.Entry> longPressKeyCodeCommand(int key, Integer metastate) { - String[] parameters = new String[] {"keycode", "metastate"}; - Object[] values = new Object[] {key, metastate}; + String[] parameters = new String[]{"keycode", "metastate"}; + Object[] values = new Object[]{key, metastate}; return new AbstractMap.SimpleEntry<>( LONG_PRESS_KEY_CODE, prepareArguments(parameters, values)); } @@ -349,7 +359,7 @@ public static ImmutableMap prepareArguments(String[] params, * device locking. * * @param duration for how long to lock the screen for. Minimum time resolution is one second - * @return a key-value pair. The key is the command name. The value is a + * @return a key-value pair. The key is the command name. The value is a * {@link java.util.Map} command arguments. */ public static Map.Entry> lockDeviceCommand(Duration duration) { @@ -402,4 +412,14 @@ public static ImmutableMap prepareArguments(String[] params, Object[] values = new Object[]{remotePath, new String(base64Data, StandardCharsets.UTF_8)}; return new AbstractMap.SimpleEntry<>(PUSH_FILE, prepareArguments(parameters, values)); } + + public static Map.Entry> startRecordingScreenCommand(BaseStartScreenRecordingOptions opts) { + return new AbstractMap.SimpleEntry<>(START_RECORDING_SCREEN, + prepareArguments("options", opts.build())); + } + + public static Map.Entry> stopRecordingScreenCommand(BaseStopScreenRecordingOptions opts) { + return new AbstractMap.SimpleEntry<>(STOP_RECORDING_SCREEN, + prepareArguments("options", opts.build())); + } } diff --git a/src/main/java/io/appium/java_client/android/AndroidDriver.java b/src/main/java/io/appium/java_client/android/AndroidDriver.java index fad692d75..8b822337d 100644 --- a/src/main/java/io/appium/java_client/android/AndroidDriver.java +++ b/src/main/java/io/appium/java_client/android/AndroidDriver.java @@ -26,6 +26,7 @@ import io.appium.java_client.LocksDevice; import io.appium.java_client.PressesKeyCode; import io.appium.java_client.remote.MobilePlatform; +import io.appium.java_client.screenrecording.CanRecordScreen; import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; import org.openqa.selenium.Capabilities; @@ -50,7 +51,7 @@ public class AndroidDriver extends AppiumDriver implements PressesKeyCode, HasNetworkConnection, PushesFiles, StartsActivity, FindsByAndroidUIAutomator, LocksDevice, HasAndroidSettings, HasDeviceDetails, - HasSupportedPerformanceDataType, AuthenticatesByFinger { + HasSupportedPerformanceDataType, AuthenticatesByFinger, CanRecordScreen { private static final String ANDROID_PLATFORM = MobilePlatform.ANDROID; diff --git a/src/main/java/io/appium/java_client/android/AndroidStartScreenRecordingOptions.java b/src/main/java/io/appium/java_client/android/AndroidStartScreenRecordingOptions.java new file mode 100644 index 000000000..e56c2d9b4 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/AndroidStartScreenRecordingOptions.java @@ -0,0 +1,85 @@ +/* + * 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.java_client.android; + +import com.google.common.collect.ImmutableMap; +import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions; + +import java.time.Duration; +import java.util.Map; + +import static java.util.Optional.ofNullable; + +public class AndroidStartScreenRecordingOptions + extends BaseStartScreenRecordingOptions { + private Integer bitRate; + private String videoSize; + + public static AndroidStartScreenRecordingOptions startScreenRecordingOptions() { + return new AndroidStartScreenRecordingOptions(); + } + + /** + * The video bit rate for the video, in megabits per second. + * The default value is 4. You can increase the bit rate to improve video quality, + * but doing so results in larger movie files. + * The value of 5000000 equals to 5Mb/sec. + * + * @param bitRate The actual bit rate (Mb/s). + * @return self instance for chaining. + */ + public AndroidStartScreenRecordingOptions withBitRate(int bitRate) { + this.bitRate = bitRate; + return this; + } + + /** + * The video size of the generated media file. The format is WIDTHxHEIGHT. + * The default value is the device's native display resolution (if supported), + * 1280x720 if not. For best results, + * use a size supported by your device's Advanced Video Coding (AVC) encoder. + * + * @param videoSize The actual video size: WIDTHxHEIGHT. + * @return self instance for chaining. + */ + public AndroidStartScreenRecordingOptions withVideoSize(String videoSize) { + this.videoSize = videoSize; + return this; + } + + /** + * The maximum recording time. The default and maximum value is 180 seconds (3 minutes). + * Setting values greater than this or less than zero will cause an exception. The minimum + * time resolution unit is one second. + * + * @param timeLimit The actual time limit of the recorded video. + * @return self instance for chaining. + */ + @Override + public AndroidStartScreenRecordingOptions withTimeLimit(Duration timeLimit) { + return super.withTimeLimit(timeLimit); + } + + @Override + public Map build() { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.putAll(super.build()); + ofNullable(bitRate).map(x -> builder.put("bitRate", x)); + ofNullable(videoSize).map(x -> builder.put("videoSize", x)); + return builder.build(); + } +} diff --git a/src/main/java/io/appium/java_client/android/AndroidStopScreenRecordingOptions.java b/src/main/java/io/appium/java_client/android/AndroidStopScreenRecordingOptions.java new file mode 100644 index 000000000..83b7ead33 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/AndroidStopScreenRecordingOptions.java @@ -0,0 +1,28 @@ +/* + * 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.java_client.android; + +import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions; + +public class AndroidStopScreenRecordingOptions extends + BaseStopScreenRecordingOptions { + + public static AndroidStopScreenRecordingOptions stopScreenRecordingOptions() { + return new AndroidStopScreenRecordingOptions(); + } + +} diff --git a/src/main/java/io/appium/java_client/ios/IOSDriver.java b/src/main/java/io/appium/java_client/ios/IOSDriver.java index 71678563f..569beaf39 100644 --- a/src/main/java/io/appium/java_client/ios/IOSDriver.java +++ b/src/main/java/io/appium/java_client/ios/IOSDriver.java @@ -26,6 +26,7 @@ import io.appium.java_client.HidesKeyboardWithKeyName; import io.appium.java_client.LocksDevice; import io.appium.java_client.remote.MobilePlatform; +import io.appium.java_client.screenrecording.CanRecordScreen; import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; import org.openqa.selenium.Alert; @@ -55,7 +56,7 @@ public class IOSDriver extends AppiumDriver implements HidesKeyboardWithKeyName, ShakesDevice, HasIOSSettings, FindsByIosUIAutomation, LocksDevice, PerformsTouchID, FindsByIosNSPredicate, - FindsByIosClassChain, PushesFiles { + FindsByIosClassChain, PushesFiles, CanRecordScreen { private static final String IOS_PLATFORM = MobilePlatform.IOS; diff --git a/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java b/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java new file mode 100644 index 000000000..61e120104 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java @@ -0,0 +1,92 @@ +/* + * 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.java_client.ios; + +import com.google.common.collect.ImmutableMap; +import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions; + +import java.time.Duration; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Optional.ofNullable; + +public class IOSStartScreenRecordingOptions + extends BaseStartScreenRecordingOptions { + private String videoType; + private String videoQuality; + + public static IOSStartScreenRecordingOptions startScreenRecordingOptions() { + return new IOSStartScreenRecordingOptions(); + } + + public enum VideoType { + H264, MP4, FMP4 + } + + /** + * The format of the screen capture to be recorded. + * Available formats: "h264", "mp4" or "fmp4". Default is "mp4". + * Only works for Simulator. + * + * @param videoType one of available format names. + * @return self instance for chaining. + */ + public IOSStartScreenRecordingOptions withVideoType(VideoType videoType) { + this.videoType = checkNotNull(videoType).name().toLowerCase(); + return this; + } + + public enum VideoQuality { + LOW, MEDIUM, HIGH, PHOTO + } + + /** + * The video encoding quality (low, medium, high, photo - defaults to medium). + * Only works for real devices. + * + * @param videoQuality one of possible quality preset names. + * @return self instance for chaining. + */ + public IOSStartScreenRecordingOptions withVideoQuality(VideoQuality videoQuality) { + this.videoQuality = checkNotNull(videoQuality).name().toLowerCase(); + return this; + } + + /** + * The maximum recording time. The default value is 180 seconds (3 minutes). + * The maximum value is 10 minutes. + * Setting values greater than this or less than zero will cause an exception. The minimum + * time resolution unit is one second. + * + * @param timeLimit The actual time limit of the recorded video. + * @return self instance for chaining. + */ + @Override + public IOSStartScreenRecordingOptions withTimeLimit(Duration timeLimit) { + return super.withTimeLimit(timeLimit); + } + + @Override + public Map build() { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.putAll(super.build()); + ofNullable(videoType).map(x -> builder.put("videoType", x)); + ofNullable(videoQuality).map(x -> builder.put("videoQuality", x)); + return builder.build(); + } +} diff --git a/src/main/java/io/appium/java_client/ios/IOSStopScreenRecordingOptions.java b/src/main/java/io/appium/java_client/ios/IOSStopScreenRecordingOptions.java new file mode 100644 index 000000000..bbe9b33dc --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/IOSStopScreenRecordingOptions.java @@ -0,0 +1,28 @@ +/* + * 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.java_client.ios; + +import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions; + +public class IOSStopScreenRecordingOptions extends + BaseStopScreenRecordingOptions { + + public static IOSStopScreenRecordingOptions stopScreenRecordingOptions() { + return new IOSStopScreenRecordingOptions(); + } + +} diff --git a/src/main/java/io/appium/java_client/screenrecording/BaseScreenRecordingOptions.java b/src/main/java/io/appium/java_client/screenrecording/BaseScreenRecordingOptions.java new file mode 100644 index 000000000..12b5a74bd --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/BaseScreenRecordingOptions.java @@ -0,0 +1,54 @@ +/* + * 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.java_client.screenrecording; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Optional.ofNullable; + +public abstract class BaseScreenRecordingOptions> { + private ScreenRecordingUploadOptions uploadOptions; + + /** + * Upload options set for the recorded screen capture. + * + * @param uploadOptions see the documentation on {@link ScreenRecordingUploadOptions} + * for more details. + * @return self instance for chaining. + */ + public T withUploadOptions(ScreenRecordingUploadOptions uploadOptions) { + this.uploadOptions = checkNotNull(uploadOptions); + //noinspection unchecked + return (T) this; + } + + /** + * Builds a map, which is ready to be passed to the subordinated + * Appium API. + * + * @return arguments mapping. + */ + public Map build() { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + //noinspection unchecked + ofNullable(uploadOptions).map(x -> builder.putAll(x.build())); + return builder.build(); + } +} diff --git a/src/main/java/io/appium/java_client/screenrecording/BaseStartScreenRecordingOptions.java b/src/main/java/io/appium/java_client/screenrecording/BaseStartScreenRecordingOptions.java new file mode 100644 index 000000000..ccb78eaac --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/BaseStartScreenRecordingOptions.java @@ -0,0 +1,86 @@ +/* + * 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.java_client.screenrecording; + +import com.google.common.collect.ImmutableMap; + +import java.time.Duration; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Optional.ofNullable; + +public abstract class BaseStartScreenRecordingOptions> + extends BaseScreenRecordingOptions> { + private Boolean forceRestart; + private Duration timeLimit; + + /** + * The remotePath upload option is the path to the remote location, + * where the resulting video should be uploaded. + * The following protocols are supported: http/https (multipart), ftp. + * Missing value (the default setting) means the content of the resulting + * file should be encoded as Base64 and passed as the endpoint response value, but + * an exception will be thrown if the generated media file is too big to + * fit into the available process memory. + * This option only has an effect if there is a screen recording session in progress + * and forced restart is not enabled (the default setting). + * + * @param uploadOptions see the documentation on {@link ScreenRecordingUploadOptions} + * for more details. + * @return self instance for chaining. + */ + @Override + public T withUploadOptions(ScreenRecordingUploadOptions uploadOptions) { + //noinspection unchecked + return (T) super.withUploadOptions(uploadOptions); + } + + /** + * The maximum recording time. + * + * @param timeLimit The actual time limit of the recorded video. + * @return self instance for chaining. + */ + public T withTimeLimit(Duration timeLimit) { + this.timeLimit = checkNotNull(timeLimit); + //noinspection unchecked + return (T) this; + } + + /** + * Whether to ignore the result of previous capture and start a new recording + * immediately. By default the endpoint will try to catch and return the result of + * the previous capture if it's still available. + * + * @return self instance for chaining. + */ + public T enableForcedRestart() { + this.forceRestart = true; + //noinspection unchecked + return (T) this; + } + + @Override + public Map build() { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.putAll(super.build()); + ofNullable(timeLimit).map(x -> builder.put("timeLimit", x.getSeconds())); + ofNullable(forceRestart).map(x -> builder.put("forceRestart", x)); + return builder.build(); + } +} diff --git a/src/main/java/io/appium/java_client/screenrecording/BaseStopScreenRecordingOptions.java b/src/main/java/io/appium/java_client/screenrecording/BaseStopScreenRecordingOptions.java new file mode 100644 index 000000000..7920b5c76 --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/BaseStopScreenRecordingOptions.java @@ -0,0 +1,40 @@ +/* + * 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.java_client.screenrecording; + +public abstract class BaseStopScreenRecordingOptions> + extends BaseScreenRecordingOptions> { + + /** + * The remotePath upload option is the path to the remote location, + * where the resulting video should be uploaded. + * The following protocols are supported: http/https (multipart), ftp. + * Missing value (the default setting) means the content of resulting + * file should be encoded as Base64 and passed as the endpoint response value, but + * an exception will be thrown if the generated media file is too big to + * fit into the available process memory. + * + * @param uploadOptions see the documentation on {@link ScreenRecordingUploadOptions} + * for more details. + * @return self instance for chaining. + */ + @Override + public T withUploadOptions(ScreenRecordingUploadOptions uploadOptions) { + //noinspection unchecked + return (T) super.withUploadOptions(uploadOptions); + } +} diff --git a/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.java b/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.java new file mode 100644 index 000000000..927b684f7 --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.java @@ -0,0 +1,72 @@ +/* + * 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.java_client.screenrecording; + +import io.appium.java_client.CommandExecutionHelper; +import io.appium.java_client.ExecutesMethod; + +import static io.appium.java_client.MobileCommand.START_RECORDING_SCREEN; +import static io.appium.java_client.MobileCommand.startRecordingScreenCommand; +import static io.appium.java_client.MobileCommand.STOP_RECORDING_SCREEN; +import static io.appium.java_client.MobileCommand.stopRecordingScreenCommand; + + +public interface CanRecordScreen extends ExecutesMethod { + + /** + * Start asynchronous screen recording process. + * + * @param options see the documentation on the {@link BaseStartScreenRecordingOptions} + * descendant for the particular platform. + * @return Base-64 encoded content of the recorded media file or an empty string + * if the file has been successfully uploaded to a remote location (depends on the actual options). + */ + default String startRecordingScreen(T options) { + return CommandExecutionHelper.execute(this, startRecordingScreenCommand(options)); + } + + /** + * Start asynchronous screen recording process with default options. + * + * @return Base-64 encoded content of the recorded media file. + */ + default String startRecordingScreen() { + return this.execute(START_RECORDING_SCREEN).getValue().toString(); + } + + /** + * Gather the output from the previously started screen recording to a media file. + * + * @param options see the documentation on the {@link BaseStopScreenRecordingOptions} + * descendant for the particular platform. + * @return Base-64 encoded content of the recorded media file or an empty string + * if the file has been successfully uploaded to a remote location (depends on the actual options). + */ + default String stopRecordingScreen(T options) { + return CommandExecutionHelper.execute(this, stopRecordingScreenCommand(options)); + } + + /** + * Gather the output from the previously started screen recording to a media file + * with default options. + * + * @return Base-64 encoded content of the recorded media file. + */ + default String stopRecordingScreen() { + return this.execute(STOP_RECORDING_SCREEN).getValue().toString(); + } +} diff --git a/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java b/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java new file mode 100644 index 000000000..717f5cc94 --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java @@ -0,0 +1,91 @@ +/* + * 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.java_client.screenrecording; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Optional.ofNullable; + +public class ScreenRecordingUploadOptions { + private String remotePath; + private String user; + private String pass; + private String method; + + public static ScreenRecordingUploadOptions uploadOptions() { + return new ScreenRecordingUploadOptions(); + } + + /** + * The path to the remote location, where the resulting video should be uploaded. + * + * @param remotePath The path to a writable remote location. + * @return self instance for chaining. + */ + public ScreenRecordingUploadOptions withRemotePath(String remotePath) { + this.remotePath = checkNotNull(remotePath); + return this; + } + + /** + * Sets the credentials for remote ftp/http authentication (if needed). + * This option only has an effect if remotePath is provided. + * + * @param user The name of the user for the remote authentication. + * @param pass The password for the remote authentication. + * @return self instance for chaining. + */ + public ScreenRecordingUploadOptions withAuthCredentials(String user, String pass) { + this.user = checkNotNull(user); + this.pass = checkNotNull(pass); + return this; + } + + public enum RequestMethod { + POST, PUT + } + + /** + * Sets the method name for http(s) upload. PUT is used by default. + * This option only has an effect if remotePath is provided. + * + * @param method The HTTP method name. + * @return self instance for chaining. + */ + public ScreenRecordingUploadOptions withHttpMethod(RequestMethod method) { + this.method = checkNotNull(method).name(); + return this; + } + + /** + * Builds a map, which is ready to be passed to the subordinated + * Appium API. + * + * @return arguments mapping. + */ + public Map build() { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + ofNullable(remotePath).map(x -> builder.put("remotePath", x)); + ofNullable(user).map(x -> builder.put("user", x)); + ofNullable(pass).map(x -> builder.put("pass", x)); + ofNullable(method).map(x -> builder.put("method", x)); + return builder.build(); + } +} diff --git a/src/test/java/io/appium/java_client/android/AndroidScreenRecordTest.java b/src/test/java/io/appium/java_client/android/AndroidScreenRecordTest.java new file mode 100644 index 000000000..43db29e7d --- /dev/null +++ b/src/test/java/io/appium/java_client/android/AndroidScreenRecordTest.java @@ -0,0 +1,39 @@ +package io.appium.java_client.android; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.WebDriverException; + +import java.time.Duration; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.isEmptyString; +import static org.hamcrest.Matchers.not; + +public class AndroidScreenRecordTest extends BaseAndroidTest { + + @Before + public void setUp() { + Activity activity = new Activity("io.appium.android.apis", ".ApiDemos"); + driver.startActivity(activity); + } + + @Test + public void verifyBasicScreenRecordingWorks() throws InterruptedException { + try { + driver.startRecordingScreen( + new AndroidStartScreenRecordingOptions() + .withTimeLimit(Duration.ofSeconds(5)) + ); + } catch (WebDriverException e) { + if (e.getMessage().toLowerCase().contains("emulator")) { + // screen recording only works on real devices + return; + } + } + Thread.sleep(5000); + String result = driver.stopRecordingScreen(); + assertThat(result, not(isEmptyString())); + } + +} diff --git a/src/test/java/io/appium/java_client/ios/IOSScreenRecordTest.java b/src/test/java/io/appium/java_client/ios/IOSScreenRecordTest.java new file mode 100644 index 000000000..de87b696b --- /dev/null +++ b/src/test/java/io/appium/java_client/ios/IOSScreenRecordTest.java @@ -0,0 +1,24 @@ +package io.appium.java_client.ios; + +import org.junit.Test; + +import java.time.Duration; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.isEmptyString; +import static org.hamcrest.Matchers.not; + +public class IOSScreenRecordTest extends AppIOSTest { + + @Test + public void verifyBasicScreenRecordingWorks() throws InterruptedException { + driver.startRecordingScreen( + new IOSStartScreenRecordingOptions() + .withTimeLimit(Duration.ofSeconds(10)) + ); + Thread.sleep(5000); + String result = driver.stopRecordingScreen(); + assertThat(result, not(isEmptyString())); + } + +}