();
+ Class> type = type();
+
+ do {
+ for (Field field : type.getDeclaredFields()) {
+ if (!isClass ^ Modifier.isStatic(field.getModifiers())) {
+ String name = field.getName();
+
+ if (!result.containsKey(name))
+ result.put(name, field(name));
+ }
+ }
+
+ type = type.getSuperclass();
+ }
+ while (type != null);
+
+ return result;
+ }
+
+ // ---------------------------------------------------------------------
+ // ObjectAPI
+ // ---------------------------------------------------------------------
+
+ /**
+ * Given the method name, call the parameterless method
+ *
+ * Equivalent to call(name, new Object[0])
+ *
+ * @param name method name
+ * @return tool class itself
+ * @see #call(String, Object...)
+ */
+ public Reflect call(String name) throws ReflectException {
+ return call(name, new Object[0]);
+ }
+
+ /**
+ * Calls a method given a method name and parameters.
+ *
+ * Encapsulated from {@link Method#invoke(Object, Object...)}, which can accept basic types
+ *
+ * @param name method name
+ * @param args method parameters
+ * @return tool class itself
+ */
+ public Reflect call(String name, Object... args) throws ReflectException {
+ Class>[] types = types(args);
+
+ // try to call method
+ try {
+ Method method = exactMethod(name, types);
+ return on(method, object, args);
+ }
+
+ //If there is no method matching the parameters,
+ // Match the method closest to the method name.
+ catch (NoSuchMethodException e) {
+ try {
+ Log.d(TAG, "no exact method found, try to find the similar one!");
+ Method method = similarMethod(name, types);
+ showMethod(method);
+ return on(method, object, args);
+ } catch (NoSuchMethodException e1) {
+ Log.e(TAG, "no similar found!");
+ throw new ReflectException(e1);
+ }
+ }
+ }
+
+ /**
+ * Get the method based on the method name and method parameters
+ */
+ private Method exactMethod(String name, Class>[] types) throws NoSuchMethodException {
+ Class> type = type();
+
+ // 先尝试直接调用
+ try {
+ showMethod(name, types);
+ return type.getMethod(name, types);
+ }
+
+ //Maybe this is a private method
+ catch (NoSuchMethodException e) {
+ do {
+ try {
+ return type.getDeclaredMethod(name, types);
+ } catch (NoSuchMethodException ignore) {
+ }
+
+ type = type.getSuperclass();
+ }
+ while (type != null);
+
+ throw new NoSuchMethodException();
+ }
+ }
+
+ /**
+ * Given a method name and parameters, match the closest method
+ */
+ private Method similarMethod(String name, Class>[] types) throws NoSuchMethodException {
+ Class> type = type();
+
+ //对于公有方法:
+ for (Method method : type.getMethods()) {
+ if (isSimilarSignature(method, name, types)) {
+ return method;
+ }
+ }
+
+ //对于私有方法:
+ do {
+ for (Method method : type.getDeclaredMethods()) {
+ if (isSimilarSignature(method, name, types)) {
+ return method;
+ }
+ }
+
+ type = type.getSuperclass();
+ }
+ while (type != null);
+
+ throw new NoSuchMethodException("No similar method " + name + " with params " + Arrays.toString(types) + " could be found on type " + type() + ".");
+ }
+
+ private List> convertObjectToList(Object obj) {
+ List> list = new ArrayList<>();
+ if (obj.getClass().isArray()) {
+ list = Arrays.asList((Object[]) obj);
+ } else if (obj instanceof Collection) {
+ list = new ArrayList<>((Collection>) obj);
+ }
+ return list;
+ }
+
+ // ---------------------------------------------------------------------
+ // internal tool methods
+ // ---------------------------------------------------------------------
+
+ /**
+ * Confirm again whether the method signature matches the actual one,
+ * Convert basic types into corresponding object types,
+ * If int is converted to Int
+ */
+ private boolean isSimilarSignature(Method possiblyMatchingMethod, String desiredMethodName, Class>[] desiredParamTypes) {
+ return possiblyMatchingMethod.getName().equals(desiredMethodName) && match(possiblyMatchingMethod.getParameterTypes(), desiredParamTypes);
+ }
+
+ /**
+ * Call a parameterless constructor
+ *
+ * Equivalent to create(new Object[0])
+ *
+ * @return tool class itself
+ * @see #create(Object...)
+ */
+ public Reflect create() throws ReflectException {
+ return create(new Object[0]);
+ }
+
+ /**
+ * Call a parameterized constructor
+ *
+ * @param args constructor parameters
+ * @return tool class itself
+ */
+ public Reflect create(Object... args) throws ReflectException {
+ Class>[] types = types(args);
+
+
+ try {
+ Constructor> constructor = type().getDeclaredConstructor(types);
+ return on(constructor, args);
+ }
+
+ //In this case, the constructor is often private, mostly used in factory methods, and the constructor is deliberately hidden.
+ catch (NoSuchMethodException e) {
+ //private阻止不了反射的脚步:)
+ for (Constructor> constructor : type().getDeclaredConstructors()) {
+ if (match(constructor.getParameterTypes(), types)) {
+ return on(constructor, args);
+ }
+ }
+
+ throw new ReflectException(e);
+ }
+ }
+
+ /**
+ * Create a proxy for the wrapped object.
+ *
+ * @param proxyType proxy type
+ * @return The delegate of the wrapping object.
+ */
+ @SuppressWarnings("unchecked")
+ public P as(Class
proxyType) {
+ final boolean isMap = (object instanceof Map);
+ final InvocationHandler handler = new InvocationHandler() {
+ @SuppressWarnings("null")
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ String name = method.getName();
+
+ try {
+ return on(object).call(name, args).get();
+ } catch (ReflectException e) {
+ if (isMap) {
+ Map map = (Map) object;
+ int length = (args == null ? 0 : args.length);
+
+ if (length == 0 && name.startsWith("get")) {
+ return map.get(property(name.substring(3)));
+ } else if (length == 0 && name.startsWith("is")) {
+ return map.get(property(name.substring(2)));
+ } else if (length == 1 && name.startsWith("set")) {
+ map.put(property(name.substring(3)), args[0]);
+ return null;
+ }
+ }
+
+ throw e;
+ }
+ }
+ };
+
+ return (P) Proxy.newProxyInstance(proxyType.getClassLoader(), new Class[]{proxyType}, handler);
+ }
+
+ private boolean match(Class>[] declaredTypes, Class>[] actualTypes) {
+ if (declaredTypes.length == actualTypes.length) {
+ for (int i = 0; i < actualTypes.length; i++) {
+ if (actualTypes[i] == NULL.class)
+ continue;
+
+ if (wrapper(declaredTypes[i]).isAssignableFrom(wrapper(actualTypes[i])))
+ continue;
+
+ return false;
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return object.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Reflect) {
+ return object.equals(((Reflect) obj).get());
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return object.toString();
+ }
+
+ /**
+ * Get the type of wrapped object
+ *
+ * @see Object#getClass()
+ */
+ public Class> type() {
+ if (isClass) {
+ return (Class>) object;
+ } else {
+ return object.getClass();
+ }
+ }
+
+ /**
+ * defines a null type
+ */
+ private static class NULL {
+ }
+
+}
diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/reflect/ReflectException.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/reflect/ReflectException.java
new file mode 100755
index 0000000..95e4ace
--- /dev/null
+++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/reflect/ReflectException.java
@@ -0,0 +1,25 @@
+package com.phonereplay.tasklogger.reflect;
+
+/**
+ * Exception thrown when reflection error occurs
+ */
+public class ReflectException extends RuntimeException {
+
+ private static final long serialVersionUID = -2243843843843438438L;
+
+ public ReflectException(String message) {
+ super(message);
+ }
+
+ public ReflectException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ReflectException() {
+ super();
+ }
+
+ public ReflectException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/service/PhoneReplayService.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/service/PhoneReplayService.java
new file mode 100644
index 0000000..94d18d9
--- /dev/null
+++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/service/PhoneReplayService.java
@@ -0,0 +1,253 @@
+package com.phonereplay.tasklogger.service;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.phonereplay.tasklogger.DeviceModel;
+import com.phonereplay.tasklogger.LocalSession;
+import com.phonereplay.tasklogger.TimeLine;
+import com.phonereplay.tasklogger.network.ApiClient;
+import com.phonereplay.tasklogger.network.GrpcClient;
+import com.phonereplay.tasklogger.network.models.reponses.CreateSessionResponse;
+import com.phonereplay.tasklogger.network.models.reponses.VerifyProjectAuthResponse;
+import com.phonereplay.tasklogger.utils.NetworkUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.zip.Deflater;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class PhoneReplayService {
+ private static final int COMPRESSION_QUALITY = 10;
+ private final ApiClient apiClient;
+ private final GrpcClient grpcClient;
+ private final Context context;
+ private byte[] fullBytesVideo;
+ private byte[] previousImageCompressed;
+ private VerifyProjectAuthResponse verifyProjectAuthResponse;
+ private CreateSessionResponse session;
+ private String accessKey;
+
+ public PhoneReplayService(Context context) {
+ this.apiClient = new ApiClient();
+ this.grpcClient = new GrpcClient();
+ this.context = context;
+ }
+
+ private static byte[] joinByteArrays(byte[] array1, byte[] array2) {
+ int length1 = array1.length;
+ int length2 = array2.length;
+ byte[] result = new byte[length1 + length2];
+ System.arraycopy(array1, 0, result, 0, length1);
+ System.arraycopy(array2, 0, result, length1, length2);
+ return result;
+ }
+
+ public static byte[] compress(byte[] data) throws IOException {
+ if (data == null) {
+ return null;
+ }
+ Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION);
+ try {
+ deflater.setInput(data);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);
+ deflater.finish();
+ byte[] buffer = new byte[1024];
+ while (!deflater.finished()) {
+ int count = deflater.deflate(buffer);
+ outputStream.write(buffer, 0, count);
+ }
+ outputStream.close();
+ return outputStream.toByteArray();
+ } finally {
+ deflater.end();
+ }
+ }
+
+ /*
+ private static String encodeToBase64(byte[] binaryData) {
+ byte[] base64Encoded = android.util.Base64.encode(binaryData, Base64.DEFAULT);
+ return new String(base64Encoded);
+ }
+ */
+
+ private static byte[] writeImageCompressedFromBitmap(Bitmap bitmap) throws IOException {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESSION_QUALITY, byteArrayOutputStream);
+ return compress(byteArrayOutputStream.toByteArray());
+ }
+
+ private static byte[] writeImageFromBitmap(Bitmap bitmap) throws IOException {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESSION_QUALITY, byteArrayOutputStream);
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ public static boolean compareByteArrays(byte[] array1, byte[] array2) {
+ return Arrays.equals(array1, array2);
+ }
+
+ private static byte[] combineIdentifierAndData(byte[] data) {
+ byte[] identifierBytes = "------".getBytes();
+ byte[] combinedData = new byte[identifierBytes.length + data.length];
+
+ System.arraycopy(identifierBytes, 0, combinedData, 0, identifierBytes.length);
+ System.arraycopy(data, 0, combinedData, identifierBytes.length, data.length);
+
+ return combinedData;
+ }
+
+ public CreateSessionResponse getSession() {
+ return session;
+ }
+
+ public VerifyProjectAuthResponse getVerifyProjectAuthResponse() {
+ return verifyProjectAuthResponse;
+ }
+
+ public void queueBytesBitmap(Bitmap bitmap) throws IOException {
+ byte[] imageCompressed = writeImageCompressedFromBitmap(bitmap);
+ byte[] combineIdentifierAndData;
+
+ if (fullBytesVideo != null) {
+ if (compareByteArrays(previousImageCompressed, imageCompressed)) {
+ combineIdentifierAndData = combineIdentifierAndData("D".getBytes());
+ } else {
+ combineIdentifierAndData = combineIdentifierAndData(imageCompressed);
+ previousImageCompressed = imageCompressed;
+ }
+ byte[] joinByteArrays;
+ joinByteArrays = joinByteArrays(fullBytesVideo, combineIdentifierAndData);
+ fullBytesVideo = joinByteArrays;
+ } else {
+ previousImageCompressed = imageCompressed;
+ combineIdentifierAndData = imageCompressed;
+ fullBytesVideo = combineIdentifierAndData;
+ }
+ bitmap.recycle();
+ }
+
+ public void queueBytesBitmapV2(Bitmap bitmap) throws IOException {
+ byte[] image = writeImageFromBitmap(bitmap);
+ byte[] combineIdentifierAndData;
+
+ if (fullBytesVideo != null) {
+ if (compareByteArrays(previousImageCompressed, image)) {
+ combineIdentifierAndData = combineIdentifierAndData("D".getBytes());
+ } else {
+ combineIdentifierAndData = combineIdentifierAndData(image);
+ previousImageCompressed = image;
+ }
+ byte[] joinByteArrays;
+ joinByteArrays = joinByteArrays(fullBytesVideo, combineIdentifierAndData);
+ fullBytesVideo = joinByteArrays;
+ } else {
+ previousImageCompressed = image;
+ combineIdentifierAndData = image;
+ fullBytesVideo = combineIdentifierAndData;
+ }
+ bitmap.recycle();
+ }
+
+ public CreateSessionResponse createSession(String projectId) {
+ if (projectId == null) {
+ return null;
+ }
+
+ Call call = apiClient.createSession(projectId);
+ try {
+ Response createSessionResponseResponse = call.execute();
+ if (createSessionResponseResponse.isSuccessful()) {
+ session = createSessionResponseResponse.body();
+ return session;
+ } else {
+ return null;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public void sendLocalSessionData(LocalSession localSession) {
+ Call call = apiClient.sendLocalSessionData(localSession);
+ call.enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.isSuccessful()) {
+ Log.d("Upload", "Dados enviados com sucesso.");
+ } else {
+ Log.d("Upload", "Falha ao enviar dados. Código de resposta: " + response.code());
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ Log.d("Upload", "Erro na chamada de rede", t);
+ }
+ });
+ }
+
+ public void sendDeviceInfo(DeviceModel deviceModel) {
+ Call call = apiClient.sendDeviceInfo(deviceModel);
+ call.enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.isSuccessful()) {
+ Log.d("Upload", "Dados enviados com sucesso.");
+ } else {
+ Log.d("Upload", "Falha ao enviar dados. Código de resposta: " + response.code());
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ Log.d("Upload", "Erro na chamada de rede", t);
+ }
+ });
+ }
+
+
+ public void verifyProjectAuth(String project_access_key) {
+ this.accessKey = project_access_key;
+ Call call = apiClient.verifyProjectAuth(project_access_key);
+
+ try {
+ Response createSessionResponseResponse = call.execute();
+ verifyProjectAuthResponse = createSessionResponseResponse.body();
+ } catch (IOException e) {
+ System.out.println();
+ // handle error
+ }
+ }
+
+ public void verifyProjectAuth() {
+ if (accessKey == null) {
+ return;
+ }
+
+ Call call = apiClient.verifyProjectAuth(accessKey);
+ try {
+ Response createSessionResponseResponse = call.execute();
+ verifyProjectAuthResponse = createSessionResponseResponse.body();
+ } catch (IOException e) {
+ System.out.println();
+ }
+ }
+
+ public void createVideo(String sessionId, Set timeLines) throws IOException {
+ grpcClient.sendBinaryData(compress(fullBytesVideo), sessionId, timeLines);
+ //fullBytesVideo = null;
+ if (NetworkUtil.isWiFiConnected(context)) {
+ } else {
+ }
+ }
+}
diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/BitmapUtils.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/BitmapUtils.java
new file mode 100644
index 0000000..c0ce4a7
--- /dev/null
+++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/BitmapUtils.java
@@ -0,0 +1,46 @@
+package com.phonereplay.tasklogger.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.view.View;
+
+public class BitmapUtils {
+
+ private static Bitmap captureAndResizeView(Bitmap originalBitmap, int originalWidth, int originalHeight, int newHeight) {
+ //if (this.orientation) {
+ // return resizeBitmap(rotateBitmap(originalBitmap, 90), mainWidth, mainHeight);
+ //}
+ float aspectRatio = (float) originalWidth / originalHeight;
+ int newWidth = Math.round(newHeight * aspectRatio);
+
+ return Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true);
+ }
+
+ public static Bitmap convertViewToDrawable(View view) {
+ Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ canvas.translate(-view.getScrollX(), -view.getScrollY());
+ view.draw(canvas);
+ return captureAndResizeView(bitmap, view.getMeasuredWidth(), view.getMeasuredHeight(), view.getMeasuredHeight());
+ }
+
+ public Bitmap rotateBitmap(Bitmap original, float degrees) {
+ Matrix matrix = new Matrix();
+ matrix.postRotate(degrees);
+ return Bitmap.createBitmap(original, 0, 0, original.getWidth(), original.getHeight(), matrix, true);
+ }
+
+ public Bitmap resizeBitmap(Bitmap original, int newWidth, int newHeight) {
+ int width = original.getWidth();
+ int height = original.getHeight();
+
+ float scaleWidth = ((float) newWidth) / width;
+ float scaleHeight = ((float) newHeight) / height;
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(scaleWidth, scaleHeight);
+
+ return Bitmap.createBitmap(original, 0, 0, width, height, matrix, false);
+ }
+}
diff --git a/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/NetworkUtil.java b/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/NetworkUtil.java
new file mode 100644
index 0000000..0199a52
--- /dev/null
+++ b/tasklogger/src/main/java/com/phonereplay/tasklogger/utils/NetworkUtil.java
@@ -0,0 +1,56 @@
+package com.phonereplay.tasklogger.utils;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+
+public class NetworkUtil {
+
+ public static boolean isWiFiConnected(Context context) {
+ ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (connectivityManager != null) {
+ NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI && networkInfo.isConnected();
+ }
+ return false;
+ }
+
+ public static String getIPAddress(boolean useIPv4) {
+ try {
+ Enumeration interfaces = NetworkInterface.getNetworkInterfaces();
+ while (interfaces.hasMoreElements()) {
+ NetworkInterface intf = interfaces.nextElement();
+ Enumeration addresses = intf.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ InetAddress addr = addresses.nextElement();
+ // Filtra endereços loopback e verifica se o endereço é IPv4 se necessário
+ if (!addr.isLoopbackAddress()) {
+ String sAddr = addr.getHostAddress();
+ // O método isIPv4Address verifica se o endereço é IPv4
+ assert sAddr != null;
+ boolean isIPv4 = sAddr.indexOf(':') < 0;
+
+ if (useIPv4) {
+ if (isIPv4)
+ return sAddr;
+ } else {
+ if (!isIPv4) {
+ // Converte o endereço IPv6 para a forma compressa
+ int delim = sAddr.indexOf('%'); // Trata endereço com zona de escopo
+ return delim < 0 ? sAddr.toUpperCase() : sAddr.substring(0, delim).toUpperCase();
+ }
+ }
+ }
+ }
+ }
+ } catch (SocketException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+}
diff --git a/tasklogger/src/test/java/com/phonereplay/tasklogger/ExampleUnitTest.java b/tasklogger/src/test/java/com/phonereplay/tasklogger/ExampleUnitTest.java
new file mode 100644
index 0000000..4ee66ef
--- /dev/null
+++ b/tasklogger/src/test/java/com/phonereplay/tasklogger/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.phonereplay.tasklogger;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file