新增:远程查询手机定位(方便找回手机/防止老少走丢) #256

pull/286/head
pppscn 1 year ago
parent 3f28080958
commit 7bc7bfa514

@ -265,10 +265,11 @@ dependencies {
//SM4 JAVA(BC) //SM4 JAVA(BC)
api 'org.bouncycastle:bcprov-jdk15on:1.70' api 'org.bouncycastle:bcprov-jdk15on:1.70'
//Location Android LocationManager https://github.com/jenly1314/Location
implementation 'com.github.pppscn:location:1.0.0'
} }
//X-Library //X-Library
apply from: 'x-library.gradle' apply from: 'x-library.gradle'
//walle //walle
//apply from: 'multiple-channel.gradle' //apply from: 'multiple-channel.gradle'

@ -54,6 +54,9 @@
<uses-permission <uses-permission
android:name="android.permission.READ_LOGS" android:name="android.permission.READ_LOGS"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application <application
android:name=".App" android:name=".App"

@ -0,0 +1,25 @@
package com.idormy.sms.forwarder.entity
import com.idormy.sms.forwarder.R
import com.xuexiang.xui.utils.ResUtils
import java.io.Serializable
data class LocationInfo(
var longitude: Double = 0.0,
var latitude: Double = 0.0,
var address: String = "",
var time: String = "",
var provider: String = ""
) : Serializable {
override fun toString(): String {
var msg = ""
msg += "\n" + String.format(ResUtils.getString(R.string.location_longitude), longitude)
msg += "\n" + String.format(ResUtils.getString(R.string.location_latitude), latitude)
if (address != "") msg += "\n" + String.format(ResUtils.getString(R.string.location_address), address)
if (time != "") msg += "\n" + String.format(ResUtils.getString(R.string.location_time), time)
if (provider != "") msg += "\n" + String.format(ResUtils.getString(R.string.location_provider), provider)
return msg
}
}

@ -259,12 +259,12 @@ class ClientFragment : BaseFragment<FragmentClientBinding?>(), View.OnClickListe
XToastUtils.error(getString(R.string.click_test_button_first)) XToastUtils.error(getString(R.string.click_test_button_first))
return return
} }
if (serverConfig != null && ((item.name == ResUtils.getString(R.string.api_sms_send) && !serverConfig!!.enableApiSmsSend) || (item.name == ResUtils.getString(R.string.api_sms_query) && !serverConfig!!.enableApiSmsQuery) || (item.name == ResUtils.getString(R.string.api_call_query) && !serverConfig!!.enableApiCallQuery) || (item.name == ResUtils.getString(R.string.api_contact_query) && !serverConfig!!.enableApiContactQuery) || (item.name == ResUtils.getString(R.string.api_battery_query) && !serverConfig!!.enableApiBatteryQuery) || (item.name == ResUtils.getString(R.string.api_wol) && !serverConfig!!.enableApiWol))) { if (serverConfig != null && ((item.name == ResUtils.getString(R.string.api_sms_send) && !serverConfig!!.enableApiSmsSend) || (item.name == ResUtils.getString(R.string.api_sms_query) && !serverConfig!!.enableApiSmsQuery) || (item.name == ResUtils.getString(R.string.api_call_query) && !serverConfig!!.enableApiCallQuery) || (item.name == ResUtils.getString(R.string.api_contact_query) && !serverConfig!!.enableApiContactQuery) || (item.name == ResUtils.getString(R.string.api_battery_query) && !serverConfig!!.enableApiBatteryQuery) || (item.name == ResUtils.getString(R.string.api_wol) && !serverConfig!!.enableApiWol) || (item.name == ResUtils.getString(R.string.api_location) && !serverConfig!!.enableApiLocation))) {
XToastUtils.error(getString(R.string.disabled_on_the_server)) XToastUtils.error(getString(R.string.disabled_on_the_server))
return return
} }
@Suppress("UNCHECKED_CAST") PageOption.to(Class.forName(item.classPath) as Class<XPageFragment>) //跳转的fragment @Suppress("UNCHECKED_CAST")
.setNewActivity(true).open(this) PageOption.to(Class.forName(item.classPath) as Class<XPageFragment>).setNewActivity(true).open(this)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
XToastUtils.error(e.message.toString()) XToastUtils.error(e.message.toString())

@ -6,6 +6,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -224,6 +225,18 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
HttpServerUtils.enableApiWol = isChecked HttpServerUtils.enableApiWol = isChecked
} }
binding!!.sbApiLocation.isChecked = HttpServerUtils.enableApiLocation
binding!!.sbApiLocation.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
HttpServerUtils.enableApiLocation = isChecked
if (ServiceUtils.isServiceRunning("com.idormy.sms.forwarder.service.HttpService")) {
Log.d("ServerFragment", "onClick: 重启服务")
appContext?.stopService(Intent(appContext, HttpService::class.java))
Thread.sleep(500)
appContext?.startService(Intent(appContext, HttpService::class.java))
refreshButtonText()
}
}
} }
@SingleClick @SingleClick
@ -235,6 +248,7 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
checkReadSmsPermission() checkReadSmsPermission()
checkCallPermission() checkCallPermission()
checkContactsPermission() checkContactsPermission()
checkLocationPermission()
if (ServiceUtils.isServiceRunning("com.idormy.sms.forwarder.service.HttpService")) { if (ServiceUtils.isServiceRunning("com.idormy.sms.forwarder.service.HttpService")) {
appContext?.stopService(Intent(appContext, HttpService::class.java)) appContext?.stopService(Intent(appContext, HttpService::class.java))
} else { } else {
@ -292,11 +306,7 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
XToastUtils.error(String.format(getString(R.string.download_first), downloadPath)) XToastUtils.error(String.format(getString(R.string.download_first), downloadPath))
return return
} }
MaterialDialog.Builder(requireContext()) MaterialDialog.Builder(requireContext()).title(getString(R.string.select_web_client_directory)).content(String.format(getString(R.string.root_directory), downloadPath)).items(dirList).itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
.title(getString(R.string.select_web_client_directory))
.content(String.format(getString(R.string.root_directory), downloadPath))
.items(dirList)
.itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
val webPath = "$downloadPath/$text" val webPath = "$downloadPath/$text"
binding!!.etWebPath.setText(webPath) binding!!.etWebPath.setText(webPath)
HttpServerUtils.serverWebPath = webPath HttpServerUtils.serverWebPath = webPath
@ -310,10 +320,7 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
} }
refreshButtonText() refreshButtonText()
true // allow selection true // allow selection
} }.positiveText(R.string.select).negativeText(R.string.cancel).show()
.positiveText(R.string.select)
.negativeText(R.string.cancel)
.show()
} }
else -> {} else -> {}
} }
@ -342,8 +349,7 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
private fun checkSendSmsPermission() { private fun checkSendSmsPermission() {
XXPermissions.with(this) XXPermissions.with(this)
// 发送短信 // 发送短信
.permission(Permission.SEND_SMS) .permission(Permission.SEND_SMS).request(object : OnPermissionCallback {
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) { override fun onGranted(permissions: List<String>, all: Boolean) {
} }
@ -369,8 +375,7 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
// 发送短信 // 发送短信
.permission(Permission.SEND_SMS) .permission(Permission.SEND_SMS)
// 读取短信 // 读取短信
.permission(Permission.READ_SMS) .permission(Permission.READ_SMS).request(object : OnPermissionCallback {
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) { override fun onGranted(permissions: List<String>, all: Boolean) {
} }
@ -396,8 +401,7 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
// 读取手机号码 // 读取手机号码
.permission(Permission.READ_PHONE_NUMBERS) .permission(Permission.READ_PHONE_NUMBERS)
// 读取通话记录 // 读取通话记录
.permission(Permission.READ_CALL_LOG) .permission(Permission.READ_CALL_LOG).request(object : OnPermissionCallback {
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) { override fun onGranted(permissions: List<String>, all: Boolean) {
} }
@ -417,9 +421,7 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
//联系人权限 //联系人权限
private fun checkContactsPermission() { private fun checkContactsPermission() {
XXPermissions.with(this) XXPermissions.with(this).permission(*Permission.Group.CONTACTS).request(object : OnPermissionCallback {
.permission(*Permission.Group.CONTACTS)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) { override fun onGranted(permissions: List<String>, all: Boolean) {
} }
@ -437,6 +439,26 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
}) })
} }
//联系人权限
private fun checkLocationPermission() {
XXPermissions.with(this).permission(Permission.ACCESS_COARSE_LOCATION).permission(Permission.ACCESS_FINE_LOCATION).permission(Permission.ACCESS_BACKGROUND_LOCATION).request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
}
override fun onDenied(permissions: List<String>, never: Boolean) {
if (never) {
XToastUtils.error(R.string.toast_denied_never)
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(R.string.toast_denied)
}
HttpServerUtils.enableApiLocation = false
binding!!.sbApiLocation.isChecked = false
}
})
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
//取消定时器 //取消定时器

@ -0,0 +1,186 @@
package com.idormy.sms.forwarder.fragment.client
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentClientLocationBinding
import com.idormy.sms.forwarder.entity.LocationInfo
import com.idormy.sms.forwarder.server.model.BaseResponse
import com.idormy.sms.forwarder.utils.*
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.cache.model.CacheMode
import com.xuexiang.xhttp2.callback.SimpleCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.utils.TextUtils
import com.xuexiang.xui.utils.CountDownButtonHelper
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.grouplist.XUIGroupListView
import com.xuexiang.xutil.data.ConvertTools
@Suppress("PropertyName")
@Page(name = "远程找手机")
class LocationFragment : BaseFragment<FragmentClientLocationBinding?>(), View.OnClickListener {
val TAG: String = LocationFragment::class.java.simpleName
private var mCountDownHelper: CountDownButtonHelper? = null
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentClientLocationBinding {
return FragmentClientLocationBinding.inflate(inflater, container, false)
}
override fun initTitle(): TitleBar? {
return super.initTitle()!!.setImmersive(false).setTitle(R.string.api_location)
}
/**
* 初始化控件
*/
override fun initViews() {
//发送按钮增加倒计时,避免重复点击
mCountDownHelper = CountDownButtonHelper(binding!!.btnRefresh, SettingUtils.requestTimeout)
mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
override fun onCountDown(time: Int) {
binding!!.btnRefresh.text = String.format(getString(R.string.seconds_n), time)
}
override fun onFinished() {
binding!!.btnRefresh.text = getString(R.string.refresh)
}
})
getLocation()
}
override fun initListeners() {
binding!!.btnRefresh.setOnClickListener(this)
}
@SingleClick
override fun onClick(v: View) {
when (v.id) {
R.id.btn_refresh -> {
getLocation()
}
else -> {}
}
}
private fun getLocation() {
val requestUrl: String = HttpServerUtils.serverAddress + "/location/query"
Log.i(TAG, "requestUrl:$requestUrl")
val msgMap: MutableMap<String, Any> = mutableMapOf()
val timestamp = System.currentTimeMillis()
msgMap["timestamp"] = timestamp
val clientSignKey = HttpServerUtils.clientSignKey
if (!TextUtils.isEmpty(clientSignKey)) {
msgMap["sign"] = HttpServerUtils.calcSign(timestamp.toString(), clientSignKey)
}
val dataMap: MutableMap<String, Any> = mutableMapOf()
msgMap["data"] = dataMap
var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg")
val postRequest = XHttp.post(requestUrl).keepJson(true).timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE).timeStamp(true)
when (HttpServerUtils.clientSafetyMeasures) {
2 -> {
try {
val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey)
requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
}
3 -> {
try {
val sm4Key = ConvertTools.hexStringToByteArray(HttpServerUtils.clientSignKey)
//requestMsg = Base64.encode(requestMsg.toByteArray())
val encryptCBC = SM4Crypt.encrypt(requestMsg.toByteArray(), sm4Key)
requestMsg = ConvertTools.bytes2HexString(encryptCBC)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
}
else -> {
postRequest.upJson(requestMsg)
}
}
mCountDownHelper?.start()
postRequest.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) {
XToastUtils.error(e.displayMessage)
mCountDownHelper?.finish()
}
override fun onSuccess(response: String) {
Log.i(TAG, response)
try {
var json = response
if (HttpServerUtils.clientSafetyMeasures == 2) {
val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey)
json = RSACrypt.decryptByPublicKey(json, publicKey)
json = String(Base64.decode(json))
} else if (HttpServerUtils.clientSafetyMeasures == 3) {
val sm4Key = ConvertTools.hexStringToByteArray(HttpServerUtils.clientSignKey)
val encryptCBC = ConvertTools.hexStringToByteArray(json)
val decryptCBC = SM4Crypt.decrypt(encryptCBC, sm4Key)
json = String(decryptCBC)
}
val resp: BaseResponse<LocationInfo> = Gson().fromJson(json, object : TypeToken<BaseResponse<LocationInfo>>() {}.type)
if (resp.code == 200) {
XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
mCountDownHelper?.finish()
val locationInfo = resp.data ?: return
val groupListView = binding!!.infoList
groupListView.removeAllViews()
val section = XUIGroupListView.newSection(context)
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.location_longitude), locationInfo.longitude))) {}
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.location_latitude), locationInfo.latitude))) {}
if (locationInfo.address != "") section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.location_address), locationInfo.address))) {}
if (locationInfo.time != "") section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.location_time), locationInfo.time))) {}
if (locationInfo.provider != "") section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.location_provider), locationInfo.provider))) {}
section.addTo(groupListView)
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
}
mCountDownHelper?.finish()
}
})
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
}

@ -37,6 +37,7 @@ class ConfigController {
HttpServerUtils.enableApiContactQuery, HttpServerUtils.enableApiContactQuery,
HttpServerUtils.enableApiBatteryQuery, HttpServerUtils.enableApiBatteryQuery,
HttpServerUtils.enableApiWol, HttpServerUtils.enableApiWol,
HttpServerUtils.enableApiLocation,
SettingUtils.extraDeviceMark, SettingUtils.extraDeviceMark,
SettingUtils.extraSim1, SettingUtils.extraSim1,
SettingUtils.extraSim2, SettingUtils.extraSim2,

@ -0,0 +1,28 @@
package com.idormy.sms.forwarder.server.controller
import android.annotation.SuppressLint
import android.util.Log
import com.idormy.sms.forwarder.entity.LocationInfo
import com.idormy.sms.forwarder.server.model.BaseRequest
import com.idormy.sms.forwarder.server.model.EmptyData
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.yanzhenjie.andserver.annotation.*
import java.util.*
@SuppressLint("SimpleDateFormat")
@Suppress("PrivatePropertyName", "DEPRECATION")
@RestController
@RequestMapping(path = ["/location"])
class LocationController {
private val TAG: String = LocationController::class.java.simpleName
//远程找手机
@CrossOrigin(methods = [RequestMethod.POST])
@PostMapping("/query")
fun query(@RequestBody bean: BaseRequest<EmptyData>): LocationInfo {
Log.d(TAG, bean.data.toString())
return HttpServerUtils.apiLocationCache
}
}

@ -19,6 +19,8 @@ data class ConfigData(
var enableApiBatteryQuery: Boolean = false, var enableApiBatteryQuery: Boolean = false,
@SerializedName("enable_api_wol") @SerializedName("enable_api_wol")
var enableApiWol: Boolean = false, var enableApiWol: Boolean = false,
@SerializedName("enable_api_location")
var enableApiLocation: Boolean = false,
@SerializedName("extra_device_mark") @SerializedName("extra_device_mark")
var extraDeviceMark: String = "", var extraDeviceMark: String = "",
@SerializedName("extra_sim1") @SerializedName("extra_sim1")

@ -1,27 +1,40 @@
package com.idormy.sms.forwarder.service package com.idormy.sms.forwarder.service
import android.annotation.SuppressLint
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.location.Criteria
import android.location.Geocoder
import android.location.Location
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.entity.LocationInfo
import com.idormy.sms.forwarder.utils.HTTP_SERVER_PORT import com.idormy.sms.forwarder.utils.HTTP_SERVER_PORT
import com.idormy.sms.forwarder.utils.HTTP_SERVER_TIME_OUT import com.idormy.sms.forwarder.utils.HTTP_SERVER_TIME_OUT
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.SettingUtils import com.idormy.sms.forwarder.utils.SettingUtils
import com.king.location.LocationClient
import com.king.location.LocationErrorCode
import com.king.location.OnExceptionListener
import com.king.location.OnLocationListener
import com.yanzhenjie.andserver.AndServer import com.yanzhenjie.andserver.AndServer
import com.yanzhenjie.andserver.Server import com.yanzhenjie.andserver.Server
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@Suppress("PrivatePropertyName") @SuppressLint("SimpleDateFormat")
@Suppress("PrivatePropertyName", "DEPRECATION")
class HttpService : Service(), Server.ServerListener { class HttpService : Service(), Server.ServerListener {
private val TAG: String = "HttpService" private val TAG: String = "HttpService"
private val server by lazy { private val server by lazy {
AndServer.webServer(this) AndServer.webServer(this).port(HTTP_SERVER_PORT).listener(this).timeout(HTTP_SERVER_TIME_OUT, TimeUnit.SECONDS).build()
.port(HTTP_SERVER_PORT)
.listener(this)
.timeout(HTTP_SERVER_TIME_OUT, TimeUnit.SECONDS)
.build()
} }
private val locationClient by lazy { LocationClient(App.context) }
private val geocoder by lazy { Geocoder(App.context) }
private val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd HH:mm:ss") }
override fun onBind(p0: Intent?): IBinder? { override fun onBind(p0: Intent?): IBinder? {
return null return null
@ -35,6 +48,69 @@ class HttpService : Service(), Server.ServerListener {
Log.i(TAG, "onCreate: ") Log.i(TAG, "onCreate: ")
server.startup() server.startup()
//远程找手机
if (HttpServerUtils.enableApiLocation) {
//可根据具体需求设置定位配置参数(这里只列出一些主要的参数)
val locationOption = locationClient.getLocationOption().setAccuracy(Criteria.ACCURACY_FINE)//设置位置精度:高精度
.setPowerRequirement(Criteria.POWER_LOW) //设置电量消耗:低电耗
.setMinTime(10000)//设置位置更新最小时间间隔(单位:毫秒); 默认间隔10000毫秒最小间隔1000毫秒
.setMinDistance(0)//设置位置更新最小距离单位默认距离0米
.setOnceLocation(false)//设置是否只定位一次,默认为 false当设置为 true 时,则只定位一次后,会自动停止定位
.setLastKnownLocation(true)//设置是否获取最后一次缓存的已知位置,默认为 true
//设置定位配置参数
locationClient.setLocationOption(locationOption)
locationClient.startLocation()
//设置定位监听
locationClient.setOnLocationListener(object : OnLocationListener() {
override fun onLocationChanged(location: Location) {
//位置信息
Log.d(TAG, "onLocationChanged(location = ${location})")
val locationInfo = LocationInfo(
location.longitude,
location.latitude,
"",
simpleDateFormat.format(Date(location.time)),
location.provider.toString()
)
//根据坐标经纬度获取位置地址信息WGS-84坐标系
val list = geocoder.getFromLocation(location.latitude, location.longitude, 1)
if (list?.isNotEmpty() == true) {
locationInfo.address = list[0].getAddressLine(0)
}
Log.d(TAG, "locationInfo = $locationInfo")
HttpServerUtils.apiLocationCache = locationInfo
}
override fun onProviderEnabled(provider: String) {
super.onProviderEnabled(provider)
Log.d(TAG, "onProviderEnabled(provider = ${provider})")
}
override fun onProviderDisabled(provider: String) {
super.onProviderDisabled(provider)
Log.d(TAG, "onProviderDisabled(provider = ${provider})")
}
})
//设置异常监听
locationClient.setOnExceptionListener(object : OnExceptionListener {
override fun onException(@LocationErrorCode errorCode: Int, e: Exception) {
//定位出现异常
Log.w(TAG, "onException(errorCode = ${errorCode}, e = ${e})")
}
})
if (locationClient.isStarted()) {//如果已经开始定位,则先停止定位
locationClient.stopLocation()
}
locationClient.startLocation()
}
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -50,6 +126,10 @@ class HttpService : Service(), Server.ServerListener {
Log.i(TAG, "onDestroy: ") Log.i(TAG, "onDestroy: ")
server.shutdown() server.shutdown()
if (HttpServerUtils.enableApiLocation && locationClient.isStarted()) {//如果已经开始定位,则先停止定位
locationClient.stopLocation()
}
} }
override fun onException(e: Exception?) { override fun onException(e: Exception?) {

@ -357,6 +357,8 @@ const val SP_ENABLE_API_CALL_QUERY = "enable_api_call_query"
const val SP_ENABLE_API_CONTACT_QUERY = "enable_api_contact_query" const val SP_ENABLE_API_CONTACT_QUERY = "enable_api_contact_query"
const val SP_ENABLE_API_BATTERY_QUERY = "enable_api_battery_query" const val SP_ENABLE_API_BATTERY_QUERY = "enable_api_battery_query"
const val SP_ENABLE_API_WOL = "enable_api_wol" const val SP_ENABLE_API_WOL = "enable_api_wol"
const val SP_ENABLE_API_LOCATION = "enable_api_location"
const val SP_API_LOCATION_CACHE = "api_location_cache"
const val SP_WOL_HISTORY = "wol_history" const val SP_WOL_HISTORY = "wol_history"
const val SP_SERVER_ADDRESS = "server_address" const val SP_SERVER_ADDRESS = "server_address"
const val SP_SERVER_HISTORY = "server_history" const val SP_SERVER_HISTORY = "server_history"
@ -414,4 +416,11 @@ var CLIENT_FRAGMENT_LIST = listOf(
CoreAnim.slide, CoreAnim.slide,
R.drawable.icon_api_wol R.drawable.icon_api_wol
), ),
PageInfo(
getString(R.string.api_location),
"com.idormy.sms.forwarder.fragment.client.LocationFragment",
"{\"\":\"\"}",
CoreAnim.slide,
R.drawable.icon_api_location
),
) )

@ -8,6 +8,7 @@ import com.google.gson.Gson
import com.idormy.sms.forwarder.R import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.Core import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.entity.CloneInfo import com.idormy.sms.forwarder.entity.CloneInfo
import com.idormy.sms.forwarder.entity.LocationInfo
import com.idormy.sms.forwarder.server.model.BaseRequest import com.idormy.sms.forwarder.server.model.BaseRequest
import com.xuexiang.xui.utils.ResUtils.getString import com.xuexiang.xui.utils.ResUtils.getString
import com.xuexiang.xutil.app.AppUtils import com.xuexiang.xutil.app.AppUtils
@ -84,6 +85,12 @@ class HttpServerUtils private constructor() {
//是否启用远程WOL //是否启用远程WOL
var enableApiWol: Boolean by SharedPreference(SP_ENABLE_API_WOL, true) var enableApiWol: Boolean by SharedPreference(SP_ENABLE_API_WOL, true)
//是否启用远程找手机
var enableApiLocation: Boolean by SharedPreference(SP_ENABLE_API_LOCATION, true)
//远程找手机定位缓存
var apiLocationCache: LocationInfo by SharedPreference(SP_API_LOCATION_CACHE, LocationInfo())
//WOL历史记录 //WOL历史记录
var wolHistory: String by SharedPreference(SP_WOL_HISTORY, "") var wolHistory: String by SharedPreference(SP_WOL_HISTORY, "")

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:background="?attr/xui_config_color_background"
android:orientation="vertical">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:contentDescription="@string/api_location"
app:srcCompat="@drawable/icon_api_location" />
<com.xuexiang.xui.widget.grouplist.XUIGroupListView
android:id="@+id/info_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="10dp">
<com.xuexiang.xui.widget.textview.supertextview.SuperButton
android:id="@+id/btn_refresh"
style="@style/SuperButton.Blue.Icon"
android:layout_marginStart="20dp"
android:drawableStart="@drawable/ic_refresh"
android:paddingStart="20dp"
android:text="@string/refresh"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</LinearLayout>

@ -732,6 +732,41 @@
</LinearLayout> </LinearLayout>
<LinearLayout
style="@style/settingBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/api_location"
android:textStyle="bold"
tools:ignore="RelativeOverlap,TooManyViews" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/api_location_tips"
android:textSize="9sp"
tools:ignore="SmallSp" />
</LinearLayout>
<com.xuexiang.xui.widget.button.switchbutton.SwitchButton
android:id="@+id/sb_api_location"
style="@style/SwitchButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

@ -808,6 +808,13 @@
<string name="api_battery_query_tips">Remotely query mobile phone power and battery status</string> <string name="api_battery_query_tips">Remotely query mobile phone power and battery status</string>
<string name="api_wol">Remotely WOL</string> <string name="api_wol">Remotely WOL</string>
<string name="api_wol_tips">Turn on your Wake-On-LAN enabled devices remotely</string> <string name="api_wol_tips">Turn on your Wake-On-LAN enabled devices remotely</string>
<string name="api_location">Location</string>
<string name="api_location_tips">Remote query mobile phone location</string>
<string name="location_longitude">Longitude%s</string>
<string name="location_latitude">Latitude%s</string>
<string name="location_address">Address%s</string>
<string name="location_time">Time%s</string>
<string name="location_provider">Provider%s</string>
<string name="sim_slot">Sim Slot</string> <string name="sim_slot">Sim Slot</string>
<string name="phone_numbers">Phone Numbers</string> <string name="phone_numbers">Phone Numbers</string>

@ -809,6 +809,13 @@
<string name="api_battery_query_tips">远程查询手机电量与电池状态</string> <string name="api_battery_query_tips">远程查询手机电量与电池状态</string>
<string name="api_wol">远程WOL</string> <string name="api_wol">远程WOL</string>
<string name="api_wol_tips">远程打开启用LAN唤醒功能(Wake-On-LAN)的设备</string> <string name="api_wol_tips">远程打开启用LAN唤醒功能(Wake-On-LAN)的设备</string>
<string name="api_location">远程找手机</string>
<string name="api_location_tips">远程查询手机定位,方便找回手机/防止老少走丢</string>
<string name="location_longitude">经度:%s</string>
<string name="location_latitude">维度:%s</string>
<string name="location_address">地址:%s</string>
<string name="location_time">时间:%s</string>
<string name="location_provider">供应商:%s</string>
<string name="sim_slot">发送卡槽</string> <string name="sim_slot">发送卡槽</string>
<string name="phone_numbers">手机号码</string> <string name="phone_numbers">手机号码</string>

@ -48,4 +48,3 @@ task clean(type: Delete) {
} }
} }
} }

Loading…
Cancel
Save